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

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

module: module_mod

Spec: REQ-MOD-001 — 模块新增

目标

新增一条 ERP 业务模块定义记录(tModule),作为系统功能与权限分组的基础单位,提交后持久化并返回新模块 iIncrement

本 REQ 是项目首个 REQ,backend/ 工程目录尚未存在,因此本 spec 同时承担最小后端脚手架建立的职责。范围抉择已与用户确认(详见「实现范围与边界抉择」一节)。

输入 / 触发

HTTP 接口(来自 docs/05 § module_mod.REQ-MOD-001)

  • Method / Path: POST /api/mod/modules
  • Auth: 必需(JWT Bearer Token)
  • Permission: 仅超级管理员(本 REQ 内 stub 为 permitAll,留待 USR-004 完成后回填硬校验
  • Content-Type: application/json

请求 DTO CreateModuleDTO

JSON 字段 Java 类型 必填 Bean Validation 业务校验
sDisplayType String @NotBlank 必须在枚举 [手机端, 前端业务, 系统配置, 接口] 内;非法 → 40010
sProcedureName String @NotBlank @Size(max=100) 系统内唯一(依赖 tModule.uk_procedure_name);冲突 → 40020
sModuleType String @NotBlank @Size(max=50) 自由文本,不做枚举约束(参 docs/03 业务注记)
sManageDeptEn String @NotBlank @Size(max=50)
bShowPermission Boolean 缺省 false(写入 0
sModuleNameZh String @NotBlank @Size(max=100)
iParentId Integer 非 null 时必须命中存在且 bDeleted=0 的记录;不存在 / 已软删 → 40021
iSortOrder Integer 缺省 0

REQ 卡 § 输入只列了前 6 个字段(业务关心的最小集合),API 契约扩展了 iParentId / iSortOrder 两个可选字段以匹配 tModule 自引用 + 排序的 schema。两者一致——前 6 必填,后 2 可选。

鉴权与上下文

  • JWT 从 Authorization: Bearer <token> 头解析
  • 解析成功 → 写入 SecurityContextHolderAuthentication.principal = sUserNo
  • 解析失败 / 缺失 → JwtAuthenticationFilter 直接返回 Result(20001, "未认证"),不进入 controller
  • 超级管理员判定本期 stubSecurityConfig/api/mod/modules 的 POST 用 permitAll(),但 Filter 仍需解析 token 写入 principal 用于 sCreatedBy

输出 / 结果

成功响应

{
  "code": 0,
  "msg": "ok",
  "data": { "iIncrement": 123 }
}

持久化效果

新增一行 tModule 记录,字段填充规则:

字段 来源
iIncrement DB 自增
sId NULL(本期不生成业务 ID)
sBrandsId 配置 erp.tenant.brands-id(默认 XLY
sSubsidiaryId 配置 erp.tenant.subsidiary-id(默认 XLY
tCreateDate LocalDateTime.now()(service 层填,不依赖 DB 默认)
sDisplayType ~ iSortOrder DTO 字段
sCreatedBy SecurityContextHolderprincipal(即 JWT 中 sUserNo);stub 期未携带 token 时该接口因 permitAll 也能进,此时 sCreatedBy = 配置 erp.security.stub-user-no(默认 STUB_ADMIN),USR-004 完成后改为强制取 token
bDeleted 0(DB 默认)
tDeletedDate / sDeletedBy NULL

业务规则

  1. 唯一性sProcedureName 系统内全局唯一,依赖 DB uk_procedure_name 索引兜底;service 层捕获 org.springframework.dao.DuplicateKeyExceptionBizException(40020, "存储过程名称已存在")。不做"先查后插"二次校验,避免竞态。
  2. 父模块存在性iParentId != null 时执行 SELECT iIncrement FROM tModule WHERE iIncrement=? AND bDeleted=0;查不到则 BizException(40021, "父模块不存在或已删除")
  3. 枚举校验sDisplayType 在白名单内(参输入表);非法 → BizException(40010, "显示类型枚举不合法")。在 service 入口处用 Set<String>.contains() 校验。
  4. 类型与长度校验:交给 Bean Validation(@Valid + @NotBlank / @Size);统一异常处理器把 MethodArgumentNotValidExceptionResult(40001, "<字段名>: <message>")
  5. 事务边界ModuleServiceImpl.create(...)@Transactional(rollbackFor = Exception.class),整个新增是单条 INSERT 不存在跨表事务,但保持事务注解以便后续业务扩展。

边界与约束

  • 必填项缺失40001(参数校验失败,由 GlobalExceptionHandler 统一处理)
  • sDisplayType 非枚举40010
  • sProcedureName 唯一冲突40020
  • iParentId 不存在或已软删40021
  • 超级管理员判定:本 REQ 不做硬校验(permitAll stub),后续在 USR-004 完成后闭环回填——SecurityConfig/api/mod/modules 的 POST 改为 hasAuthority('SUPER_ADMIN')JwtAuthenticationFilter 增加 tUser 查询填充 authorities(本 REQ 不实现,仅在代码注释中以 // REQ-MOD-001 stub: see USR-004 follow-up 形式标注待回填位置)
  • 接口异常响应禁回显堆栈(docs/04 § 1.4),Result 仅含 code + msg,堆栈走 logback
  • 错误码段位差异说明:docs/04 § 1.3 定义段位 1xxxx/2xxxx/3xxxx/5xxxx,但 docs/05 接口契约 § REQ-MOD-001 用 40001/40010/40020/40021/40300(沿用 HTTP 语义前缀)。本 spec 以 docs/05 为准——docs/04docs/05 的不一致属跨 REQ 文档问题,本 REQ 不修复。

实现范围与边界抉择(与用户确认)

feature-brainstorm Q&A 确认(2026-04-29):

  1. 范围 = 脚手架 + 接口 + Filter(角色 stub)
    • 建立 backend/ Spring Boot 工程(pom + Application + 基础配置)
    • 引入 Flyway、MyBatis-Plus、Spring Security 依赖
    • 实现统一响应 Result<T> + 全局异常处理 GlobalExceptionHandler + 业务异常 BizException
    • 实现 JwtAuthenticationFilter(解析 token 写 principal)+ 简易 SecurityConfig(除 MOD 接口允许 permitAll,其余 authenticated(),但本 REQ 仅 MOD-001 一个接口)
    • 实现 MOD-001 接口完整链路(controller / service / mapper / entity / dto)
  2. 多租户字段处理 = 用配置默认值 XLY / XLY:写入 application.ymlerp.tenant.*,由 @ConfigurationProperties 注入 service。

依赖的 schema 表 / 字段

写入表:tModule

字段 用途 来源
iIncrement 主键,DB 自增返回 useGeneratedKeys=true
sBrandsId / sSubsidiaryId 多租户列 application.yml 配置
tCreateDate 创建时间 service 填 LocalDateTime.now()
sDisplayType / sProcedureName / sModuleType / sManageDeptEn / bShowPermission / sModuleNameZh / iParentId / iSortOrder DTO 透传 CreateModuleDTO
sCreatedBy 创建人 JWT principal(stub 期回退到配置)
bDeleted 软删除标记 DB 默认 0
sId / tDeletedDate / sDeletedBy 暂未使用 NULL

约束:唯一索引 uk_procedure_name(sProcedureName);外键 fk_module_parent: iParentId → tModule.iIncrement (ON DELETE RESTRICT)

依赖的接口

无(首个 REQ,无前置接口依赖)。

验收标准

单元测试(ModuleServiceImplTest,Mockito 隔离 mapper)

  • 给定合法 DTO + 父模块存在 → service 调用 mapper.insert(...) 一次,返回 iIncrement
  • 给定合法 DTO + iParentId=null → 跳过父模块校验直接 insert
  • 给定 sDisplayType 非枚举值 → 抛 BizException(40010)
  • 给定 iParentId 在 mapper 中查不到 → 抛 BizException(40021)
  • mapper.insert 抛 DuplicateKeyException → 转抛 BizException(40020)
  • tCreateDate / sBrandsId / sSubsidiaryId / sCreatedBy 字段被正确填充进传给 mapper 的 entity

集成测试(ModuleControllerIT,Spring Boot Test + 真实 MySQL via setup-test-db.sh + Flyway)

测试库由 scripts/setup-test-db.sh DROP + CREATE,启动时 Flyway 自动 apply V1__initial_schema.sql。每个用例前后清空 tModule

  • 正常路径:POST 合法 body + 测试用 JWT → 200 / code=0,DB 中存在新行,sCreatedBy = JWT 解出的 sUserNo
  • 缺必填:POST {} → 400 / code=40001,body 含字段定位
  • 枚举非法sDisplayType=未知code=40010
  • 唯一冲突:先 POST 一次成功;再 POST 同 sProcedureNamecode=40020
  • 父模块不存在iParentId=99999code=40021
  • 未携带 JWT:本 REQ 因 stub permitAll,仍返回 200 但 sCreatedBy=STUB_ADMIN(仅验证当前 stub 行为;USR-004 闭环后该用例期望值改为 code=20001
  • JWT 解析失败(恶意 token) → code=20001(filter 拦截)

工程脚手架验收

  • mvn -f backend/pom.xml clean test 全绿
  • Spring Boot 启动后 Flyway 自动 apply V1(如已 apply 则跳过)
  • Result<T> 在 controller 返回类型上一致使用
  • GlobalExceptionHandler 至少处理 BizException / MethodArgumentNotValidException / 兜底 Exception
  • JwtAuthenticationFilter 正确解析 Authorization 头;token 缺失时不阻断 stub permitAll 路径