diff --git a/docs/superpowers/specs/2026-06-01-REQ-USR-001.md b/docs/superpowers/specs/2026-06-01-REQ-USR-001.md new file mode 100644 index 0000000..5824e8d --- /dev/null +++ b/docs/superpowers/specs/2026-06-01-REQ-USR-001.md @@ -0,0 +1,114 @@ +# REQ-USR-001 增加用户 — 实现规格(后端) + +> 阶段:后端(backend)。作用域限定 controller / service / repository / DTO / 校验 / SQL migration / REST 契约。 +> SSoT 引用:需求卡片 `docs/01-需求清单/USR-用户管理/REQ-USR-001.md`;DB 设计 `docs/03-数据库设计文档.md`;API 契约 `docs/05-API接口契约.md`;技术规范 `docs/04-技术规范.md`。 +> 本规格只消费已锁定事实,忽略 UI 描述(控件类型/按钮位置/布局),但校验规则与业务规则全部下沉到后端 DTO + Service。 + +--- + +## 1. Goal(目标) + +后台管理员新建用户账号:指定用户名、密码(默认初始化)、用户类型、语言、可选关联职员、可选权限组授权,保存后账号立即生效可用。对外仅提供一个端点:`POST /api/usr/users`。 + +--- + +## 2. 输入 / 输出 + +### 2.1 输入(请求) + +- **Method / Path**:`POST /api/usr/users` +- **Auth**:需要 Bearer JWT,且调用方必须为管理员 / 超级管理员(`sUserType = 超级管理员`)。普通用户调用返回 `40301`。 +- **请求体(JSON)→ `CreateUserDTO`**: + +| DTO 字段 | 类型 | 必填 | 校验 | 落库列(`usr_user`) | 说明 | +|---|---|---|---|---|---| +| `sUserName` | String | 是 | `@NotBlank` + `@Pattern(regexp="^[A-Za-z0-9_]{3,20}$")` | `sUserName` | 登录账号,3-20 位字母/数字/下划线,全局唯一 | +| `sUserNo` | String | 否 | `@Size(max=50)` | `sUserNo` | 用户号,可由前端在选择职员后带出(后端按传入值落库;为空则存 null) | +| `iEmployeeId` | Integer | 否 | — | `iEmployeeId` | 关联职员 ID;传入时必须为 `usr_employee` 中存在的记录,否则 `40001` | +| `sUserType` | String | 是 | `@NotBlank` + 取值 ∈ {`普通用户`,`超级管理员`},默认 `普通用户` | `sUserType` | 用户类型;前端未传时由 Service 兜底为 `普通用户` | +| `sLanguage` | String | 是 | `@NotBlank` + 取值 ∈ {`中文`,`英文`,`繁体`} | `sLanguage` | 界面语言 | +| `iCanModifyBill` | Integer | 否 | 取值 ∈ {0,1},默认 0 | `iCanModifyBill` | 单据修改权限 | +| `permissionIds` | List\ | 否 | 元素须为 `usr_permission` 中存在的 id(去重) | 写 `usr_user_permission` | 权限组勾选;为空 / null 表示不授权 | +| `initialPassword` | String | 否 | `@Size(max=100)`,默认 `666666` | `sPassword`(BCrypt 哈希) | 初始密码;未传时取默认 `666666`,BCrypt 哈希后入库 | + +> 系统生成字段(不接受前端传入,由后端填充):`tCreateDate`=当前时间;`sCreator`=当前登录用户(从 JWT/SecurityContext 取 `sUserName`);`sPassword`=BCrypt(initialPassword);`iIsVoid`=0(新建即生效);`sBrandsId`/`sSubsidiaryId`=表默认值 `1111111111`(标准列默认,多租户隔离)。`tLastLoginDate` 保持 null。 + +### 2.2 输出(响应) + +- 成功:`Result<{ id: number }>`,`code=0`,`data.id` = 新建用户主键 `usr_user.iIncrement`。 +- 失败:统一 `Result`(见 § 6 错误码),`message` 给可读中文提示,不抛栈。 + +--- + +## 3. 业务规则 + +1. **用户名全局唯一**:插入前按 `sUserName` 查重(命中唯一索引 `uk_usr_user_username`),存在则抛 `BusinessException(40901)`。即便并发命中唯一约束(`DuplicateKeyException`),也统一转换为 `40901`。 +2. **密码初始化 + 哈希存储**:`initialPassword` 缺省为 `666666`;一律经 `BCryptPasswordEncoder` 哈希后写入 `sPassword`,**禁止明文落库 / 进日志 / 进响应**。响应与任何查询均不返回密码。 +3. **用户类型默认与约束**:`sUserType` 缺省 `普通用户`;取值仅限 {`普通用户`,`超级管理员`},越界 `40001`。 +4. **语言取值约束**:`sLanguage` 仅限 {`中文`,`英文`,`繁体`}(依据 docs/03 业务含义与 REQ 卡片下拉来源),越界 `40001`。默认值见 § 8 决策记录。 +5. **关联职员可选且需存在**:传 `iEmployeeId` 时校验 `usr_employee` 存在;不存在 `40001`。职员删除时外键 `ON DELETE SET NULL`,与本 REQ 写入无冲突。 +6. **权限组授权(多对多)**:`permissionIds` 非空时,校验每个 id 在 `usr_permission` 存在(不存在 `40001`),去重后为新用户在 `usr_user_permission` 批量插入 `(iUserId, iPermissionId)` 行;唯一索引 `uk_usr_user_permission` 防重复授权。 +7. **新建即生效**:`iIsVoid=0`,无需额外激活流程,保存后即可登录(登录由 REQ-USR-004 负责)。 +8. **制单人 / 创建时间审计**:`sCreator` 取当前登录用户名,`tCreateDate` 取当前时间;按 docs/04 § 3.4 可由 `BaseEntity` + MP 自动填充,或在 Service 显式赋值(实现二选一,结果一致)。 +9. **权限校验前置**:仅 `超级管理员` / 管理员可调用;非管理员 `40301`(在 Spring Security 或 Service 入口判定,先于业务校验)。 + +--- + +## 4. 约束(技术 / 安全) + +- **分层**(docs/04 § 1.2):`UsrUserController`(仅 `@Valid` 校验 + 委派)→ `UsrUserService` / `UsrUserServiceImpl`(业务)→ `UsrUserMapper` / `UsrUserPermissionMapper`(MyBatis-Plus)。Controller 禁止直接操作 Mapper。 +- **包路径**:`com.xly.erp.modules.usr.{controller,service,service.impl,mapper,entity,dto,vo}`。本 REQ 仅触及 `modules/usr/**`,不跨模块。 +- **命名**(docs/04 § 1.3):类 `UsrUserController` / `UsrUserServiceImpl` / `UsrUserMapper`;方法 `createUser`;REST 路径 `/api/usr/users`。 +- **统一响应**(docs/04 § 1.4):返回 `Result`,`code=0` 成功;错误码集中在 `ResultCode` 枚举。 +- **异常处理**(docs/04 § 1.5):业务错误抛 `BusinessException(ResultCode,msg)`,由 `GlobalExceptionHandler` 转 `Result`;`@Valid` 校验失败由全局处理器转 `40001`。 +- **事务**(docs/04 § 1.6):`createUser` 涉及 `usr_user` + `usr_user_permission` 多表写,方法上加 `@Transactional(rollbackFor = Exception.class)`,任一步失败整体回滚。 +- **认证 / 密码**(docs/04 § 1.7):受保护接口经 `JwtAuthenticationFilter`;密码用 `BCryptPasswordEncoder`,禁明文。 +- **数据访问**(docs/04 § 3.4):只走 Mapper;查重 / 校验用 `LambdaQueryWrapper` 或 MP 内置。 +- **配置**:JWT 密钥、DB 凭据只从 `config-vars.yaml` / `application.yml` 读取,不硬编码。`admin_init`(admin/666666)为系统初始管理员,与本 REQ 默认密码 `666666` 一致。 +- **schema**:本 REQ 所需表(`usr_user` / `usr_employee` / `usr_permission` / `usr_user_permission`)已由 `sql/migrations/V1__initial_schema.sql` 建好,**无需新增 migration**。 + +--- + +## 5. Schema 引用(docs/03 SSoT) + +- **写**:`usr_user`(主键 `iIncrement`;唯一索引 `uk_usr_user_username` on `sUserName`;列见 docs/03 § usr_user)。 +- **写**:`usr_user_permission`(关联表;唯一索引 `uk_usr_user_permission` on `(iUserId,iPermissionId)`;外键 `fk_usr_up_user` / `fk_usr_up_permission` 均 CASCADE)。 +- **读**:`usr_employee`(校验 `iEmployeeId` 存在);`usr_permission`(校验 `permissionIds` 存在)。 +- 实体:`UsrUser` ↔ `usr_user`,`UsrUserPermission` ↔ `usr_user_permission`,`UsrEmployee` ↔ `usr_employee`,`UsrPermission` ↔ `usr_permission`(匈牙利前缀列名,实体字段与列名映射保持一致)。 + +--- + +## 6. API 引用 / 错误码(docs/05 SSoT) + +- 端点契约见 `docs/05-API接口契约.md` § REQ-USR-001。 +- 错误码: + - `40001` — 参数校验失败(字段格式 / 必填 / 枚举越界 / 关联 id 不存在)。 + - `40901` — 用户名已存在(`sUserName` 唯一冲突)。 + - `40301` — 无权限(非管理员调用)。 + - `0` — 成功,返回 `data.id`。 + +--- + +## 7. 验收标准(Acceptance Criteria) + +1. **正常新增**:管理员携带合法 body(`sUserName` 合规、`sUserType`/`sLanguage` 合法)调用 `POST /api/usr/users` → `code=0`,`data.id` 为正整数;`usr_user` 新增一行,`sPassword` 为 BCrypt 哈希(非明文,可被 `BCryptPasswordEncoder.matches("666666", hash)` 校验通过),`iIsVoid=0`,`sCreator`=调用者用户名,`tCreateDate` 已填。 +2. **重复用户名**:以已存在的 `sUserName` 调用 → `code=40901`,无新增行(事务回滚)。 +3. **参数非法**:`sUserName` 不满足 3-20 位字母数字下划线 / `sUserType` 或 `sLanguage` 越界 / `iEmployeeId` 或 `permissionIds` 引用不存在的 id → `code=40001`,无副作用。 +4. **权限组授权**:传 `permissionIds=[a,b]`(均存在)→ `usr_user_permission` 新增 2 行 `(newUserId, a/b)`;重复 id 去重后仅写一次。 +5. **越权访问**:普通用户(`sUserType=普通用户`)或无 / 失效 token 调用 → `code=40301`(无权限)/ 401(未认证),不创建记录。 +6. **默认密码**:不传 `initialPassword` 时,按 `666666` 哈希入库;可用 `666666` 经后续登录流程通过。 +7. **响应不含密码**:成功响应体仅含 `data.id`,绝不含 `sPassword` 或明文密码。 + +--- + +## 8. 自主决策记录(decisions) + +| # | 问题 | 选择 | 依据 | 置信度 | +|---|---|---|---|---| +| D1 | `sLanguage` 默认值(REQ 卡片标"无默认",docs/03 标默认 `中文` 且带"需用户审阅"占位) | 后端**不强制默认**,`sLanguage` 为必填、必须由请求显式传入合法值;若 DB 层因 NOT NULL DEFAULT `中文` 而存在兜底,仅作为防御性约束,不作为业务默认 | REQ 卡片「语言 必填=是、默认值=—」优先表达业务意图;docs/03 的 `中文` 默认带审阅占位,不作为锁定事实。取必填可避免歧义 | medium | +| D2 | 管理员判定口径 | 以 `sUserType=超级管理员` 视为有调用权限;普通用户禁止。具体角色映射若后续有 Spring Security 角色体系再细化 | docs/05 标注"仅管理员/超级管理员可调用",本库用户类型枚举仅 {普通用户,超级管理员},无独立管理员角色表 | medium | +| D3 | `sUserNo` 来源 | 后端按请求传入值落库(前端在选择职员后带出员工编号/姓名填入),后端不主动从职员表反查覆盖 | REQ 卡片业务规则"关联职员选择后自动输入"属前端交互;契约 `sUserNo(可选)` 直接接收,后端阶段不重复实现前端联动 | high | +| D4 | 不新增 migration | 复用 `V1__initial_schema.sql` 已建的 4 张表 | 4 张依赖表(usr_user 写、usr_employee/usr_permission 读、usr_user_permission 写)均已在 V1 建好,结构与 docs/03 一致,无 schema 变更 | high | +| D5 | 密码字段命名 | 请求体用 `initialPassword`,落库列 `sPassword`(BCrypt) | 与 docs/05 契约一致(`initialPassword(可选,默认 666666)`);docs/04 § 1.7 要求 BCrypt | high | + +> 注:docs/03 中 `sLanguage` / `usr_company.sVersion` / `usr_permission` 粒度的「需用户审阅」占位为 DB 文档遗留标记,不在本后端 REQ 作用域内消解;本规格按上表 D1 取最有依据解读继续,不阻塞。