Browse Source

commit

master
luoweijian 4 weeks ago
parent
commit
b50002f47f
  1. 2
      .idea/compiler.xml
  2. 1
      .idea/encodings.xml
  3. 9
      .idea/modules.xml
  4. 121
      .scratch/handoff-2026-08-10.md
  5. 1
      Dockerfile
  6. BIN
      crm-app/target/crm-app-1.0.0-SNAPSHOT.jar
  7. BIN
      crm-app/target/crm-app-1.0.0-SNAPSHOT.jar.original
  8. 3
      crm-app/target/maven-archiver/pom.properties
  9. 60
      crm-auth/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst
  10. 1
      crm-auth/target/maven-status/maven-compiler-plugin/testCompile/default-testCompile/createdFiles.lst
  11. 64
      crm-auth/target/surefire-reports/TEST-com.crm.auth.security.scope.DataScopeIntegrationTest.xml
  12. 58
      crm-auth/target/surefire-reports/TEST-com.crm.auth.service.client.DingTalkAuthClientTest.xml
  13. 10
      crm-auth/target/surefire-reports/TEST-com.crm.auth.service.client.ThirdPartyAuthClientFactoryTest.xml
  14. 40
      crm-auth/target/surefire-reports/TEST-com.crm.auth.service.impl.AuthServiceImplTest.xml
  15. 25
      crm-auth/target/surefire-reports/TEST-com.crm.auth.service.impl.AuthUserServiceImplTest.xml
  16. 22
      crm-auth/target/surefire-reports/TEST-com.crm.auth.service.impl.SysDeptServiceImplTest.xml
  17. 36
      crm-auth/target/surefire-reports/TEST-com.crm.auth.service.impl.UserListIntegrationTest.xml
  18. 2
      crm-auth/target/surefire-reports/com.crm.auth.security.scope.DataScopeIntegrationTest.txt
  19. 2
      crm-auth/target/surefire-reports/com.crm.auth.service.client.DingTalkAuthClientTest.txt
  20. 2
      crm-auth/target/surefire-reports/com.crm.auth.service.client.ThirdPartyAuthClientFactoryTest.txt
  21. 2
      crm-auth/target/surefire-reports/com.crm.auth.service.impl.AuthServiceImplTest.txt
  22. 2
      crm-auth/target/surefire-reports/com.crm.auth.service.impl.AuthUserServiceImplTest.txt
  23. 2
      crm-auth/target/surefire-reports/com.crm.auth.service.impl.SysDeptServiceImplTest.txt
  24. 2
      crm-auth/target/surefire-reports/com.crm.auth.service.impl.UserListIntegrationTest.txt
  25. BIN
      crm-base/target/classes/com/crm/base/domain/dto/BaseDTO.class
  26. BIN
      crm-base/target/classes/com/crm/base/service/IBaseService.class
  27. 2
      crm-base/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst
  28. 38
      crm-base/target/surefire-reports/TEST-com.crm.base.security.DataVisibilityTest.xml
  29. 2
      crm-base/target/surefire-reports/com.crm.base.security.DataVisibilityTest.txt
  30. BIN
      crm-base/target/test-classes/com/crm/base/security/DataVisibilityTest.class
  31. 12
      crm-file/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst
  32. 7
      crm-file/target/maven-status/maven-compiler-plugin/testCompile/default-testCompile/createdFiles.lst
  33. 10
      crm-file/target/surefire-reports/TEST-com.crm.file.service.KkFileViewClientTest.xml
  34. 104
      crm-file/target/surefire-reports/TEST-com.crm.file.service.impl.FileApiImplTest.xml
  35. 16
      crm-file/target/surefire-reports/TEST-com.crm.file.task.OrphanChunkCleanupTaskTest.xml
  36. 2
      crm-file/target/surefire-reports/com.crm.file.service.KkFileViewClientTest.txt
  37. 2
      crm-file/target/surefire-reports/com.crm.file.service.impl.FileApiImplTest.txt
  38. 2
      crm-file/target/surefire-reports/com.crm.file.task.OrphanChunkCleanupTaskTest.txt
  39. BIN
      crm-file/target/test-classes/com/crm/file/service/KkFileViewClientTest.class
  40. BIN
      crm-file/target/test-classes/com/crm/file/service/impl/FileApiImplTest.class
  41. BIN
      crm-file/target/test-classes/com/crm/file/task/OrphanChunkCleanupTaskTest.class
  42. 26
      docker-compose.yml
  43. 315
      docs/research/2026-08-10-dto-vo-param-conventions.md
  44. 25
      nginx/docs/crm-api-docs-documentation.html
  45. 105
      nginx/nginx.conf

2
.idea/compiler.xml

@ -6,6 +6,7 @@
<sourceOutputDir name="target/generated-sources/annotations" />
<sourceTestOutputDir name="target/generated-test-sources/test-annotations" />
<outputRelativeToContentRoot value="true" />
<module name="crm-dict" />
<module name="crm-base" />
<module name="crm-app" />
<module name="crm-auth" />
@ -19,6 +20,7 @@
<module name="crm-app" options="-parameters" />
<module name="crm-auth" options="-parameters" />
<module name="crm-base" options="-parameters" />
<module name="crm-dict" options="-parameters" />
<module name="crm-file" options="-parameters" />
</option>
</component>

1
.idea/encodings.xml

@ -7,6 +7,7 @@
<file url="file://$PROJECT_DIR$/crm-auth/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/crm-auth/src/main/resources" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/crm-base/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/crm-dict/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/crm-file/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/crm-file/src/main/resources" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/src/main/java" charset="UTF-8" />

9
.idea/modules.xml

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/crm-auth/crm-auth.iml" filepath="$PROJECT_DIR$/crm-auth/crm-auth.iml" />
<module fileurl="file://$PROJECT_DIR$/crm-base/crm-base.iml" filepath="$PROJECT_DIR$/crm-base/crm-base.iml" />
</modules>
</component>
</project>

121
.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: <description> (ADR-0017 ticket 0N)`
- 代码风格:匹配周围代码的注释密度、命名、惯用法
- 不要提交 `target/` 目录下的构建产物
- 不要提交 `.idea/` 目录下的 IDE 配置
- ADR + spec + tickets 已存在,勿重复创建,只更新

1
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

BIN
crm-app/target/crm-app-1.0.0-SNAPSHOT.jar

Binary file not shown.

BIN
crm-app/target/crm-app-1.0.0-SNAPSHOT.jar.original

Binary file not shown.

3
crm-app/target/maven-archiver/pom.properties

@ -1,3 +0,0 @@
artifactId=crm-app
groupId=com.crm
version=1.0.0-SNAPSHOT

60
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

1
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

64
crm-auth/target/surefire-reports/TEST-com.crm.auth.security.scope.DataScopeIntegrationTest.xml

File diff suppressed because one or more lines are too long

58
crm-auth/target/surefire-reports/TEST-com.crm.auth.service.client.DingTalkAuthClientTest.xml

File diff suppressed because one or more lines are too long

10
crm-auth/target/surefire-reports/TEST-com.crm.auth.service.client.ThirdPartyAuthClientFactoryTest.xml

File diff suppressed because one or more lines are too long

40
crm-auth/target/surefire-reports/TEST-com.crm.auth.service.impl.AuthServiceImplTest.xml

File diff suppressed because one or more lines are too long

25
crm-auth/target/surefire-reports/TEST-com.crm.auth.service.impl.AuthUserServiceImplTest.xml

File diff suppressed because one or more lines are too long

22
crm-auth/target/surefire-reports/TEST-com.crm.auth.service.impl.SysDeptServiceImplTest.xml

File diff suppressed because one or more lines are too long

36
crm-auth/target/surefire-reports/TEST-com.crm.auth.service.impl.UserListIntegrationTest.xml

File diff suppressed because one or more lines are too long

2
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

2
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

2
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

2
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

2
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

2
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

2
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

BIN
crm-base/target/classes/com/crm/base/domain/dto/BaseDTO.class

Binary file not shown.

BIN
crm-base/target/classes/com/crm/base/service/IBaseService.class

Binary file not shown.

2
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

38
crm-base/target/surefire-reports/TEST-com.crm.base.security.DataVisibilityTest.xml

File diff suppressed because one or more lines are too long

2
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

BIN
crm-base/target/test-classes/com/crm/base/security/DataVisibilityTest.class

Binary file not shown.

12
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

7
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

10
crm-file/target/surefire-reports/TEST-com.crm.file.service.KkFileViewClientTest.xml

File diff suppressed because one or more lines are too long

104
crm-file/target/surefire-reports/TEST-com.crm.file.service.impl.FileApiImplTest.xml

File diff suppressed because one or more lines are too long

16
crm-file/target/surefire-reports/TEST-com.crm.file.task.OrphanChunkCleanupTaskTest.xml

File diff suppressed because one or more lines are too long

2
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

2
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

2
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

BIN
crm-file/target/test-classes/com/crm/file/service/KkFileViewClientTest.class

Binary file not shown.

BIN
crm-file/target/test-classes/com/crm/file/service/impl/FileApiImplTest.class

Binary file not shown.

BIN
crm-file/target/test-classes/com/crm/file/task/OrphanChunkCleanupTaskTest.class

Binary file not shown.

26
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:

315
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<PageResult<SysRole>>`,直接把实体投给前端,`creatorId`/`updaterId`/`createTime`/`updateTime`/`deleted`/`builtin` 全部裸奔。**[盘查]** 同类还有 `SystemController.menuTree`(`List<SysMenu>`)、`SystemController.deptTree`(`List<SysDept>`)、`DictGroupController.enabledList`(`List<DictGroup>`)、`DictItemController.enabledList`(`List<DictItem>`)。
读方向不构成越权,但会让前端对内部字段形成依赖,日后改表即破坏前端契约。
### 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` 页内已验证的原文,未引用该深入章节。

25
nginx/docs/crm-api-docs-documentation.html

File diff suppressed because one or more lines are too long

105
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;
}
}
}
Loading…
Cancel
Save