2026-05-06-REQ-MOD-001.md 19.3 KB

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<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-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=8080spring.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<T>:字段 int codeString messageT datalong 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_setsCode200AndDataAndTimestampApiResponseTest#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 处理:

    • BizExceptionApiResponse.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_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(仅头部 <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 属性命名采用保留前缀(如 iIncrement Java 字段名也叫 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_persistsAllFieldsModuleMapperIT#selectCountByProcedureName_returnsExisting
    • 测试方式: @SpringBootTest @ActiveProfiles("test") + @Transactional(自动回滚),DI ModuleMapper,构造 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_yieldsViolationsModuleCreateDTOValidationTest#invalidDisplayTypeEnum_yieldsViolationModuleCreateDTOValidationTest#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 != nullmoduleMapper.selectById(iParentId),结果为 nullbDeleted == 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 → falseiSortOrder null → 0tCreateDate = LocalDateTime.now()bDeleted = 0sBrandsId/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.insertDuplicateKeyException
    • 测试方式: @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_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)