From 7f051839048e3398fa938526cf6f4440abd432aa Mon Sep 17 00:00:00 2001 From: ouyangxq <775153912@qq.com> Date: Wed, 26 Aug 2026 14:08:41 +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 | 1 + .../InternalBusinessFileController.java | 15 ---------- .../baiyue/file/mapper/FileObjectMapper.java | 9 ------ .../file/service/BusinessFileStorage.java | 10 ------- .../service/InternalBusinessFileService.java | 11 -------- .../InternalBusinessFileControllerTest.java | 8 ------ .../InternalBusinessFileServiceTest.java | 24 ---------------- firstleg/pom.xml | 2 +- .../client/LoadingDocumentFileClient.java | 7 ----- .../firstleg/service/LoadingListService.java | 28 ++----------------- .../service/LoadingListServiceTest.java | 28 ------------------- scripts/agents-index-contract.ps1 | 7 +++++ 12 files changed, 11 insertions(+), 139 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 2b039d350..c399798f3 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -81,6 +81,7 @@ - 供应商查询接口固定只使用 `platform:supplier:list`;IAM 中 `platform:supplier:list` 是正式菜单资源,`platform:supplier:view` 必须停用/移除,不得重新提升或兼容。 - 权限语义变更必须同步 Controller `@PreAuthorize`、OpenAPI、模块权限 SQL、IAM 菜单资源、统一增量脚本和契约测试。并行模块任务不得覆盖菜单与权限总控已写入共享 SQL 或权限契约的改动。 - 数据库只访问所属服务库。模块表结构位于 `/src/main/resources/db/`;当前开发阶段直接更新最新模块基线和 `deploy/sql/头程数据库统一增量-MySQL8.sql`,不为历史开发数据新增兼容迁移。已有名称含 `migration` 的脚本仅作现有结构来源和开发追溯,部署说明见 `deploy/sql/README.md`。 +- 后端同样遵守根指引中的“开发数据和上传文件可丢弃”边界:不得为现有开发库孤儿文件、旧文件归属或历史部分失败新增删除补偿接口、兼容复用、回填或清理链路;只修复当前正式上传与持久化路径,既有开发文件由人工重新上传。 - 通用配置在 `config/nacos/{local,cn}/`;服务日志、上传文件、`target/`、`reports/` 不属于源码。 - Java 签名变更的最低编译门禁是 `mvn -pl -am test-compile`,它必须在部署前完成并覆盖测试源码;有行为变化时继续运行 `mvn -pl -am test`。`-DskipTests` 可跳过测试执行但仍须保留测试编译,`-Dmaven.test.skip=true` 会跳过测试编译,不得作为签名变更的交付验证。全量验证仍使用 `mvn -DskipTests validate`、`mvn test`;不要在本地启动 Java/Nacos/数据库,除非用户明确要求。 - `AGENTS.md` 索引维护完成后运行 `./scripts/agents-index-contract.ps1`;脚本只读源码目录和指定指引,验证根快速表唯一性、部署单元、Client active 路由、四期范围漂移、订舱关联候选与柜量语义、海外 Gateway 覆盖、PDA 运行配置、Lastleg 海外安全投影、同步 Trace、模块覆盖、关键入口与排除目录,不访问 HTTP、数据库或部署环境。 diff --git a/file/src/main/java/com/baiyue/file/controller/InternalBusinessFileController.java b/file/src/main/java/com/baiyue/file/controller/InternalBusinessFileController.java index 05d90c270..046b1ac3a 100644 --- a/file/src/main/java/com/baiyue/file/controller/InternalBusinessFileController.java +++ b/file/src/main/java/com/baiyue/file/controller/InternalBusinessFileController.java @@ -13,7 +13,6 @@ import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestHeader; @@ -75,20 +74,6 @@ public class InternalBusinessFileController { .body(value.content()); } - @DeleteMapping("/{tenantId}/{businessType}/{businessId}/{fileId}") - public ApiResponse delete( - @RequestHeader(value = "X-Internal-Service-Token", required = false) String token, - @RequestHeader("X-Operator-Id") long operatorId, - @PathVariable long tenantId, - @PathVariable @NotBlank String businessType, - @PathVariable @NotBlank String businessId, - @PathVariable @Positive long fileId) { - check(token); - checkTenantId(tenantId); - service.delete(tenantId, businessType, businessId, fileId, operatorId); - return ApiResponse.success(null); - } - private void check(String token) { if (token == null || !MessageDigest.isEqual( token.getBytes(StandardCharsets.UTF_8), expected.getBytes(StandardCharsets.UTF_8))) { diff --git a/file/src/main/java/com/baiyue/file/mapper/FileObjectMapper.java b/file/src/main/java/com/baiyue/file/mapper/FileObjectMapper.java index f0faec11e..729476614 100644 --- a/file/src/main/java/com/baiyue/file/mapper/FileObjectMapper.java +++ b/file/src/main/java/com/baiyue/file/mapper/FileObjectMapper.java @@ -3,16 +3,7 @@ package com.baiyue.file.mapper; import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.baiyue.file.entity.FileObjectEntity; import org.apache.ibatis.annotations.Mapper; -import org.apache.ibatis.annotations.Param; -import org.apache.ibatis.annotations.Update; @Mapper public interface FileObjectMapper extends BaseMapper { - @Update("UPDATE file_object SET status='VOID',deleted=1,updated_by=#{operatorId}," - + "updated_at=CURRENT_TIMESTAMP(3),version=version+1 WHERE id=#{fileId} AND tenant_id=#{tenantId} " - + "AND business_type=#{businessType} AND business_id=#{businessId} AND status='ACTIVE' " - + "AND deleted=0 AND version=#{version}") - int voidOwned(@Param("tenantId") long tenantId, @Param("businessType") String businessType, - @Param("businessId") String businessId, @Param("fileId") long fileId, - @Param("version") int version, @Param("operatorId") long operatorId); } diff --git a/file/src/main/java/com/baiyue/file/service/BusinessFileStorage.java b/file/src/main/java/com/baiyue/file/service/BusinessFileStorage.java index f528b7cda..5087c9ae9 100644 --- a/file/src/main/java/com/baiyue/file/service/BusinessFileStorage.java +++ b/file/src/main/java/com/baiyue/file/service/BusinessFileStorage.java @@ -65,16 +65,6 @@ public class BusinessFileStorage { } } - boolean delete(String objectKey) { - Path target = root.resolve(objectKey).normalize(); - if (!target.startsWith(root)) return false; - try { - return Files.deleteIfExists(target); - } catch (IOException exception) { - return false; - } - } - private void verifyContent(MultipartFile file, String type) { try (InputStream input = file.getInputStream()) { byte[] prefix = input.readNBytes(4096); diff --git a/file/src/main/java/com/baiyue/file/service/InternalBusinessFileService.java b/file/src/main/java/com/baiyue/file/service/InternalBusinessFileService.java index 7ac68d48b..45ec8d2bd 100644 --- a/file/src/main/java/com/baiyue/file/service/InternalBusinessFileService.java +++ b/file/src/main/java/com/baiyue/file/service/InternalBusinessFileService.java @@ -7,7 +7,6 @@ import com.baiyue.file.entity.FileObjectEntity; import com.baiyue.file.mapper.FileObjectMapper; import jakarta.annotation.Resource; import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; import org.springframework.web.multipart.MultipartFile; import java.time.LocalDateTime; @@ -71,16 +70,6 @@ public class InternalBusinessFileService { return new Download(value.getOriginalName(), value.getContentType(), storage.read(value.getObjectKey())); } - @Transactional - public void delete(long tenantId, String businessType, String businessId, long fileId, long operatorId) { - if (operatorId <= 0) throw new BizException(ResultCode.BAD_REQUEST, "操作人必须有效"); - FileObjectEntity value = required(tenantId, businessType, businessId, fileId); - int changed = files.voidOwned(tenantId, businessType, businessId, fileId, - value.getVersion(), operatorId); - if (changed != 1) throw new BizException(ResultCode.CONFLICT, "业务附件已变化,请刷新后重试"); - storage.delete(value.getObjectKey()); - } - private FileObjectEntity required(long tenantId, String businessType, String businessId, long fileId) { validateScope(tenantId, businessType, businessId, fileId); FileObjectEntity value = files.selectOne(new LambdaQueryWrapper() diff --git a/file/src/test/java/com/baiyue/file/controller/InternalBusinessFileControllerTest.java b/file/src/test/java/com/baiyue/file/controller/InternalBusinessFileControllerTest.java index 65ea3cbae..04819c77e 100644 --- a/file/src/test/java/com/baiyue/file/controller/InternalBusinessFileControllerTest.java +++ b/file/src/test/java/com/baiyue/file/controller/InternalBusinessFileControllerTest.java @@ -45,14 +45,6 @@ class InternalBusinessFileControllerTest { verify(service).download(0L, "LOADING_CUSTOMS_DECLARATION", "51", 91L); } - @Test void exactOwnershipCompensationDeleteRemainsBehindInternalToken() { - var response = controller.delete("internal-token", 7L, 0L, - "CONTAINER_DELIVERY_ORDER", "41", 91L); - - assertThat(response.successful()).isTrue(); - verify(service).delete(0L, "CONTAINER_DELIVERY_ORDER", "41", 91L, 7L); - } - @Test void rejectsInvalidTokenAndNegativeTenantBeforeServiceAccess() { var file = new MockMultipartFile("file", "x.txt", "text/plain", "x".getBytes()); assertThatThrownBy(() -> controller.upload("wrong", 7L, 0L, diff --git a/file/src/test/java/com/baiyue/file/service/InternalBusinessFileServiceTest.java b/file/src/test/java/com/baiyue/file/service/InternalBusinessFileServiceTest.java index 2840fa856..b5a5100e1 100644 --- a/file/src/test/java/com/baiyue/file/service/InternalBusinessFileServiceTest.java +++ b/file/src/test/java/com/baiyue/file/service/InternalBusinessFileServiceTest.java @@ -138,28 +138,4 @@ class InternalBusinessFileServiceTest { verify(storage).read(value.getObjectKey()); } - @Test void compensationDeleteRequiresExactOwnershipAndLogicallyVoidsBeforeStorageCleanup() { - FileObjectEntity value = new FileObjectEntity(); - value.setId(91L); value.setTenantId(0L); value.setBusinessType("CONTAINER_DELIVERY_ORDER"); - value.setBusinessId("41"); value.setStatus("ACTIVE"); value.setDeleted(false); value.setVersion(0); - value.setObjectKey("business-files/0/41/do.pdf"); - when(files.selectOne(any())).thenReturn(value); - when(files.voidOwned(0L, "CONTAINER_DELIVERY_ORDER", "41", 91L, 0, 7L)).thenReturn(1); - when(storage.delete(value.getObjectKey())).thenReturn(true); - - service.delete(0L, "CONTAINER_DELIVERY_ORDER", "41", 91L, 7L); - - verify(files).voidOwned(0L, "CONTAINER_DELIVERY_ORDER", "41", 91L, 0, 7L); - verify(storage).delete(value.getObjectKey()); - } - - @Test void compensationMapperUsesExactCompositeOwnershipAndOptimisticVersion() throws Exception { - String sql = String.join(" ", FileObjectMapper.class.getMethod("voidOwned", long.class, - String.class, String.class, long.class, int.class, long.class) - .getAnnotation(org.apache.ibatis.annotations.Update.class).value()); - assertThat(sql) - .contains("id=#{fileId}", "tenant_id=#{tenantId}", "business_type=#{businessType}") - .contains("business_id=#{businessId}", "status='ACTIVE'", "deleted=0", "version=#{version}") - .contains("status='VOID'", "deleted=1", "version=version+1"); - } } diff --git a/firstleg/pom.xml b/firstleg/pom.xml index 1a35216ef..0ffa97146 100644 --- a/firstleg/pom.xml +++ b/firstleg/pom.xml @@ -16,7 +16,7 @@ com.alibaba.cloudspring-cloud-starter-alibaba-nacos-discovery com.alibaba.cloudspring-cloud-starter-alibaba-nacos-config com.alibaba.cloudspring-cloud-starter-alibaba-sentinel - org.springframework.cloudspring-cloud-starter-openfeign + org.springframework.cloudspring-cloud-starter-openfeignio.github.openfeign.formfeign-form-spring org.springframework.cloudspring-cloud-starter-loadbalancer com.alibaba.cloudspring-cloud-starter-stream-rocketmq org.apache.poipoi-ooxml5.3.0 diff --git a/firstleg/src/main/java/com/baiyue/firstleg/client/LoadingDocumentFileClient.java b/firstleg/src/main/java/com/baiyue/firstleg/client/LoadingDocumentFileClient.java index 307360ec6..e025b7d3b 100644 --- a/firstleg/src/main/java/com/baiyue/firstleg/client/LoadingDocumentFileClient.java +++ b/firstleg/src/main/java/com/baiyue/firstleg/client/LoadingDocumentFileClient.java @@ -3,7 +3,6 @@ package com.baiyue.firstleg.client; import com.baiyue.common.core.api.ApiResponse; import org.springframework.cloud.openfeign.FeignClient; import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestHeader; @@ -30,12 +29,6 @@ public interface LoadingDocumentFileClient { ResponseEntity download(@RequestHeader("X-Internal-Service-Token") String token, @PathVariable long tenantId,@PathVariable String businessType,@PathVariable String businessId,@PathVariable long fileId); - @DeleteMapping("/{tenantId}/{businessType}/{businessId}/{fileId}") - ApiResponse delete(@RequestHeader("X-Internal-Service-Token") String token, - @RequestHeader("X-Operator-Id") long operatorId, - @PathVariable long tenantId, @PathVariable String businessType, - @PathVariable String businessId, @PathVariable long fileId); - record Metadata(Long fileId, Long tenantId, String businessType, String businessId, String fileName, String sha256, String contentType, Long sizeBytes) {} } diff --git a/firstleg/src/main/java/com/baiyue/firstleg/service/LoadingListService.java b/firstleg/src/main/java/com/baiyue/firstleg/service/LoadingListService.java index 4d6d63f49..c77be6c49 100644 --- a/firstleg/src/main/java/com/baiyue/firstleg/service/LoadingListService.java +++ b/firstleg/src/main/java/com/baiyue/firstleg/service/LoadingListService.java @@ -38,8 +38,6 @@ import org.springframework.transaction.annotation.Transactional; import org.springframework.util.StringUtils; import org.springframework.web.multipart.MultipartFile; import org.springframework.http.MediaType; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import java.nio.charset.StandardCharsets; import java.security.MessageDigest; @@ -52,7 +50,6 @@ import java.util.Set; @Service public class LoadingListService { - private static final Logger log = LoggerFactory.getLogger(LoadingListService.class); private static final Set PRELOAD_STATUSES = Set.of("EDITING", "LOADING"); public static final String DESTINATION_CLEARANCE = "DESTINATION_CLEARANCE"; public static final String CUSTOMS_DECLARATION = "CUSTOMS_DECLARATION"; @@ -321,16 +318,8 @@ public class LoadingListService { requireReplaceableDocument(current, expectedVersion); LoadingDocumentFileClient.Metadata metadata = uploadDocumentFile( businessType, businessId, operatorId, file, fileHash); - try { - return persistDocument(plan, documentType, current, metadata, operatorId, operatorName, - operationId, "UPLOAD_" + documentType, requestHash, operationReason); - } catch (RuntimeException exception) { - compensateUploadedFile(businessType, businessId, metadata.fileId(), operatorId); - if (exception instanceof DataIntegrityViolationException) { - throw conflict("资料状态或数据库约束已变化,请刷新后重试"); - } - throw exception; - } + return persistDocument(plan, documentType, current, metadata, operatorId, operatorName, + operationId, "UPLOAD_" + documentType, requestHash, operationReason); } private BusinessDocument persistDocument(AggregateRow plan, String documentType, @@ -432,19 +421,6 @@ public class LoadingListService { } } - private void compensateUploadedFile(String businessType, String businessId, long fileId, long operatorId) { - try { - var response = documentFiles.delete(internalToken, operatorId, 0, businessType, businessId, fileId); - if (response == null || !response.successful()) { - log.error("Business file compensation failed: type={}, businessId={}, fileId={}", - businessType, businessId, fileId); - } - } catch (RuntimeException exception) { - log.error("Business file compensation request failed: type={}, businessId={}, fileId={}", - businessType, businessId, fileId, exception); - } - } - private static void requireReplaceableDocument(ContainerDocumentEntity current, int expectedVersion) { if (current == null && expectedVersion != 0) throw conflict("资料版本已变化,请刷新后重试"); if (current != null && !java.util.Objects.equals(current.getVersion(), expectedVersion)) { diff --git a/firstleg/src/test/java/com/baiyue/firstleg/service/LoadingListServiceTest.java b/firstleg/src/test/java/com/baiyue/firstleg/service/LoadingListServiceTest.java index de1ef024c..4e59f2857 100644 --- a/firstleg/src/test/java/com/baiyue/firstleg/service/LoadingListServiceTest.java +++ b/firstleg/src/test/java/com/baiyue/firstleg/service/LoadingListServiceTest.java @@ -625,7 +625,6 @@ class LoadingListServiceTest { .containsExactly(LoadingListService.DELIVERY_ORDER, 101L, 0); verify(repository).appendHistory(eq(10L), anyString(), eq("UPLOAD_DELIVERY_ORDER"), eq("COMPLETED"), eq("COMPLETED"), anyString(), anyString(), eq(9L), eq("operator"), isNull()); - verify(documentFiles, never()).delete(anyString(), anyLong(), anyLong(), anyString(), anyString(), anyLong()); } @Test @@ -694,33 +693,6 @@ class LoadingListServiceTest { verify(documentFiles, never()).upload(anyString(), anyLong(), anyLong(), anyString(), anyString(), any()); } - @Test - void databaseFailureAfterFileUploadTriggersExactOwnershipCompensationAndBusinessConflict() { - AggregateRow plan = row("COMPLETED", 4); - byte[] content = "delivery-order".getBytes(java.nio.charset.StandardCharsets.UTF_8); - var file = new MockMultipartFile("file", "do.pdf", "application/pdf", content); - String contentHash = sha256Bytes(content); - var metadata = new LoadingDocumentFileClient.Metadata(103L, 0L, - "CONTAINER_DELIVERY_ORDER", "30", "do.pdf", contentHash, "application/pdf", (long) content.length); - when(repository.aggregateByBooking(30L)).thenReturn(plan); - when(repository.history(anyString())).thenReturn(null); - when(repository.document(30L, LoadingListService.DELIVERY_ORDER)).thenReturn(null); - when(documentFiles.upload(anyString(), eq(9L), eq(0L), eq("CONTAINER_DELIVERY_ORDER"), - eq("30"), eq(file))).thenReturn(ApiResponse.success(metadata)); - when(documentFiles.metadata(anyString(), eq(0L), eq("CONTAINER_DELIVERY_ORDER"), - eq("30"), eq(103L))).thenReturn(ApiResponse.success(metadata)); - when(repository.createDocument(any())).thenThrow( - new org.springframework.dao.DataIntegrityViolationException("trigger rejected")); - when(documentFiles.delete(anyString(), eq(9L), eq(0L), eq("CONTAINER_DELIVERY_ORDER"), - eq("30"), eq(103L))).thenReturn(ApiResponse.success(null)); - - assertThatThrownBy(() -> service.uploadContainerLogisticsDocument(30L, - LoadingListService.DELIVERY_ORDER, 9L, "operator", "do-db-fail", 0, null, file)) - .isInstanceOf(BizException.class) - .extracting(error -> ((BizException) error).getResultCode()).isEqualTo(ResultCode.CONFLICT); - verify(documentFiles).delete(anyString(), eq(9L), eq(0L), eq("CONTAINER_DELIVERY_ORDER"), - eq("30"), eq(103L)); - } @Test void confirmsEveryLinkedPreloadAndCreatesSinglePdaTaskAtomically() { diff --git a/scripts/agents-index-contract.ps1 b/scripts/agents-index-contract.ps1 index fe61a180d..b160d35e0 100644 --- a/scripts/agents-index-contract.ps1 +++ b/scripts/agents-index-contract.ps1 @@ -84,6 +84,13 @@ $clientAgents = Read-RequiredText $clientAgentsPath $pdaAgents = Read-RequiredText $pdaAgentsPath $overseasPdaAgents = Read-RequiredText $overseasPdaAgentsPath $backendAgents = Read-RequiredText $backendAgentsPath +$engineeringConstraints = Read-RequiredText (Join-Path $workspaceRoot 'docs/AI工程执行与协作约束.md') + +# 当前未上线阶段的无兼容原则必须覆盖全部实现与运行状态,不能再次缩窄为文件或数据库专项规则。 +Assert-Contains $rootAgents '源码结构、接口与 DTO、字段、状态机、权限、配置键、数据库结构与初始化 SQL、事件、路由、页面、Mock/Fixture、测试和脚本' '根无兼容收敛范围' +Assert-Contains $rootAgents '数据库记录、上传文件、缓存、会话、消息、搜索索引和其他运行状态均视为可丢弃' '根开发运行状态范围' +Assert-Contains $engineeringConstraints '禁止为旧代码、旧调用或开发环境存量保留旧接口/旧字段别名' '工程无兼容执行规则' +Assert-Contains $engineeringConstraints '不授权自动重置数据库、删除数据/文件、执行 SQL、部署或修改环境' '无兼容安全边界' # 部署单元必须能从根索引进入。 Get-ChildItem -LiteralPath (Join-Path $workspaceRoot 'apps') -Directory |