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-tddexecutes 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/modules 走 permitAll stub(角色硬校验留 USR-004 闭环)。多租户字段 sBrandsId / sSubsidiaryId 通过 application.yml 的 erp.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.java—RuntimeException子类,含code、msg -
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.java—OncePerRequestFilter,存在Authorization: Bearer时尝试解析;解析失败写Result(20001,"未认证")并短路;缺失则 chain 透传(permitAll 路径需要这种放行能力) -
backend/src/main/java/com/xly/erp/common/security/SecurityConfig.java—SecurityFilterChain:禁 CSRF,POST /api/mod/modulespermitAll,其他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.xml—existsActiveById的 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-web、spring-boot-starter-validation、spring-boot-starter-security、mybatis-plus-spring-boot3-starter、mysql-connector-j、flyway-core、flyway-mysql、io.jsonwebtoken:jjwt-api/impl/jackson 0.12.x、spring-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/Shanghaispring.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=trueserver.port=8080erp.tenant.brands-id=XLYerp.tenant.subsidiary-id=XLYerp.security.stub-user-no=STUB_ADMINerp.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)extendsRuntimeException -
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=40001且msg包含字段名 -
uncaughtException_returns50000期望code=50000 - 子会话确认 FAIL(类不存在)
- 测试文件:
-
Step 2: 实现最小代码
- 涉及文件:
Result.java、BizException.java、GlobalExceptionHandler.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_roundTrip—parse(sign("u1")) == "u1" parseTamperedToken_throwsBizException20001- 用
@SpringBootTest注入JwtUtil,密钥走 application-test.yml 的${JWT_SECRET}
-
Step 2: 实现最小代码
-
TenantProperties/StubSecurityProperties+ 在ErpApplication加@EnableConfigurationProperties({TenantProperties.class, StubSecurityProperties.class}) -
JwtUtil用io.jsonwebtoken.Jwts.builder()/parser() -
TestJwtHelper@Component注JwtUtil
-
-
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— 读SecurityContextHolderprincipal;无认证返回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 伪造 → JSONcode=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
- 三个类按 API shape 实现;filter 中"短路 + 写 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类字段(与tModule1: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 bShowPermissionString 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 - 流程:
- 校验
sDisplayType∈{手机端,前端业务,系统配置,接口},否则BizException(40010,"显示类型枚举不合法") - 若
iParentId != null调mapper.existsActiveById(iParentId);不存在 →BizException(40021,"父模块不存在或已删除") - 构造
Module实例:DTO 字段透传 + 标准列填充:tCreateDate = LocalDateTime.now()sBrandsId = tenantProps.brandsIdsSubsidiaryId = tenantProps.subsidiaryId-
sCreatedBy = SecurityContextHelper.currentUserNo()或stubProps.stubUserNo(前者为 null 时回退) bShowPermission = dto.bShowPermission != null ? dto : falseiSortOrder = dto.iSortOrder != null ? dto : 0bDeleted = false
-
mapper.insert(entity);MyBatis-Plus 自动回填iIncrement return entity.getIIncrement()
- 校验
类上加
@Transactional(rollbackFor = Exception.class)-
Step 1: 写失败测试
ModuleServiceImplTest(本任务两用例)- 测试名:
-
createWithValidDto_persistsWithStandardCols— MockModuleMapper.insert(用Answer设置 entity.iIncrement=99)+ MockexistsActiveById(true),断言:返回 99;捕获ArgumentCaptor<Module>校验sBrandsId="XLY"/sSubsidiaryId="XLY"/tCreateDate != null/sCreatedBy="STUB_ADMIN"(无认证上下文,回退 stub) -
createWithParentNotFound_throws40021— MockexistsActiveById(false)+ DTOiParentId=42,断言抛BizException,code=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— DTOsDisplayType="未知",期望BizException.code=40010 -
createWithNullParentId_skipsParentCheck— DTOiParentId=null,期望mapper.existsActiveById不被调用,且仍走完插入 -
mapperDuplicateKey_throws40020— Mockmapper.insert抛org.springframework.dao.DuplicateKeyException,期望转抛BizException.code=40020 -
usesAuthenticatedUserNoAsCreatedBy—SecurityContextHolder注入userNo="ALICE",断言传给 mapper 的 entity.sCreatedBy="ALICE"(用@AfterEach SecurityContextHolder.clearContext()隔离) - 子会话先跑 → 4 用例 FAIL
-
-
Step 2: 在 ServiceImpl 中补充分支
- 枚举校验(白名单
Set.of("手机端","前端业务","系统配置","接口")) - try/catch
DuplicateKeyException→BizException(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/modulesbody{sDisplayType:"手机端", sProcedureName:"sp_test_001", sModuleType:"业务模块", sManageDeptEn:"IT", sModuleNameZh:"测试模块"},headerAuthorization: Bearer <token> - 期望 HTTP 200,响应
code=0,data.iIncrement是正整数 N - JdbcTemplate 查
tModule WHERE iIncrement=N,断言sCreatedBy="ADMIN001"、sBrandsId="XLY" -
测试隔离:
@BeforeEach/@AfterEach用 JdbcTemplateDELETE 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=40001,msg含sProcedureName等任一字段名 -
postInvalidDisplayType_returns40010— bodysDisplayType="火星",期望code=40010 -
postDuplicateProcedureName_returns40020— 先插一条(直接 JdbcTemplate 或先 POST 一次),再 POST 同名 →code=40020 -
postWithMissingParent_returns40021—iParentId=999999→code=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
- 多数用例的服务端逻辑已在 Task 7 + Task 4 实现;本步骤主要是排查测试中 RestTemplate 行为(如 4xx 是否抛、是否需要用
-
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 |