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>头解析 - 解析成功 → 写入
SecurityContextHolder的Authentication.principal = sUserNo - 解析失败 / 缺失 →
JwtAuthenticationFilter直接返回Result(20001, "未认证"),不进入 controller -
超级管理员判定本期 stub(
SecurityConfig对/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 |
SecurityContextHolder 中 principal(即 JWT 中 sUserNo);stub 期未携带 token 时该接口因 permitAll 也能进,此时 sCreatedBy = 配置 erp.security.stub-user-no(默认 STUB_ADMIN),USR-004 完成后改为强制取 token
|
bDeleted |
0(DB 默认) |
tDeletedDate / sDeletedBy
|
NULL |
业务规则
-
唯一性:
sProcedureName系统内全局唯一,依赖 DBuk_procedure_name索引兜底;service 层捕获org.springframework.dao.DuplicateKeyException→BizException(40020, "存储过程名称已存在")。不做"先查后插"二次校验,避免竞态。 -
父模块存在性:
iParentId != null时执行SELECT iIncrement FROM tModule WHERE iIncrement=? AND bDeleted=0;查不到则BizException(40021, "父模块不存在或已删除")。 -
枚举校验:
sDisplayType在白名单内(参输入表);非法 →BizException(40010, "显示类型枚举不合法")。在 service 入口处用Set<String>.contains()校验。 -
类型与长度校验:交给 Bean Validation(
@Valid+@NotBlank/@Size);统一异常处理器把MethodArgumentNotValidException转Result(40001, "<字段名>: <message>")。 -
事务边界:
ModuleServiceImpl.create(...)上@Transactional(rollbackFor = Exception.class),整个新增是单条 INSERT 不存在跨表事务,但保持事务注解以便后续业务扩展。
边界与约束
-
必填项缺失 →
40001(参数校验失败,由 GlobalExceptionHandler 统一处理) -
sDisplayType非枚举 →40010 -
sProcedureName唯一冲突 →40020 -
iParentId不存在或已软删 →40021 -
超级管理员判定:本 REQ 不做硬校验(
permitAllstub),后续在 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/04与docs/05的不一致属跨 REQ 文档问题,本 REQ 不修复。
实现范围与边界抉择(与用户确认)
经 feature-brainstorm Q&A 确认(2026-04-29):
-
范围 = 脚手架 + 接口 + 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)
- 建立
-
多租户字段处理 = 用配置默认值
XLY/XLY:写入application.yml的erp.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 同
sProcedureName→code=40020 - 父模块不存在:
iParentId=99999→code=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 路径