--- req_id: REQ-MOD-001 date: 2026-05-06 spec_ref: docs/superpowers/specs/2026-05-06-REQ-MOD-001.md --- # REQ-MOD-001 模块新增 Implementation Plan > **Execution:** Parent skill `feature-tdd` executes this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** 实现 `POST /api/modules` 接口,把一条新业务模块写入 `tModule`,并顺带建立服务于本 REQ 的最小 Spring Boot 骨架。 **Architecture:** 标准三层(Controller → Service → Mapper)。Spring Boot 3 + MyBatis-Plus + Flyway 自动 apply migrations。鉴权暂以 `SecurityConfig` permitAll 占位,REQ-USR-004 时回头收紧。统一响应 `ApiResponse`、统一异常 `BizException` + `GlobalExceptionHandler`。 **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。 --- ## Schema 改动 无(`tModule` 已由 A4 `V1__initial_schema.sql` 创建)。 ## 文件变更清单 - `backend/pom.xml` — 创建(Spring Boot 父 pom + 依赖清单) - `backend/src/main/java/com/xly/erp/ErpApplication.java` — 创建(启动类) - `backend/src/main/resources/application.yml` — 创建(DB / Flyway / MyBatis-Plus / 端口配置,从环境变量注入) - `backend/src/main/resources/application-test.yml` — 创建(测试 profile,连 DB_SCHEMA test 库) - `backend/src/main/java/com/xly/erp/common/response/ApiResponse.java` — 创建(统一响应包装) - `backend/src/main/java/com/xly/erp/common/response/ErrorCode.java` — 创建(错误码枚举) - `backend/src/main/java/com/xly/erp/common/exception/BizException.java` — 创建(业务异常) - `backend/src/main/java/com/xly/erp/common/exception/GlobalExceptionHandler.java` — 创建(@RestControllerAdvice) - `backend/src/main/java/com/xly/erp/config/SecurityConfig.java` — 创建(permitAll 临时配置) - `backend/src/main/java/com/xly/erp/config/MybatisPlusConfig.java` — 创建(分页插件,本 REQ 不用,REQ-MOD-004 用,本任务先建空骨架) - `backend/src/main/java/com/xly/erp/module/mod/entity/ModuleEntity.java` — 创建(@TableName("tModule")) - `backend/src/main/java/com/xly/erp/module/mod/mapper/ModuleMapper.java` — 创建(继承 BaseMapper) - `backend/src/main/resources/mapper/mod/ModuleMapper.xml` — 创建(空骨架,本 REQ 仅用 BaseMapper 默认 SQL) - `backend/src/main/java/com/xly/erp/module/mod/dto/ModuleCreateDTO.java` — 创建(入参 + Bean Validation) - `backend/src/main/java/com/xly/erp/module/mod/vo/ModuleVO.java` — 创建(出参) - `backend/src/main/java/com/xly/erp/module/mod/service/ModuleService.java` — 创建(接口) - `backend/src/main/java/com/xly/erp/module/mod/service/impl/ModuleServiceImpl.java` — 创建(实现,含 create 方法) - `backend/src/main/java/com/xly/erp/module/mod/controller/ModuleController.java` — 创建(POST /api/modules) - `backend/src/test/java/com/xly/erp/common/response/ApiResponseTest.java` — 创建(单元) - `backend/src/test/java/com/xly/erp/common/exception/GlobalExceptionHandlerTest.java` — 创建(单元) - `backend/src/test/java/com/xly/erp/module/mod/mapper/ModuleMapperIT.java` — 创建(@SpringBootTest 集成) - `backend/src/test/java/com/xly/erp/module/mod/service/ModuleServiceImplTest.java` — 创建(单元 + Mockito) - `backend/src/test/java/com/xly/erp/module/mod/controller/ModuleControllerIT.java` — 创建(MockMvc + @SpringBootTest) - `backend/src/test/resources/application-test.yml` — 创建(测试 profile 副本,便于 IDE 跑测试) - `.env.local` — 不修改(已有 DB_HOST / DB_PORT / DB_USER / DB_PASSWORD / DB_SCHEMA / JWT_SECRET) --- ## 任务步骤 ### Task 1: 引导 Spring Boot 项目骨架(compile + boot 通过) **Files:** - Create: `backend/pom.xml` - Create: `backend/src/main/java/com/xly/erp/ErpApplication.java` - Create: `backend/src/main/resources/application.yml` - Create: `backend/src/main/resources/application-test.yml` - Create: `backend/src/test/resources/application-test.yml`(与上一行同内容,Spring Boot 测试时从 test/resources 优先加载) - Create: `backend/src/main/java/com/xly/erp/ErpApplicationTest.java`(验证 context 启动) **API shape:** - `ErpApplication` 主类标 `@SpringBootApplication`、`@MapperScan("com.xly.erp.**.mapper")` - `application.yml` 通过 `${DB_HOST}` / `${DB_PORT}` / `${DB_USER}` / `${DB_PASSWORD}` / `${DB_SCHEMA}` / `${JWT_SECRET}` 占位(值由本地启动脚本或 IDE EnvFile 插件从 `.env.local` 注入;详见 docs/04 § 3.5) - 启用 Flyway:`spring.flyway.locations=classpath:db/migration` —— 但项目 migrations 在 `sql/migrations/`;改为 `spring.flyway.locations=filesystem:../sql/migrations` 让 backend 直接复用仓库根的 V*.sql **pom.xml 锁定依赖**(写死,不让 TDD 自由选): - `spring-boot-starter-parent` 3.2.x - `spring-boot-starter-web`、`-validation`、`-security`、`-test` - `spring-security-test` - `mybatis-plus-spring-boot3-starter` 最新稳定版(写明确版本号,本任务由 TDD 确认 mvn central 最新可用) - `flyway-core` + `flyway-mysql` 10.x - `mysql-connector-j` 8.x - `org.projectlombok:lombok`(编译期)+ `lombok-mapstruct-binding` - `org.mapstruct:mapstruct` 1.5.x + `mapstruct-processor` - `cn.hutool:hutool-all` 5.x - Java 编译目标 17 - [ ] **Step 1.1 写测试** - 文件: `backend/src/test/java/com/xly/erp/ErpApplicationTest.java` - 测试名: `ErpApplicationTest#contextLoads` - 内容: 空 `@SpringBootTest @ActiveProfiles("test")` + 空 test 方法 - 意图: ApplicationContext 能在 test profile 下成功启动 + Flyway 能 apply V1(apply 到现有 test schema 应是 no-op,因为表已存在;Flyway 跑完会建 `flyway_schema_history` 表 + 注册 V1 已 baseline) - 子会话确认 FAIL(项目还没建好) - [ ] **Step 1.2 创建 pom.xml** - 仅声明依赖、Java 17、surefire 配置 - `mvn -B -f backend/pom.xml dependency:resolve` 应通过 - [ ] **Step 1.3 创建 ErpApplication + application.yml** - `ErpApplication` 仅做启动 + `@MapperScan` - `application.yml` 主键:`server.port=8080`、`spring.profiles.active=${SPRING_PROFILES_ACTIVE:dev}`、datasource、mybatis-plus、flyway - `application-test.yml`:override `spring.flyway.baseline-on-migrate=true` + `spring.flyway.baseline-version=0`(让 Flyway 把当前已存在的 schema 作为 baseline,再追 apply) - 子会话 `cd backend && mvn -B test -Dtest=ErpApplicationTest#contextLoads -DSPRING_PROFILES_ACTIVE=test` 应 PASS - [ ] **Step 1.4 提交** - `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` - `git commit -m "chore(mod): bootstrap spring boot skeleton REQ-MOD-001"` --- ### Task 2: 统一响应与错误码(ApiResponse + ErrorCode) **Files:** - Create: `backend/src/main/java/com/xly/erp/common/response/ApiResponse.java` - Create: `backend/src/main/java/com/xly/erp/common/response/ErrorCode.java` - Test: `backend/src/test/java/com/xly/erp/common/response/ApiResponseTest.java` **API shape:** - `ApiResponse`:字段 `int code`、`String message`、`T data`、`long timestamp`;静态工厂 `ok(T data)`、`ok(String message, T data)`、`fail(ErrorCode ec)`、`fail(int code, String message)` - `ErrorCode` 枚举(int code, String message)至少包含: - `SUCCESS(200, "操作成功")` - `PARAM_INVALID(40010, "参数错误")` - `MODULE_PARENT_NOT_FOUND(40411, "父模块不存在或已删除")` - `MODULE_PROCEDURE_NAME_DUPLICATE(40911, "存储过程名称已存在")` - `INTERNAL_ERROR(50000, "服务器内部错误")` **锁定常量**(跨模块复用,写死): ``` SUCCESS = 200 PARAM_INVALID = 40010 MOD_PARENT_NOT_FOUND = 40411 MOD_PROC_NAME_DUP = 40911 INTERNAL_ERROR = 50000 ``` - [ ] **Step 2.1 写失败测试** - 测试名: `ApiResponseTest#ok_setsCode200AndDataAndTimestamp`、`ApiResponseTest#fail_mapsErrorCodeFields` - 意图: `ok(data)` → code=200,message="操作成功",data=入参,timestamp 在合理范围;`fail(ErrorCode.PARAM_INVALID)` → code=40010,message="参数错误" - 子会话 PASS:FAIL(class 不存在) - [ ] **Step 2.2 实现 ApiResponse + ErrorCode** - 用 Lombok `@Data` 减少样板,但保留显式工厂方法 - 子会话 PASS - [ ] **Step 2.3 提交** - `git commit -m "feat(common): unified ApiResponse and ErrorCode REQ-MOD-001"` --- ### Task 3: 业务异常 + 全局异常处理器 **Files:** - Create: `backend/src/main/java/com/xly/erp/common/exception/BizException.java` - Create: `backend/src/main/java/com/xly/erp/common/exception/GlobalExceptionHandler.java` - Test: `backend/src/test/java/com/xly/erp/common/exception/GlobalExceptionHandlerTest.java`(用 MockMvc + 一个临时 controller 触发各类异常) **API shape:** - `BizException extends RuntimeException`:构造 `BizException(ErrorCode ec)`、`BizException(ErrorCode ec, String detail)`;getter: `code()`, `message()` - `GlobalExceptionHandler` 处理: - `BizException` → `ApiResponse.fail(ec)` 200 OK(业务错误也走 200 + 内部 code,docs/05 § 全局约定) - `MethodArgumentNotValidException`(@Valid 失败)→ 提取 fieldError,组合 `ApiResponse.fail(PARAM_INVALID, ": ")` - `Exception`(兜底)→ `ApiResponse.fail(INTERNAL_ERROR)` + log error;**响应不含堆栈**(docs/04 § 1.4) - [ ] **Step 3.1 写失败测试** - 测试名(4 个): - `GlobalExceptionHandlerTest#bizException_returns200WithBizCode` - `GlobalExceptionHandlerTest#validationException_returns200WithParamInvalidCode` - `GlobalExceptionHandlerTest#uncaughtException_returns200WithInternalErrorCode` - `GlobalExceptionHandlerTest#response_doesNotContainStackTrace` - 测试方式: `@WebMvcTest(controllers=DummyController.class)` 内置一个 `DummyController` 故意抛各类异常,断言 `MockMvc.perform(...).andExpect(jsonPath("$.code").value(...))` - 子会话: FAIL - [ ] **Step 3.2 实现 BizException + Handler** - 子会话: PASS - [ ] **Step 3.3 提交** - `git commit -m "feat(common): biz exception and global handler REQ-MOD-001"` --- ### Task 4: SecurityConfig(permitAll 占位) **Files:** - Create: `backend/src/main/java/com/xly/erp/config/SecurityConfig.java` - Test: `backend/src/test/java/com/xly/erp/config/SecurityConfigTest.java` **API shape:** - `SecurityFilterChain securityFilterChain(HttpSecurity http)`:禁用 CSRF(API 项目)、禁用默认表单登录、所有 `/api/**` permitAll - 显式注释:`// REQ-USR-004 完成后改为 .authenticated() + JWT filter` - [ ] **Step 4.1 写失败测试** - 测试名: `SecurityConfigTest#anyApiEndpoint_isPermittedWithoutAuth` - 测试方式: `@SpringBootTest @AutoConfigureMockMvc` + 在 controller package 临时加一个 `/api/__ping` GET(**或**复用 Task 3 的 `DummyController`),无 token 请求应返回 200 而非 401/403 - 子会话: FAIL(默认 Spring Security 401) - [ ] **Step 4.2 实现 SecurityConfig** - 子会话: PASS - [ ] **Step 4.3 提交** - `git commit -m "feat(config): permitAll security skeleton REQ-MOD-001"` --- ### Task 5: ModuleEntity + ModuleMapper(Mapper 集成测试落库) **Files:** - Create: `backend/src/main/java/com/xly/erp/module/mod/entity/ModuleEntity.java` - Create: `backend/src/main/java/com/xly/erp/module/mod/mapper/ModuleMapper.java` - Create: `backend/src/main/resources/mapper/mod/ModuleMapper.xml`(仅头部 ``,预留扩展) - Test: `backend/src/test/java/com/xly/erp/module/mod/mapper/ModuleMapperIT.java` **API shape:** - `@TableName("tModule")` ;字段名严格匹配 docs/03(保留匈牙利前缀,使用 `@TableField` 显式映射),主键 `iIncrement` 用 `@TableId(type = IdType.AUTO)` - 列 ↔ Java 属性命名采用**保留前缀**(如 `iIncrement` Java 字段名也叫 `iIncrement`,方便对照 schema),与 docs/04 § 1.2 命名约定中的"小驼峰"略有出入——本约束**优先**:DB schema SSoT 是 docs/03,Java 字段直接复用列名以避免双向映射歧义。Lombok `@Data`。 - `ModuleMapper extends BaseMapper`,本 REQ 仅用 `insert` + `selectById` + `selectCount`(条件);不写自定义 SQL - [ ] **Step 5.1 写失败测试** - 测试名: `ModuleMapperIT#insertAndSelectById_persistsAllFields`、`ModuleMapperIT#selectCountByProcedureName_returnsExisting` - 测试方式: `@SpringBootTest @ActiveProfiles("test")` + `@Transactional`(自动回滚),DI `ModuleMapper`,构造 entity 调 `insert`,再 `selectById` 断言全部字段;用 `selectCount(new LambdaQueryWrapper().eq(ModuleEntity::getSProcedureName, ...))` 验证唯一查询 - 子会话: FAIL - [ ] **Step 5.2 实现 Entity + Mapper** - 子会话: PASS(DDL 已存在,直接 insert) - [ ] **Step 5.3 提交** - `git commit -m "feat(mod): module entity and mapper REQ-MOD-001"` --- ### Task 6: DTO + VO **Files:** - Create: `backend/src/main/java/com/xly/erp/module/mod/dto/ModuleCreateDTO.java` - Create: `backend/src/main/java/com/xly/erp/module/mod/vo/ModuleVO.java` - Test: `backend/src/test/java/com/xly/erp/module/mod/dto/ModuleCreateDTOValidationTest.java` **API shape:** `ModuleCreateDTO` 字段(带 Bean Validation): - `@NotBlank @Pattern(regexp="^(手机端|前端业务|系统配置|接口)$") String sDisplayType` - `@NotBlank @Size(max=100) String sProcedureName` - `@NotBlank @Size(max=50) String sModuleType` - `@NotBlank @Size(max=50) String sManageDeptEn` - `Boolean bShowPermission`(可空,service 层 default false) - `@NotBlank @Size(max=100) String sModuleNameZh` - `Integer iParentId`(可空) - `@Min(0) Integer iSortOrder`(可空,default 0) `ModuleVO` 字段(11 个,见 spec § 输出):`iIncrement`, `sDisplayType`, `sProcedureName`, `sModuleType`, `sManageDeptEn`, `bShowPermission`, `sModuleNameZh`, `iParentId`, `iSortOrder`, `tCreateDate`, `bDeleted`。 > Entity → VO 的转换写在 `ModuleVO` 的静态工厂 `from(ModuleEntity)` 里,不引入 MapStruct 单独 mapper 类(YAGNI;REQ 多了再抽)。 - [ ] **Step 6.1 写失败测试** - 测试名: `ModuleCreateDTOValidationTest#blankRequiredFields_yieldsViolations`、`ModuleCreateDTOValidationTest#invalidDisplayTypeEnum_yieldsViolation`、`ModuleCreateDTOValidationTest#allValidFields_yieldsNoViolations` - 测试方式: `Validation.buildDefaultValidatorFactory().getValidator()` + 断言 violation 集合 - 子会话: FAIL - [ ] **Step 6.2 实现 DTO + VO** - 子会话: PASS - [ ] **Step 6.3 提交** - `git commit -m "feat(mod): module create DTO and VO REQ-MOD-001"` --- ### Task 7: ModuleService.create(核心业务) **Files:** - Create: `backend/src/main/java/com/xly/erp/module/mod/service/ModuleService.java` - Create: `backend/src/main/java/com/xly/erp/module/mod/service/impl/ModuleServiceImpl.java` - Test: `backend/src/test/java/com/xly/erp/module/mod/service/ModuleServiceImplTest.java`(Mockito 单元测试,mock ModuleMapper) **API shape:** - `interface ModuleService { ModuleVO create(ModuleCreateDTO dto); }` - `@Service @RequiredArgsConstructor class ModuleServiceImpl implements ModuleService`,字段 `private final ModuleMapper moduleMapper;` - `create` 方法步骤(写在 plan 锁定,不让 TDD 自由发挥逻辑顺序): 1. 若 `dto.iParentId != null`:`moduleMapper.selectById(iParentId)`,结果为 `null` 或 `bDeleted == true` → 抛 `BizException(MOD_PARENT_NOT_FOUND)` 2. 唯一性预检:`moduleMapper.selectCount(LambdaQueryWrapper.eq(ModuleEntity::getSProcedureName, dto.sProcedureName).eq(ModuleEntity::getBDeleted, 0))` > 0 → 抛 `BizException(MOD_PROC_NAME_DUP)` 3. 构造 `ModuleEntity`:复制 dto 字段;`bShowPermission` null → `false`;`iSortOrder` null → `0`;`tCreateDate = LocalDateTime.now()`;`bDeleted = 0`;`sBrandsId/sSubsidiaryId/sCreatedBy/sId/tDeletedDate/sDeletedBy = null` 4. `moduleMapper.insert(entity)`;MyBatis-Plus 回写 `iIncrement` 到 entity 5. 捕获 `DuplicateKeyException`(并发下 uk_procedure_name 兜底)→ 抛 `BizException(MOD_PROC_NAME_DUP)` 6. `return ModuleVO.from(entity)` - 标 `@Transactional(rollbackFor = Exception.class)` - [ ] **Step 7.1 写失败测试(6 个)** - `ModuleServiceImplTest#create_rootModule_returnsVOWithGeneratedId` - `ModuleServiceImplTest#create_childModule_validatesParentExists` - `ModuleServiceImplTest#create_parentNotFound_throwsBizException40411` - `ModuleServiceImplTest#create_parentSoftDeleted_throwsBizException40411` - `ModuleServiceImplTest#create_duplicateProcedureName_preCheck_throwsBizException40911` - `ModuleServiceImplTest#create_duplicateProcedureName_concurrentInsert_throwsBizException40911`(mock `moduleMapper.insert` 抛 `DuplicateKeyException`) - 测试方式: `@ExtendWith(MockitoExtension.class)` + `@InjectMocks ModuleServiceImpl` + `@Mock ModuleMapper` - 子会话: FAIL - [ ] **Step 7.2 实现 ModuleService + ModuleServiceImpl** - 子会话: PASS - [ ] **Step 7.3 提交** - `git commit -m "feat(mod): create module service REQ-MOD-001"` --- ### Task 8: ModuleController + 端到端集成测试 **Files:** - Create: `backend/src/main/java/com/xly/erp/module/mod/controller/ModuleController.java` - Test: `backend/src/test/java/com/xly/erp/module/mod/controller/ModuleControllerIT.java` **API shape:** - `@RestController @RequestMapping("/api/modules")` - `@PostMapping public ApiResponse create(@Valid @RequestBody ModuleCreateDTO dto)` → `ApiResponse.ok(moduleService.create(dto))` - 类上保留注释:`// REQ-USR-004 完成后追加 @PreAuthorize("hasAuthority('MOD:CREATE')")` - [ ] **Step 8.1 写失败测试(6 个验收用例)** - `ModuleControllerIT#post_validRootModule_returns200WithVO` - `ModuleControllerIT#post_validChildModule_returns200` - `ModuleControllerIT#post_duplicateProcedureName_returns200WithCode40911` - `ModuleControllerIT#post_parentNotFound_returns200WithCode40411` - `ModuleControllerIT#post_missingRequiredField_returns200WithCode40010` - `ModuleControllerIT#post_invalidDisplayTypeEnum_returns200WithCode40010` - 测试方式: `@SpringBootTest @AutoConfigureMockMvc @ActiveProfiles("test") @Transactional`(自动回滚),用 `MockMvc` 发 POST,断言 `$.code` / `$.data.iIncrement` / `$.data.sProcedureName` 等 - 子会话: FAIL - [ ] **Step 8.2 实现 ModuleController** - 子会话: PASS - [ ] **Step 8.3 全量 backend 测试** - `cd backend && mvn -B test -DSPRING_PROFILES_ACTIVE=test` - 期望全绿(Task 1-8 累计 ~25 个测试方法) - [ ] **Step 8.4 提交** - `git commit -m "feat(mod): POST /api/modules controller REQ-MOD-001"` --- ## 提交计划 - `chore(mod): bootstrap spring boot skeleton REQ-MOD-001`(覆盖 Task 1) - `feat(common): unified ApiResponse and ErrorCode REQ-MOD-001`(覆盖 Task 2) - `feat(common): biz exception and global handler REQ-MOD-001`(覆盖 Task 3) - `feat(config): permitAll security skeleton REQ-MOD-001`(覆盖 Task 4) - `feat(mod): module entity and mapper REQ-MOD-001`(覆盖 Task 5) - `feat(mod): module create DTO and VO REQ-MOD-001`(覆盖 Task 6) - `feat(mod): create module service REQ-MOD-001`(覆盖 Task 7) - `feat(mod): POST /api/modules controller REQ-MOD-001`(覆盖 Task 8)