From 264458920bef53d19eb59c1190bde1dd64efadcb Mon Sep 17 00:00:00 2001 From: ouyangxq <775153912@qq.com> Date: Wed, 5 Aug 2026 13:50:37 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- AGENTS.md | 18 +++++++++++++----- warehouse/AGENTS.md | 2 +- .../PlatformWarehouseController.java | 11 +++++------ .../repository/WarehouseRepository.java | 6 +++--- .../service/WarehouseApplicationService.java | 2 +- ...atformMasterDataControllerContractTest.java | 8 ++++++++ .../WarehouseApplicationServiceTest.java | 10 ++++++++++ 7 files changed, 41 insertions(+), 16 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index b4d191662..89fb22ef1 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -6,6 +6,14 @@ - 普通开发先读本文件、目标模块 `AGENTS.md` 和相邻代码;项目概览或聚合构建问题再读 `README.md`、`pom.xml`,跨服务架构、端口数据库、根目录结构、启动或部署问题再读 `docs/头程项目目录结构与服务说明.md`。 - 本仓只承载国内头程业务。尾程源码、数据库、Redis、Nacos 和内部接口不得并入本仓。 +## 当前主体层级(重要) + +- 完整目标模型原计划为“租户—平台—客户”三级,但当前阶段采用 **单租户、多客户**:整个系统只有一个隐式的默认租户,平台在该租户内运营和管理多个客户,客户之间必须严格隔离。 +- 当前不向业务侧引入或开放租户概念,不建设租户管理、租户切换、租户级配置或多租户隔离能力;现阶段需求、接口、权限、数据范围和用户可见文案均按“平台—客户”两级使用。 +- 当前真正的数据隔离边界是客户。服务端必须校验客户归属和访问权限,不得因为系统处于单租户模式而放松客户之间的数据隔离。 +- 代码、数据库和接口中已有的 `tenant`、`Tenant*`、`tenant_id`、`merchant` 等历史结构和兼容契约不得仅因当前未开放租户概念而贸然删除或重命名;修改前必须核对其当前实际承载的是默认租户、客户归属还是历史命名,避免破坏现有调用方和未来演进空间。 +- 预计约一年后再独立引入显式租户能力。届时存量客户统一归属到一个默认租户,再基于确认后的三级关系、租户隔离规则和迁移方案扩展多租户;当前开发不得提前实现未经确认的多租户业务,也不得做会显著增加未来迁移成本的不可逆改动。 + ## 模块快速索引 | 模块 | 职责 | 首选入口 | @@ -19,8 +27,8 @@ | `common-event-contract/` | 跨区域事件信封、类型、标识、哈希与校验 | `EventEnvelope.java`、`EventTypes.java`、`EventEnvelopeValidator.java` | | `gateway/` | 路由、CORS、入口安全、请求标识、访问域隔离 | `config/`、`filter/CorrelationIdWebFilter.java`、`src/main/resources/application*.yml` | | `auth/` | 登录、令牌、注册、账号、密码与账号归属范围 | `InternalAccountController.java`、`AccountManagementService.java`、`AccountManagementDtos.java`、`src/main/resources/db/` | -| `iam/` | 租户、商户成员、平台统一用户管理、企业资料/资质、RBAC 与授权快照 | `PlatformUserManagementController.java`、`PlatformTenantController.java`、`ProfileResourceController.java`、`RbacController.java` | -| `order/` | 订单主数据、租户运单数、持久化幂等、状态编排、快照、货物明细内部查询与 Outbox | `PlatformFirstLegOrderController.java`、`OrderApplicationService.java`、`InternalOrderCargoController.java`、`README-ORDER.md` | +| `iam/` | 单租户模式下的客户、客户成员、平台统一用户管理、企业资料/资质、RBAC 与授权快照;部分代码仍保留 `Tenant*` 历史命名 | `PlatformUserManagementController.java`、`PlatformTenantController.java`、`ProfileResourceController.java`、`RbacController.java` | +| `order/` | 订单主数据、客户运单数、持久化幂等、状态编排、快照、货物明细内部查询与 Outbox;客户之间按归属隔离,部分字段/接口仍保留 `tenant` 历史命名 | `PlatformFirstLegOrderController.java`、`OrderApplicationService.java`、`InternalOrderCargoController.java`、`README-ORDER.md` | | `firstleg/` | 履约任务与分配;二期入库单、测量/OCR 结果、审核、FBA 计费和自动完成入库 | 履约:`PlatformFulfillmentAssignmentController.java`、`FulfillmentTaskService.java`;入库:`controller/PlatformInbound*.java`、`service/Inbound*.java`、`service/Fba*.java` | | `warehouse/` | 仓库、地址、服务能力、往来单位、快递公司、海外仓阶梯费用;二期托盘、库位、预占和占用 | 主数据:`controller/`、`PlatformPartnerQueryService.java`、`PlatformOverseasStorageFeeController.java`;二期:`PlatformPalletController.java`、`PlatformLocationController.java`、`PalletLocationAllocationService.java` | | `file/` | 企业资质文件、公共导出/下载中心;入库测量图片与重量 OCR 协作 | 资质:`QualificationFileController.java`;导出:`PlatformExportTaskController.java`、`ExportStateService.java`;测量图片:`PlatformMeasurementImageController.java`、`InternalMeasurementImageController.java`、`MeasurementImageService.java` | @@ -33,7 +41,7 @@ | 搜索目标 | 先读 | |---|---| -| 后台用户管理、平台/企业账号、跨租户调动 | `iam/.../controller/PlatformUserManagementController.java` → `service/PlatformUserManagementService.java` → `auth/.../controller/InternalAccountController.java` 的 scope 接口 | +| 后台用户管理、平台/客户账号、跨客户调动 | `iam/.../controller/PlatformUserManagementController.java` → `service/PlatformUserManagementService.java` → `auth/.../controller/InternalAccountController.java` 的 scope 接口 | | 企业客户档案、审核、必填资质 | `iam/.../controller/PlatformTenantController.java`、`service/TenantService.java`、`service/ProfileResourceService.java` | | 客户列表真实运单数 | `order/.../controller/PlatformFirstLegOrderController.java` 的 `/tenant-counts`;不要在 IAM 查订单表 | | 平台代客户上传或替换资质 | `file/.../controller/PlatformQualificationFileController.java` → `client/IamQualificationClient.java` → `iam/.../controller/InternalTenantController.java` 的 `/platform-qualifications` | @@ -53,7 +61,7 @@ | 第三方尺寸测量回传 | `firstleg/.../controller/OpenApiInboundMeasurementController.java` → `security/MeasurementRequestAuthenticator.java` → `service/InboundMeasurementOpenApiService.java` | 订单货物校验走 `client/OrderCargoClient.java` → `order/.../controller/InternalOrderCargoController.java`;证据图片走 File 内部接口;配置 `config/MeasurementOpenApiProperties.java`,测试 `service/InboundMeasurementOpenApiServiceTest.java` | | 测量审核、异常放行与自动完成入库 | `firstleg/.../controller/PlatformInboundMeasurementReviewController.java` → `service/MeasurementReviewService.java` → `AutomaticInboundCompletionService.java` | 费用与入库状态编排在 `FbaPricingService.java`、`InboundCompletionOrchestrator.java`;测试 `service/MeasurementReviewServiceTest.java`、`InboundCompletionOrchestratorTest.java` | | FBA 客户规则、承运商价格与费用快照 | 主数据 `firstleg/.../controller/PlatformFbaMasterDataController.java` → `service/FbaMasterDataService.java`;计费 `PlatformFbaPricingController.java` → `service/FbaPricingService.java` | `repository/FbaPricingRepository.java`、`src/main/resources/openapi/fba-pricing-v1.yaml`、`src/main/resources/db/phase2-fba-pricing-migration.sql`;测试 `service/FbaMasterDataServiceTest.java`、`FbaPricingServiceTest.java` | -| 商户查看入库费用 | `firstleg/.../controller/MerchantInboundBillingController.java` → `service/MerchantInboundBillingService.java` | 只按已认证商户租户查询;接口为 `/api/merchant/v1/first-leg/orders/{orderId}/inbound-billing` | +| 客户查看入库费用 | `firstleg/.../controller/MerchantInboundBillingController.java` → `service/MerchantInboundBillingService.java` | 只按已认证客户查询;`Merchant*` 及 `/api/merchant/` 为历史兼容命名,接口为 `/api/merchant/v1/first-leg/orders/{orderId}/inbound-billing` | | 托盘、库位与预占/占用 | 平台 CRUD:`warehouse/.../controller/PlatformPalletController.java`、`PlatformLocationController.java`;内部事务接口:`InternalPalletLocationController.java` | `service/PalletApplicationService.java`、`LocationApplicationService.java`、`PalletLocationAllocationService.java`;表与迁移在 `warehouse/src/main/resources/db/phase2-*.sql` | | 称重图片、OCR 重试与第三方测量证据 | 平台入口 `file/.../controller/PlatformMeasurementImageController.java`;Firstleg 内部入口 `InternalMeasurementImageController.java` | `service/MeasurementImageService.java`、`MeasurementImageStorage.java`、`client/WeightOcrClient.java`;重量识别服务入口 `integration-service/.../controller/WeightOcrController.java` | | 入库状态、幂等、历史与 Outbox | `firstleg/.../domain/InboundProcessStatus.java`、`service/InboundIdempotencyService.java`、`repository/InboundWorkflowRepository.java` | 表结构与增量在 `firstleg/src/main/resources/db/phase2-*.sql`;SQL 契约测试 `repository/InboundWorkflowSqlContractTest.java` | @@ -83,7 +91,7 @@ ## 通用修改约束 - Controller 只做协议转换,Service 负责编排,Repository/Mapper 负责持久化;不要跨层堆放业务逻辑。 -- 商户数据必须保留租户维度,并由服务端执行租户、访问域、数据范围和按钮权限校验。 +- 当前采用单租户多客户模式。客户数据必须保留客户归属维度,并由服务端执行平台/客户访问域、客户数据隔离、数据范围和按钮权限校验;默认租户不能替代客户隔离条件,也不得据此允许跨客户访问。 - 修改 SQL、DTO、事件或公共组件时同步检查所有生产者与消费者;优先保持兼容,不能兼容时写清迁移顺序。 - 对外 HTTP JSON 中的 Java `Long/long` 统一输出字符串,请求同时接受字符串 ID;业务模块禁止重复添加局部 ID 转换器,OpenAPI 必须同步声明字符串契约。 - 普通业务成功与失败统一使用 HTTP 200,并以响应体字符串 `code` 判断;`code == "200"` 才成功。网关/Servlet Security 的真实 401/403、协议与基础设施错误、Warehouse readiness 503、Audit 202 和 Event Bridge 内部鉴权边界除外;完整约定见 `docs/统一响应约定改造方案-HTTP200.md`。 diff --git a/warehouse/AGENTS.md b/warehouse/AGENTS.md index 85481bc52..492eb2c89 100644 --- a/warehouse/AGENTS.md +++ b/warehouse/AGENTS.md @@ -34,7 +34,7 @@ ## 修改与验证 - 商户只看到可用且符合租户/服务范围的数据;平台启停必须检查业务约束,不能只改状态字段。 -- 平台仓库管理接口必须同时校验平台访问域和按钮权限:列表/详情使用 `platform:warehouse:list`,新增使用 `platform:warehouse:create`,编辑使用 `platform:warehouse:edit`,启停使用 `platform:warehouse:status`;OpenAPI 的 403 响应说明必须与权限码同步。 +- 平台仓库是平台公共主数据:列表接口默认查询全部仓库,并可用兼容字段 `tenantId` 做可选客户范围筛选;不得要求 Admin 先选择客户才能取得仓库候选。详情和写操作现阶段仍按数据表兼容字段校验范围。接口同时校验平台访问域和按钮权限:列表/详情使用 `platform:warehouse:list`,新增使用 `platform:warehouse:create`,编辑使用 `platform:warehouse:edit`,启停使用 `platform:warehouse:status`;OpenAPI 的 403 响应说明必须与权限码同步。 - 往来单位与仓库查询供 Order 使用,DTO 或有效性语义变化需同步检查 `order/client/` 和订单快照逻辑。 - 往来单位类型以 `types` 多值集合和 `wh_partner_type` 为准,允许同一单位同时作为客户、供应商等;`wh_partner.partner_type` 与 DTO 的 `partnerType` 仅保留旧数据兼容。写入时 Entity、Mapper、Repository、平台保存/导入和 Order 客户端必须一起核对。 - 往来单位与快递公司导入只接收标准 XLSX 模板,文件上限 5MB、最多 1000 行;先完成整表头、字段、文件内重复和数据库重复校验,再原子写入,任一行失败整批不落库。权限分别是 `platform:counterparty:import` 与 `platform:express:import`。 diff --git a/warehouse/src/main/java/com/baiyue/warehouse/controller/PlatformWarehouseController.java b/warehouse/src/main/java/com/baiyue/warehouse/controller/PlatformWarehouseController.java index 71522cfd8..612346ab9 100644 --- a/warehouse/src/main/java/com/baiyue/warehouse/controller/PlatformWarehouseController.java +++ b/warehouse/src/main/java/com/baiyue/warehouse/controller/PlatformWarehouseController.java @@ -89,8 +89,8 @@ public class PlatformWarehouseController { } @Operation( - summary = "List tenant warehouses / 查询租户仓库", - description = "Returns a tenant-scoped warehouse page with optional country, capability and status filters. / 按租户分页查询仓库,可按国家、能力和状态筛选。" + summary = "List platform warehouses / 查询平台仓库", + description = "Returns all platform warehouses by default; customer ID is an optional compatibility filter. / 默认分页查询平台全部仓库,客户 ID 仅作为兼容筛选条件。" ) @ApiResponses({ @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "Query succeeded / 查询成功"), @@ -101,11 +101,11 @@ public class PlatformWarehouseController { @PreAuthorize("hasAuthority('platform:warehouse:list')") public ApiResponse> page( @AuthenticationPrincipal AuthenticatedUser user, - @Parameter(description = "Target tenant ID / 目标租户 ID", example = "10001", required = true) - @RequestParam @NotNull Long tenantId, + @Parameter(description = "Optional customer ID / 可选客户 ID", example = "10001") + @RequestParam(required = false) Long tenantId, @Valid WarehouseRequests.QueryRequest request) { requirePlatform(user); - requireTenant(tenantId); + if (tenantId != null) requireTenant(tenantId); return ApiResponse.success(service.page(tenantId, request)); } @@ -169,4 +169,3 @@ public class PlatformWarehouseController { } } } - diff --git a/warehouse/src/main/java/com/baiyue/warehouse/repository/WarehouseRepository.java b/warehouse/src/main/java/com/baiyue/warehouse/repository/WarehouseRepository.java index 3950ee3fd..64249a398 100644 --- a/warehouse/src/main/java/com/baiyue/warehouse/repository/WarehouseRepository.java +++ b/warehouse/src/main/java/com/baiyue/warehouse/repository/WarehouseRepository.java @@ -56,9 +56,9 @@ public class WarehouseRepository { .ne(excludedId != null, WarehouseEntity::getId, excludedId)) > 0; } - public PageResult page(long tenantId, WarehouseRequests.QueryRequest query) { + public PageResult page(Long tenantId, WarehouseRequests.QueryRequest query) { LambdaQueryWrapper wrapper = new LambdaQueryWrapper() - .eq(WarehouseEntity::getTenantId, tenantId) + .eq(tenantId != null, WarehouseEntity::getTenantId, tenantId) .eq(StringUtils.hasText(query.status()), WarehouseEntity::getStatus, query.status()) .eq(StringUtils.hasText(query.warehouseType()), WarehouseEntity::getWarehouseType, query.warehouseType()) .eq(StringUtils.hasText(query.countryCode()), WarehouseEntity::getCountryCode, query.countryCode()) @@ -67,7 +67,7 @@ public class WarehouseRepository { if (StringUtils.hasText(query.capabilityCode())) { List ids = capabilities.selectList(new LambdaQueryWrapper() .select(ServiceCapabilityEntity::getWarehouseId) - .eq(ServiceCapabilityEntity::getTenantId, tenantId) + .eq(tenantId != null, ServiceCapabilityEntity::getTenantId, tenantId) .eq(ServiceCapabilityEntity::getCapabilityCode, query.capabilityCode()) .eq(ServiceCapabilityEntity::getEnabled, true)) .stream() diff --git a/warehouse/src/main/java/com/baiyue/warehouse/service/WarehouseApplicationService.java b/warehouse/src/main/java/com/baiyue/warehouse/service/WarehouseApplicationService.java index 8c16805a4..808534c68 100644 --- a/warehouse/src/main/java/com/baiyue/warehouse/service/WarehouseApplicationService.java +++ b/warehouse/src/main/java/com/baiyue/warehouse/service/WarehouseApplicationService.java @@ -63,7 +63,7 @@ public class WarehouseApplicationService { insertChildren(tenantId, id, request.address(), request.capabilities()); } - public PageResult page(long tenantId, WarehouseRequests.QueryRequest query) { + public PageResult page(Long tenantId, WarehouseRequests.QueryRequest query) { return repository.page(tenantId, query); } diff --git a/warehouse/src/test/java/com/baiyue/warehouse/controller/PlatformMasterDataControllerContractTest.java b/warehouse/src/test/java/com/baiyue/warehouse/controller/PlatformMasterDataControllerContractTest.java index 887d51245..571c8daf9 100644 --- a/warehouse/src/test/java/com/baiyue/warehouse/controller/PlatformMasterDataControllerContractTest.java +++ b/warehouse/src/test/java/com/baiyue/warehouse/controller/PlatformMasterDataControllerContractTest.java @@ -10,6 +10,7 @@ import org.springframework.web.bind.annotation.PatchMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PutMapping; import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; import java.lang.reflect.Method; @@ -45,6 +46,13 @@ class PlatformMasterDataControllerContractTest { assertWarehouseMethod("create", PostMapping.class, "platform:warehouse:create"); assertWarehouseMethod("update", PutMapping.class, "platform:warehouse:edit"); assertWarehouseMethod("status", PatchMapping.class, "platform:warehouse:status"); + + Method page = java.util.Arrays.stream(PlatformWarehouseController.class.getDeclaredMethods()) + .filter(candidate -> candidate.getName().equals("page")) + .findFirst() + .orElseThrow(); + assertThat(page.getParameterTypes()[1]).isEqualTo(Long.class); + assertThat(page.getParameters()[1].getAnnotation(RequestParam.class).required()).isFalse(); } @Test diff --git a/warehouse/src/test/java/com/baiyue/warehouse/service/WarehouseApplicationServiceTest.java b/warehouse/src/test/java/com/baiyue/warehouse/service/WarehouseApplicationServiceTest.java index ead9bdefe..aa73a9395 100644 --- a/warehouse/src/test/java/com/baiyue/warehouse/service/WarehouseApplicationServiceTest.java +++ b/warehouse/src/test/java/com/baiyue/warehouse/service/WarehouseApplicationServiceTest.java @@ -4,6 +4,8 @@ import com.baiyue.common.core.exception.BizException; import com.baiyue.common.core.api.ResultCode; import com.baiyue.warehouse.entity.WarehouseEntity; import com.baiyue.warehouse.repository.WarehouseRepository; +import com.baiyue.warehouse.dto.WarehouseRequests; +import com.baiyue.common.core.api.PageResult; import org.junit.jupiter.api.Test; import jakarta.annotation.Resource; import org.springframework.boot.test.mock.mockito.MockBean; @@ -36,4 +38,12 @@ class WarehouseApplicationServiceTest { assertThatThrownBy(() -> service.activeDetail(10001,1)).isInstanceOf(BizException.class) .extracting(exception -> ((BizException) exception).getResultCode()).isEqualTo(ResultCode.CONFLICT); } + @Test + void pagesAllPlatformWarehousesWithoutCustomerFilter() { + WarehouseRequests.QueryRequest query = new WarehouseRequests.QueryRequest(1, 200, null, null, null, null); + when(repository.page(null, query)).thenReturn(new PageResult<>(java.util.List.of(), 1, 200, 0, 0)); + + assertThat(service.page(null, query).records()).isEmpty(); + verify(repository).page(null, query); + } }