req_id: REQ-MOD-001 date: 2026-05-06
module: module_mod
Spec: REQ-MOD-001 — 模块新增
目标
实现后端 POST /api/modules 接口:将一条新的业务模块定义写入 tModule 表,作为 ERP 系统功能与权限分组的基础单位,返回新模块主键 iIncrement 与完整 VO。
本 REQ 是 module_mod 模块的第一个 REQ,也是整个 B 阶段的首个 REQ;需顺带建立 Spring Boot 项目骨架(最小可运行单元),仅落地能让本接口工作的横切组件。其余横切组件(JWT 鉴权、登录用户上下文)按 docs/02 § 三 约定由 REQ-USR-004 首次落地。
输入 / 触发
接口:POST /api/modules,Content-Type application/json。
Request body(ModuleCreateDTO)字段:
| 字段 | 类型 | 必填 | 校验 / 取值 | 落库列 |
|---|---|---|---|---|
sDisplayType |
String | 是 | 枚举:手机端 / 前端业务 / 系统配置 / 接口
|
tModule.sDisplayType |
sProcedureName |
String | 是 | 长度 1-100;系统内唯一(bDeleted=0 范围内) |
tModule.sProcedureName |
sModuleType |
String | 是 | 长度 1-50(自由文本) | tModule.sModuleType |
sManageDeptEn |
String | 是 | 长度 1-50(自由文本) | tModule.sManageDeptEn |
bShowPermission |
Boolean | 否 | 默认 false
|
tModule.bShowPermission |
sModuleNameZh |
String | 是 | 长度 1-100 | tModule.sModuleNameZh |
iParentId |
Integer | 否 | 必须指向已存在且未软删除的 tModule.iIncrement
|
tModule.iParentId |
iSortOrder |
Integer | 否 | 默认 0;非负整数 |
tModule.iSortOrder |
鉴权:契约要求 Authorization: Bearer <accessToken> + 权限码 MOD:CREATE。本 REQ 暂不实施实际鉴权(见 § 边界与约束),但 Controller 仍按需要鉴权的形态编写(无 @AnonymousAccess 标注),以便 REQ-USR-004 加入 SecurityFilterChain 后零改动。
输出 / 结果
HTTP 200,响应体(统一响应格式):
{
"code": 200,
"message": "操作成功",
"data": {
"iIncrement": 12,
"sDisplayType": "前端业务",
"sProcedureName": "sp_audit_user_module",
"sModuleType": "USR",
"sManageDeptEn": "IT",
"bShowPermission": false,
"sModuleNameZh": "用户管理",
"iParentId": null,
"iSortOrder": 0,
"tCreateDate": "2026-05-06T10:30:00",
"bDeleted": false
},
"timestamp": 1746528600000
}
返回 VO(ModuleVO)字段:iIncrement / sDisplayType / sProcedureName / sModuleType / sManageDeptEn / bShowPermission / sModuleNameZh / iParentId / iSortOrder / tCreateDate / bDeleted。其他标准列(sId / sBrandsId / sSubsidiaryId / sCreatedBy / tDeletedDate / sDeletedBy)不对外暴露。
业务规则
-
唯一性:
sProcedureName在未软删除范围内(bDeleted=0)系统内唯一。冲突返回错误码40911。 -
父模块校验:若
iParentId非空,必须指向tModule中存在且bDeleted=0的记录;不存在或已删除返回错误码40411。 -
bShowPermission 默认值:未传入或传
null→ 落库0。 -
iSortOrder 默认值:未传入 → 落库
0。 -
新建记录初始状态:
bDeleted=0、tDeletedDate=NULL、sDeletedBy=NULL、tCreateDate=now()(应用层取系统时间,不依赖 DB DEFAULT)。 -
多租户字段 (
sBrandsId/sSubsidiaryId):本 REQ 不写入(落库NULL,列已 nullable)。多租户上下文将由 REQ-USR-004 引入登录会话后注入;后续 REQ 通过迁移补齐已有数据。 -
sCreatedBy:本 REQ 落库NULL。等待 REQ-USR-004 引入登录用户上下文后从SecurityContextHolder取当前用户号回写。 -
sId(业务 ID 标准列):本 REQ 落库NULL,不在本接口分配。后续若有外部对接需求再以独立 migration 补齐策略。
边界与约束
鉴权策略(本 REQ 限定)
- 项目首个 REQ,尚无 SecurityFilterChain。本 REQ 在
SecurityConfig里配置permitAll()临时放行所有/api/**。 - Controller 不写
@PreAuthorize("hasAuthority('MOD:CREATE')")(无 SecurityContext 时该注解会因Authentication=null抛AccessDeniedException);改为在 Controller 上方写一行说明性注释:// REQ-USR-004 完成后改为 @PreAuthorize("hasAuthority('MOD:CREATE')")。 - REQ-USR-004 完成后会回头给本接口加
@PreAuthorize,并把permitAll改为authenticated()。这是已知技术债,仅在 REQ-USR-004 范围内偿还,本 REQ 不擅自前置。
字段长度与字符集
- 字符集统一
utf8mb4(DDL 已约束),允许中文落库。 - 字符串字段超出 DDL 长度限制视为参数错误(
40010),不截断。
事务
- Service 方法标注
@Transactional(rollbackFor = Exception.class),单表写入即可,事务范围最小化。
性能与并发
- 本 REQ 是单条 INSERT,预期无高并发。
uk_procedure_name唯一约束兜底并发竞争;唯一冲突映射为40911。
项目骨架引导(首 REQ 一次性附带)
本 REQ 顺带建立最小可运行的 Spring Boot 项目骨架(仅引入服务于本 REQ 的部分;后续 REQ 按需扩充)。预期新增目录与文件:
backend/
├── pom.xml
└── src/main/
├── java/com/xly/erp/
│ ├── ErpApplication.java
│ ├── config/
│ │ ├── MybatisPlusConfig.java
│ │ └── SecurityConfig.java // 临时 permitAll
│ ├── common/
│ │ ├── response/
│ │ │ ├── ApiResponse.java
│ │ │ └── ErrorCode.java
│ │ └── exception/
│ │ ├── BizException.java
│ │ └── GlobalExceptionHandler.java
│ └── module/mod/
│ ├── controller/ModuleController.java
│ ├── service/ModuleService.java
│ ├── service/impl/ModuleServiceImpl.java
│ ├── mapper/ModuleMapper.java
│ ├── entity/Module.java
│ ├── dto/ModuleCreateDTO.java
│ └── vo/ModuleVO.java
└── resources/
├── application.yml // ${DB_HOST}/${DB_PORT}/... 占位,从 .env.local 注入
└── mapper/mod/ModuleMapper.xml
pom.xml 依赖:spring-boot-starter-web / -validation / -security、mybatis-plus-spring-boot3-starter、flyway-core + flyway-mysql、mysql-connector-j、mapstruct、hutool-all、spring-boot-starter-test、spring-security-test。
application.yml 通过 Spring Profile + dotenv-java(或本地启动脚本)加载 .env.local;不在仓内硬编码任何凭据(与 docs/04 § 3.5 一致)。
依赖的 schema 表 / 字段
写表:tModule(详见 docs/03-数据库设计文档.md § tModule)
| 字段 | 落库逻辑 |
|---|---|
iIncrement |
DB 自增分配 |
sId |
落 NULL(本 REQ 不分配业务 ID) |
sBrandsId |
落 NULL(多租户 REQ-USR-004 后引入) |
sSubsidiaryId |
落 NULL(同上) |
tCreateDate |
应用层 LocalDateTime.now()
|
sDisplayType |
入参(必填) |
sProcedureName |
入参(必填,唯一) |
sModuleType |
入参(必填) |
sManageDeptEn |
入参(必填) |
bShowPermission |
入参(默认 false) |
sModuleNameZh |
入参(必填) |
iParentId |
入参(可选;FK 校验通过的 tModule.iIncrement) |
iSortOrder |
入参(默认 0) |
sCreatedBy |
落 NULL(REQ-USR-004 后引入登录上下文) |
bDeleted |
落 0
|
tDeletedDate |
落 NULL
|
sDeletedBy |
落 NULL
|
索引利用:
-
uk_procedure_nameUNIQUE:唯一性约束触发并发兜底 -
idx_parent:父模块查询场景(FK 校验时按iParentId查tModule)
外键:本 REQ 利用 fk_module_parent 作为 DB 层兜底;应用层在写入前先查父模块存在性,提前返回 40411,避免直接抛 SQL 完整性异常。
依赖的接口
无(本接口是 module_mod 的入口接口,不依赖其他业务接口)。
后续在 REQ-USR-004 完成后,需要回归补齐本接口的 @PreAuthorize("hasAuthority('MOD:CREATE')"),并要求请求携带有效 JWT。
验收标准
功能正确性
-
正向 — 根模块:提交完整字段、
iParentId=null,返回 200 +data.iIncrement非空;DB 中查询新记录字段与入参一致;bDeleted=0/tCreateDate已写入。 -
正向 — 子模块:先创建 root 再以其
iIncrement作为iParentId创建子模块,返回 200。 -
唯一性冲突:用相同
sProcedureName二次提交,返回code=40911+ 中文 message;DB 不产生重复记录。 -
父模块不存在:传入
iParentId=999999,返回code=40411。 -
必填缺失 / 枚举非法 / 长度超限:返回
code=40010+ 错误字段名定位。 -
可选默认值:不传
bShowPermission/iSortOrder→ DB 落false / 0。
接口契约一致性
- 响应格式严格符合
{code, message, data, timestamp}(docs/05 § 全局约定)。 - 错误码段位与 docs/05 一致:
200/40010/40911/40411/500xx。 - 异常堆栈不出现在响应里(GlobalExceptionHandler 拦截并映射为友好错误,docs/04 § 1.4)。
测试覆盖(feature-tdd 阶段)
-
单元测试:
ModuleServiceImpl#create覆盖 6 类正/反路径(含唯一冲突的DuplicateKeyException映射)。 -
集成测试(MockMvc + 真实 MySQL test schema):
- 正向:根模块、子模块创建后查 DB 验证字段
- 反向:唯一冲突、父模块不存在、必填缺失、枚举非法、长度超限 ≥ 5 个 case
- 并发:两线程同时插入同
sProcedureName,断言一条成功 + 一条 40911(可选,若 CI 跑得动)
-
测试数据隔离:每个测试方法
@Transactional自动回滚;不污染其他测试。
代码与文档
-
// REQ-MOD-001注释贴在 Controller / Service / Mapper / DTO / VO 关键类。 - 提交按
feat(mod): add module create endpoint REQ-MOD-001规范。 - 不引入 docs/04 § 零 技术栈外的依赖(如需,按软规则 S1 经 AskUserQuestion)。