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-tddexecutes 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<T>、统一异常 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-parent3.2.x -
spring-boot-starter-web、-validation、-security、-test spring-security-test-
mybatis-plus-spring-boot3-starter最新稳定版(写明确版本号,本任务由 TDD 确认 mvn central 最新可用) -
flyway-core+flyway-mysql10.x -
mysql-connector-j8.x -
org.projectlombok:lombok(编译期)+lombok-mapstruct-binding -
org.mapstruct:mapstruct1.5.x +mapstruct-processor -
cn.hutool:hutool-all5.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:overridespring.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.javagit 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<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) -
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
- 用 Lombok
-
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, "<field>: <reason>") -
Exception(兜底)→ApiResponse.fail(INTERNAL_ERROR)+ log error;响应不含堆栈(docs/04 § 1.4)
-
-
Step 3.1 写失败测试
- 测试名(4 个):
GlobalExceptionHandlerTest#bizException_returns200WithBizCodeGlobalExceptionHandlerTest#validationException_returns200WithParamInvalidCodeGlobalExceptionHandlerTest#uncaughtException_returns200WithInternalErrorCodeGlobalExceptionHandlerTest#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/__pingGET(或复用 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(仅头部<mapper namespace="...ModuleMapper"/>,预留扩展) - 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 属性命名采用保留前缀(如
iIncrementJava 字段名也叫iIncrement,方便对照 schema),与 docs/04 § 1.2 命名约定中的"小驼峰"略有出入——本约束优先:DB schema SSoT 是 docs/03,Java 字段直接复用列名以避免双向映射歧义。Lombok@Data。 ModuleMapper extends BaseMapper<ModuleEntity>,本 REQ 仅用insert+selectById+selectCount(条件);不写自定义 SQL-
Step 5.1 写失败测试
- 测试名:
ModuleMapperIT#insertAndSelectById_persistsAllFields、ModuleMapperIT#selectCountByProcedureName_returnsExisting - 测试方式:
@SpringBootTest @ActiveProfiles("test")+@Transactional(自动回滚),DIModuleMapper,构造 entity 调insert,再selectById断言全部字段;用selectCount(new LambdaQueryWrapper<ModuleEntity>().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 自由发挥逻辑顺序):- 若
dto.iParentId != null:moduleMapper.selectById(iParentId),结果为null或bDeleted == true→ 抛BizException(MOD_PARENT_NOT_FOUND) - 唯一性预检:
moduleMapper.selectCount(LambdaQueryWrapper.eq(ModuleEntity::getSProcedureName, dto.sProcedureName).eq(ModuleEntity::getBDeleted, 0))> 0 → 抛BizException(MOD_PROC_NAME_DUP) - 构造
ModuleEntity:复制 dto 字段;bShowPermissionnull →false;iSortOrdernull →0;tCreateDate = LocalDateTime.now();bDeleted = 0;sBrandsId/sSubsidiaryId/sCreatedBy/sId/tDeletedDate/sDeletedBy = null -
moduleMapper.insert(entity);MyBatis-Plus 回写iIncrement到 entity - 捕获
DuplicateKeyException(并发下 uk_procedure_name 兜底)→ 抛BizException(MOD_PROC_NAME_DUP) return ModuleVO.from(entity)
- 若
标
@Transactional(rollbackFor = Exception.class)-
Step 7.1 写失败测试(6 个)
ModuleServiceImplTest#create_rootModule_returnsVOWithGeneratedIdModuleServiceImplTest#create_childModule_validatesParentExistsModuleServiceImplTest#create_parentNotFound_throwsBizException40411ModuleServiceImplTest#create_parentSoftDeleted_throwsBizException40411ModuleServiceImplTest#create_duplicateProcedureName_preCheck_throwsBizException40911-
ModuleServiceImplTest#create_duplicateProcedureName_concurrentInsert_throwsBizException40911(mockmoduleMapper.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<ModuleVO> create(@Valid @RequestBody ModuleCreateDTO dto)→ApiResponse.ok(moduleService.create(dto)) 类上保留注释:
// REQ-USR-004 完成后追加 @PreAuthorize("hasAuthority('MOD:CREATE')")-
Step 8.1 写失败测试(6 个验收用例)
ModuleControllerIT#post_validRootModule_returns200WithVOModuleControllerIT#post_validChildModule_returns200ModuleControllerIT#post_duplicateProcedureName_returns200WithCode40911ModuleControllerIT#post_parentNotFound_returns200WithCode40411ModuleControllerIT#post_missingRequiredField_returns200WithCode40010ModuleControllerIT#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)