diff --git a/.idea/compiler.xml b/.idea/compiler.xml index d462caf..fc26b39 100644 --- a/.idea/compiler.xml +++ b/.idea/compiler.xml @@ -6,6 +6,7 @@ + @@ -19,6 +20,7 @@ + diff --git a/.idea/encodings.xml b/.idea/encodings.xml index ba0a23e..45c571a 100644 --- a/.idea/encodings.xml +++ b/.idea/encodings.xml @@ -7,6 +7,7 @@ + diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..9fbf358 --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/.scratch/handoff-2026-08-10.md b/.scratch/handoff-2026-08-10.md new file mode 100644 index 0000000..2f85e96 --- /dev/null +++ b/.scratch/handoff-2026-08-10.md @@ -0,0 +1,121 @@ +# Handoff: CRM Controller IO Conventions Implementation + +**Date:** 2026-08-10 +**Branch:** master +**Last commits:** `ae103a1` (ticket 01 main), `80bc063` (ticket 01 status update) + +--- + +## 1. What This Project Is + +CRM 后端(Java 17 + Spring Boot 3 + MyBatis-Plus,Maven 多模块单体)。正在统一 Controller 出入参约定,由 ADR-0017 驱动,推翻旧的三类方案(SaveParam/PageParam/DTO),改为两类(Param/DTO)。 + +**核心工件(勿重复,引用即可):** +- **ADR-0017:** `docs/adr/0017-controller-io-param-dto-conventions.md` +- **Spec:** `.scratch/controller-io-conventions/spec.md` +- **7 个 Ticket:** `.scratch/controller-io-conventions/issues/01-*.md` 至 `07-*.md` +- **研究文档:** `docs/research/2026-08-10-dto-vo-param-conventions.md` + +--- + +## 2. What's Done + +### Ticket 01 — BaseDTO 重构 + IBaseService 清理(resolved) + +Commit `ae103a1`,15 files, +360/-62。 + +| 文件 | 改动 | +|---|---| +| `crm-base/.../domain/dto/BaseDTO.java` | `abstract`,仅 `id`/`createTime`/`updateTime`(`@JsonInclude NON_NULL`),移除 `creatorId`/`updaterId`/`deleted`/`toEntity(Supplier)` | +| `crm-base/.../service/IBaseService.java` | 删除 `saveDTO`/`updateDTO` 死代码 | +| `crm-base/.../utils/BeanCopyUtils.java` | javadoc 移除 `BaseDTO.toEntity` 引用 | +| `README.md` | §3/§4/§10 同步更新 | +| `crm-auth/.../ResourceController.java` | 修复预存 UTF-8 BOM | +| `crm-auth/.../RoleController.java` | 修复预存 UTF-8 BOM | + +验证:`mvn clean compile` 全 6 模块 SUCCESS;`mvn test` 45 tests, 0 failures, 0 errors。 + +--- + +## 3. Key Technical Decisions(ADR-0017 摘要) + +1. **两类对象**:`XxxParam`(查询/分页入参,extends `BaseParam`)+ `XxxDTO`(写入参+出参双向,Route A) +2. **BaseDTO**:abstract,仅 `id`/`createTime`/`updateTime`,`@JsonInclude(NON_NULL)`;统计/聚合类不继承 +3. **Mass assignment 防御**:全局 `@ControllerAdvice` + `@InitBinder` + `setDisallowedFields`,strip 掉 `createTime`/`updateTime`/`creatorId`/`updaterId`/`deleted`/`builtin`(尚未实现 = ticket 02) +4. **硬约束**:Entity 不出现在 Controller 签名;`deleted`/`creatorId`/`updaterId` 不出现在 HTTP 响应 +5. **封装阈值**:≥3 参数封 `XxxDTO`,≤2 保持 `@RequestParam`(纯参数个数) +6. **转换**:`fromEntity()`/`toEntity()` 写在各 DTO 类上,Service 层调用 +7. **表单绑定**:隐式 `@ModelAttribute`(不加 `@RequestBody`),契合 `application/x-www-form-urlencoded` 契约 +8. **全量回填**:所有存量违规代码全改(非仅新代码) +9. **命名**:`dto/` 包内 HTTP 出入参类一律 `XxxDTO` 后缀;存量 VO 已重命名 + +--- + +## 4. Frontier — What's Next + +### Ticket 02 — 全局 @InitBinder Mass Assignment 防御(无阻塞,可立即开始) + +**目标**:创建全局 `@ControllerAdvice` + `@InitBinder`,用 `setDisallowedFields` strip 掉服务端裁定的字段。 + +**strip 字段列表**:`createTime`, `updateTime`, `creatorId`, `updaterId`, `deleted`, `builtin` + +**实现位置**:`crm-base/src/main/java/com/crm/base/advice/` 包下新建类(已有 `GlobalExceptionHandler` 在同包) + +**验收标准**(见 `02-init-binder-mass-assignment.md`): +- [ ] 创建 `@ControllerAdvice` 类,含 `@InitBinder` 方法 +- [ ] 调用 `binder.setDisallowedFields(...)` 覆盖 6 个字段 +- [ ] 不影响已有 `@ModelAttribute` 绑定的正常工作 +- [ ] 编译通过 + 测试通过 + +### Ticket 02 完成后解锁 + +Tickets 03/04/05/06 同时解锁: +- **03** DictGroup Controller IO(blocked by 02) +- **04** DictItem Controller IO(blocked by 02) +- **05** ResourceNode 重命名为 ResourceDTO(blocked by 02) +- **06** RoleController IO 改造(blocked by 02) + +Ticket 07(SystemController IO)blocked by 05。 + +### 依赖图 + +``` +01 (resolved) ──┐ + ├─► 02 (ready) ──┬─► 03 + │ ├─► 04 + │ ├─► 05 ──► 07 + │ └─► 06 + └─────────────────────────────────► (done) +``` + +--- + +## 5. Environment Notes + +- **Maven 路径**:`D:\apache-maven-3.9.9\bin\mvn.cmd`(不在系统 PATH 上,需用全路径调用) +- **PowerShell**:不支持 `&&`,用 `;` 分隔 +- **Java**:JDK 17 +- **编译命令**:`cd d:\code\crm-backend-matt; D:\apache-maven-3.9.9\bin\mvn.cmd clean compile` +- **测试命令**:`cd d:\code\crm-backend-matt; D:\apache-maven-3.9.9\bin\mvn.cmd test` + +--- + +## 6. Suggested Skills + +| Skill | When to use | +|---|---| +| `/implement` | 实现 ticket 02-07,每次一个 ticket | +| `/code-review` | 每个 ticket 实现后审查(Standards + Spec 双轴) | +| `/diagnosing-bugs` | 如果编译或测试出现意外错误 | +| `/tdd` | 如果想先写测试再实现(ticket 02 的 @InitBinder 适合 TDD) | + +--- + +## 7. Conventions to Follow + +- 每个 ticket 完成后:编译 + 测试 + 提交 + 更新 ticket 状态为 `resolved` +- 提交信息格式:`refactor: (ADR-0017 ticket 0N)` +- 代码风格:匹配周围代码的注释密度、命名、惯用法 +- 不要提交 `target/` 目录下的构建产物 +- 不要提交 `.idea/` 目录下的 IDE 配置 +- ADR + spec + tickets 已存在,勿重复创建,只更新 diff --git a/Dockerfile b/Dockerfile index 4c3cf61..a33d3ed 100644 --- a/Dockerfile +++ b/Dockerfile @@ -8,6 +8,7 @@ COPY pom.xml . COPY crm-base/pom.xml crm-base/ COPY crm-auth/pom.xml crm-auth/ COPY crm-file/pom.xml crm-file/ +COPY crm-dict/pom.xml crm-dict/ COPY crm-app/pom.xml crm-app/ # 先拉依赖(利用 Docker 层缓存)。不加 || true,拉失败直接报错,不掩盖问题 RUN mvn dependency:go-offline -B diff --git a/crm-app/target/crm-app-1.0.0-SNAPSHOT.jar b/crm-app/target/crm-app-1.0.0-SNAPSHOT.jar deleted file mode 100644 index 8f17bbd..0000000 Binary files a/crm-app/target/crm-app-1.0.0-SNAPSHOT.jar and /dev/null differ diff --git a/crm-app/target/crm-app-1.0.0-SNAPSHOT.jar.original b/crm-app/target/crm-app-1.0.0-SNAPSHOT.jar.original deleted file mode 100644 index a9a8a6e..0000000 Binary files a/crm-app/target/crm-app-1.0.0-SNAPSHOT.jar.original and /dev/null differ diff --git a/crm-app/target/maven-archiver/pom.properties b/crm-app/target/maven-archiver/pom.properties deleted file mode 100644 index cf9283b..0000000 --- a/crm-app/target/maven-archiver/pom.properties +++ /dev/null @@ -1,3 +0,0 @@ -artifactId=crm-app -groupId=com.crm -version=1.0.0-SNAPSHOT diff --git a/crm-auth/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst b/crm-auth/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst index 3d0eb81..12991ea 100644 --- a/crm-auth/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst +++ b/crm-auth/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst @@ -1,19 +1,79 @@ +com\crm\auth\security\AuthLoginUser.class +com\crm\auth\service\client\ThirdPartyUserInfo.class +com\crm\auth\controller\DebugTokenController.class +com\crm\auth\security\TokenService.class +com\crm\auth\service\impl\AuthUserServiceImpl.class com\crm\auth\service\impl\UserQueryServiceImpl.class +com\crm\auth\domain\entity\SysRoleMenu.class +com\crm\auth\domain\entity\AuthIdentity.class com\crm\auth\service\impl\ResourceServiceImpl$1.class +com\crm\auth\domain\dto\UserStatsVO.class +com\crm\auth\service\client\ThirdPartyAuthClient.class +com\crm\auth\domain\entity\SysUserRole.class +com\crm\auth\service\ISysMenuService.class com\crm\auth\service\impl\ResourceServiceImpl.class +com\crm\auth\service\client\ThirdPartyAuthClientFactory.class +com\crm\auth\mapper\SysRoleMenuMapper.class +com\crm\auth\domain\entity\SysMenu.class +com\crm\auth\service\DeptTreeCache.class +com\crm\auth\service\ISysRoleService.class com\crm\auth\domain\dto\ResourceNode.class +com\crm\auth\service\ISysDeptService.class com\crm\auth\security\ApiPermissionCache.class com\crm\auth\service\IResourceService.class com\crm\auth\security\ApiPermissionInterceptor.class com\crm\auth\security\PermissionResolver.class +com\crm\auth\domain\dto\UserInfoDTO.class com\crm\auth\constant\AuthConstants.class +com\crm\auth\service\impl\AuthIdentityServiceImpl.class com\crm\auth\config\WebMvcConfig.class +com\crm\auth\mapper\AuthIdentityMapper.class +com\crm\auth\domain\enums\IdentityTypeEnum.class +com\crm\auth\service\client\DingTalkAuthClient.class +com\crm\auth\service\impl\AuthServiceImpl.class +com\crm\auth\controller\AuthController.class +com\crm\auth\domain\param\UserPageParam.class +com\crm\auth\controller\SystemController.class com\crm\auth\controller\RoleController.class com\crm\auth\security\DataScopeInterceptor$1.class +com\crm\auth\config\DataInitializer.class +com\crm\auth\security\DataScopeTables.class com\crm\auth\domain\enums\MenuType.class +com\crm\auth\service\IAuthUserService.class com\crm\auth\security\PermissionGrant.class +com\crm\auth\config\AuthProperties$Jwt.class +com\crm\auth\service\impl\SysMenuServiceImpl.class +com\crm\auth\mapper\AuthUserMapper.class +com\crm\auth\config\AuthProperties$DingTalk.class +com\crm\auth\service\IAuthIdentityService.class +com\crm\auth\domain\dto\LoginResultDTO.class +com\crm\auth\domain\entity\SysUserDept.class +com\crm\auth\security\JwtAuthenticationFilter.class +com\crm\auth\service\IUserQueryService.class +com\crm\auth\domain\entity\SysDept.class +com\crm\auth\service\impl\SysDeptServiceImpl.class com\crm\auth\domain\dto\RoleDetailVO.class +com\crm\auth\service\IAuthService.class com\crm\auth\controller\ResourceController.class +com\crm\auth\domain\dto\UserListDTO$RoleRef.class +com\crm\auth\domain\enums\DataScopeEnum.class +com\crm\auth\mapper\SysRoleMapper.class +com\crm\auth\config\AuthProperties.class +com\crm\auth\service\impl\PermissionSeederImpl.class +com\crm\auth\security\DataScopeInterceptor.class +com\crm\auth\domain\entity\AuthUser.class com\crm\auth\security\ApiPermissionRule.class +com\crm\auth\config\SecurityConfig.class +com\crm\auth\mapper\SysMenuMapper.class com\crm\auth\security\PermissionResolverImpl.class +com\crm\auth\mapper\SysUserDeptMapper.class +com\crm\auth\security\SecurityExceptionHandlers.class +com\crm\auth\AuthApplication.class com\crm\auth\domain\enums\MenuType$1.class +com\crm\auth\mapper\SysUserRoleMapper.class +com\crm\auth\domain\entity\SysRole.class +com\crm\auth\mapper\SysDeptMapper.class +com\crm\auth\config\PermissionConfig.class +com\crm\auth\domain\dto\UserListDTO$DeptRef.class +com\crm\auth\domain\dto\UserListDTO.class +com\crm\auth\service\impl\SysRoleServiceImpl.class diff --git a/crm-auth/target/maven-status/maven-compiler-plugin/testCompile/default-testCompile/createdFiles.lst b/crm-auth/target/maven-status/maven-compiler-plugin/testCompile/default-testCompile/createdFiles.lst index 12c449d..cb4eb66 100644 --- a/crm-auth/target/maven-status/maven-compiler-plugin/testCompile/default-testCompile/createdFiles.lst +++ b/crm-auth/target/maven-status/maven-compiler-plugin/testCompile/default-testCompile/createdFiles.lst @@ -7,6 +7,7 @@ com\crm\auth\service\impl\UserListIntegrationTest.class com\crm\auth\service\client\DingTalkAuthClientTest$StubbedClient.class com\crm\auth\service\impl\SysMenuServiceImplTest.class com\crm\auth\security\ApiPermissionInterceptorTest.class +com\crm\auth\service\impl\PermissionSeederImplTest.class com\crm\auth\security\scope\DataScopeIntegrationTest.class com\crm\auth\service\impl\AuthServiceImplTest.class com\crm\auth\service\client\ThirdPartyAuthClientFactoryTest$StubClient.class diff --git a/crm-auth/target/surefire-reports/TEST-com.crm.auth.security.scope.DataScopeIntegrationTest.xml b/crm-auth/target/surefire-reports/TEST-com.crm.auth.security.scope.DataScopeIntegrationTest.xml index d5d7c73..00fc8fb 100644 --- a/crm-auth/target/surefire-reports/TEST-com.crm.auth.security.scope.DataScopeIntegrationTest.xml +++ b/crm-auth/target/surefire-reports/TEST-com.crm.auth.security.scope.DataScopeIntegrationTest.xml @@ -1,5 +1,5 @@ - + @@ -15,7 +15,7 @@ - + @@ -30,7 +30,7 @@ - + @@ -49,7 +49,7 @@ - + @@ -57,32 +57,32 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/crm-auth/target/surefire-reports/TEST-com.crm.auth.service.client.DingTalkAuthClientTest.xml b/crm-auth/target/surefire-reports/TEST-com.crm.auth.service.client.DingTalkAuthClientTest.xml index b304409..0dc21ab 100644 --- a/crm-auth/target/surefire-reports/TEST-com.crm.auth.service.client.DingTalkAuthClientTest.xml +++ b/crm-auth/target/surefire-reports/TEST-com.crm.auth.service.client.DingTalkAuthClientTest.xml @@ -1,5 +1,5 @@ - + @@ -15,7 +15,7 @@ - + @@ -30,7 +30,7 @@ - + @@ -49,7 +49,7 @@ - + @@ -57,47 +57,47 @@ - - - + + - - + - - + - - - + + - - + - - - + + - - + - - - + + - - - + + - + \ No newline at end of file diff --git a/crm-auth/target/surefire-reports/TEST-com.crm.auth.service.client.ThirdPartyAuthClientFactoryTest.xml b/crm-auth/target/surefire-reports/TEST-com.crm.auth.service.client.ThirdPartyAuthClientFactoryTest.xml index 3e71155..6a6515f 100644 --- a/crm-auth/target/surefire-reports/TEST-com.crm.auth.service.client.ThirdPartyAuthClientFactoryTest.xml +++ b/crm-auth/target/surefire-reports/TEST-com.crm.auth.service.client.ThirdPartyAuthClientFactoryTest.xml @@ -1,5 +1,5 @@ - + @@ -15,7 +15,7 @@ - + @@ -30,7 +30,7 @@ - + @@ -49,7 +49,7 @@ - + @@ -57,6 +57,6 @@ - + \ No newline at end of file diff --git a/crm-auth/target/surefire-reports/TEST-com.crm.auth.service.impl.AuthServiceImplTest.xml b/crm-auth/target/surefire-reports/TEST-com.crm.auth.service.impl.AuthServiceImplTest.xml index a204e17..edb82e0 100644 --- a/crm-auth/target/surefire-reports/TEST-com.crm.auth.service.impl.AuthServiceImplTest.xml +++ b/crm-auth/target/surefire-reports/TEST-com.crm.auth.service.impl.AuthServiceImplTest.xml @@ -1,5 +1,5 @@ - + @@ -15,7 +15,7 @@ - + @@ -30,7 +30,7 @@ - + @@ -49,7 +49,7 @@ - + @@ -57,28 +57,28 @@ - - - + + - - - - + + + - - - + + - - + - + \ No newline at end of file diff --git a/crm-auth/target/surefire-reports/TEST-com.crm.auth.service.impl.AuthUserServiceImplTest.xml b/crm-auth/target/surefire-reports/TEST-com.crm.auth.service.impl.AuthUserServiceImplTest.xml index 37add94..8be2092 100644 --- a/crm-auth/target/surefire-reports/TEST-com.crm.auth.service.impl.AuthUserServiceImplTest.xml +++ b/crm-auth/target/surefire-reports/TEST-com.crm.auth.service.impl.AuthUserServiceImplTest.xml @@ -1,5 +1,5 @@ - + @@ -15,9 +15,8 @@ - + - @@ -31,7 +30,7 @@ - + @@ -40,7 +39,6 @@ - @@ -59,14 +57,11 @@ - - - - - - - - - + + + + + + + \ No newline at end of file diff --git a/crm-auth/target/surefire-reports/TEST-com.crm.auth.service.impl.SysDeptServiceImplTest.xml b/crm-auth/target/surefire-reports/TEST-com.crm.auth.service.impl.SysDeptServiceImplTest.xml index f316540..f300687 100644 --- a/crm-auth/target/surefire-reports/TEST-com.crm.auth.service.impl.SysDeptServiceImplTest.xml +++ b/crm-auth/target/surefire-reports/TEST-com.crm.auth.service.impl.SysDeptServiceImplTest.xml @@ -1,5 +1,5 @@ - + @@ -15,7 +15,7 @@ - + @@ -30,7 +30,7 @@ - + @@ -49,7 +49,7 @@ - + @@ -57,13 +57,13 @@ - - - - + + + + - - - + + + \ No newline at end of file diff --git a/crm-auth/target/surefire-reports/TEST-com.crm.auth.service.impl.UserListIntegrationTest.xml b/crm-auth/target/surefire-reports/TEST-com.crm.auth.service.impl.UserListIntegrationTest.xml index 992546e..ebd1666 100644 --- a/crm-auth/target/surefire-reports/TEST-com.crm.auth.service.impl.UserListIntegrationTest.xml +++ b/crm-auth/target/surefire-reports/TEST-com.crm.auth.service.impl.UserListIntegrationTest.xml @@ -1,5 +1,5 @@ - + @@ -15,9 +15,8 @@ - + - @@ -31,7 +30,7 @@ - + @@ -40,7 +39,6 @@ - @@ -59,18 +57,18 @@ - - - - - - - - - - - - - - + + + + + + + + + + + + + + \ No newline at end of file diff --git a/crm-auth/target/surefire-reports/com.crm.auth.security.scope.DataScopeIntegrationTest.txt b/crm-auth/target/surefire-reports/com.crm.auth.security.scope.DataScopeIntegrationTest.txt index 67dfb4b..cac32d9 100644 --- a/crm-auth/target/surefire-reports/com.crm.auth.security.scope.DataScopeIntegrationTest.txt +++ b/crm-auth/target/surefire-reports/com.crm.auth.security.scope.DataScopeIntegrationTest.txt @@ -1,4 +1,4 @@ ------------------------------------------------------------------------------- Test set: com.crm.auth.security.scope.DataScopeIntegrationTest ------------------------------------------------------------------------------- -Tests run: 28, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 1.527 s -- in com.crm.auth.security.scope.DataScopeIntegrationTest +Tests run: 28, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 3.906 s -- in com.crm.auth.security.scope.DataScopeIntegrationTest diff --git a/crm-auth/target/surefire-reports/com.crm.auth.service.client.DingTalkAuthClientTest.txt b/crm-auth/target/surefire-reports/com.crm.auth.service.client.DingTalkAuthClientTest.txt index 3155df7..704132c 100644 --- a/crm-auth/target/surefire-reports/com.crm.auth.service.client.DingTalkAuthClientTest.txt +++ b/crm-auth/target/surefire-reports/com.crm.auth.service.client.DingTalkAuthClientTest.txt @@ -1,4 +1,4 @@ ------------------------------------------------------------------------------- Test set: com.crm.auth.service.client.DingTalkAuthClientTest ------------------------------------------------------------------------------- -Tests run: 15, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.456 s -- in com.crm.auth.service.client.DingTalkAuthClientTest +Tests run: 15, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 1.749 s -- in com.crm.auth.service.client.DingTalkAuthClientTest diff --git a/crm-auth/target/surefire-reports/com.crm.auth.service.client.ThirdPartyAuthClientFactoryTest.txt b/crm-auth/target/surefire-reports/com.crm.auth.service.client.ThirdPartyAuthClientFactoryTest.txt index 877347a..e7e5ac1 100644 --- a/crm-auth/target/surefire-reports/com.crm.auth.service.client.ThirdPartyAuthClientFactoryTest.txt +++ b/crm-auth/target/surefire-reports/com.crm.auth.service.client.ThirdPartyAuthClientFactoryTest.txt @@ -1,4 +1,4 @@ ------------------------------------------------------------------------------- Test set: com.crm.auth.service.client.ThirdPartyAuthClientFactoryTest ------------------------------------------------------------------------------- -Tests run: 2, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.003 s -- in com.crm.auth.service.client.ThirdPartyAuthClientFactoryTest +Tests run: 2, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.029 s -- in com.crm.auth.service.client.ThirdPartyAuthClientFactoryTest diff --git a/crm-auth/target/surefire-reports/com.crm.auth.service.impl.AuthServiceImplTest.txt b/crm-auth/target/surefire-reports/com.crm.auth.service.impl.AuthServiceImplTest.txt index 1e82fda..f56ace5 100644 --- a/crm-auth/target/surefire-reports/com.crm.auth.service.impl.AuthServiceImplTest.txt +++ b/crm-auth/target/surefire-reports/com.crm.auth.service.impl.AuthServiceImplTest.txt @@ -1,4 +1,4 @@ ------------------------------------------------------------------------------- Test set: com.crm.auth.service.impl.AuthServiceImplTest ------------------------------------------------------------------------------- -Tests run: 9, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.480 s -- in com.crm.auth.service.impl.AuthServiceImplTest +Tests run: 9, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 1.025 s -- in com.crm.auth.service.impl.AuthServiceImplTest diff --git a/crm-auth/target/surefire-reports/com.crm.auth.service.impl.AuthUserServiceImplTest.txt b/crm-auth/target/surefire-reports/com.crm.auth.service.impl.AuthUserServiceImplTest.txt index 607e1b0..bf7b302 100644 --- a/crm-auth/target/surefire-reports/com.crm.auth.service.impl.AuthUserServiceImplTest.txt +++ b/crm-auth/target/surefire-reports/com.crm.auth.service.impl.AuthUserServiceImplTest.txt @@ -1,4 +1,4 @@ ------------------------------------------------------------------------------- Test set: com.crm.auth.service.impl.AuthUserServiceImplTest ------------------------------------------------------------------------------- -Tests run: 7, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 2.181 s -- in com.crm.auth.service.impl.AuthUserServiceImplTest +Tests run: 7, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.336 s -- in com.crm.auth.service.impl.AuthUserServiceImplTest diff --git a/crm-auth/target/surefire-reports/com.crm.auth.service.impl.SysDeptServiceImplTest.txt b/crm-auth/target/surefire-reports/com.crm.auth.service.impl.SysDeptServiceImplTest.txt index be44742..85d71f9 100644 --- a/crm-auth/target/surefire-reports/com.crm.auth.service.impl.SysDeptServiceImplTest.txt +++ b/crm-auth/target/surefire-reports/com.crm.auth.service.impl.SysDeptServiceImplTest.txt @@ -1,4 +1,4 @@ ------------------------------------------------------------------------------- Test set: com.crm.auth.service.impl.SysDeptServiceImplTest ------------------------------------------------------------------------------- -Tests run: 9, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.053 s -- in com.crm.auth.service.impl.SysDeptServiceImplTest +Tests run: 9, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.119 s -- in com.crm.auth.service.impl.SysDeptServiceImplTest diff --git a/crm-auth/target/surefire-reports/com.crm.auth.service.impl.UserListIntegrationTest.txt b/crm-auth/target/surefire-reports/com.crm.auth.service.impl.UserListIntegrationTest.txt index a7dfb05..b49d493 100644 --- a/crm-auth/target/surefire-reports/com.crm.auth.service.impl.UserListIntegrationTest.txt +++ b/crm-auth/target/surefire-reports/com.crm.auth.service.impl.UserListIntegrationTest.txt @@ -1,4 +1,4 @@ ------------------------------------------------------------------------------- Test set: com.crm.auth.service.impl.UserListIntegrationTest ------------------------------------------------------------------------------- -Tests run: 14, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 1.132 s -- in com.crm.auth.service.impl.UserListIntegrationTest +Tests run: 14, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.532 s -- in com.crm.auth.service.impl.UserListIntegrationTest diff --git a/crm-base/target/classes/com/crm/base/domain/dto/BaseDTO.class b/crm-base/target/classes/com/crm/base/domain/dto/BaseDTO.class index e23f88f..982bdcf 100644 Binary files a/crm-base/target/classes/com/crm/base/domain/dto/BaseDTO.class and b/crm-base/target/classes/com/crm/base/domain/dto/BaseDTO.class differ diff --git a/crm-base/target/classes/com/crm/base/service/IBaseService.class b/crm-base/target/classes/com/crm/base/service/IBaseService.class index 5db36ef..bd958b8 100644 Binary files a/crm-base/target/classes/com/crm/base/service/IBaseService.class and b/crm-base/target/classes/com/crm/base/service/IBaseService.class differ diff --git a/crm-base/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst b/crm-base/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst index 2980f3e..98ffc48 100644 --- a/crm-base/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst +++ b/crm-base/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst @@ -29,6 +29,7 @@ com\crm\base\domain\exception\ResourceNotExistException.class com\crm\base\utils\ServletUtils.class com\crm\base\config\RedisConfig.class com\crm\base\security\DataVisibility$1.class +com\crm\base\service\PermissionSeeder.class com\crm\base\domain\exception\PermissionErrorException.class com\crm\base\config\CustomIdGenerator.class com\crm\base\domain\result\Result.class @@ -37,6 +38,7 @@ com\crm\base\domain\entity\OwnedEntity.class com\crm\base\security\LoginUser.class com\crm\base\domain\exception\MissingParameterException.class com\crm\base\domain\result\PageResult.class +com\crm\base\domain\dto\ButtonSeed.class com\crm\base\security\DataVisibility.class com\crm\base\config\MetaObjectFillHandler.class com\crm\base\security\DataVisibilityContext.class diff --git a/crm-base/target/surefire-reports/TEST-com.crm.base.security.DataVisibilityTest.xml b/crm-base/target/surefire-reports/TEST-com.crm.base.security.DataVisibilityTest.xml index 70a1357..429f1b2 100644 --- a/crm-base/target/surefire-reports/TEST-com.crm.base.security.DataVisibilityTest.xml +++ b/crm-base/target/surefire-reports/TEST-com.crm.base.security.DataVisibilityTest.xml @@ -1,5 +1,5 @@ - + @@ -15,7 +15,7 @@ - + @@ -30,7 +30,7 @@ - + @@ -49,7 +49,7 @@ - + @@ -57,21 +57,21 @@ - - - + + + - - - - - - - - + + + + + + + + - - - - + + + + \ No newline at end of file diff --git a/crm-base/target/surefire-reports/com.crm.base.security.DataVisibilityTest.txt b/crm-base/target/surefire-reports/com.crm.base.security.DataVisibilityTest.txt index f65e757..00789a0 100644 --- a/crm-base/target/surefire-reports/com.crm.base.security.DataVisibilityTest.txt +++ b/crm-base/target/surefire-reports/com.crm.base.security.DataVisibilityTest.txt @@ -1,4 +1,4 @@ ------------------------------------------------------------------------------- Test set: com.crm.base.security.DataVisibilityTest ------------------------------------------------------------------------------- -Tests run: 17, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.099 s -- in com.crm.base.security.DataVisibilityTest +Tests run: 17, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.198 s -- in com.crm.base.security.DataVisibilityTest diff --git a/crm-base/target/test-classes/com/crm/base/security/DataVisibilityTest.class b/crm-base/target/test-classes/com/crm/base/security/DataVisibilityTest.class index 2423c3c..f11a7a8 100644 Binary files a/crm-base/target/test-classes/com/crm/base/security/DataVisibilityTest.class and b/crm-base/target/test-classes/com/crm/base/security/DataVisibilityTest.class differ diff --git a/crm-file/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst b/crm-file/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst index 9bfb42d..9822ae0 100644 --- a/crm-file/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst +++ b/crm-file/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst @@ -1,14 +1,22 @@ +com\crm\file\domain\dto\ThumbnailDTO.class com\crm\file\config\FileProperties.class com\crm\file\service\KkFileViewClient.class +com\crm\file\service\ThumbnailPlaceholderService.class com\crm\file\service\impl\FileInfoServiceImpl.class +com\crm\file\domain\dto\ThumbnailDTO$ThumbnailDTOBuilder.class com\crm\file\mapper\FileInfoMapper.class +com\crm\file\task\ThumbnailGenerationTask.class com\crm\file\domain\dto\FileInfoDTO.class com\crm\file\task\OrphanChunkCleanupTask.class com\crm\file\service\impl\FileApiImpl.class com\crm\file\config\FileProperties$Preview.class com\crm\file\domain\dto\FileDownloadDTO$FileDownloadDTOBuilder.class +com\crm\file\config\FileProperties$Thumbnail.class +com\crm\file\service\impl\ThumbnailImageUtils.class com\crm\file\config\SchedulingConfig.class com\crm\file\config\MinioConfig.class +com\crm\file\service\impl\PdfThumbnailRenderer.class +com\crm\file\util\FileNames.class com\crm\file\domain\dto\UploadSession.class com\crm\file\domain\dto\FileDownloadDTO.class com\crm\file\domain\dto\MultipartInitDTO.class @@ -16,5 +24,9 @@ com\crm\file\service\IFileInfoService.class com\crm\file\config\FileProperties$Minio.class com\crm\file\controller\FileController.class com\crm\file\constant\FileConstants.class +com\crm\file\service\impl\ImageThumbnailRenderer.class +com\crm\file\service\impl\OfficeThumbnailRenderer.class +com\crm\file\service\ThumbnailRenderer.class +com\crm\file\task\ThumbnailRetryTask.class com\crm\file\api\FileApi.class com\crm\file\domain\entity\FileInfo.class diff --git a/crm-file/target/maven-status/maven-compiler-plugin/testCompile/default-testCompile/createdFiles.lst b/crm-file/target/maven-status/maven-compiler-plugin/testCompile/default-testCompile/createdFiles.lst index 15dc6cb..9a3146d 100644 --- a/crm-file/target/maven-status/maven-compiler-plugin/testCompile/default-testCompile/createdFiles.lst +++ b/crm-file/target/maven-status/maven-compiler-plugin/testCompile/default-testCompile/createdFiles.lst @@ -1 +1,8 @@ +com\crm\file\service\impl\ImageThumbnailRendererTest.class +com\crm\file\service\impl\OfficeThumbnailRendererTest.class +com\crm\file\task\OrphanChunkCleanupTaskTest.class +com\crm\file\task\ThumbnailGenerationTaskTest.class +com\crm\file\task\ThumbnailRetryTaskTest.class com\crm\file\service\KkFileViewClientTest.class +com\crm\file\service\impl\FileApiImplTest.class +com\crm\file\service\impl\PdfThumbnailRendererTest.class diff --git a/crm-file/target/surefire-reports/TEST-com.crm.file.service.KkFileViewClientTest.xml b/crm-file/target/surefire-reports/TEST-com.crm.file.service.KkFileViewClientTest.xml index d221e64..fb53ff4 100644 --- a/crm-file/target/surefire-reports/TEST-com.crm.file.service.KkFileViewClientTest.xml +++ b/crm-file/target/surefire-reports/TEST-com.crm.file.service.KkFileViewClientTest.xml @@ -1,5 +1,5 @@ - + @@ -15,7 +15,7 @@ - + @@ -30,7 +30,7 @@ - + @@ -49,7 +49,7 @@ - + @@ -57,7 +57,7 @@ - + \ No newline at end of file diff --git a/crm-file/target/surefire-reports/TEST-com.crm.file.service.impl.FileApiImplTest.xml b/crm-file/target/surefire-reports/TEST-com.crm.file.service.impl.FileApiImplTest.xml index c571b81..d791488 100644 --- a/crm-file/target/surefire-reports/TEST-com.crm.file.service.impl.FileApiImplTest.xml +++ b/crm-file/target/surefire-reports/TEST-com.crm.file.service.impl.FileApiImplTest.xml @@ -1,5 +1,5 @@ - + @@ -15,7 +15,7 @@ - + @@ -30,7 +30,7 @@ - + @@ -49,7 +49,7 @@ - + @@ -57,55 +57,55 @@ - - - - - - - - - - - - - - + + + + + + + + + + + + + - - - + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/crm-file/target/surefire-reports/TEST-com.crm.file.task.OrphanChunkCleanupTaskTest.xml b/crm-file/target/surefire-reports/TEST-com.crm.file.task.OrphanChunkCleanupTaskTest.xml index 3459a39..dbecdb1 100644 --- a/crm-file/target/surefire-reports/TEST-com.crm.file.task.OrphanChunkCleanupTaskTest.xml +++ b/crm-file/target/surefire-reports/TEST-com.crm.file.task.OrphanChunkCleanupTaskTest.xml @@ -1,5 +1,5 @@ - + @@ -15,7 +15,7 @@ - + @@ -30,7 +30,7 @@ - + @@ -49,7 +49,7 @@ - + @@ -57,8 +57,8 @@ - - - - + + + + \ No newline at end of file diff --git a/crm-file/target/surefire-reports/com.crm.file.service.KkFileViewClientTest.txt b/crm-file/target/surefire-reports/com.crm.file.service.KkFileViewClientTest.txt index dedb62b..4bfdb5f 100644 --- a/crm-file/target/surefire-reports/com.crm.file.service.KkFileViewClientTest.txt +++ b/crm-file/target/surefire-reports/com.crm.file.service.KkFileViewClientTest.txt @@ -1,4 +1,4 @@ ------------------------------------------------------------------------------- Test set: com.crm.file.service.KkFileViewClientTest ------------------------------------------------------------------------------- -Tests run: 3, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.023 s -- in com.crm.file.service.KkFileViewClientTest +Tests run: 3, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.015 s -- in com.crm.file.service.KkFileViewClientTest diff --git a/crm-file/target/surefire-reports/com.crm.file.service.impl.FileApiImplTest.txt b/crm-file/target/surefire-reports/com.crm.file.service.impl.FileApiImplTest.txt index 9209a58..ba84278 100644 --- a/crm-file/target/surefire-reports/com.crm.file.service.impl.FileApiImplTest.txt +++ b/crm-file/target/surefire-reports/com.crm.file.service.impl.FileApiImplTest.txt @@ -1,4 +1,4 @@ ------------------------------------------------------------------------------- Test set: com.crm.file.service.impl.FileApiImplTest ------------------------------------------------------------------------------- -Tests run: 48, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 3.841 s -- in com.crm.file.service.impl.FileApiImplTest +Tests run: 48, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 6.533 s -- in com.crm.file.service.impl.FileApiImplTest diff --git a/crm-file/target/surefire-reports/com.crm.file.task.OrphanChunkCleanupTaskTest.txt b/crm-file/target/surefire-reports/com.crm.file.task.OrphanChunkCleanupTaskTest.txt index 7fe6c5d..2f595c5 100644 --- a/crm-file/target/surefire-reports/com.crm.file.task.OrphanChunkCleanupTaskTest.txt +++ b/crm-file/target/surefire-reports/com.crm.file.task.OrphanChunkCleanupTaskTest.txt @@ -1,4 +1,4 @@ ------------------------------------------------------------------------------- Test set: com.crm.file.task.OrphanChunkCleanupTaskTest ------------------------------------------------------------------------------- -Tests run: 4, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.022 s -- in com.crm.file.task.OrphanChunkCleanupTaskTest +Tests run: 4, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.053 s -- in com.crm.file.task.OrphanChunkCleanupTaskTest diff --git a/crm-file/target/test-classes/com/crm/file/service/KkFileViewClientTest.class b/crm-file/target/test-classes/com/crm/file/service/KkFileViewClientTest.class index 29f38f9..fa0f867 100644 Binary files a/crm-file/target/test-classes/com/crm/file/service/KkFileViewClientTest.class and b/crm-file/target/test-classes/com/crm/file/service/KkFileViewClientTest.class differ diff --git a/crm-file/target/test-classes/com/crm/file/service/impl/FileApiImplTest.class b/crm-file/target/test-classes/com/crm/file/service/impl/FileApiImplTest.class index 7114203..473cecf 100644 Binary files a/crm-file/target/test-classes/com/crm/file/service/impl/FileApiImplTest.class and b/crm-file/target/test-classes/com/crm/file/service/impl/FileApiImplTest.class differ diff --git a/crm-file/target/test-classes/com/crm/file/task/OrphanChunkCleanupTaskTest.class b/crm-file/target/test-classes/com/crm/file/task/OrphanChunkCleanupTaskTest.class index 61c3a23..69f0872 100644 Binary files a/crm-file/target/test-classes/com/crm/file/task/OrphanChunkCleanupTaskTest.class and b/crm-file/target/test-classes/com/crm/file/task/OrphanChunkCleanupTaskTest.class differ diff --git a/docker-compose.yml b/docker-compose.yml index 62315ca..5a77060 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,10 +1,9 @@ # ============================================================ -# 缩略图功能验证专用 · 全套自包含部署(172.16.204.61) -# 严禁用于生产!crm-app 以 verify profile 启动(含调试 token 后门接口)。 -# 用法见 docs/deploy-verify.md +# 测试环境自包含部署(复用 verify compose + nginx 转发) +# crm-app 不直接暴露端口,全部经 nginx 80 转发访问。 # -# docker compose up -d --build # 首次:构建 crm-app 镜像 + 拉起四容器 -# docker compose down -v # 验证完毕:清理容器 + 数据卷(不留痕) +# docker compose -f docker-compose-test.yml up -d --build +# docker compose -f docker-compose-test.yml down -v # ============================================================ services: mysql: @@ -67,7 +66,7 @@ services: crm-app: build: context: . - dockerfile: Dockerfile # 服务器上直接 mvn package + 装 LibreOffice + dockerfile: Dockerfile depends_on: mysql: condition: service_healthy @@ -77,7 +76,6 @@ services: condition: service_healthy environment: SPRING_PROFILES_ACTIVE: verify - # 中间件指向 compose 服务名(覆盖 application.yml 的默认远程地址) CRM_DB_URL: "jdbc:mysql://mysql:3306/crm?serverTimezone=GMT%2B8&useUnicode=true&characterEncoding=utf-8&allowMultiQueries=true&rewriteBatchedStatements=true" CRM_DB_USERNAME: root CRM_DB_PASSWORD: verify123 @@ -88,10 +86,20 @@ services: CRM_MINIO_ENDPOINT: http://minio:9000 CRM_MINIO_AK: minioadmin CRM_MINIO_SK: minioadmin - # kkFileView 完整预览本次验证用不到,留空(缩略图管线不依赖它) CRM_KKFILEVIEW_URL: "" + # 不直接暴露 8080,由 nginx 转发 + expose: + - "8080" + + nginx: + image: nginx:stable-alpine + depends_on: + - crm-app ports: - - "8080:8080" + - "80:80" # 对外只开 nginx 80,crm-app 8080 不可达 + volumes: + - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro + - ./nginx/docs:/usr/share/nginx/docs:ro # API 文档静态页 volumes: mysql-data: diff --git a/docs/research/2026-08-10-dto-vo-param-conventions.md b/docs/research/2026-08-10-dto-vo-param-conventions.md new file mode 100644 index 0000000..f312a7f --- /dev/null +++ b/docs/research/2026-08-10-dto-vo-param-conventions.md @@ -0,0 +1,315 @@ +# Controller 出入参对象规范调研(DTO / VO / Param / Entity) + +> 调研日期:2026-08-10 +> 缘起:`RoleController.saveOrUpdate` 用 6 个 `@RequestParam` 平铺接参,希望封装成对象;进而需要为全项目确立 DTO / VO / Param 的统一职责与命名。 +> 存放位置说明:仓库此前没有调研笔记的约定(`docs/` 下是 ADR、前端对接指南、部署文档)。本文新建 `docs/research/` 目录存放此类「先查证、再定规范」的过程性文档,与结论性的 `docs/adr/` 区分开。 + +--- + +## 一、一手结论 + +本节每条都追到拥有该说法的源头,不引用二手转述。中文技术社区关于 DO/DTO/BO/VO 的文章绝大多数是对阿里手册的转述,本文一律回到手册本身。 + +### 1.1 DTO 的原始定义(Fowler, PoEAA) + +> "An object that carries data between processes in order to reduce the number of method calls." +> "Usually an assembler is used on the server side to transfer data between the DTO and any domain objects." + +—— [Martin Fowler, *Data Transfer Object*, PoEAA catalog](https://martinfowler.com/eaaCatalog/dataTransferObject.html) + +两点对本项目有直接约束力: + +1. DTO 的原始动机是**跨进程**减少调用次数,不是「Controller 接参专用类」。用 DTO 命名 HTTP 入参属于语义扩张,不是错,但要知道它不是原意。 +2. **转换由 assembler 在服务端完成**,即 DTO 与领域对象之间的映射有明确归属方,不散落在调用点。这与本项目「转换放 Service 层」的决定同源。 + +同页 2013 年补注还澄清了一个长期混淆: + +> "At the time I wrote this book the Sun/Java community used the term 'value object' for data transfer objects. This caused considerable confusion since others used that term to mean a Value Object. Fortunately after a few years the Sun/Java community switched to using 'Transfer Object' as the name for this pattern." + +**所以「VO = View Object(视图对象)」不是 Fowler 的定义**,Fowler 语境里 Value Object 是「按值相等的不可变对象」。VO 作为「展示层对象」是中文 Java 生态(阿里手册)的约定。项目内用 VO 表示出参没问题,但不要引用 Fowler 为其背书。 + +### 1.2 领域模型分层与命名(阿里巴巴 Java 开发手册) + +引自第一方英文版(`alibaba.github.io`,非社区转述): + +> **Layers of Domain Model** [For Reference] +> - DO (Data Object): Corresponding to the database table structure, the data source object is transferred upward through DAO Layer. +> - DTO (Data Transfer Object): Objects which are transferred upward by Service Layer and Manager Layer. +> - BO (Business Object): Objects that encapsulate business logic, which can be outputted by Service Layer. +> - **Query: Data query objects that carry query request from upper layers. Note: Prohibit the use of Map if there are more than 2 query conditions.** +> - VO (View Object): Objects that are used in Display Layer, which is normally transferred from Web Layer. + +> **Naming conventions for Domain models** [Mandatory] +> 1) Data Object: *DO, where * is the table name. +> 2) Data Transfer Object: *DTO, where * is a domain-related name. +> 3) Value Object: *VO, where * is a website name in most cases. +> 4) POJO generally point to DO/DTO/BO/VO but cannot be used in naming as *POJO. + +—— [Alibaba Java Coding Guidelines(第一方英文版)](https://alibaba.github.io/Alibaba-Java-Coding-Guidelines/) + +对本项目的三点含义: + +1. **手册里查询对象叫 `Query`,不叫 `Param`。** 本项目用 `XxxParam` 是自有约定,与手册不一致但内部自洽,无需改(改名成本大于收益)。此处记录清楚,避免以后有人拿手册来「纠正」。 +2. **手册的 DTO 指「Service/Manager 层向上传输的对象」**,即跨层传输,本身并不指 HTTP 入参。这一条对「写入参该叫 SaveDTO 还是 SaveParam」的取舍有实质影响,见 §四。 +3. 注意手册对 VO 的命名解释("* is a website name")是早期电商语境的遗留措辞,与其分层表里的「Display Layer 对象」并不严格一致。**手册自身在此处措辞不严谨**,不必逐字遵从,取「展示层出参」这一层意思即可。 + +另有一条与本项目现有代码直接冲突的强制条款: + +> [Mandatory] While defining POJO classes like DO, DTO, VO, etc., do not assign any default values to the members. + +本项目 `BaseParam` 赋了默认值(`current = 1`、`size = 10`、`asc = Boolean.FALSE`),`BaseDTO` 赋了 `deleted = StatusEnum.NORMAL.getValue()`,`BaseEntity` 同样赋了 `deleted`。这是**有意偏离**:分页参数的默认值让前端可省略传参,收益明确。本文记录为「已知偏离」,不作为待修问题。 + +### 1.3 Spring 官方对「拿什么对象接 HTTP 参数」的安全建议 + +Spring Framework 参考文档在 `@ModelAttribute` 一节明确写道: + +> "By default, both constructor and property data binding are applied. However, model object design requires careful consideration, and **for security reasons it is recommended either to use an object tailored specifically for web binding, or to apply constructor binding only.** If property binding must still be used, then `allowedFields` patterns should be set to limit which properties can be set." + +—— [Spring Framework Reference, Web MVC → `@ModelAttribute`](https://docs.spring.io/spring-framework/reference/web/webmvc/mvc-controller/ann-methods/modelattrib-method-args.html) + +这是「**禁止用 Entity 接参**」最权威的依据:框架自己说要用「专门为 web 绑定裁剪过的对象」。本项目现有的 `saveOrUpdate(DictGroup group)` 正是文档劝阻的写法。 + +同页还解释了本项目表单绑定为何「不写注解也能生效」: + +> "Using `@ModelAttribute` is optional. By default, any parameter that is not a simple value type as determined by `BeanUtils#isSimpleProperty` AND that is not resolved by any other argument resolver is treated as an implicit `@ModelAttribute`." + +即 `page(GroupPageParam param)` 这种签名走的是**隐式 `@ModelAttribute`**,从 `application/x-www-form-urlencoded` 表单字段与 query 参数绑定。这与本项目「POST 用表单、不用 JSON body」的 API 契约天然契合——**对象封装不需要改成 `@RequestBody`**。 + +文档另有一条提示(本项目未使用 GraalVM native image,故仅备录): + +> "When compiling to a native image with GraalVM, the implicit `@ModelAttribute` support described above does not allow proper ahead-of-time inference of related data binding reflection hints. As a consequence, it is recommended to explicitly annotate method parameters with `@ModelAttribute`." + +### 1.4 Entity 直接接参是一个有名字的漏洞(OWASP) + +> "Software frameworks sometimes allow developers to automatically bind HTTP request parameters into program code variables or objects... Attackers can sometimes use this methodology to create new parameters that the developer never intended. **This is called a Mass Assignment vulnerability.**" +> Alternative Names: "**Autobinding: Spring MVC, ASP NET MVC.**" + +OWASP 给出的示例与本项目的情形高度同构——表单只有三个字段,绑定的对象却多一个 `isAdmin`: + +> ``` +> POST /addUser +> userid=bobbytables&password=hashedpass&email=...&isAdmin=true +> ``` + +其推荐的架构级解法: + +> "An architectural approach is to create Data Transfer Objects and **avoid binding input directly to domain objects. Only the fields that are meant to be editable by the user are included in the DTO.**" +> ```java +> public class UserRegistrationFormDTO { +> private String userid; +> private String password; +> private String email; +> //NOTE: isAdmin field is not present +> } +> ``` + +Spring MVC 的兜底手段(非首选): + +> ```java +> @InitBinder +> public void initBinder(WebDataBinder binder, WebRequest request) { +> binder.setAllowedFields(["userid","password","email"]); +> } +> ``` + +—— [OWASP Cheat Sheet Series, *Mass Assignment*](https://cheatsheetseries.owasp.org/cheatsheets/Mass_Assignment_Cheat_Sheet.html) + +**本项目 `SysRole.builtin`、`DictGroup.builtin`、`BaseEntity.deleted` 就是 `isAdmin` 的等价物。** 这不是理论风险,见 §二。 + +--- + +## 二、本项目现状(事实) + +以下事实分两类标注:**[已核实]** 为本次直接读源码确认;**[盘查]** 来自子代理全量扫描、未逐条复核。子代理报告有两处错误已修正(它把 `service/impl/` 写成 `service/implement/`,并在摘要里误称 `RoleController` 有 Entity 直收——其明细部分自身是正确的)。 + +### 2.1 已经为「Entity 直收」打过的补丁 + +**[已核实]** `crm-dict/src/main/java/com/crm/dict/service/impl/DictGroupServiceImpl.java` L72-77: + +```java +// 审计字段由框架 strictInsertFill/strictUpdateFill 自动填充(仅 null 时填充), +// 客户端传入的审计值不可信且会被保留,统一清空防止伪造 +group.setCreateTime(null); +group.setUpdateTime(null); +group.setCreatorId(null); +group.setUpdaterId(null); +``` + +同文件 L99: + +```java +group.setBuiltin(false); // 用户不能创建内置分组 +``` + +`DictItemServiceImpl.saveItem` 有同构逻辑 **[盘查]**。 + +这段代码就是 §1.4 那个漏洞的**手工补丁**:因为 `DictGroupController.saveOrUpdate(DictGroup group)` 把实体交给了 Spring 绑定,客户端可以传 `createTime`、`creatorId`、`builtin=true`,而 MyBatis-Plus 的填充策略是「仅 null 时填充」,伪造值会被原样保留。补丁有效,但它是**每个新接口都要重写一遍的纪律要求**,而不是结构性防御——一旦漏写就是漏洞。这正是 OWASP 与 Spring 文档都建议改用专用绑定对象的原因。 + +**[盘查]** 缺同类防护的位置:`ResourceController.saveOrUpdate`(`ResourceNode.toEntity()` 未处理审计字段)。`RoleController.saveOrUpdate` 与 `SystemController.saveDept` 因为是逐个 `@RequestParam` 手工装配实体,反而不受此漏洞影响——平铺参数虽然丑,但恰好只允许列出的字段进来。**这是个值得记住的反直觉点:封装成对象若做不对,安全性反而不如平铺。** + +### 2.2 出参侧泄露内部字段 + +**[已核实]** `RoleController.page` 返回 `Result>`,直接把实体投给前端,`creatorId`/`updaterId`/`createTime`/`updateTime`/`deleted`/`builtin` 全部裸奔。**[盘查]** 同类还有 `SystemController.menuTree`(`List`)、`SystemController.deptTree`(`List`)、`DictGroupController.enabledList`(`List`)、`DictItemController.enabledList`(`List`)。 + +读方向不构成越权,但会让前端对内部字段形成依赖,日后改表即破坏前端契约。 + +### 2.3 命名与包结构已经分裂 + +**[已核实]** 全部「非实体」类都堆在 `domain/dto/` 里,**没有任何模块存在 `domain/vo/` 目录**,但类名却 VO/DTO 混用: + +| 类 | 所在包 | 类名后缀 | 实际角色 | +| --- | --- | --- | --- | +| `RoleDetailVO` | `dto/` | VO | 出参 | +| `UserStatsVO` | `dto/` | VO | 出参 | +| `DictGroupVO` | `dto/` | VO | 出参 | +| `DictItemVO` | `dto/` | VO | 出参 | +| `UserListDTO` | `dto/` | DTO | 出参(反范式,含 deptName/roleName) | +| `UserInfoDTO` | `dto/` | DTO | 出参 | +| `LoginResultDTO` | `dto/` | DTO | 出参 | +| `ResourceNode` | `dto/` | 无后缀 | **双向**(入参 + 出参) | + +同一个角色(HTTP 出参)在同一个包里有三种命名。**[已核实]** 更能说明混乱程度的是:`DictGroupVO` 是通过 `BaseEntity` 上名为 `toDTO` 的方法生成的——`g.toDTO(DictGroupVO::new)`(`DictGroupServiceImpl` L61)。 + +### 2.4 `ResourceNode`:双向复用的实际代价 + +**[已核实]** `ResourceNode` 同时是 `saveOrUpdate` 的入参和 `listAll` 的出参,代价写在它自己的注解里: + +```java +@Schema(description = "节点 ID,新增时不传、编辑时必传") // 同一字段两种契约 +@Schema(description = "子节点列表(仅 listAll 返回时填充)") // 入参侧永远无意义 +``` + +需要靠文字描述解释「这个字段何时有效」,说明类型本身表达不了契约。且该类同时持有 `fromEntity()` 与 `toEntity()`,一个类知道两个方向。 + +### 2.5 入参形态统计 **[盘查]** + +| 形态 | 数量 | 代表 | +| --- | --- | --- | +| `RAW_PARAMS`(≥3 个 `@RequestParam` 平铺) | 6 | `RoleController.saveOrUpdate`(6)、`SystemController.saveDept`(5)、`assignUserDepts`(3)、`RoleController.page`(3)、`FileController.multipart/init`(4)、`multipart/upload`(3) | +| `ENTITY_BIND`(实体直收) | 2 | `DictGroupController.saveOrUpdate`、`DictItemController.saveOrUpdate` | +| `DTO_BIND`(对象表单绑定) | 3 | `DictGroupController.page`、`DictItemController.page`、`ResourceController.saveOrUpdate` | +| `JSON_BIND`(`@RequestBody`) | 1 | `SystemController.userPage` | + +`JSON_BIND` 那一处**违反项目 API 契约**(契约规定 POST 用 `application/x-www-form-urlencoded`,不用 JSON body)。 + +### 2.6 分页安全 **[已核实 + 盘查]** + +`BaseParam` 的 javadoc 要求经 `PageConverter#toMpPage` 转换,「其内部做了 size 上限与排序字段防注入处理」。**[盘查]** `RoleController.page` 自行声明 `current`/`size`/`keyword` 并直接 `new Page<>(current, size)`,绕过了 size 上限与排序防注入。 + +### 2.7 校验注解使用率为零 **[盘查]** + +全项目无 `@Valid`/`@Validated`,入参类上无 `@NotNull`/`@NotBlank`/`@Size`。校验全靠 Service 层手写 `StrUtil.isBlank` 判断(如 `DictGroupServiceImpl` L78-83 **[已核实]**)。 + +### 2.8 现有成文规范自相矛盾 **[已核实]** + +`README.md` §3 工程结构已经声明了四层包划分: + +``` +├── domain/ +│ ├── entity/ # 实体 Xxx extends BaseEntity(与表一一对应) +│ ├── dto/ # XxxDTO extends BaseDTO(对外传输对象) +│ ├── vo/ # XxxVO(仅用于视图展示的聚合对象,可选) +│ ├── param/ # XxxParam extends BaseParam(查询入参)/ XxxSaveParam(写入参) +``` + +`README.md` §4 类命名规范却给出不同口径: + +| 类型 | 命名 | 继承/实现 | 说明 | +| --- | --- | --- | --- | +| 传输对象 | `CustomerDTO` | `BaseDTO` | **接口出入参主要载体** | +| 视图对象 | `CustomerVO` | - | 多表聚合展示时使用 | +| 查询参数 | `CustomerParam` | `BaseParam` | 分页/条件查询入参 | + +三处冲突: + +1. §3 说写入参是 `XxxSaveParam`(放 `param/`),§4 完全没提写入参,只说 `CustomerDTO extends BaseDTO` 是「出入参主要载体」。 +2. §4 让入参载体继承 `BaseDTO`,而 `BaseDTO` 携带 `creatorId`/`createTime`/`updaterId`/`updateTime`/`deleted` —— **照 §4 写就等于主动制造 §1.4 的漏洞**。 +3. §3 规定的 `domain/vo/` 目录在四个模块中一个都不存在,VO 类全在 `dto/` 下。 + +**结论:本项目不是「没有规范」,而是「成文规范内部矛盾、且与代码脱节」。** 这决定了本次工作的性质是修正与收敛,不是从零制定。 + +--- + +## 三、推荐方案 + +设计目标按优先级:① 结构性消除 mass assignment,② 不增加过多类,③ 与既有 API 契约(表单提交)零冲突,④ 尽量复用 `README` §3 已写下的划分以减少文档变更。 + +### 3.1 四类对象的职责 + +| 用途 | 包 | 命名 | 基类 | 硬约束 | +| --- | --- | --- | --- | --- | +| 表结构映射 | `domain/entity/` | `Xxx` | `BaseEntity` | **禁止出现在任何 Controller 方法签名中**(入参、出参都禁) | +| 写入参 | `domain/param/` | `XxxSaveParam` | **无,纯 POJO** | 只声明前端可写字段;禁止出现审计字段与服务端裁定字段(`builtin`/`deleted`/`status` 视业务而定) | +| 查询入参 | `domain/param/` | `XxxPageParam` | `BaseParam` | 分页查询必须继承 `BaseParam` 并经 `PageConverter.toMpPage` | +| 出参 | `domain/vo/` | `XxxVO` | 无 | 列表与详情**优先共用一个 VO** | +| 跨层/跨模块传输 | `domain/dto/` | `XxxDTO` | 视情况 | 不面向 HTTP 的传输对象;若已是跨模块 API 返回类型,可直接复用为 HTTP 出参,不再包 VO | + +要点说明: + +- **写入参用 `XxxSaveParam` 放 `param/`,不用 `XxxSaveDTO` 放 `dto/`。** 依据有三:`README` §3 已经这么写了(零文档变更);`param/` 这个包边界从此有了统一含义——「客户端送进来的,一律不可信」,读写都在里面,边界清晰;且按 §1.2,阿里手册的 DTO 本指「Service 层向上传输的对象」,用它命名 HTTP 入参偏离手册原意,而 `Param` 是本项目自有约定,不与任何外部定义冲突。 +- **纯 POJO,不继承 `BaseDTO`。** 继承即把审计字段暴露给绑定器,等于把 §1.4 的漏洞请回来。这是结构性防御与纪律性防御的区别:字段不存在,就没有「忘记置空」的可能。 +- **`domain/dto/` 保留给真正的跨层对象**,如 `UploadSession`(Redis 会话对象,非 API 出参)、`ButtonSeed`、`PermissionModuleDescriptor`;`FileInfoDTO` 既是 `crm-file` 对外 API 的返回类型又是 HTTP 出参,属允许的双重身份,不必再造 `FileInfoVO`。 + +### 3.2 控制类数量膨胀的四条规则 + +用户明确关切「不想建太多类」。以下规则把每个业务对象的类数压到 **通常 2 个、最多 3 个**: + +1. **出参只建一个 `XxxVO`,列表与详情共用。** 不要 `XxxListVO` + `XxxDetailVO` 各一份。详情比列表多出的重字段(如 `RoleDetailVO.resourceIds` 需额外查询)在列表场景留 `null` 即可——但必须在 `@Schema` 注明,且这类字段不超过 2 个;超了才拆。 +2. **没有分页查询的对象不建 `XxxPageParam`。** 单参数查询继续用 `@RequestParam`。 +3. **VO 的构造成本已经接近零**:`BaseEntity.toDTO(XxxVO::new)` 一行生成(现成机制,`DictGroupVO` 即如此)。所以「多一个 VO 类」的真实成本是一份字段声明,不是一套映射代码。 +4. **2 个及以下参数的写操作不必封装。** `status(id, status)`、`setDefault(groupId, itemId)`、`delete(id)` 保持 `@RequestParam`。阈值定在 **3 个参数**,与 §1.2 手册「查询条件超过 2 个禁用 Map」的精神一致。 + +诚实的成本披露:这套规范落地会**净增约 6 个类**(`RoleSaveParam`、`RolePageParam`、`RoleVO`、`DeptSaveParam`、`DictGroupSaveParam`、`DictItemSaveParam`、`ResourceSaveParam`、`ResourceVO`,减去可由 `ResourceNode` 拆分复用的部分),并需要把现有 7 个 VO 类从 `dto/` 移到 `vo/`。换来的是消除 2 处实体直收、5 处实体出参泄露、1 处分页防注入绕过。 + +### 3.3 转换位置 + +DTO/Param → Entity 的转换**放 Service 层**。Controller 只做接参、包 `Result`。理由:Controller 是薄适配层;含业务语义的字段映射属于 service;同一转换可能被多个入口复用。 + +Entity → VO 走 `BaseEntity.toDTO(XxxVO::new)`;字段名不一致时(如 `SysMenu.menuName` → `ResourceVO.name`)在 VO 上写静态 `fromEntity`,沿用 `RoleDetailVO.fromEntity` 的现有写法。 + +### 3.4 绑定方式 + +沿用**隐式表单绑定**(`saveOrUpdate(RoleSaveParam param)`,不加 `@RequestBody`),依据 §1.3:Spring 对非简单类型参数默认按 `@ModelAttribute` 处理,从表单字段与 query 参数绑定,与本项目 `application/x-www-form-urlencoded` 契约完全契合。 + +需顺带修正 `SystemController.userPage` 的 `@RequestBody`——它违反项目 API 契约。 + +### 3.5 校验(建议分期,不阻塞本次) + +入参类上加 JSR-380 注解、Controller 加 `@Valid`,把 Service 层手写的 `StrUtil.isBlank` 判断前移。当前使用率为零,全量补齐工作量大,建议只对**新增与本次改动**的入参类要求,存量不强制。 + +--- + +## 四、待决策项 + +**写入参的命名**:本文 §3.1 推荐 `XxxSaveParam`(放 `param/`),依据是 `README` §3 已有此约定 + `param/` 包语义统一 + 不与阿里手册 DTO 定义冲突。此前口头讨论中曾倾向 `XxxSaveDTO`(放 `dto/`),当时尚未发现 `README` §3 的既有约定。两者都可自洽,需拍一个: + +| | `XxxSaveParam` @ `param/`(本文推荐) | `XxxSaveDTO` @ `dto/` | +| --- | --- | --- | +| 与 `README` §3 | 一致,零文档变更 | 需改 `README` §3 | +| 包语义 | `param/` = 一切客户端入参,边界含安全意义 | `dto/` 混装入参与跨层对象 | +| 与阿里手册 | 无冲突(`Param` 是自有约定) | DTO 原指跨层传输,语义偏移 | +| 直觉性 | 「Param 像查询专用」是唯一弱点 | 「DTO 就是接口对象」符合多数人习惯 | + +--- + +## 五、落地后需同步更新的文档 + +- `README.md` §4 类命名规范表:删除「`CustomerDTO extends BaseDTO` 是接口出入参主要载体」,补入写入参与出参两行,消除与 §3 的矛盾。 +- `README.md` §3:`param/` 注释与最终命名对齐。 +- 新增 ADR(下一个可用编号 `0017`):记录「Controller 禁用 Entity 收发 + 三类对象职责划分」这一决策及其安全依据。 +- 各模块 `CONTEXT.md`:若涉及跨模块 seam 的对象(如 `FileInfoDTO`)身份变化,需补充说明。 +- `docs/frontend-*.md`:出参从 Entity 改为 VO 会改变返回字段集合(去掉审计字段),**属破坏性变更**,需与前端确认后同步文档。 + +--- + +## 六、引用源清单 + +| 源 | 性质 | 用途 | +| --- | --- | --- | +| [Martin Fowler, *Data Transfer Object* (PoEAA)](https://martinfowler.com/eaaCatalog/dataTransferObject.html) | 模式原始定义 | DTO 原意、assembler 归属、VO 术语混淆史 | +| [Alibaba Java Coding Guidelines(第一方英文版)](https://alibaba.github.io/Alibaba-Java-Coding-Guidelines/) | 行业规范 | DO/DTO/BO/Query/VO 分层与命名、POJO 禁默认值 | +| [Spring Framework Reference — `@ModelAttribute`](https://docs.spring.io/spring-framework/reference/web/webmvc/mvc-controller/ann-methods/modelattrib-method-args.html) | 框架官方文档 | 「用专为 web 绑定裁剪的对象」的安全建议、隐式 `@ModelAttribute` 机制、`allowedFields` | +| [OWASP Cheat Sheet — Mass Assignment](https://cheatsheetseries.owasp.org/cheatsheets/Mass_Assignment_Cheat_Sheet.html) | 安全规范 | 漏洞定名(Spring MVC 语境称 Autobinding)、DTO 作为架构级解法、`setAllowedFields` 兜底 | +| 本仓库源码 | 一手 | §二 全部现状事实 | + +未能取证的一项:Spring 文档 `@ModelAttribute` 页链接的 "Model Design" 深入章节,两个候选 URL 均返回 404,故本文只引用 `@ModelAttribute` 页内已验证的原文,未引用该深入章节。 diff --git a/nginx/docs/crm-api-docs-documentation.html b/nginx/docs/crm-api-docs-documentation.html new file mode 100644 index 0000000..93f50a0 --- /dev/null +++ b/nginx/docs/crm-api-docs-documentation.html @@ -0,0 +1,25 @@ + + + + + + crm-api-docs - API Documentation + + + + + +
+ + + \ No newline at end of file diff --git a/nginx/nginx.conf b/nginx/nginx.conf new file mode 100644 index 0000000..3ade9df --- /dev/null +++ b/nginx/nginx.conf @@ -0,0 +1,105 @@ +worker_processes auto; + +events { + worker_connections 1024; +} + +http { + include mime.types; + default_type application/octet-stream; + + # 对齐 application.yml 的 spring.servlet.multipart.max-request-size(25M) + client_max_body_size 25M; + + sendfile on; + keepalive_timeout 65; + gzip on; + gzip_types text/css application/javascript application/json image/svg+xml; + + upstream crm_app { + server crm-app:8080; + } + + # 缩略图代理缓存区(10MB 内存索引,100MB 磁盘,1天不访问则淘汰) + proxy_cache_path /var/cache/nginx/thumbnails + levels=1:2 + keys_zone=thumb_cache:10m + max_size=100m + inactive=1d + use_temp_path=off; + + server { + listen 80; + server_name _; + + # ========== 静态文档页(浏览器缓存 1 天) ========== + location /crm-api/docs/ { + alias /usr/share/nginx/docs/; + index crm-api-docs-documentation.html; + expires 1d; + add_header Cache-Control "public, max-age=86400"; + } + location = /crm-api/docs { + return 301 /crm-api/docs/; + } + location /docs/ { + alias /usr/share/nginx/docs/; + index crm-api-docs-documentation.html; + expires 1d; + add_header Cache-Control "public, max-age=86400"; + } + location = /docs { + return 301 /docs/; + } + + # ========== 缩略图(nginx 代理缓存) ========== + # 后端对 READY 缩略图返回 Cache-Control: max-age=86400 → nginx 缓存 + # 后端对占位图返回 Cache-Control: no-cache → nginx 不缓存(尊重后端) + # 带 /crm-api 前缀 + location /crm-api/api/file/thumbnail { + proxy_pass http://crm_app/api/file/thumbnail; + proxy_cache thumb_cache; + proxy_cache_key "$scheme$request_method$host$request_uri"; + proxy_cache_valid 200 1d; + proxy_cache_valid 404 1m; + proxy_cache_use_stale error timeout updating; + add_header X-Cache-Status $upstream_cache_status; + proxy_set_header Host $host; + } + # 不带前缀 + location /api/file/thumbnail { + proxy_pass http://crm_app; + proxy_cache thumb_cache; + proxy_cache_key "$scheme$request_method$host$request_uri"; + proxy_cache_valid 200 1d; + proxy_cache_valid 404 1m; + proxy_cache_use_stale error timeout updating; + add_header X-Cache-Status $upstream_cache_status; + proxy_set_header Host $host; + } + + # ========== 带 /crm-api 前缀的 API(去掉前缀转发) ========== + location /crm-api/ { + proxy_pass http://crm_app/; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_read_timeout 300s; + proxy_send_timeout 300s; + proxy_request_buffering off; + } + + # ========== 不带前缀直接转发 ========== + location / { + proxy_pass http://crm_app; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_read_timeout 300s; + proxy_send_timeout 300s; + proxy_request_buffering off; + } + } +}