--- 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 ` + 权限码 `MOD:CREATE`。本 REQ 暂不实施实际鉴权(见 § 边界与约束),但 Controller 仍按需要鉴权的形态编写(无 `@AnonymousAccess` 标注),以便 REQ-USR-004 加入 SecurityFilterChain 后零改动。 ## 输出 / 结果 **HTTP 200,响应体**(统一响应格式): ```json { "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`)不对外暴露。 ## 业务规则 1. **唯一性**:`sProcedureName` 在未软删除范围内(`bDeleted=0`)系统内唯一。冲突返回错误码 `40911`。 2. **父模块校验**:若 `iParentId` 非空,必须指向 `tModule` 中存在且 `bDeleted=0` 的记录;不存在或已删除返回错误码 `40411`。 3. **bShowPermission 默认值**:未传入或传 `null` → 落库 `0`。 4. **iSortOrder 默认值**:未传入 → 落库 `0`。 5. **新建记录初始状态**:`bDeleted=0`、`tDeletedDate=NULL`、`sDeletedBy=NULL`、`tCreateDate=now()`(应用层取系统时间,不依赖 DB DEFAULT)。 6. **多租户字段 (`sBrandsId` / `sSubsidiaryId`)**:本 REQ **不写入**(落库 `NULL`,列已 nullable)。多租户上下文将由 REQ-USR-004 引入登录会话后注入;后续 REQ 通过迁移补齐已有数据。 7. **`sCreatedBy`**:本 REQ 落库 `NULL`。等待 REQ-USR-004 引入登录用户上下文后从 `SecurityContextHolder` 取当前用户号回写。 8. **`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_name` UNIQUE:唯一性约束触发并发兜底 - `idx_parent`:父模块查询场景(FK 校验时按 `iParentId` 查 `tModule`) **外键**:本 REQ 利用 `fk_module_parent` 作为 DB 层兜底;应用层在写入前先查父模块存在性,提前返回 `40411`,避免直接抛 SQL 完整性异常。 ## 依赖的接口 无(本接口是 module_mod 的入口接口,不依赖其他业务接口)。 后续在 REQ-USR-004 完成后,需要回归补齐本接口的 `@PreAuthorize("hasAuthority('MOD:CREATE')")`,并要求请求携带有效 JWT。 ## 验收标准 ### 功能正确性 1. **正向 — 根模块**:提交完整字段、`iParentId=null`,返回 200 + `data.iIncrement` 非空;DB 中查询新记录字段与入参一致;`bDeleted=0` / `tCreateDate` 已写入。 2. **正向 — 子模块**:先创建 root 再以其 `iIncrement` 作为 `iParentId` 创建子模块,返回 200。 3. **唯一性冲突**:用相同 `sProcedureName` 二次提交,返回 `code=40911` + 中文 message;DB 不产生重复记录。 4. **父模块不存在**:传入 `iParentId=999999`,返回 `code=40411`。 5. **必填缺失 / 枚举非法 / 长度超限**:返回 `code=40010` + 错误字段名定位。 6. **可选默认值**:不传 `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)。