2026-04-29-REQ-MOD-001.md 26.7 KB

req_id: REQ-MOD-001 date: 2026-04-29

spec_ref: docs/superpowers/specs/2026-04-29-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: 在尚未存在的 backend/ Spring Boot 工程中,从零搭起最小后端脚手架(统一响应 / 异常 / JWT Filter / Flyway / MyBatis-Plus)+ 实现 POST /api/mod/modules 完成模块新增(写入 tModule),并以单元 + 集成双层覆盖 spec 验收清单。

Architecture: 三层(controller / service / mapper) + 通用层(response / exception / security / config)。Spring Security 启 JwtAuthenticationFilter 解析 token 写 principal=sUserNo;本 REQ 对 POST /api/mod/modulespermitAll stub(角色硬校验留 USR-004 闭环)。多租户字段 sBrandsId / sSubsidiaryId 通过 application.ymlerp.tenant.* 注入,默认 XLY/XLY。错误码沿用 docs/05 已声明值(40001/40010/40020/40021),认证层错误用 20001

Tech Stack: Spring Boot 3.x · MyBatis-Plus · Spring Security · JJWT · Flyway 10.x · MySQL 8 · Java 17 · Maven 3.9 · JUnit 5 · Mockito · Spring Boot Test。


Schema 改动

无(tModule 已在 sql/migrations/V1__initial_schema.sql 由 A4 落地,本 REQ 仅写入数据,不动 DDL)。

文件变更清单

工程脚手架

  • backend/pom.xml — 新建(声明依赖 + 编译/测试插件)
  • backend/src/main/java/com/xly/erp/ErpApplication.java — 新建(@SpringBootApplication 启动类)
  • backend/src/main/java/com/xly/erp/common/config/MybatisPlusConfig.java — 新建(@MapperScan("com.xly.erp.**.mapper")
  • backend/src/main/resources/application.yml — 新建(默认 profile + Flyway + datasource 用 ${ENV} 占位)
  • backend/src/main/resources/application-test.yml — 新建(test profile,沿用同一测试库)
  • backend/src/main/resources/logback-spring.xml — 新建(最小日志配置,关闭 SQL 详细日志以外的噪音)
  • backend/src/test/resources/application-test.yml — 与 main 同名 override 用,仅在测试时生效
  • backend/.gitignore — 新建(target/*.iml

通用层

  • backend/src/main/java/com/xly/erp/common/response/Result.java — 通用响应 {code,msg,data},提供 ok(T) / fail(int,String) 静态工厂
  • backend/src/main/java/com/xly/erp/common/exception/BizException.javaRuntimeException 子类,含 codemsg
  • backend/src/main/java/com/xly/erp/common/exception/GlobalExceptionHandler.java@RestControllerAdvice,处理 BizException / MethodArgumentNotValidException / Exception 兜底
  • backend/src/main/java/com/xly/erp/common/config/TenantProperties.java@ConfigurationProperties("erp.tenant"),字段 brandsId / subsidiaryId
  • backend/src/main/java/com/xly/erp/common/config/StubSecurityProperties.java@ConfigurationProperties("erp.security"),字段 stubUserNo(默认 STUB_ADMIN
  • backend/src/main/java/com/xly/erp/common/security/JwtUtil.java — 签发 + 解析 HS256;sign(String userNo) / parse(String token) : String userNo;密钥读 JWT_SECRET
  • backend/src/main/java/com/xly/erp/common/security/JwtAuthenticationFilter.javaOncePerRequestFilter,存在 Authorization: Bearer 时尝试解析;解析失败写 Result(20001,"未认证") 并短路;缺失则 chain 透传(permitAll 路径需要这种放行能力)
  • backend/src/main/java/com/xly/erp/common/security/SecurityConfig.javaSecurityFilterChain:禁 CSRF,POST /api/mod/modules permitAll,其他 authenticated(),注册 Filter
  • backend/src/main/java/com/xly/erp/common/security/SecurityContextHelper.java — 静态方法 currentUserNo() : String(无认证返回 null

MOD 业务模块

  • backend/src/main/java/com/xly/erp/module/mod/entity/Module.java — MyBatis-Plus @TableName("tModule") PO(含 iIncrement/sId/sBrandsId/sSubsidiaryId/tCreateDate/sDisplayType/sProcedureName/sModuleType/sManageDeptEn/bShowPermission/sModuleNameZh/iParentId/iSortOrder/sCreatedBy/bDeleted/tDeletedDate/sDeletedBy
  • backend/src/main/java/com/xly/erp/module/mod/dto/CreateModuleDTO.java — Bean Validation 注解的入参 DTO
  • backend/src/main/java/com/xly/erp/module/mod/mapper/ModuleMapper.java — 继承 BaseMapper<Module>,自定义 boolean existsActiveById(Integer iIncrement)
  • backend/src/main/resources/mapper/mod/ModuleMapper.xmlexistsActiveById 的 SELECT 1 实现
  • backend/src/main/java/com/xly/erp/module/mod/service/ModuleService.java — 接口,方法 Integer create(CreateModuleDTO dto)
  • backend/src/main/java/com/xly/erp/module/mod/service/impl/ModuleServiceImpl.java — 实现,@Transactional,含枚举校验/父校验/标准列填充/唯一冲突捕获
  • backend/src/main/java/com/xly/erp/module/mod/controller/ModuleController.java@PostMapping("/api/mod/modules") 接受 @Valid CreateModuleDTO,返回 Result<Map<String,Integer>>

测试

  • backend/src/test/java/com/xly/erp/SmokeTest.java@SpringBootTest 启动 + 验 Flyway 已 apply(tModule 表存在)
  • backend/src/test/java/com/xly/erp/common/security/TestJwtHelper.java — 测试辅助,signFor(String userNo) : String
  • backend/src/test/java/com/xly/erp/common/exception/GlobalExceptionHandlerTest.java — Mock MVC 单测,@WebMvcTest 限定 + 一个抛 BizException 的 stub controller
  • backend/src/test/java/com/xly/erp/common/security/JwtUtilTest.java — 单测,signAndParse_roundTrip
  • backend/src/test/java/com/xly/erp/module/mod/service/ModuleServiceImplTest.java — Mockito 单测,6 个用例(参 spec 单元测试清单)
  • backend/src/test/java/com/xly/erp/module/mod/controller/ModuleControllerIT.java@SpringBootTest(webEnvironment=RANDOM_PORT) + TestRestTemplate,7 个用例(参 spec 集成测试清单)

任务步骤

全局约束:每个 commit 形如 <type>(mod): <subject> REQ-MOD-001;测试运行强制派发到子会话执行(mvn -B test -pl backend 或带 -Dtest= 单测过滤);spec 中任意 permitAll stub 注释统一带 // REQ-MOD-001 stub: see USR-004 follow-up 形式锚点,便于后续 grep 替换。

Task 1: 工程脚手架立起来(pom + Application + yml + smoke)

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/main/java/com/xly/erp/common/config/MybatisPlusConfig.java
  • Create: backend/src/main/resources/logback-spring.xml
  • Create: backend/.gitignore
  • Test: backend/src/test/java/com/xly/erp/SmokeTest.java

API shape: N/A(脚手架)

关键依赖(pom.xml 必须含):spring-boot-starter-webspring-boot-starter-validationspring-boot-starter-securitymybatis-plus-spring-boot3-startermysql-connector-jflyway-coreflyway-mysqlio.jsonwebtoken:jjwt-api/impl/jackson 0.12.xspring-boot-starter-test。Java 17,编码 UTF-8。

application.yml 必填字段(值通过环境变量注入,对照 .env.local):

  • spring.datasource.url=jdbc:mysql://${DB_HOST}:${DB_PORT}/${DB_SCHEMA}?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai
  • spring.datasource.username=${DB_USER}
  • spring.datasource.password=${DB_PASSWORD}
  • spring.flyway.enabled=true / locations=classpath:db/migration,filesystem:./sql/migrations(指向仓库根 sql/migrations/,不复制文件)
  • spring.flyway.baseline-on-migrate=true
  • server.port=8080
  • erp.tenant.brands-id=XLY
  • erp.tenant.subsidiary-id=XLY
  • erp.security.stub-user-no=STUB_ADMIN
  • erp.security.jwt-secret=${JWT_SECRET}
  • mybatis-plus.mapper-locations=classpath:mapper/**/*.xml

  • Step 1: 建 pom.xml + ErpApplication + yml + logback-spring + .gitignore

    • 直接创建上述文件骨架,不写任何业务代码
    • ErpApplication@SpringBootApplication + main
  • Step 2: 写失败测试 SmokeTest

    • 测试名: SmokeTest#contextLoads_andFlywayApplied
    • 意图: @SpringBootTest(webEnvironment=NONE) 启动 + 注入 JdbcTemplate,断言 SHOW TABLES LIKE 'tModule' 命中 1 行
    • 子会话先跑:编译应通过、测试失败的原因应是 Spring 启动错误(如缺 driver)或 Flyway 未 apply
  • Step 3: 子会话验证 PASS

    • 命令:cd backend && mvn -B test -Dtest=SmokeTest
    • 期望:BUILD SUCCESS,1 test passed
    • 排查点:DB 连接(用 .env.local 现有凭据 118.178.19.35:3318)、Flyway location 是否能解析到 sql/migrations/V1
    • 测试前置:本会话先在主会话调 bash scripts/setup-test-db.sh 清库,让 SmokeTest 依赖的 Flyway apply 从 V1 重放
  • Step 4: Commit

    • git add backend/
    • git commit -m "chore(mod): bootstrap backend scaffold REQ-MOD-001"

Task 2: 通用响应 + 异常处理框架

Files:

  • Create: backend/src/main/java/com/xly/erp/common/response/Result.java
  • 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

API shape:

  • Result<T> 字段: int code, String msg, T data;静态方法 ok(T) / ok() / fail(int, String)
  • BizException(int code, String msg) extends RuntimeException
  • GlobalExceptionHandler @RestControllerAdvice

    • handleBiz(BizException) -> Result.fail(e.code, e.msg),HTTP 200
    • handleValidation(MethodArgumentNotValidException) -> Result.fail(40001, "<field>: <message>"),HTTP 200
    • handleAny(Exception) -> Result.fail(50000, "系统繁忙"),HTTP 200,只记日志、不回显堆栈
  • Step 1: 写失败测试

    • 测试文件: GlobalExceptionHandlerTest
    • @WebMvcTest 限定 + 一个 stub controller /__test/throw-biz(抛 BizException(30001,"x"))/ /__test/throw-validate(接 @Valid 入参)/ /__test/throw-runtime(抛 IllegalStateException
    • 三个测试:
    • bizException_returnsResultWithBizCode 期望 body {code:30001,msg:"x"}
    • validationException_returns40001WithFieldHint 期望 code=40001msg 包含字段名
    • uncaughtException_returns50000 期望 code=50000
    • 子会话确认 FAIL(类不存在)
  • Step 2: 实现最小代码

    • 涉及文件:Result.javaBizException.javaGlobalExceptionHandler.java
    • 不超出 spec § 边界与约束 列出的错误码语义
  • Step 3: 子会话验证 PASS

    • 命令:cd backend && mvn -B test -Dtest=GlobalExceptionHandlerTest
    • 期望:3 tests passed
  • Step 4: Commit

    • git commit -m "feat(mod): unified Result + global exception handler REQ-MOD-001"

Task 3: 租户配置 + JWT 工具 + 测试辅助

Files:

  • Create: backend/src/main/java/com/xly/erp/common/config/TenantProperties.java
  • Create: backend/src/main/java/com/xly/erp/common/config/StubSecurityProperties.java
  • Create: backend/src/main/java/com/xly/erp/common/security/JwtUtil.java
  • Test: backend/src/test/java/com/xly/erp/common/security/JwtUtilTest.java
  • Test: backend/src/test/java/com/xly/erp/common/security/TestJwtHelper.java(生产代码意义上是测试基础设施)

API shape:

  • TenantProperties 字段 String brandsId, String subsidiaryId,绑定前缀 erp.tenant
  • StubSecurityProperties 字段 String stubUserNo, String jwtSecret,绑定前缀 erp.security
  • JwtUtil#sign(String userNo) : String(HS256,subject=userNo,过期 8 小时)
  • JwtUtil#parse(String token) : String(返回 subject;过期/签名错抛 BizException(20001,"未认证或 token 已失效")
  • TestJwtHelper#signFor(String userNo) : String(包装 JwtUtil,方便 IT 用)

  • Step 1: 写失败测试 JwtUtilTest

    • 测试名:
    • signAndParse_roundTripparse(sign("u1")) == "u1"
    • parseTamperedToken_throwsBizException20001
    • @SpringBootTest 注入 JwtUtil,密钥走 application-test.yml 的 ${JWT_SECRET}
  • Step 2: 实现最小代码

    • TenantProperties / StubSecurityProperties + 在 ErpApplication@EnableConfigurationProperties({TenantProperties.class, StubSecurityProperties.class})
    • JwtUtilio.jsonwebtoken.Jwts.builder()/parser()
    • TestJwtHelper @ComponentJwtUtil
  • Step 3: 子会话验证 PASS

    • 命令:cd backend && mvn -B test -Dtest=JwtUtilTest
  • Step 4: Commit

    • git commit -m "feat(mod): tenant + jwt config + util REQ-MOD-001"

Task 4: JWT Filter + SecurityConfig

Files:

  • Create: backend/src/main/java/com/xly/erp/common/security/JwtAuthenticationFilter.java
  • Create: backend/src/main/java/com/xly/erp/common/security/SecurityConfig.java
  • Create: backend/src/main/java/com/xly/erp/common/security/SecurityContextHelper.java
  • Test: backend/src/test/java/com/xly/erp/common/security/SecurityFilterIT.java

API shape:

  • JwtAuthenticationFilter extends OncePerRequestFilter
    • 头解析:Authorization: Bearer <token>
    • 有 token 且解析成功 → SecurityContextHolder.getContext().setAuthentication(new UsernamePasswordAuthenticationToken(userNo, null, List.of()))
    • 有 token 但解析失败 → 写 Result.fail(20001,"未认证或 token 已失效") JSON 到 response,状态码 200,短路 chain
    • 无 token → chain 直接放行(让 SecurityConfig 的 permitAll/authenticated 规则决定)
  • SecurityConfig#filterChain(HttpSecurity):
    • csrf().disable() / sessionManagement().sessionCreationPolicy(STATELESS)
    • authorizeHttpRequests 顺序:
    • requestMatchers(HttpMethod.POST, "/api/mod/modules").permitAll() — REQ-MOD-001 stub: see USR-004 follow-up
    • anyRequest().authenticated()
    • addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class)
  • SecurityContextHelper.currentUserNo() : String — 读 SecurityContextHolder principal;无认证返回 null

  • Step 1: 写失败测试 SecurityFilterIT

    • 测试名(用 @SpringBootTest(webEnvironment=RANDOM_PORT) + TestRestTemplate,命中一个真实接口;本任务先 stub 一个 /__test/principal 暴露 currentUserNo(),但更稳妥的做法是放在 Task 8 实现 controller 后回填,故本任务测试范围限制在 SecurityConfig 的路径规则):
    • validJwt_authenticatedEndpoint_returnsPrincipal — POST /api/mod/modules 带合法 token,期望 200(permitAll,sCreatedBy 由后续 Task 验证)
    • tamperedJwt_anyEndpoint_returns20001 — Authorization 伪造 → JSON code=20001
    • noJwt_permitAllEndpoint_passes — 无 Authorization 头 → 200(不阻断 permitAll)
    • noJwt_protectedEndpoint_returns20001OrSpring403 — 命中默认 protected,期望状态码非 200 或 code=20001(Spring Security 默认会返回 403/401,二者皆可)
    • 上述测试 POST /api/mod/modules 在本任务尚未实现 controller,会返回 404;将 405/404 视作 "permitAll 通过 filter 链" 的间接证据,断言不要求 200
    • 子会话确认 FAIL(filter/config 类不存在)
  • Step 2: 实现最小代码

    • 三个类按 API shape 实现;filter 中"短路 + 写 JSON" 用 ObjectMapper 序列化 Result.fail(20001,...),content-type=application/json
  • Step 3: 子会话验证 PASS

    • 命令:cd backend && mvn -B test -Dtest=SecurityFilterIT
  • Step 4: Commit

    • git commit -m "feat(mod): jwt filter + security config (role stub) REQ-MOD-001"

Task 5: tModule Entity + Mapper(数据层)

Files:

  • Create: backend/src/main/java/com/xly/erp/module/mod/entity/Module.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:

  • Module 类字段(与 tModule 1:1,使用 @TableName("tModule") + @TableField 显式映射):
    • Integer iIncrement (@TableId(type=IdType.AUTO))
    • String sId, String sBrandsId, String sSubsidiaryId
    • LocalDateTime tCreateDate
    • String sDisplayType, String sProcedureName, String sModuleType, String sManageDeptEn
    • Boolean bShowPermission
    • String sModuleNameZh
    • Integer iParentId, Integer iSortOrder
    • String sCreatedBy
    • Boolean bDeleted, LocalDateTime tDeletedDate, String sDeletedBy
  • ModuleMapper extends BaseMapper<Module>,附加方法:

    • boolean existsActiveById(@Param("id") Integer iIncrement) — XML 中 SELECT 1 FROM tModule WHERE iIncrement=#{id} AND bDeleted=0 LIMIT 1,返回非空结果即 true
  • Step 1: 写失败测试 ModuleMapperIT

    • 测试名:
    • insertAndSelectById_persistsAllStandardCols — 构造 Module 实例 set 全字段后 mapper.insert(...)mapper.selectById(...) 比较各字段(含 sBrandsId='XLY'
    • existsActiveById_trueForAlive_falseForDeleted — 插两条,一条 bDeleted=0、一条 bDeleted=1,分别校验
    • @SpringBootTest + @Transactional(自动回滚不污染库),@Autowired ModuleMapper
    • 子会话先跑 → FAIL(类不存在)
  • Step 2: 实现最小代码

    • Module entity + Mapper 接口 + XML
    • 标准列 tCreateDate 由测试代码填,不依赖 DB 默认(spec 边界对齐)
  • Step 3: 子会话验证 PASS

    • 命令:cd backend && mvn -B test -Dtest=ModuleMapperIT
  • Step 4: Commit

    • git commit -m "feat(mod): tModule entity + mapper REQ-MOD-001"

Task 6: CreateModuleDTO + ModuleService 主流程(合法路径 + 父校验)

Files:

  • Create: backend/src/main/java/com/xly/erp/module/mod/dto/CreateModuleDTO.java
  • 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

API shape:

  • CreateModuleDTO 字段(带 Bean Validation):
    • @NotBlank String sDisplayType
    • @NotBlank @Size(max=100) String sProcedureName
    • @NotBlank @Size(max=50) String sModuleType
    • @NotBlank @Size(max=50) String sManageDeptEn
    • Boolean bShowPermission(可空,service 层默认 false)
    • @NotBlank @Size(max=100) String sModuleNameZh
    • Integer iParentId(可空)
    • Integer iSortOrder(可空,service 层默认 0)
  • ModuleService#create(CreateModuleDTO dto) : Integer(返回新 iIncrement
  • ModuleServiceImpl 依赖:ModuleMapper / TenantProperties / StubSecurityProperties
  • 流程:
    1. 校验 sDisplayType{手机端,前端业务,系统配置,接口},否则 BizException(40010,"显示类型枚举不合法")
    2. iParentId != nullmapper.existsActiveById(iParentId);不存在 → BizException(40021,"父模块不存在或已删除")
    3. 构造 Module 实例:DTO 字段透传 + 标准列填充:
      • tCreateDate = LocalDateTime.now()
      • sBrandsId = tenantProps.brandsId
      • sSubsidiaryId = tenantProps.subsidiaryId
      • sCreatedBy = SecurityContextHelper.currentUserNo()stubProps.stubUserNo(前者为 null 时回退)
      • bShowPermission = dto.bShowPermission != null ? dto : false
      • iSortOrder = dto.iSortOrder != null ? dto : 0
      • bDeleted = false
    4. mapper.insert(entity);MyBatis-Plus 自动回填 iIncrement
    5. return entity.getIIncrement()
  • 类上加 @Transactional(rollbackFor = Exception.class)

  • Step 1: 写失败测试 ModuleServiceImplTest(本任务两用例)

    • 测试名:
    • createWithValidDto_persistsWithStandardCols — Mock ModuleMapper.insert(用 Answer 设置 entity.iIncrement=99)+ Mock existsActiveById(true),断言:返回 99;捕获 ArgumentCaptor<Module> 校验 sBrandsId="XLY" / sSubsidiaryId="XLY" / tCreateDate != null / sCreatedBy="STUB_ADMIN"(无认证上下文,回退 stub)
    • createWithParentNotFound_throws40021 — Mock existsActiveById(false) + DTO iParentId=42,断言抛 BizExceptioncode=40021
    • 子会话先跑 → FAIL
  • Step 2: 实现最小代码

    • DTO + Service interface + Impl,仅覆盖本任务两用例的最小逻辑(枚举校验和重复键捕获放下个 Task 写测试驱动)
  • Step 3: 子会话验证 PASS

    • 命令:cd backend && mvn -B test -Dtest=ModuleServiceImplTest
  • Step 4: Commit

    • git commit -m "feat(mod): module create dto + service happy path REQ-MOD-001"

Task 7: Service 层异常分支补全(枚举非法 + 唯一冲突)

Files:

  • Modify: backend/src/main/java/com/xly/erp/module/mod/service/impl/ModuleServiceImpl.java
  • Modify: backend/src/test/java/com/xly/erp/module/mod/service/ModuleServiceImplTest.java

API shape: 同 Task 6(仅补分支逻辑)

  • Step 1: 在测试类中追加 4 个用例

    • createWithInvalidDisplayType_throws40010 — DTO sDisplayType="未知",期望 BizException.code=40010
    • createWithNullParentId_skipsParentCheck — DTO iParentId=null,期望 mapper.existsActiveById 不被调用,且仍走完插入
    • mapperDuplicateKey_throws40020 — Mock mapper.insertorg.springframework.dao.DuplicateKeyException,期望转抛 BizException.code=40020
    • usesAuthenticatedUserNoAsCreatedBySecurityContextHolder 注入 userNo="ALICE",断言传给 mapper 的 entity.sCreatedBy="ALICE"(@AfterEach SecurityContextHolder.clearContext() 隔离
    • 子会话先跑 → 4 用例 FAIL
  • Step 2: 在 ServiceImpl 中补充分支

    • 枚举校验(白名单 Set.of("手机端","前端业务","系统配置","接口")
    • try/catch DuplicateKeyExceptionBizException(40020,"存储过程名称已存在")
    • sCreatedBy 优先取 SecurityContextHelper.currentUserNo()
  • Step 3: 子会话验证 PASS

    • 命令:cd backend && mvn -B test -Dtest=ModuleServiceImplTest
    • 期望:6 tests passed(含 Task 6 的 2 个)
  • Step 4: Commit

    • git commit -m "feat(mod): module create error branches REQ-MOD-001"

Task 8: Controller + 集成测试(正常路径)

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/mod")
  • @PostMapping("/modules") public Result<Map<String,Integer>> create(@Valid @RequestBody CreateModuleDTO dto)
  • 返回 Result.ok(Map.of("iIncrement", service.create(dto)))

  • Step 1: 写失败测试 IT 正常路径

    • 测试名: ModuleControllerIT#postValidBody_with_jwt_returns200_andPersists
    • @SpringBootTest(webEnvironment=RANDOM_PORT) + TestRestTemplate + @Autowired TestJwtHelper + @Autowired JdbcTemplate
    • 步骤:
    • String token = testJwtHelper.signFor("ADMIN001")
    • POST /api/mod/modules body {sDisplayType:"手机端", sProcedureName:"sp_test_001", sModuleType:"业务模块", sManageDeptEn:"IT", sModuleNameZh:"测试模块"},header Authorization: Bearer <token>
    • 期望 HTTP 200,响应 code=0data.iIncrement 是正整数 N
    • JdbcTemplate 查 tModule WHERE iIncrement=N,断言 sCreatedBy="ADMIN001"sBrandsId="XLY"
    • 测试隔离@BeforeEach/@AfterEach 用 JdbcTemplate DELETE FROM tModule WHERE sProcedureName LIKE 'sp_test_%',避免污染
    • 子会话先跑 → FAIL(controller 不存在 → 404)
  • Step 2: 实现 ModuleController

    • 严格按 API shape,不加任何额外路径
  • Step 3: 子会话验证 PASS

    • 命令:cd backend && mvn -B test -Dtest=ModuleControllerIT#postValidBody_with_jwt_returns200_andPersists
  • Step 4: Commit

    • git commit -m "feat(mod): POST /api/mod/modules controller REQ-MOD-001"

Task 9: 集成测试异常路径(参数缺失 / 枚举非法 / 唯一冲突 / 父不存在 / 鉴权 stub 行为)

Files:

  • Modify: backend/src/test/java/com/xly/erp/module/mod/controller/ModuleControllerIT.java

API shape: 不新增(覆盖现有接口 6 条异常路径)

  • Step 1: 在 IT 中追加 6 个用例

    • postEmptyBody_returns40001_withFieldHint — body {},期望 code=40001msgsProcedureName 等任一字段名
    • postInvalidDisplayType_returns40010 — body sDisplayType="火星",期望 code=40010
    • postDuplicateProcedureName_returns40020 — 先插一条(直接 JdbcTemplate 或先 POST 一次),再 POST 同名 → code=40020
    • postWithMissingParent_returns40021iParentId=999999code=40021
    • postWithoutJwt_permitAllStub_returns200_andCreatedBySTUBADMIN — 不带 Authorization 头,期望 200 + 新行 sCreatedBy="STUB_ADMIN"(验证 stub 行为,spec 已声明 USR-004 后改为 401)
    • postWithTamperedJwt_returns20001 — Authorization 头伪造(Bearer xxx.yyy.zzz),期望 code=20001(filter 拦截短路)
    • 6 个用例先跑 → FAIL(缺分支/HTTP 状态预期不符)
  • Step 2: 让测试通过

    • 多数用例的服务端逻辑已在 Task 7 + Task 4 实现;本步骤主要是排查测试中 RestTemplate 行为(如 4xx 是否抛、是否需要用 String.class 接收 body 再手动 parse JSON)
    • 不应当为让测试通过新增业务分支;如发现确实缺分支,回炉对应 Task 的 service/filter
  • Step 3: 子会话验证全 IT PASS

    • 命令:cd backend && mvn -B test -Dtest=ModuleControllerIT
    • 期望:7 tests passed(含 Task 8 的 1 个)
  • Step 4: 子会话跑全模块单测套件做回归

    • 命令:cd backend && mvn -B test
    • 期望:所有用例 PASS(SmokeTest + GlobalExceptionHandlerTest + JwtUtilTest + SecurityFilterIT + ModuleMapperIT + ModuleServiceImplTest + ModuleControllerIT)
  • Step 5: Commit

    • git commit -m "test(mod): module create integration coverage REQ-MOD-001"

提交计划

commit 覆盖
chore(mod): bootstrap backend scaffold REQ-MOD-001 Task 1
feat(mod): unified Result + global exception handler REQ-MOD-001 Task 2
feat(mod): tenant + jwt config + util REQ-MOD-001 Task 3
feat(mod): jwt filter + security config (role stub) REQ-MOD-001 Task 4
feat(mod): tModule entity + mapper REQ-MOD-001 Task 5
feat(mod): module create dto + service happy path REQ-MOD-001 Task 6
feat(mod): module create error branches REQ-MOD-001 Task 7
feat(mod): POST /api/mod/modules controller REQ-MOD-001 Task 8
test(mod): module create integration coverage REQ-MOD-001 Task 9