This commit is contained in:
2026-08-14 13:03:09 +08:00
parent ce6963a25e
commit 06c264db0a
7 changed files with 32 additions and 7 deletions
+2 -1
View File
@@ -42,12 +42,13 @@
## 契约、数据库与验证入口 ## 契约、数据库与验证入口
- 跨服务修改先检查调用方 `client/`、被调用方 Controller/DTO、`src/main/resources/openapi/` 和双方契约测试;禁止共享领域 Entity。 - 跨服务修改先检查调用方 `client/`、被调用方 Controller/DTO、`src/main/resources/openapi/` 和双方契约测试;禁止共享领域 Entity。
- 修改 Java 方法、构造器、Controller、Client、DTO 或 Mapper 参数签名时,先用 `rg` 定向查找符号及旧参数形态,并同步更新 `src/main/`、`src/test/`、Mock、Fixture、契约测试和脚本中的全部调用点。新增可选参数也必须补齐旧测试调用,不能因生产调用已更新就视为完成。
- 平台后台存在正式列表页时,`*:list` 同时授权列表、详情及只读辅助接口;`create`、`edit`、`delete`、`status`、`approve`、`review`、`export`、`upload`、`complete` 等操作保持独立。当前未上线,不兼容旧独立 `*:view`,Controller 不得使用 `hasAnyAuthority(list, view)`,OpenAPI 和角色初始化只保留 `*:list`。 - 平台后台存在正式列表页时,`*:list` 同时授权列表、详情及只读辅助接口;`create`、`edit`、`delete`、`status`、`approve`、`review`、`export`、`upload`、`complete` 等操作保持独立。当前未上线,不兼容旧独立 `*:view`,Controller 不得使用 `hasAnyAuthority(list, view)`,OpenAPI 和角色初始化只保留 `*:list`。
- 供应商查询接口固定只使用 `platform:supplier:list`;IAM 中 `platform:supplier:list` 是正式菜单资源,`platform:supplier:view` 必须停用/移除,不得重新提升或兼容。 - 供应商查询接口固定只使用 `platform:supplier:list`;IAM 中 `platform:supplier:list` 是正式菜单资源,`platform:supplier:view` 必须停用/移除,不得重新提升或兼容。
- 权限语义变更必须同步 Controller `@PreAuthorize`、OpenAPI、模块权限 SQL、IAM 菜单资源、统一增量脚本和契约测试。并行模块任务不得覆盖菜单与权限总控已写入共享 SQL 或权限契约的改动。 - 权限语义变更必须同步 Controller `@PreAuthorize`、OpenAPI、模块权限 SQL、IAM 菜单资源、统一增量脚本和契约测试。并行模块任务不得覆盖菜单与权限总控已写入共享 SQL 或权限契约的改动。
- 数据库只访问所属服务库。模块迁移位于 `<module>/src/main/resources/db/`;测试/存量环境唯一部署入口是 `deploy/sql/头程数据库统一增量-MySQL8.sql`,说明见 `deploy/sql/README.md`,历史日期脚本仅追溯。 - 数据库只访问所属服务库。模块迁移位于 `<module>/src/main/resources/db/`;测试/存量环境唯一部署入口是 `deploy/sql/头程数据库统一增量-MySQL8.sql`,说明见 `deploy/sql/README.md`,历史日期脚本仅追溯。
- 通用配置在 `config/nacos/{local,cn}/`;服务日志、上传文件、`target/`、`reports/` 不属于源码。 - 通用配置在 `config/nacos/{local,cn}/`;服务日志、上传文件、`target/`、`reports/` 不属于源码。
- 全量验证:`mvn -DskipTests validate`、`mvn test`;单服务优先 `mvn -pl <module> -am test`。不要在本地启动 Java/Nacos/数据库,除非用户明确要求。 - Java 签名变更的最低编译门禁是 `mvn -pl <module> -am test-compile`,它必须在部署前完成并覆盖测试源码;有行为变化时继续运行 `mvn -pl <module> -am test`。`-DskipTests` 可跳过测试执行但仍须保留测试编译,`-Dmaven.test.skip=true` 会跳过测试编译,不得作为签名变更的交付验证。全量验证仍使用 `mvn -DskipTests validate`、`mvn test`;不要在本地启动 Java/Nacos/数据库,除非用户明确要求。
## 全量 HTTP 验收脚本路由 ## 全量 HTTP 验收脚本路由
@@ -25,6 +25,11 @@ public interface ContainerBookingMapper extends BaseMapper<ContainerBookingEntit
AND (#{query.cabinetNo} IS NULL OR b.cabinet_no = #{query.cabinetNo}) AND (#{query.cabinetNo} IS NULL OR b.cabinet_no = #{query.cabinetNo})
AND (#{query.billOfLadingNo} IS NULL OR b.bill_of_lading_no = #{query.billOfLadingNo}) AND (#{query.billOfLadingNo} IS NULL OR b.bill_of_lading_no = #{query.billOfLadingNo})
AND (#{query.customerId} IS NULL OR EXISTS ( AND (#{query.customerId} IS NULL OR EXISTS (
SELECT 1 FROM fl_ocean_booking_container_allocation customer_allocation
JOIN fl_ocean_booking customer_booking ON customer_booking.id = customer_allocation.ocean_booking_id
WHERE customer_allocation.container_booking_id = b.id
AND customer_booking.customer_id = #{query.customerId}
) OR EXISTS (
SELECT 1 FROM fl_loading_plan_item customer_item SELECT 1 FROM fl_loading_plan_item customer_item
WHERE customer_item.loading_plan_id = p.id WHERE customer_item.loading_plan_id = p.id
AND customer_item.deleted = 0 AND customer_item.deleted = 0
@@ -32,6 +32,22 @@ class ContainerBookingMapperContractTest {
.doesNotContain("account_id"); .doesNotContain("account_id");
} }
@Test
@DisplayName("货柜分页按装柜明细客户归属精确筛选")
void filtersPageByStableCustomerId() throws Exception {
String page = String.join("\n", ContainerBookingMapper.class
.getDeclaredMethod("selectBookingPage", com.baomidou.mybatisplus.extension.plugins.pagination.Page.class,
com.baiyue.firstleg.dto.ContainerDtos.Query.class)
.getAnnotation(Select.class).value());
assertThat(page)
.contains("fl_ocean_booking_container_allocation customer_allocation")
.contains("customer_allocation.container_booking_id = b.id")
.contains("customer_booking.customer_id = #{query.customerId}")
.contains("customer_item.loading_plan_id = p.id")
.contains("customer_item.deleted = 0")
.contains("customer_item.account_id = #{query.customerId}");
}
@Test @Test
@DisplayName("草稿完整更新和预装运输白名单都带版本与状态门禁") @DisplayName("草稿完整更新和预装运输白名单都带版本与状态门禁")
void guardsUpdatesInSql() { void guardsUpdatesInSql() {
@@ -37,12 +37,14 @@ class LoadingListQueryMapperContractTest {
.contains("p.preload_pushed_at IS NOT NULL") .contains("p.preload_pushed_at IS NOT NULL")
.contains("p.plan_status IN ('EDITING', 'LOADING')") .contains("p.plan_status IN ('EDITING', 'LOADING')")
.contains("active_item.deleted = 0") .contains("active_item.deleted = 0")
.contains("customer_item.account_id = #{query.customerId}")
.contains("b.warehouse_id = #{query.warehouseId}") .contains("b.warehouse_id = #{query.warehouseId}")
.doesNotContain("query.accountId") .doesNotContain("query.accountId")
.doesNotContain("PRELOAD_PUSHED"); .doesNotContain("PRELOAD_PUSHED");
assertThat(confirmed) assertThat(confirmed)
.contains("FROM fl_loading_plan p") .contains("FROM fl_loading_plan p")
.contains("p.plan_status = 'COMPLETED'") .contains("p.plan_status = 'COMPLETED'")
.contains("customer_item.account_id = #{query.customerId}")
.contains("b.warehouse_id = #{query.warehouseId}") .contains("b.warehouse_id = #{query.warehouseId}")
.doesNotContain("query.accountId") .doesNotContain("query.accountId")
.doesNotContain("fl_loading_list"); .doesNotContain("fl_loading_list");
@@ -13,7 +13,7 @@ class TruckAppointmentMapperContractTest {
@Test void mapsToPhase3TableAndUsesOptimisticGuards() throws Exception { @Test void mapsToPhase3TableAndUsesOptimisticGuards() throws Exception {
assertThat(TruckAppointmentEntity.class.getAnnotation(TableName.class).value()).isEqualTo("fl_truck_appointment"); assertThat(TruckAppointmentEntity.class.getAnnotation(TableName.class).value()).isEqualTo("fl_truck_appointment");
String page = TruckAppointmentMapper.class.getMethod("page", com.baomidou.mybatisplus.extension.plugins.pagination.Page.class, com.baiyue.firstleg.dto.TruckAppointmentDtos.Query.class).getAnnotation(Select.class).value()[0]; String page = TruckAppointmentMapper.class.getMethod("page", com.baomidou.mybatisplus.extension.plugins.pagination.Page.class, com.baiyue.firstleg.dto.TruckAppointmentDtos.Query.class).getAnnotation(Select.class).value()[0];
assertThat(page).contains("fl_truck_appointment", "appointment_no", "vehicle_no", "status=#{q.status}", "container_booking_id=#{q.containerBookingId}"); assertThat(page).contains("fl_truck_appointment", "appointment_no", "vehicle_no", "status=#{q.status}", "customer_id=#{q.customerId}", "container_booking_id=#{q.containerBookingId}");
String transition = TruckAppointmentMapper.class.getMethod("transition", long.class, String.class, String.class, int.class).getAnnotation(Update.class).value()[0]; String transition = TruckAppointmentMapper.class.getMethod("transition", long.class, String.class, String.class, int.class).getAnnotation(Update.class).value()[0];
assertThat(transition).contains("status=#{toStatus}", "status=#{fromStatus}", "version=#{version}", "revision=revision+1", "version=version+1"); assertThat(transition).contains("status=#{toStatus}", "status=#{fromStatus}", "version=#{version}", "revision=revision+1", "version=version+1");
String reschedule = TruckAppointmentMapper.class.getMethod("reschedule", long.class, java.time.LocalDateTime.class, String.class, int.class).getAnnotation(Update.class).value()[0]; String reschedule = TruckAppointmentMapper.class.getMethod("reschedule", long.class, java.time.LocalDateTime.class, String.class, int.class).getAnnotation(Update.class).value()[0];
@@ -61,7 +61,7 @@ class LoadingListServiceTest {
@Test @Test
void shouldAcceptEditingStatusFromPreloadList() { void shouldAcceptEditingStatusFromPreloadList() {
PreloadQuery query = new PreloadQuery(null, "EDITING", null, 100L, null, null, 1, 20); PreloadQuery query = new PreloadQuery(null, "EDITING", 200L, 100L, null, null, 1, 20);
service.preloadPage(query); service.preloadPage(query);
@@ -184,14 +184,14 @@ class LoadingListServiceTest {
@Test @Test
void confirmedPageAlwaysUsesRepositoryConfirmedProjection() { void confirmedPageAlwaysUsesRepositoryConfirmedProjection() {
ConfirmedQuery query = new ConfirmedQuery(" plan ", null, 100L, null, null, 1, 20); ConfirmedQuery query = new ConfirmedQuery(" plan ", 200L, 100L, null, null, 1, 20);
when(repository.confirmedPage(org.mockito.ArgumentMatchers.any())).thenReturn( when(repository.confirmedPage(org.mockito.ArgumentMatchers.any())).thenReturn(
new PageResult<>(List.of(), 1, 20, 0, 0)); new PageResult<>(List.of(), 1, 20, 0, 0));
service.confirmedPage(query); service.confirmedPage(query);
verify(repository).confirmedPage(org.mockito.ArgumentMatchers.argThat(value -> verify(repository).confirmedPage(org.mockito.ArgumentMatchers.argThat(value ->
"plan".equals(value.keyword()))); "plan".equals(value.keyword()) && Long.valueOf(200L).equals(value.customerId())));
} }
@Test @Test
@@ -44,12 +44,13 @@ class InternalCustomerExportContractTest {
@Test @Test
void exposesFinanceCustomerMasterDataOnlyWithInternalToken() { void exposesFinanceCustomerMasterDataOnlyWithInternalToken() {
BizException denied = assertThrows(BizException.class, BizException denied = assertThrows(BizException.class,
() -> controller.financeCustomers(null, 1, 20, "Alice", "ACTIVE")); () -> controller.financeCustomers(null, 1, 20, "Alice", "ACTIVE", null));
assertEquals(ResultCode.FORBIDDEN, denied.getResultCode()); assertEquals(ResultCode.FORBIDDEN, denied.getResultCode());
when(service.pageFinanceCustomers(1, 20, "Alice", "ACTIVE", null)) when(service.pageFinanceCustomers(1, 20, "Alice", "ACTIVE", null))
.thenReturn(new com.baiyue.common.core.api.PageResult<>(List.of(), 1, 20, 0, 0)); .thenReturn(new com.baiyue.common.core.api.PageResult<>(List.of(), 1, 20, 0, 0));
var response = controller.financeCustomers("test-internal-service-token-at-least-32-bytes", 1, 20, "Alice", "ACTIVE"); var response = controller.financeCustomers("test-internal-service-token-at-least-32-bytes", 1, 20,
"Alice", "ACTIVE", null);
assertEquals(ResultCode.SUCCESS.code(), response.code()); assertEquals(ResultCode.SUCCESS.code(), response.code());
verify(service).pageFinanceCustomers(1, 20, "Alice", "ACTIVE", null); verify(service).pageFinanceCustomers(1, 20, "Alice", "ACTIVE", null);
} }