Commit 7bf1cae5b997697b1ea8e444d330455511d15acf
1 parent
c0da0c5f
docs(mod): review approval REQ-MOD-001
Showing
4 changed files
with
571 additions
and
1 deletions
docs/08-模块任务管理.md
docs/superpowers/plans/2026-05-06-REQ-MOD-001.md
0 → 100644
| 1 | +--- | |
| 2 | +req_id: REQ-MOD-001 | |
| 3 | +date: 2026-05-06 | |
| 4 | +spec_ref: docs/superpowers/specs/2026-05-06-REQ-MOD-001.md | |
| 5 | +--- | |
| 6 | + | |
| 7 | +# REQ-MOD-001 模块新增 Implementation Plan | |
| 8 | + | |
| 9 | +> **Execution:** Parent skill `feature-tdd` executes this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. | |
| 10 | + | |
| 11 | +**Goal:** 实现 `POST /api/modules` 接口,把一条新业务模块写入 `tModule`,并顺带建立服务于本 REQ 的最小 Spring Boot 骨架。 | |
| 12 | + | |
| 13 | +**Architecture:** 标准三层(Controller → Service → Mapper)。Spring Boot 3 + MyBatis-Plus + Flyway 自动 apply migrations。鉴权暂以 `SecurityConfig` permitAll 占位,REQ-USR-004 时回头收紧。统一响应 `ApiResponse<T>`、统一异常 `BizException` + `GlobalExceptionHandler`。 | |
| 14 | + | |
| 15 | +**Tech Stack:** Spring Boot 3.x、MyBatis-Plus(spring-boot3-starter)、Flyway 10.x、MySQL 8、JUnit 5 + Spring Boot Test + MockMvc + Spring Security Test、Hutool、MapStruct(可选)、Maven 3.9。 | |
| 16 | + | |
| 17 | +--- | |
| 18 | + | |
| 19 | +## Schema 改动 | |
| 20 | + | |
| 21 | +无(`tModule` 已由 A4 `V1__initial_schema.sql` 创建)。 | |
| 22 | + | |
| 23 | +## 文件变更清单 | |
| 24 | + | |
| 25 | +- `backend/pom.xml` — 创建(Spring Boot 父 pom + 依赖清单) | |
| 26 | +- `backend/src/main/java/com/xly/erp/ErpApplication.java` — 创建(启动类) | |
| 27 | +- `backend/src/main/resources/application.yml` — 创建(DB / Flyway / MyBatis-Plus / 端口配置,从环境变量注入) | |
| 28 | +- `backend/src/main/resources/application-test.yml` — 创建(测试 profile,连 DB_SCHEMA test 库) | |
| 29 | +- `backend/src/main/java/com/xly/erp/common/response/ApiResponse.java` — 创建(统一响应包装) | |
| 30 | +- `backend/src/main/java/com/xly/erp/common/response/ErrorCode.java` — 创建(错误码枚举) | |
| 31 | +- `backend/src/main/java/com/xly/erp/common/exception/BizException.java` — 创建(业务异常) | |
| 32 | +- `backend/src/main/java/com/xly/erp/common/exception/GlobalExceptionHandler.java` — 创建(@RestControllerAdvice) | |
| 33 | +- `backend/src/main/java/com/xly/erp/config/SecurityConfig.java` — 创建(permitAll 临时配置) | |
| 34 | +- `backend/src/main/java/com/xly/erp/config/MybatisPlusConfig.java` — 创建(分页插件,本 REQ 不用,REQ-MOD-004 用,本任务先建空骨架) | |
| 35 | +- `backend/src/main/java/com/xly/erp/module/mod/entity/ModuleEntity.java` — 创建(@TableName("tModule")) | |
| 36 | +- `backend/src/main/java/com/xly/erp/module/mod/mapper/ModuleMapper.java` — 创建(继承 BaseMapper) | |
| 37 | +- `backend/src/main/resources/mapper/mod/ModuleMapper.xml` — 创建(空骨架,本 REQ 仅用 BaseMapper 默认 SQL) | |
| 38 | +- `backend/src/main/java/com/xly/erp/module/mod/dto/ModuleCreateDTO.java` — 创建(入参 + Bean Validation) | |
| 39 | +- `backend/src/main/java/com/xly/erp/module/mod/vo/ModuleVO.java` — 创建(出参) | |
| 40 | +- `backend/src/main/java/com/xly/erp/module/mod/service/ModuleService.java` — 创建(接口) | |
| 41 | +- `backend/src/main/java/com/xly/erp/module/mod/service/impl/ModuleServiceImpl.java` — 创建(实现,含 create 方法) | |
| 42 | +- `backend/src/main/java/com/xly/erp/module/mod/controller/ModuleController.java` — 创建(POST /api/modules) | |
| 43 | +- `backend/src/test/java/com/xly/erp/common/response/ApiResponseTest.java` — 创建(单元) | |
| 44 | +- `backend/src/test/java/com/xly/erp/common/exception/GlobalExceptionHandlerTest.java` — 创建(单元) | |
| 45 | +- `backend/src/test/java/com/xly/erp/module/mod/mapper/ModuleMapperIT.java` — 创建(@SpringBootTest 集成) | |
| 46 | +- `backend/src/test/java/com/xly/erp/module/mod/service/ModuleServiceImplTest.java` — 创建(单元 + Mockito) | |
| 47 | +- `backend/src/test/java/com/xly/erp/module/mod/controller/ModuleControllerIT.java` — 创建(MockMvc + @SpringBootTest) | |
| 48 | +- `backend/src/test/resources/application-test.yml` — 创建(测试 profile 副本,便于 IDE 跑测试) | |
| 49 | +- `.env.local` — 不修改(已有 DB_HOST / DB_PORT / DB_USER / DB_PASSWORD / DB_SCHEMA / JWT_SECRET) | |
| 50 | + | |
| 51 | +--- | |
| 52 | + | |
| 53 | +## 任务步骤 | |
| 54 | + | |
| 55 | +### Task 1: 引导 Spring Boot 项目骨架(compile + boot 通过) | |
| 56 | + | |
| 57 | +**Files:** | |
| 58 | +- Create: `backend/pom.xml` | |
| 59 | +- Create: `backend/src/main/java/com/xly/erp/ErpApplication.java` | |
| 60 | +- Create: `backend/src/main/resources/application.yml` | |
| 61 | +- Create: `backend/src/main/resources/application-test.yml` | |
| 62 | +- Create: `backend/src/test/resources/application-test.yml`(与上一行同内容,Spring Boot 测试时从 test/resources 优先加载) | |
| 63 | +- Create: `backend/src/main/java/com/xly/erp/ErpApplicationTest.java`(验证 context 启动) | |
| 64 | + | |
| 65 | +**API shape:** | |
| 66 | +- `ErpApplication` 主类标 `@SpringBootApplication`、`@MapperScan("com.xly.erp.**.mapper")` | |
| 67 | +- `application.yml` 通过 `${DB_HOST}` / `${DB_PORT}` / `${DB_USER}` / `${DB_PASSWORD}` / `${DB_SCHEMA}` / `${JWT_SECRET}` 占位(值由本地启动脚本或 IDE EnvFile 插件从 `.env.local` 注入;详见 docs/04 § 3.5) | |
| 68 | +- 启用 Flyway:`spring.flyway.locations=classpath:db/migration` —— 但项目 migrations 在 `sql/migrations/`;改为 `spring.flyway.locations=filesystem:../sql/migrations` 让 backend 直接复用仓库根的 V*.sql | |
| 69 | + | |
| 70 | +**pom.xml 锁定依赖**(写死,不让 TDD 自由选): | |
| 71 | +- `spring-boot-starter-parent` 3.2.x | |
| 72 | +- `spring-boot-starter-web`、`-validation`、`-security`、`-test` | |
| 73 | +- `spring-security-test` | |
| 74 | +- `mybatis-plus-spring-boot3-starter` 最新稳定版(写明确版本号,本任务由 TDD 确认 mvn central 最新可用) | |
| 75 | +- `flyway-core` + `flyway-mysql` 10.x | |
| 76 | +- `mysql-connector-j` 8.x | |
| 77 | +- `org.projectlombok:lombok`(编译期)+ `lombok-mapstruct-binding` | |
| 78 | +- `org.mapstruct:mapstruct` 1.5.x + `mapstruct-processor` | |
| 79 | +- `cn.hutool:hutool-all` 5.x | |
| 80 | +- Java 编译目标 17 | |
| 81 | + | |
| 82 | +- [ ] **Step 1.1 写测试** | |
| 83 | + - 文件: `backend/src/test/java/com/xly/erp/ErpApplicationTest.java` | |
| 84 | + - 测试名: `ErpApplicationTest#contextLoads` | |
| 85 | + - 内容: 空 `@SpringBootTest @ActiveProfiles("test")` + 空 test 方法 | |
| 86 | + - 意图: ApplicationContext 能在 test profile 下成功启动 + Flyway 能 apply V1(apply 到现有 test schema 应是 no-op,因为表已存在;Flyway 跑完会建 `flyway_schema_history` 表 + 注册 V1 已 baseline) | |
| 87 | + - 子会话确认 FAIL(项目还没建好) | |
| 88 | + | |
| 89 | +- [ ] **Step 1.2 创建 pom.xml** | |
| 90 | + - 仅声明依赖、Java 17、surefire 配置 | |
| 91 | + - `mvn -B -f backend/pom.xml dependency:resolve` 应通过 | |
| 92 | + | |
| 93 | +- [ ] **Step 1.3 创建 ErpApplication + application.yml** | |
| 94 | + - `ErpApplication` 仅做启动 + `@MapperScan` | |
| 95 | + - `application.yml` 主键:`server.port=8080`、`spring.profiles.active=${SPRING_PROFILES_ACTIVE:dev}`、datasource、mybatis-plus、flyway | |
| 96 | + - `application-test.yml`:override `spring.flyway.baseline-on-migrate=true` + `spring.flyway.baseline-version=0`(让 Flyway 把当前已存在的 schema 作为 baseline,再追 apply) | |
| 97 | + - 子会话 `cd backend && mvn -B test -Dtest=ErpApplicationTest#contextLoads -DSPRING_PROFILES_ACTIVE=test` 应 PASS | |
| 98 | + | |
| 99 | +- [ ] **Step 1.4 提交** | |
| 100 | + - `git add backend/pom.xml backend/src/main/java/com/xly/erp/ErpApplication.java backend/src/main/resources/application*.yml backend/src/test/resources/application*.yml backend/src/test/java/com/xly/erp/ErpApplicationTest.java` | |
| 101 | + - `git commit -m "chore(mod): bootstrap spring boot skeleton REQ-MOD-001"` | |
| 102 | + | |
| 103 | +--- | |
| 104 | + | |
| 105 | +### Task 2: 统一响应与错误码(ApiResponse + ErrorCode) | |
| 106 | + | |
| 107 | +**Files:** | |
| 108 | +- Create: `backend/src/main/java/com/xly/erp/common/response/ApiResponse.java` | |
| 109 | +- Create: `backend/src/main/java/com/xly/erp/common/response/ErrorCode.java` | |
| 110 | +- Test: `backend/src/test/java/com/xly/erp/common/response/ApiResponseTest.java` | |
| 111 | + | |
| 112 | +**API shape:** | |
| 113 | +- `ApiResponse<T>`:字段 `int code`、`String message`、`T data`、`long timestamp`;静态工厂 `ok(T data)`、`ok(String message, T data)`、`fail(ErrorCode ec)`、`fail(int code, String message)` | |
| 114 | +- `ErrorCode` 枚举(int code, String message)至少包含: | |
| 115 | + - `SUCCESS(200, "操作成功")` | |
| 116 | + - `PARAM_INVALID(40010, "参数错误")` | |
| 117 | + - `MODULE_PARENT_NOT_FOUND(40411, "父模块不存在或已删除")` | |
| 118 | + - `MODULE_PROCEDURE_NAME_DUPLICATE(40911, "存储过程名称已存在")` | |
| 119 | + - `INTERNAL_ERROR(50000, "服务器内部错误")` | |
| 120 | + | |
| 121 | +**锁定常量**(跨模块复用,写死): | |
| 122 | +``` | |
| 123 | +SUCCESS = 200 | |
| 124 | +PARAM_INVALID = 40010 | |
| 125 | +MOD_PARENT_NOT_FOUND = 40411 | |
| 126 | +MOD_PROC_NAME_DUP = 40911 | |
| 127 | +INTERNAL_ERROR = 50000 | |
| 128 | +``` | |
| 129 | + | |
| 130 | +- [ ] **Step 2.1 写失败测试** | |
| 131 | + - 测试名: `ApiResponseTest#ok_setsCode200AndDataAndTimestamp`、`ApiResponseTest#fail_mapsErrorCodeFields` | |
| 132 | + - 意图: `ok(data)` → code=200,message="操作成功",data=入参,timestamp 在合理范围;`fail(ErrorCode.PARAM_INVALID)` → code=40010,message="参数错误" | |
| 133 | + - 子会话 PASS:FAIL(class 不存在) | |
| 134 | + | |
| 135 | +- [ ] **Step 2.2 实现 ApiResponse + ErrorCode** | |
| 136 | + - 用 Lombok `@Data` 减少样板,但保留显式工厂方法 | |
| 137 | + - 子会话 PASS | |
| 138 | + | |
| 139 | +- [ ] **Step 2.3 提交** | |
| 140 | + - `git commit -m "feat(common): unified ApiResponse and ErrorCode REQ-MOD-001"` | |
| 141 | + | |
| 142 | +--- | |
| 143 | + | |
| 144 | +### Task 3: 业务异常 + 全局异常处理器 | |
| 145 | + | |
| 146 | +**Files:** | |
| 147 | +- Create: `backend/src/main/java/com/xly/erp/common/exception/BizException.java` | |
| 148 | +- Create: `backend/src/main/java/com/xly/erp/common/exception/GlobalExceptionHandler.java` | |
| 149 | +- Test: `backend/src/test/java/com/xly/erp/common/exception/GlobalExceptionHandlerTest.java`(用 MockMvc + 一个临时 controller 触发各类异常) | |
| 150 | + | |
| 151 | +**API shape:** | |
| 152 | +- `BizException extends RuntimeException`:构造 `BizException(ErrorCode ec)`、`BizException(ErrorCode ec, String detail)`;getter: `code()`, `message()` | |
| 153 | +- `GlobalExceptionHandler` 处理: | |
| 154 | + - `BizException` → `ApiResponse.fail(ec)` 200 OK(业务错误也走 200 + 内部 code,docs/05 § 全局约定) | |
| 155 | + - `MethodArgumentNotValidException`(@Valid 失败)→ 提取 fieldError,组合 `ApiResponse.fail(PARAM_INVALID, "<field>: <reason>")` | |
| 156 | + - `Exception`(兜底)→ `ApiResponse.fail(INTERNAL_ERROR)` + log error;**响应不含堆栈**(docs/04 § 1.4) | |
| 157 | + | |
| 158 | +- [ ] **Step 3.1 写失败测试** | |
| 159 | + - 测试名(4 个): | |
| 160 | + - `GlobalExceptionHandlerTest#bizException_returns200WithBizCode` | |
| 161 | + - `GlobalExceptionHandlerTest#validationException_returns200WithParamInvalidCode` | |
| 162 | + - `GlobalExceptionHandlerTest#uncaughtException_returns200WithInternalErrorCode` | |
| 163 | + - `GlobalExceptionHandlerTest#response_doesNotContainStackTrace` | |
| 164 | + - 测试方式: `@WebMvcTest(controllers=DummyController.class)` 内置一个 `DummyController` 故意抛各类异常,断言 `MockMvc.perform(...).andExpect(jsonPath("$.code").value(...))` | |
| 165 | + - 子会话: FAIL | |
| 166 | + | |
| 167 | +- [ ] **Step 3.2 实现 BizException + Handler** | |
| 168 | + - 子会话: PASS | |
| 169 | + | |
| 170 | +- [ ] **Step 3.3 提交** | |
| 171 | + - `git commit -m "feat(common): biz exception and global handler REQ-MOD-001"` | |
| 172 | + | |
| 173 | +--- | |
| 174 | + | |
| 175 | +### Task 4: SecurityConfig(permitAll 占位) | |
| 176 | + | |
| 177 | +**Files:** | |
| 178 | +- Create: `backend/src/main/java/com/xly/erp/config/SecurityConfig.java` | |
| 179 | +- Test: `backend/src/test/java/com/xly/erp/config/SecurityConfigTest.java` | |
| 180 | + | |
| 181 | +**API shape:** | |
| 182 | +- `SecurityFilterChain securityFilterChain(HttpSecurity http)`:禁用 CSRF(API 项目)、禁用默认表单登录、所有 `/api/**` permitAll | |
| 183 | +- 显式注释:`// REQ-USR-004 完成后改为 .authenticated() + JWT filter` | |
| 184 | + | |
| 185 | +- [ ] **Step 4.1 写失败测试** | |
| 186 | + - 测试名: `SecurityConfigTest#anyApiEndpoint_isPermittedWithoutAuth` | |
| 187 | + - 测试方式: `@SpringBootTest @AutoConfigureMockMvc` + 在 controller package 临时加一个 `/api/__ping` GET(**或**复用 Task 3 的 `DummyController`),无 token 请求应返回 200 而非 401/403 | |
| 188 | + - 子会话: FAIL(默认 Spring Security 401) | |
| 189 | + | |
| 190 | +- [ ] **Step 4.2 实现 SecurityConfig** | |
| 191 | + - 子会话: PASS | |
| 192 | + | |
| 193 | +- [ ] **Step 4.3 提交** | |
| 194 | + - `git commit -m "feat(config): permitAll security skeleton REQ-MOD-001"` | |
| 195 | + | |
| 196 | +--- | |
| 197 | + | |
| 198 | +### Task 5: ModuleEntity + ModuleMapper(Mapper 集成测试落库) | |
| 199 | + | |
| 200 | +**Files:** | |
| 201 | +- Create: `backend/src/main/java/com/xly/erp/module/mod/entity/ModuleEntity.java` | |
| 202 | +- Create: `backend/src/main/java/com/xly/erp/module/mod/mapper/ModuleMapper.java` | |
| 203 | +- Create: `backend/src/main/resources/mapper/mod/ModuleMapper.xml`(仅头部 `<mapper namespace="...ModuleMapper"/>`,预留扩展) | |
| 204 | +- Test: `backend/src/test/java/com/xly/erp/module/mod/mapper/ModuleMapperIT.java` | |
| 205 | + | |
| 206 | +**API shape:** | |
| 207 | +- `@TableName("tModule")` ;字段名严格匹配 docs/03(保留匈牙利前缀,使用 `@TableField` 显式映射),主键 `iIncrement` 用 `@TableId(type = IdType.AUTO)` | |
| 208 | +- 列 ↔ Java 属性命名采用**保留前缀**(如 `iIncrement` Java 字段名也叫 `iIncrement`,方便对照 schema),与 docs/04 § 1.2 命名约定中的"小驼峰"略有出入——本约束**优先**:DB schema SSoT 是 docs/03,Java 字段直接复用列名以避免双向映射歧义。Lombok `@Data`。 | |
| 209 | +- `ModuleMapper extends BaseMapper<ModuleEntity>`,本 REQ 仅用 `insert` + `selectById` + `selectCount`(条件);不写自定义 SQL | |
| 210 | + | |
| 211 | +- [ ] **Step 5.1 写失败测试** | |
| 212 | + - 测试名: `ModuleMapperIT#insertAndSelectById_persistsAllFields`、`ModuleMapperIT#selectCountByProcedureName_returnsExisting` | |
| 213 | + - 测试方式: `@SpringBootTest @ActiveProfiles("test")` + `@Transactional`(自动回滚),DI `ModuleMapper`,构造 entity 调 `insert`,再 `selectById` 断言全部字段;用 `selectCount(new LambdaQueryWrapper<ModuleEntity>().eq(ModuleEntity::getSProcedureName, ...))` 验证唯一查询 | |
| 214 | + - 子会话: FAIL | |
| 215 | + | |
| 216 | +- [ ] **Step 5.2 实现 Entity + Mapper** | |
| 217 | + - 子会话: PASS(DDL 已存在,直接 insert) | |
| 218 | + | |
| 219 | +- [ ] **Step 5.3 提交** | |
| 220 | + - `git commit -m "feat(mod): module entity and mapper REQ-MOD-001"` | |
| 221 | + | |
| 222 | +--- | |
| 223 | + | |
| 224 | +### Task 6: DTO + VO | |
| 225 | + | |
| 226 | +**Files:** | |
| 227 | +- Create: `backend/src/main/java/com/xly/erp/module/mod/dto/ModuleCreateDTO.java` | |
| 228 | +- Create: `backend/src/main/java/com/xly/erp/module/mod/vo/ModuleVO.java` | |
| 229 | +- Test: `backend/src/test/java/com/xly/erp/module/mod/dto/ModuleCreateDTOValidationTest.java` | |
| 230 | + | |
| 231 | +**API shape:** | |
| 232 | + | |
| 233 | +`ModuleCreateDTO` 字段(带 Bean Validation): | |
| 234 | +- `@NotBlank @Pattern(regexp="^(手机端|前端业务|系统配置|接口)$") String sDisplayType` | |
| 235 | +- `@NotBlank @Size(max=100) String sProcedureName` | |
| 236 | +- `@NotBlank @Size(max=50) String sModuleType` | |
| 237 | +- `@NotBlank @Size(max=50) String sManageDeptEn` | |
| 238 | +- `Boolean bShowPermission`(可空,service 层 default false) | |
| 239 | +- `@NotBlank @Size(max=100) String sModuleNameZh` | |
| 240 | +- `Integer iParentId`(可空) | |
| 241 | +- `@Min(0) Integer iSortOrder`(可空,default 0) | |
| 242 | + | |
| 243 | +`ModuleVO` 字段(11 个,见 spec § 输出):`iIncrement`, `sDisplayType`, `sProcedureName`, `sModuleType`, `sManageDeptEn`, `bShowPermission`, `sModuleNameZh`, `iParentId`, `iSortOrder`, `tCreateDate`, `bDeleted`。 | |
| 244 | + | |
| 245 | +> Entity → VO 的转换写在 `ModuleVO` 的静态工厂 `from(ModuleEntity)` 里,不引入 MapStruct 单独 mapper 类(YAGNI;REQ 多了再抽)。 | |
| 246 | + | |
| 247 | +- [ ] **Step 6.1 写失败测试** | |
| 248 | + - 测试名: `ModuleCreateDTOValidationTest#blankRequiredFields_yieldsViolations`、`ModuleCreateDTOValidationTest#invalidDisplayTypeEnum_yieldsViolation`、`ModuleCreateDTOValidationTest#allValidFields_yieldsNoViolations` | |
| 249 | + - 测试方式: `Validation.buildDefaultValidatorFactory().getValidator()` + 断言 violation 集合 | |
| 250 | + - 子会话: FAIL | |
| 251 | + | |
| 252 | +- [ ] **Step 6.2 实现 DTO + VO** | |
| 253 | + - 子会话: PASS | |
| 254 | + | |
| 255 | +- [ ] **Step 6.3 提交** | |
| 256 | + - `git commit -m "feat(mod): module create DTO and VO REQ-MOD-001"` | |
| 257 | + | |
| 258 | +--- | |
| 259 | + | |
| 260 | +### Task 7: ModuleService.create(核心业务) | |
| 261 | + | |
| 262 | +**Files:** | |
| 263 | +- Create: `backend/src/main/java/com/xly/erp/module/mod/service/ModuleService.java` | |
| 264 | +- Create: `backend/src/main/java/com/xly/erp/module/mod/service/impl/ModuleServiceImpl.java` | |
| 265 | +- Test: `backend/src/test/java/com/xly/erp/module/mod/service/ModuleServiceImplTest.java`(Mockito 单元测试,mock ModuleMapper) | |
| 266 | + | |
| 267 | +**API shape:** | |
| 268 | +- `interface ModuleService { ModuleVO create(ModuleCreateDTO dto); }` | |
| 269 | +- `@Service @RequiredArgsConstructor class ModuleServiceImpl implements ModuleService`,字段 `private final ModuleMapper moduleMapper;` | |
| 270 | +- `create` 方法步骤(写在 plan 锁定,不让 TDD 自由发挥逻辑顺序): | |
| 271 | + 1. 若 `dto.iParentId != null`:`moduleMapper.selectById(iParentId)`,结果为 `null` 或 `bDeleted == true` → 抛 `BizException(MOD_PARENT_NOT_FOUND)` | |
| 272 | + 2. 唯一性预检:`moduleMapper.selectCount(LambdaQueryWrapper.eq(ModuleEntity::getSProcedureName, dto.sProcedureName).eq(ModuleEntity::getBDeleted, 0))` > 0 → 抛 `BizException(MOD_PROC_NAME_DUP)` | |
| 273 | + 3. 构造 `ModuleEntity`:复制 dto 字段;`bShowPermission` null → `false`;`iSortOrder` null → `0`;`tCreateDate = LocalDateTime.now()`;`bDeleted = 0`;`sBrandsId/sSubsidiaryId/sCreatedBy/sId/tDeletedDate/sDeletedBy = null` | |
| 274 | + 4. `moduleMapper.insert(entity)`;MyBatis-Plus 回写 `iIncrement` 到 entity | |
| 275 | + 5. 捕获 `DuplicateKeyException`(并发下 uk_procedure_name 兜底)→ 抛 `BizException(MOD_PROC_NAME_DUP)` | |
| 276 | + 6. `return ModuleVO.from(entity)` | |
| 277 | +- 标 `@Transactional(rollbackFor = Exception.class)` | |
| 278 | + | |
| 279 | +- [ ] **Step 7.1 写失败测试(6 个)** | |
| 280 | + - `ModuleServiceImplTest#create_rootModule_returnsVOWithGeneratedId` | |
| 281 | + - `ModuleServiceImplTest#create_childModule_validatesParentExists` | |
| 282 | + - `ModuleServiceImplTest#create_parentNotFound_throwsBizException40411` | |
| 283 | + - `ModuleServiceImplTest#create_parentSoftDeleted_throwsBizException40411` | |
| 284 | + - `ModuleServiceImplTest#create_duplicateProcedureName_preCheck_throwsBizException40911` | |
| 285 | + - `ModuleServiceImplTest#create_duplicateProcedureName_concurrentInsert_throwsBizException40911`(mock `moduleMapper.insert` 抛 `DuplicateKeyException`) | |
| 286 | + - 测试方式: `@ExtendWith(MockitoExtension.class)` + `@InjectMocks ModuleServiceImpl` + `@Mock ModuleMapper` | |
| 287 | + - 子会话: FAIL | |
| 288 | + | |
| 289 | +- [ ] **Step 7.2 实现 ModuleService + ModuleServiceImpl** | |
| 290 | + - 子会话: PASS | |
| 291 | + | |
| 292 | +- [ ] **Step 7.3 提交** | |
| 293 | + - `git commit -m "feat(mod): create module service REQ-MOD-001"` | |
| 294 | + | |
| 295 | +--- | |
| 296 | + | |
| 297 | +### Task 8: ModuleController + 端到端集成测试 | |
| 298 | + | |
| 299 | +**Files:** | |
| 300 | +- Create: `backend/src/main/java/com/xly/erp/module/mod/controller/ModuleController.java` | |
| 301 | +- Test: `backend/src/test/java/com/xly/erp/module/mod/controller/ModuleControllerIT.java` | |
| 302 | + | |
| 303 | +**API shape:** | |
| 304 | +- `@RestController @RequestMapping("/api/modules")` | |
| 305 | +- `@PostMapping public ApiResponse<ModuleVO> create(@Valid @RequestBody ModuleCreateDTO dto)` → `ApiResponse.ok(moduleService.create(dto))` | |
| 306 | +- 类上保留注释:`// REQ-USR-004 完成后追加 @PreAuthorize("hasAuthority('MOD:CREATE')")` | |
| 307 | + | |
| 308 | +- [ ] **Step 8.1 写失败测试(6 个验收用例)** | |
| 309 | + - `ModuleControllerIT#post_validRootModule_returns200WithVO` | |
| 310 | + - `ModuleControllerIT#post_validChildModule_returns200` | |
| 311 | + - `ModuleControllerIT#post_duplicateProcedureName_returns200WithCode40911` | |
| 312 | + - `ModuleControllerIT#post_parentNotFound_returns200WithCode40411` | |
| 313 | + - `ModuleControllerIT#post_missingRequiredField_returns200WithCode40010` | |
| 314 | + - `ModuleControllerIT#post_invalidDisplayTypeEnum_returns200WithCode40010` | |
| 315 | + - 测试方式: `@SpringBootTest @AutoConfigureMockMvc @ActiveProfiles("test") @Transactional`(自动回滚),用 `MockMvc` 发 POST,断言 `$.code` / `$.data.iIncrement` / `$.data.sProcedureName` 等 | |
| 316 | + - 子会话: FAIL | |
| 317 | + | |
| 318 | +- [ ] **Step 8.2 实现 ModuleController** | |
| 319 | + - 子会话: PASS | |
| 320 | + | |
| 321 | +- [ ] **Step 8.3 全量 backend 测试** | |
| 322 | + - `cd backend && mvn -B test -DSPRING_PROFILES_ACTIVE=test` | |
| 323 | + - 期望全绿(Task 1-8 累计 ~25 个测试方法) | |
| 324 | + | |
| 325 | +- [ ] **Step 8.4 提交** | |
| 326 | + - `git commit -m "feat(mod): POST /api/modules controller REQ-MOD-001"` | |
| 327 | + | |
| 328 | +--- | |
| 329 | + | |
| 330 | +## 提交计划 | |
| 331 | + | |
| 332 | +- `chore(mod): bootstrap spring boot skeleton REQ-MOD-001`(覆盖 Task 1) | |
| 333 | +- `feat(common): unified ApiResponse and ErrorCode REQ-MOD-001`(覆盖 Task 2) | |
| 334 | +- `feat(common): biz exception and global handler REQ-MOD-001`(覆盖 Task 3) | |
| 335 | +- `feat(config): permitAll security skeleton REQ-MOD-001`(覆盖 Task 4) | |
| 336 | +- `feat(mod): module entity and mapper REQ-MOD-001`(覆盖 Task 5) | |
| 337 | +- `feat(mod): module create DTO and VO REQ-MOD-001`(覆盖 Task 6) | |
| 338 | +- `feat(mod): create module service REQ-MOD-001`(覆盖 Task 7) | |
| 339 | +- `feat(mod): POST /api/modules controller REQ-MOD-001`(覆盖 Task 8) | ... | ... |
docs/superpowers/reviews/2026-05-06-REQ-MOD-001.md
0 → 100644
| 1 | +--- | |
| 2 | +req_id: REQ-MOD-001 | |
| 3 | +date: 2026-05-06 | |
| 4 | +round: 1 | |
| 5 | +reviewer: superpower-code-reviewer | |
| 6 | +--- | |
| 7 | + | |
| 8 | +# Review: REQ-MOD-001 — round 1 | |
| 9 | + | |
| 10 | +## 结论 | |
| 11 | +approve | |
| 12 | + | |
| 13 | +## Must-fix | |
| 14 | +(无) | |
| 15 | + | |
| 16 | +## Nice-to-have | |
| 17 | + | |
| 18 | +- backend/src/main/java/com/xly/erp/common/exception/GlobalExceptionHandler.java:30 — 兜底 `@ExceptionHandler(Exception.class)` 会吃掉 `HttpMessageNotReadableException` / `HttpRequestMethodNotSupportedException` / `MissingServletRequestParameterException` 等参数类异常并映射到 50000;语义上更接近 40010。建议下一个 REQ 顺手为这几类异常补独立 handler。 | |
| 19 | +- backend/src/main/java/com/xly/erp/config/SecurityConfig.java:18 — `csrf -> csrf.disable()` 等三处 lambda 可改用方法引用 `AbstractHttpConfigurer::disable`,更符合 Spring Security 6 官方示例风格。 | |
| 20 | +- backend/src/main/java/com/xly/erp/module/mod/service/impl/ModuleServiceImpl.java:39 — `.eq(ModuleEntity::getBDeleted, false)` 可读性建议统一用 `Boolean.FALSE` 或抽常量,依赖 JDBC 默认转换不影响正确性。 | |
| 21 | +- backend/src/main/java/com/xly/erp/module/mod/service/impl/ModuleServiceImpl.java:26 / Controller / Mapper / Entity — spec § 代码与文档要求关键类贴 `// REQ-MOD-001 模块新增` 标签;目前 Controller 仅有 REQ-USR-004 备忘注释,缺 REQ-MOD-001 标签,Service / Mapper / Entity / DTO / VO 也均未显式标。建议补一行类级注释。 | |
| 22 | +- backend/src/main/java/com/xly/erp/config/JacksonConfig.java:18 — JacksonConfig 是计划外补丁(修复匈牙利前缀字段 JSON 序列化);plan 文件清单未列出。建议补一条 ObjectMapper 序列化测试断言 `iIncrement` 而非 `IIncrement`,把这个隐式契约钉死。 | |
| 23 | +- backend/src/test/java/com/xly/erp/module/mod/controller/ModuleControllerIT.java:55 — `post_validRootModule` 应额外断言 VO 字段值(sDisplayType / sModuleNameZh / iSortOrder / tCreateDate / bShowPermission),spec 验收 § 1 要求"DB 中查询新记录字段与入参一致"。 | |
| 24 | +- backend/src/test/java/com/xly/erp/module/mod/controller/ModuleControllerIT.java:77 — `post_validChildModule` 没有断言 iSortOrder / bShowPermission / sCreatedBy 留 NULL 等持久化字段,建议补 select-after-insert 验证。 | |
| 25 | +- backend/src/main/java/com/xly/erp/common/response/ApiResponse.java:28 — `fail(ErrorCode ec, String detail)` 把 message 替换为 detail,前端会吃到带英文字段名的中文混合串。建议将 detail 单列字段或保留 ec.getMessage() 作为 message。 | |
| 26 | +- backend/src/main/resources/application.yml:21 — 全局 `map-underscore-to-camel-case: false` 对未来非匈牙利前缀字段有传染影响;当前 entity 已显式 `@TableField`,可改回 MyBatis-Plus 默认 true。 | |
| 27 | + | |
| 28 | +## 反例 / 测试覆盖缺口 | |
| 29 | + | |
| 30 | +1. spec § 验收 § 功能正确性 1 要求"DB 中查询新记录字段与入参一致"——目前 `ControllerIT` 只断言 3 个字段、未回查 DB;`ModuleMapperIT` 单独覆盖 mapper 层往返,端到端层面字段一致性不直观。 | |
| 31 | +2. spec § 测试覆盖列出"并发同 sProcedureName"用例标可选,未实现——可接受。 | |
| 32 | +3. spec § 业务规则 5 要求 `bDeleted=0` / `tDeletedDate=NULL` / `sDeletedBy=NULL` / `sBrandsId/sSubsidiaryId/sCreatedBy=NULL` 在 INSERT 后真正落库——`ModuleServiceImplTest` 用 mock 不能验证 entity 在传给 `mapper.insert` 前的赋值;建议加 `ArgumentCaptor` 断言。 | |
| 33 | +4. Controller 层缺 `HttpMessageNotReadableException` 用例(POST 非法 JSON 当前会走 50000 兜底);spec 未显式要求,但 docs/04 § 1.4 友好错误码原则下属盲区。 | |
| 34 | +5. 计划清单列出的 `MybatisPlusConfig.java` 未创建——plan 自身注明"REQ-MOD-001 不用,先建空骨架",YAGNI 角度可接受;建议在 plan 上同步勾掉或删除该行。 | |
| 35 | +6. 未发现硬编码凭据、未跨模块改动、未引入技术栈外组件、commit 全部带 `REQ-MOD-001` 标签、响应不回显堆栈——全部合规。spec 写明的 REQ-USR-004 技术债(permitAll + 多租户字段 NULL + sCreatedBy NULL)落地受控,无失控扩散。 | ... | ... |
docs/superpowers/specs/2026-05-06-REQ-MOD-001.md
0 → 100644
| 1 | +--- | |
| 2 | +req_id: REQ-MOD-001 | |
| 3 | +date: 2026-05-06 | |
| 4 | +module: module_mod | |
| 5 | +--- | |
| 6 | + | |
| 7 | +# Spec: REQ-MOD-001 — 模块新增 | |
| 8 | + | |
| 9 | +## 目标 | |
| 10 | + | |
| 11 | +实现后端 `POST /api/modules` 接口:将一条新的业务模块定义写入 `tModule` 表,作为 ERP 系统功能与权限分组的基础单位,返回新模块主键 `iIncrement` 与完整 VO。 | |
| 12 | + | |
| 13 | +本 REQ 是 module_mod 模块的第一个 REQ,也是整个 B 阶段的首个 REQ;需顺带建立 Spring Boot 项目骨架(最小可运行单元),仅落地能让本接口工作的横切组件。其余横切组件(JWT 鉴权、登录用户上下文)按 docs/02 § 三 约定由 REQ-USR-004 首次落地。 | |
| 14 | + | |
| 15 | +## 输入 / 触发 | |
| 16 | + | |
| 17 | +**接口**:`POST /api/modules`,Content-Type `application/json`。 | |
| 18 | + | |
| 19 | +**Request body**(`ModuleCreateDTO`)字段: | |
| 20 | + | |
| 21 | +| 字段 | 类型 | 必填 | 校验 / 取值 | 落库列 | | |
| 22 | +|---|---|---|---|---| | |
| 23 | +| `sDisplayType` | String | 是 | 枚举:`手机端` / `前端业务` / `系统配置` / `接口` | `tModule.sDisplayType` | | |
| 24 | +| `sProcedureName` | String | 是 | 长度 1-100;系统内唯一(`bDeleted=0` 范围内) | `tModule.sProcedureName` | | |
| 25 | +| `sModuleType` | String | 是 | 长度 1-50(自由文本) | `tModule.sModuleType` | | |
| 26 | +| `sManageDeptEn` | String | 是 | 长度 1-50(自由文本) | `tModule.sManageDeptEn` | | |
| 27 | +| `bShowPermission` | Boolean | 否 | 默认 `false` | `tModule.bShowPermission` | | |
| 28 | +| `sModuleNameZh` | String | 是 | 长度 1-100 | `tModule.sModuleNameZh` | | |
| 29 | +| `iParentId` | Integer | 否 | 必须指向已存在且未软删除的 `tModule.iIncrement` | `tModule.iParentId` | | |
| 30 | +| `iSortOrder` | Integer | 否 | 默认 `0`;非负整数 | `tModule.iSortOrder` | | |
| 31 | + | |
| 32 | +**鉴权**:契约要求 `Authorization: Bearer <accessToken>` + 权限码 `MOD:CREATE`。本 REQ 暂不实施实际鉴权(见 § 边界与约束),但 Controller 仍按需要鉴权的形态编写(无 `@AnonymousAccess` 标注),以便 REQ-USR-004 加入 SecurityFilterChain 后零改动。 | |
| 33 | + | |
| 34 | +## 输出 / 结果 | |
| 35 | + | |
| 36 | +**HTTP 200,响应体**(统一响应格式): | |
| 37 | + | |
| 38 | +```json | |
| 39 | +{ | |
| 40 | + "code": 200, | |
| 41 | + "message": "操作成功", | |
| 42 | + "data": { | |
| 43 | + "iIncrement": 12, | |
| 44 | + "sDisplayType": "前端业务", | |
| 45 | + "sProcedureName": "sp_audit_user_module", | |
| 46 | + "sModuleType": "USR", | |
| 47 | + "sManageDeptEn": "IT", | |
| 48 | + "bShowPermission": false, | |
| 49 | + "sModuleNameZh": "用户管理", | |
| 50 | + "iParentId": null, | |
| 51 | + "iSortOrder": 0, | |
| 52 | + "tCreateDate": "2026-05-06T10:30:00", | |
| 53 | + "bDeleted": false | |
| 54 | + }, | |
| 55 | + "timestamp": 1746528600000 | |
| 56 | +} | |
| 57 | +``` | |
| 58 | + | |
| 59 | +返回 VO(`ModuleVO`)字段:`iIncrement` / `sDisplayType` / `sProcedureName` / `sModuleType` / `sManageDeptEn` / `bShowPermission` / `sModuleNameZh` / `iParentId` / `iSortOrder` / `tCreateDate` / `bDeleted`。其他标准列(`sId` / `sBrandsId` / `sSubsidiaryId` / `sCreatedBy` / `tDeletedDate` / `sDeletedBy`)不对外暴露。 | |
| 60 | + | |
| 61 | +## 业务规则 | |
| 62 | + | |
| 63 | +1. **唯一性**:`sProcedureName` 在未软删除范围内(`bDeleted=0`)系统内唯一。冲突返回错误码 `40911`。 | |
| 64 | +2. **父模块校验**:若 `iParentId` 非空,必须指向 `tModule` 中存在且 `bDeleted=0` 的记录;不存在或已删除返回错误码 `40411`。 | |
| 65 | +3. **bShowPermission 默认值**:未传入或传 `null` → 落库 `0`。 | |
| 66 | +4. **iSortOrder 默认值**:未传入 → 落库 `0`。 | |
| 67 | +5. **新建记录初始状态**:`bDeleted=0`、`tDeletedDate=NULL`、`sDeletedBy=NULL`、`tCreateDate=now()`(应用层取系统时间,不依赖 DB DEFAULT)。 | |
| 68 | +6. **多租户字段 (`sBrandsId` / `sSubsidiaryId`)**:本 REQ **不写入**(落库 `NULL`,列已 nullable)。多租户上下文将由 REQ-USR-004 引入登录会话后注入;后续 REQ 通过迁移补齐已有数据。 | |
| 69 | +7. **`sCreatedBy`**:本 REQ 落库 `NULL`。等待 REQ-USR-004 引入登录用户上下文后从 `SecurityContextHolder` 取当前用户号回写。 | |
| 70 | +8. **`sId`**(业务 ID 标准列):本 REQ 落库 `NULL`,不在本接口分配。后续若有外部对接需求再以独立 migration 补齐策略。 | |
| 71 | + | |
| 72 | +## 边界与约束 | |
| 73 | + | |
| 74 | +### 鉴权策略(本 REQ 限定) | |
| 75 | + | |
| 76 | +- 项目首个 REQ,尚无 SecurityFilterChain。本 REQ 在 `SecurityConfig` 里配置 `permitAll()` 临时放行所有 `/api/**`。 | |
| 77 | +- Controller 不写 `@PreAuthorize("hasAuthority('MOD:CREATE')")`(无 SecurityContext 时该注解会因 `Authentication=null` 抛 `AccessDeniedException`);改为在 Controller 上方写一行说明性注释:`// REQ-USR-004 完成后改为 @PreAuthorize("hasAuthority('MOD:CREATE')")`。 | |
| 78 | +- REQ-USR-004 完成后会回头给本接口加 `@PreAuthorize`,并把 `permitAll` 改为 `authenticated()`。这是已知技术债,**仅在 REQ-USR-004 范围内偿还**,本 REQ 不擅自前置。 | |
| 79 | + | |
| 80 | +### 字段长度与字符集 | |
| 81 | + | |
| 82 | +- 字符集统一 `utf8mb4`(DDL 已约束),允许中文落库。 | |
| 83 | +- 字符串字段超出 DDL 长度限制视为参数错误(`40010`),不截断。 | |
| 84 | + | |
| 85 | +### 事务 | |
| 86 | + | |
| 87 | +- Service 方法标注 `@Transactional(rollbackFor = Exception.class)`,单表写入即可,事务范围最小化。 | |
| 88 | + | |
| 89 | +### 性能与并发 | |
| 90 | + | |
| 91 | +- 本 REQ 是单条 INSERT,预期无高并发。`uk_procedure_name` 唯一约束兜底并发竞争;唯一冲突映射为 `40911`。 | |
| 92 | + | |
| 93 | +### 项目骨架引导(首 REQ 一次性附带) | |
| 94 | + | |
| 95 | +本 REQ 顺带建立**最小可运行**的 Spring Boot 项目骨架(仅引入服务于本 REQ 的部分;后续 REQ 按需扩充)。预期新增目录与文件: | |
| 96 | + | |
| 97 | +``` | |
| 98 | +backend/ | |
| 99 | +├── pom.xml | |
| 100 | +└── src/main/ | |
| 101 | + ├── java/com/xly/erp/ | |
| 102 | + │ ├── ErpApplication.java | |
| 103 | + │ ├── config/ | |
| 104 | + │ │ ├── MybatisPlusConfig.java | |
| 105 | + │ │ └── SecurityConfig.java // 临时 permitAll | |
| 106 | + │ ├── common/ | |
| 107 | + │ │ ├── response/ | |
| 108 | + │ │ │ ├── ApiResponse.java | |
| 109 | + │ │ │ └── ErrorCode.java | |
| 110 | + │ │ └── exception/ | |
| 111 | + │ │ ├── BizException.java | |
| 112 | + │ │ └── GlobalExceptionHandler.java | |
| 113 | + │ └── module/mod/ | |
| 114 | + │ ├── controller/ModuleController.java | |
| 115 | + │ ├── service/ModuleService.java | |
| 116 | + │ ├── service/impl/ModuleServiceImpl.java | |
| 117 | + │ ├── mapper/ModuleMapper.java | |
| 118 | + │ ├── entity/Module.java | |
| 119 | + │ ├── dto/ModuleCreateDTO.java | |
| 120 | + │ └── vo/ModuleVO.java | |
| 121 | + └── resources/ | |
| 122 | + ├── application.yml // ${DB_HOST}/${DB_PORT}/... 占位,从 .env.local 注入 | |
| 123 | + └── mapper/mod/ModuleMapper.xml | |
| 124 | +``` | |
| 125 | + | |
| 126 | +`pom.xml` 依赖:`spring-boot-starter-web` / `-validation` / `-security`、`mybatis-plus-spring-boot3-starter`、`flyway-core` + `flyway-mysql`、`mysql-connector-j`、`mapstruct`、`hutool-all`、`spring-boot-starter-test`、`spring-security-test`。 | |
| 127 | + | |
| 128 | +`application.yml` 通过 Spring Profile + `dotenv-java`(或本地启动脚本)加载 `.env.local`;不在仓内硬编码任何凭据(与 `docs/04 § 3.5` 一致)。 | |
| 129 | + | |
| 130 | +## 依赖的 schema 表 / 字段 | |
| 131 | + | |
| 132 | +**写表**:`tModule`(详见 `docs/03-数据库设计文档.md` § tModule) | |
| 133 | + | |
| 134 | +| 字段 | 落库逻辑 | | |
| 135 | +|---|---| | |
| 136 | +| `iIncrement` | DB 自增分配 | | |
| 137 | +| `sId` | 落 `NULL`(本 REQ 不分配业务 ID) | | |
| 138 | +| `sBrandsId` | 落 `NULL`(多租户 REQ-USR-004 后引入) | | |
| 139 | +| `sSubsidiaryId` | 落 `NULL`(同上) | | |
| 140 | +| `tCreateDate` | 应用层 `LocalDateTime.now()` | | |
| 141 | +| `sDisplayType` | 入参(必填) | | |
| 142 | +| `sProcedureName` | 入参(必填,唯一) | | |
| 143 | +| `sModuleType` | 入参(必填) | | |
| 144 | +| `sManageDeptEn` | 入参(必填) | | |
| 145 | +| `bShowPermission` | 入参(默认 `false`) | | |
| 146 | +| `sModuleNameZh` | 入参(必填) | | |
| 147 | +| `iParentId` | 入参(可选;FK 校验通过的 `tModule.iIncrement`) | | |
| 148 | +| `iSortOrder` | 入参(默认 `0`) | | |
| 149 | +| `sCreatedBy` | 落 `NULL`(REQ-USR-004 后引入登录上下文) | | |
| 150 | +| `bDeleted` | 落 `0` | | |
| 151 | +| `tDeletedDate` | 落 `NULL` | | |
| 152 | +| `sDeletedBy` | 落 `NULL` | | |
| 153 | + | |
| 154 | +**索引利用**: | |
| 155 | +- `uk_procedure_name` UNIQUE:唯一性约束触发并发兜底 | |
| 156 | +- `idx_parent`:父模块查询场景(FK 校验时按 `iParentId` 查 `tModule`) | |
| 157 | + | |
| 158 | +**外键**:本 REQ 利用 `fk_module_parent` 作为 DB 层兜底;应用层在写入前先查父模块存在性,提前返回 `40411`,避免直接抛 SQL 完整性异常。 | |
| 159 | + | |
| 160 | +## 依赖的接口 | |
| 161 | + | |
| 162 | +无(本接口是 module_mod 的入口接口,不依赖其他业务接口)。 | |
| 163 | + | |
| 164 | +后续在 REQ-USR-004 完成后,需要回归补齐本接口的 `@PreAuthorize("hasAuthority('MOD:CREATE')")`,并要求请求携带有效 JWT。 | |
| 165 | + | |
| 166 | +## 验收标准 | |
| 167 | + | |
| 168 | +### 功能正确性 | |
| 169 | + | |
| 170 | +1. **正向 — 根模块**:提交完整字段、`iParentId=null`,返回 200 + `data.iIncrement` 非空;DB 中查询新记录字段与入参一致;`bDeleted=0` / `tCreateDate` 已写入。 | |
| 171 | +2. **正向 — 子模块**:先创建 root 再以其 `iIncrement` 作为 `iParentId` 创建子模块,返回 200。 | |
| 172 | +3. **唯一性冲突**:用相同 `sProcedureName` 二次提交,返回 `code=40911` + 中文 message;DB 不产生重复记录。 | |
| 173 | +4. **父模块不存在**:传入 `iParentId=999999`,返回 `code=40411`。 | |
| 174 | +5. **必填缺失 / 枚举非法 / 长度超限**:返回 `code=40010` + 错误字段名定位。 | |
| 175 | +6. **可选默认值**:不传 `bShowPermission` / `iSortOrder` → DB 落 `false / 0`。 | |
| 176 | + | |
| 177 | +### 接口契约一致性 | |
| 178 | + | |
| 179 | +- 响应格式严格符合 `{code, message, data, timestamp}`(docs/05 § 全局约定)。 | |
| 180 | +- 错误码段位与 docs/05 一致:`200` / `40010` / `40911` / `40411` / `500xx`。 | |
| 181 | +- 异常堆栈不出现在响应里(GlobalExceptionHandler 拦截并映射为友好错误,docs/04 § 1.4)。 | |
| 182 | + | |
| 183 | +### 测试覆盖(feature-tdd 阶段) | |
| 184 | + | |
| 185 | +- **单元测试**:`ModuleServiceImpl#create` 覆盖 6 类正/反路径(含唯一冲突的 `DuplicateKeyException` 映射)。 | |
| 186 | +- **集成测试(MockMvc + 真实 MySQL test schema)**: | |
| 187 | + - 正向:根模块、子模块创建后查 DB 验证字段 | |
| 188 | + - 反向:唯一冲突、父模块不存在、必填缺失、枚举非法、长度超限 ≥ 5 个 case | |
| 189 | + - 并发:两线程同时插入同 `sProcedureName`,断言一条成功 + 一条 40911(可选,若 CI 跑得动) | |
| 190 | +- **测试数据隔离**:每个测试方法 `@Transactional` 自动回滚;不污染其他测试。 | |
| 191 | + | |
| 192 | +### 代码与文档 | |
| 193 | + | |
| 194 | +- `// REQ-MOD-001` 注释贴在 Controller / Service / Mapper / DTO / VO 关键类。 | |
| 195 | +- 提交按 `feat(mod): add module create endpoint REQ-MOD-001` 规范。 | |
| 196 | +- 不引入 docs/04 § 零 技术栈外的依赖(如需,按软规则 S1 经 AskUserQuestion)。 | ... | ... |