front/docs/rbac系统设计-050901.md
2026-06-04 19:15:28 +08:00

2612 lines
107 KiB
Markdown
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# RBAC 与统一认证 — 规范说明
## 导读
### 这份文档解决什么问题?
用直白话说,系统要搞清楚四件事:
1. **谁能登录**(内部员工、客户主账号、客户子账号)——账号与凭证放在哪张表、怎么区分类型。
2. **登录之后凭什么进业务**——访问令牌、刷新令牌,以及改权限、改密码后旧令牌如何作废。
3. **能操作哪些功能**——有效权限码来自 **`t_principal_role`** 的角色并集;**客户子账号**还可由主账号在 **`t_customer_sub_permission`** 配**可选白名单**,与角色并集**求交**再收窄(无任何白名单行时等于仅用角色)。用意是:同一角色模板下仍可把某个子账号缩到更少能力,而不必为每人微调就新建一堆细角色;**菜单由同一套有效权限码推导**,并非与白名单并列的另一套「来源」。**菜单只负责界面展示,不能代替接口鉴权**。
4. **出了问题怎么追溯**——RBAC 关键动作与普通业务增删改,统一落在 **`t_operation_log`**(支持字段级、JSON 快照或 RBAC 动作行)。
更细的表字段、接口示例见后文各节。
### 正文层级怎么读?
编号用 **点分十进制**,层级越深小数位越多(与 Markdown 标题 `##` → `###` → `####` → `#####` 对应):
| 层级 | 示例 | 说明 |
| --- | --- | --- |
| 章 | **1**、**2**、… **5** | 五大块(表结构、令牌、接口清单、详细设计、客户域) |
| 节 | **1.1**、**2.1**、**4.2** | 章内分节(`###`) |
| 条 | **1.1.1**、**3.1.3**、**4.3.2** | 更细条目或接口分组(`####`) |
| 款 | **4.2.1.1**、**4.2.1.2** | 单个接口下的「请求示例」「业务逻辑」(`#####`) |
| 目 | **4.2.1.2.1**、**4.2.1.2.2** | 「核心流程」「校验与异常」等(`######`) |
**五章一览**:**1** 表结构 · **2** 令牌与鉴权 · **3** 接口路径速查 · **4** 接口详细设计 · **5** 客户域接口详细设计(**5.1** OMS-Customer 资料 **6** 接口 · **5.2** 主/子账号 RBAC 目标 **8** 接口,结构与 **4** 相同)。
### 建议怎么读?
| 你是谁 | 建议路径 |
| --- | --- |
| **后端 / DBA** | **1.1** 概念与 **1.1.6** 落地自检 → **1.2** 起各表;需要时看 **4** |
| **前端 / BFF** | **2** → **3** → **4**(管理端 RBAC);客户资料与 **5.1**、客户 RBAC 目标 **5.2**;参数与异常看对应章节 |
| **产品 / 测试** | **1.1** 前几段 + **2.2** 客户子账号表 + **5**(**5.1** 客户资料 OpenAPI + **5.2** RBAC 设计项) |
**第一次通读**:**1**(概念 + 流程图 + **1.1.6**)→ **2** → 再按需打开 **3~5**。
## 1 表结构(按链路自上而下)
> **本章导读**:先读 **1.1**「概念模型」四件事和流程图,建立「**登录账号 → 主体 → 角色 → 权限码**」;再按需翻到 **1.2** 起各数据表。三张枢纽表:**`t_auth_account`**、**`t_principal_role`**、**`role_permission`**。
### 1.1 RBAC 概念模型与设计原则(主体·组织·角色·权限·菜单)
本段回答四件事:**谁**在登录、**凭什么**进系统、**能做什么**(权限从哪来)、**菜单与 API 的分工**。以下与 **2**、**2.2** 一致。
#### 1.1.1 设计目标
- **统一身份**:所有可登录主体(内部用户、客户主账号、客户子账号)各对应 **`t_auth_account` 一行**(`PrincipalType` + `PrincipalId` 唯一),登录凭证(手机、邮箱、口令、微信等)只落在认证表,**不**散落在业务主体表。
- **统一授权**:主体与角色的绑定**只通过** **`t_principal_role`**;角色拥有的权限码**只通过** **`role_permission`**;权限码的**合法集合**由 **`t_permission_definition`** 定义。API 与后台任务**不得以**未注册的字符串当作权限码。
- **可吊销、可版本化**:授权变更、改密、全员登出等通过 **`PermissionRevision`** / **`SessionVersion`** 与令牌中的 `permission_rev` / `session_ver` 对齐(详见 **2**),避免「改权限后旧令牌长期有效」。
- **可审计**:**RBAC 关键动作**与**任意业务**对数据的增删改均落 **`t_operation_log`**(见 **1.13**):业务侧可选 **字段级**、**快照**(`OldSnapshotJson`/`NewSnapshotJson`,前端 diff);RBAC 动作用 **`Action`** 为动作码 + **`DetailJson`** / **`TargetPrincipal*`** / **`RoleId`** 等,与订单/客户等共用同一追溯模型。
#### 1.1.2 三层分工(身份 / 授权 / 展示)
| 层次 | 回答的问题 | 核心对象 |
| --- | --- | --- |
| **身份(Authentication)** | 你是谁?凭证是否有效? | `t_auth_account` → 映射到业务主体 |
| **授权(Authorization)** | 你被允许执行哪些 **PermissionCode**? | `t_principal_role` → `role` → `role_permission` → ⊆ `t_permission_definition` |
| **展示(UI,非鉴权替代)** | 控制台显示哪些菜单项? | `menu`(每节点绑定一个 `PermissionCode`,用于**裁剪**,**不能替代** API 侧校验) |
#### 1.1.3 有效权限码集合 \(P\) 怎么算
记某主体通过 `t_principal_role` 绑定的所有角色,其 `role_permission` 中权限码的并集为 **\(P_{\text{role}}\)**(均在 `t_permission_definition` 中有定义且未删除)。
- **`PrincipalType = user` 或 `customer`(主账号)**
**\(P = P_{\text{role}}\)**。
- **`PrincipalType = customer_sub`(子账号)**
先算 **\(P_{\text{role}}\)**。若 **`t_customer_sub_permission`** 对该子账号存在未删除白名单行,则 **\(P = P_{\text{role}} \cap \text{白名单码集}\)**;若**无**白名单行,则 **\(P = P_{\text{role}}\)**。
**风控约束**:子账号权限必须**不超过**其主客户主账号的权限集合(**\(P \subseteq P_{\text{主}}\)**),保存时事务内强校验;细节与接口见 **2.2**、**5**。
#### 1.1.4 组织、字典、客户域在 RBAC 中的位置
- **组织 `organization`**:**仅**与内部用户 **`user.OrganizationId`** 关联,用于人事与**行级数据范围**等业务查询;**不是**权限码,也**不**替代 `PermissionCode`。客户主账号、子账号**不挂**组织树。
- **字典 `t_dict`**:存放岗位、学历、客户类型、结算方式等**可配置枚举**(亦可供其他模块扩展 `DictType`,如 `notice_type`、`warehouse_type`);业务表存 **`DictCode`**,按 **`DictType` + `DictCode`** 联查 **`DictName`**,须 **`IsDeleted=0`**;列表可按 **`DictSort`** 排序(见 **1.14**)。
- **令牌 `org_id`**:对客户类主体通常**为空**;内部用户可按实现携带,用于 BFF 做数据范围过滤(与权限码校验正交)。
#### 1.1.5 分层职责一览表
| 层级 | 对象 | 职责 |
| --- | --- | --- |
| 认证 | `t_auth_account` | **手机 + 邮箱注册必填**;口令;**微信(可选)**;**`PermissionRevision`/`SessionVersion`**;`PrincipalType` + `PrincipalId` → 业务主体 |
| 主体 | `user` / `t_customer` / `t_customer_sub_account` | 业务专有资料;**不存**口令/手机/邮箱、**微信头像/微信昵称**(见 `t_auth_account`);`user` 另存**员工档案**(岗位/学历等为 **`t_dict` 编码**等);`t_customer` 另存**工商+执照影像**、**客户类型/结算方式(字典)**、**应收财务**等;`user.Avatar` 仅为本地上传等非微信展示;**不**直接存权限集合 |
| 组织 | `organization` | **仅** `user.OrganizationId`;数据范围/人事,**不是**权限码 |
| 权限目录 | `t_permission_definition` | 权限码**权威库**;API / `menu` / `role_permission` 的 `Code` 须存在且未删 |
| 授权 | `t_principal_role` → `role` → `role_permission` | **PermissionCode** 鉴权,码集 ⊆ 权限目录 |
| 菜单 | `menu` | 按 `PermissionCode` **裁剪**前端路由/菜单,**不替代** API 校验 |
| 审计与操作日志 | **`t_operation_log`** | **全系统统一表**:**RBAC 动作**(一行一动作,`Action` 为动作码)与 **业务数据** **`ADD`/`UPDATE`/`DELETE`**(**字段级**或 **快照**);业务与定时任务、登录主体均可写入 |
**关联小结**:主体 → 角色 **只经** `t_principal_role`;客户子账号可再经 **`t_customer_sub_permission`** 做**交集**收紧。下图与上表一致,从登录身份走到「权限码集合 \(P\)」再分流到 API 与菜单。
```mermaid
flowchart LR
A[t_auth_account] --> B["主体 user / customer / customer_sub"]
B --> C[t_principal_role]
C --> D[role]
D --> E[role_permission]
E --> F["有效权限码集合 P"]
E -.-> I[t_permission_definition]
W[t_customer_sub_permission] -.->|"子账号:与白名单求交(可选)"| F
F --> G["API:校验 PermissionCode"]
F --> H["menu:按 PermissionCode 过滤"]
H -.-> I
```
#### 1.1.6 落地自检(实现/联调)
- 任意受保护 API 的 PermissionCode **必须** ∈ `t_permission_definition` 且未删除;**`menu.PermissionCode`** 列长度与定义表一致(**1.11**)。
- 写入 **`t_principal_role`** 或子账号 **`t_customer_sub_permission`** 成功后,对应主体 **`PermissionRevision`+1**(与 **2** 令牌策略一致)。
- 分配角色前校验 **`role.AllowedPrincipalTypes`** 与主体类型匹配。
- 子账号保存角色/白名单时 **\(P_{\text{子}}\subseteq P_{\text{主}}\)** 强校验;违反则整单 **400**(见 **2.2**)。
- 关键写操作(含 RBAC 与业务数据)写 **`t_operation_log`**;改密、吊销会话等 **`SessionVersion`** 规则见 **2**。
- 业务数据 **ADD/UPDATE/DELETE** 按 **`t_operation_log`** 规则落库(**字段级或快照二选一为主**、批量不合并、系统操作 **`OpPrincipalType`=`system`** 等,见 **1.13**)。
### 1.2 统一认证账号表 `t_auth_account`
| 字段名 | 字段类型 | 长度 | 主键/外键 | 非空 | 说明 |
| --- | --- | --- | --- | --- | --- |
| Id | BIGINT | - | 主键 | 是 | 自增主键 |
| Phone | VARCHAR | 16 | - | 是 | **注册必填**;全局唯一 |
| Email | VARCHAR | 64 | - | 是 | **注册必填**;全局唯一 |
| PasswordHash | VARCHAR | 256 | - | 否 | 密码哈希;可与微信并存;纯微信快捷登录且无本地口令时可空(须已完成手机+邮箱注册并绑定微信) |
| PasswordSalt | VARCHAR | 128 | - | 否 | 密码盐值;无本地口令时可为空 |
| WechatOpenid | VARCHAR | 64 | - | 否 | 可选;**便捷登录**,非开户凭证;绑定前须已有手机+邮箱注册行 |
| WechatUnionid | VARCHAR | 64 | - | 否 | 微信 UnionID(跨应用识别,可选) |
| WechatAvatar | VARCHAR | 255 | - | 否 | 微信头像 URL(OAuth/授权回调同步;各主体认证行均可有值) |
| WechatNickname | VARCHAR | 64 | - | 否 | 微信昵称(展示;与 OpenID 同源更新) |
| PrincipalType | VARCHAR | 32 | - | 是 | 主体类型:`user`(内部用户)/ `customer`(客户主账号)/ `customer_sub`(客户子账号) |
| PrincipalId | BIGINT | - | - | 是 | 主体业务主键:分别关联 `user.Id`、`t_customer.Id`、`t_customer_sub_account.Id`(均为 BIGINT,非 GUID) |
| Status | TINYINT | - | - | 是 | 状态:0-禁用,1-启用 |
| LastLoginTime | DATETIME | - | - | 否 | 最后登录时间 |
| LastLoginIp | VARCHAR | 64 | - | 否 | 最后登录 IP |
| PermissionRevision | INT | - | - | 是 | 该主体 `t_principal_role` 或(`customer_sub`)`t_customer_sub_permission` 白名单变更时 **+1**;access token `permission_rev` 与库一致(见 **2**) |
| SessionVersion | INT | - | - | 是 | 改密、全部登出、冻结等 **+1**;refresh 内 `session_ver` 对齐 |
| IsDeleted | TINYINT | - | - | 是 | 是否删除:0-否,1-是,默认0 |
| CreateUserId | INT | - | - | 是 | 创建人 |
| CreateTime | DATETIME | - | - | 是 | 创建时间 |
| UpdateUserId | INT | - | - | 否 | 更新人 |
| UpdateTime | DATETIME | - | - | 否 | 更新时间 |
> **注册**:`Phone`、`Email` **均必填、全局唯一**。微信 OpenID **可选**,仅快捷登录/展示,**不可替代**手机+邮箱开户。`WechatAvatar`/`WechatNickname` 同源;业务表不重复存手机/邮箱。**`IsDeleted=0` 时 `PrincipalType+PrincipalId` 唯一**(每主体一行认证账号)。**先扫码绑微信再开户 / 先开户再补绑** 见 **2.3**。
> **`Id` 与 `PrincipalId`**:本表 **`Id`** 为认证行主键;**对外主体标识**(登录响应 **`principalId`**、令牌 claims)使用 **`PrincipalId`**(业务主体主键),二者勿混用;请求参数约定见 **4.1.1**。
### 1.3 用户表 `user`
| 字段名 | 字段类型 | 长度 | 主键/外键 | 非空 | 说明 |
| --- | --- | --- | --- | --- | --- |
| Id | BIGINT | - | 主键 | 是 | 自增主键 |
| Name | VARCHAR | 32 | - | 是 | 用户姓名(展示) |
| Avatar | VARCHAR | 128 | - | 否 | 本地上传等**非微信**头像 URL;**微信头像**见 `t_auth_account.WechatAvatar` |
| PositionCode | VARCHAR | 64 | - | 否 | **岗位**:存 `t_dict.DictCode`,`DictType`=`user_position`;联查 `DictName`(**`IsDeleted=0`**);下拉可按 `DictSort` |
| Age | TINYINT | - | - | 否 | 年龄(周岁);可与 `Birthday` 冗余或仅维护其一 |
| IdCardNo | VARCHAR | 18 | - | 否 | 身份证号;敏感须脱敏;建议 `IsDeleted=0` 时全局唯一 |
| Birthday | DATE | - | - | 否 | 生日 |
| EducationCode | VARCHAR | 64 | - | 否 | **学历**:存 `t_dict.DictCode`,`DictType`=`user_education`;联查 `DictName`(**`IsDeleted=0`**);下拉可按 `DictSort` |
| Address | VARCHAR | 255 | - | 否 | 住址 |
| GraduateSchool | VARCHAR | 128 | - | 否 | 毕业学校 |
| EmergencyContact | VARCHAR | 64 | - | 否 | 紧急联系人姓名 |
| EmergencyContactPhone | VARCHAR | 32 | - | 否 | 紧急联系电话 |
| HireDate | DATE | - | - | 否 | 入职时间 |
| LeaveDate | DATE | - | - | 否 | 离职时间;在职为空 |
| WarehouseId | BIGINT | - | 外键(`t_warehouse_address.Id`) | 否 | 归属/作业仓库 |
| ConcurrencyStamp | INT | - | - | 是 | 乐观并发版本号(从 0 递增) |
| CreateUserId | INT | - | - | 是 | 创建人 |
| CreateTime | DATETIME(6) | - | - | 是 | 创建时间 |
| UpdateUserId | INT | - | - | 是 | 更新人 |
| UpdateTime | DATETIME(6) | - | - | 是 | 更新时间 |
| IsDeleted | TINYINT(1) | - | - | 是 | 是否删除:0-否,1-是,默认0 |
| OrganizationId | BIGINT | - | - | 否 | 所属组织ID |
> 手机、邮箱、**微信头像/昵称**见 **`t_auth_account`**(`PrincipalType=user`,`PrincipalId=本表.Id`)。**员工人事类字段**(证件、入离职、仓库等)仅存本表;**岗位/学历**存 **`t_dict.DictCode`**(`user_position`、`user_education`),联查与校验见 **1.14**。**档案字段的读写**:管理端 **`POST /rbac/user/create`**、**`PUT /rbac/user/update`**、**`GET /rbac/user/get`**、列表见 **4.6**(与下表字段一一对应;注册场景不必带档案,见导读与 **4.6** 说明)。
### 1.4 客户表 `t_customer`
| 字段名 | 字段类型 | 长度 | 主键/外键 | 非空 | 说明 |
| --- | --- | --- | --- | --- | --- |
| Id | BIGINT | - | 主键 | 是 | 自增唯一标识 |
| CustomerCode | VARCHAR | 32 | - | 是 | 客户编号,规则:前缀w+年份+序号(如W2501) |
| CustomerName | VARCHAR | 128 | - | 是 | 客户名称(平台主展示;可与 `CompanyName` 并存) |
| CompanyName | VARCHAR | 256 | - | 否 | 公司名称(工商全称) |
| BusinessLicenseNo | VARCHAR | 32 | - | 否 | 营业执照**号码**;敏感须脱敏 |
| BusinessLicenseImageUrl | VARCHAR | 512 | - | 否 | 执照扫描件/照片 URL |
| LegalRepresentative | VARCHAR | 64 | - | 否 | 法定代表人 |
| Website | VARCHAR | 255 | - | 否 | 官网 URL |
| SettlementMethodCode | VARCHAR | 64 | - | 否 | **结算方式**:存 `t_dict.DictCode`,`DictType`=`customer_settlement_method`;联查 `DictName`(**`IsDeleted=0`**);下拉可按 `DictSort` |
| CustomerTypeCode | VARCHAR | 64 | - | 否 | **客户类型**:存 `t_dict.DictCode`,`DictType`=`customer_type`;联查 `DictName`(**`IsDeleted=0`**);下拉可按 `DictSort` |
| AppId | VARCHAR | 64 | - | 否 | 应用 Id(开放平台/子应用/对接侧约定的字符串标识) |
| ReceivableFinanceUserId | BIGINT | - | 外键(`user.Id`) | 否 | 所属应收财务(内部用户) |
| Balance | DECIMAL | 18,2 | - | 是 | 账户余额,默认0 |
| TotalQuota | DECIMAL | 18,2 | - | 是 | 总额度,默认0 |
| UsedQuota | DECIMAL | 18,2 | - | 是 | 已用额度,默认0 |
| Status | TINYINT | - | - | 是 | 状态:0-禁用,1-正常,2-冻结 |
| Remark | VARCHAR | 512 | - | 否 | 备注信息 |
| CreateUserId | INT | - | - | 是 | 创建人(系统/管理员账号) |
| CreateTime | DATETIME | - | - | 是 | 创建时间,默认当前时间 |
| UpdateUserId | INT | - | - | 否 | 更新人 |
| UpdateTime | DATETIME | - | - | 否 | 更新时间 |
| Salesman | VARCHAR | 32 | - | 否 | 所属销售员账号 |
| CustomerService | VARCHAR | 32 | - | 否 | 所属客服账号 |
| CustomerGroup | TINYINT | - | - | 否 | 客户分组:0-普通客户,1-VIP1,2-VIP2,3-VIP3,4-VIP4,5-VIP5 |
| IsDeleted | TINYINT | - | - | 是 | 是否删除:0-否,1-是,默认0 |
**CustomerGroup 枚举**:0-普通客户、1-VIP1、2-VIP2、3-VIP3、4-VIP4、5-VIP5
> 权限 **`t_principal_role`**;手机/邮箱与微信/最后登录见 **`t_auth_account`**(微信非注册必须)。**客户类型、结算方式**存 **`t_dict.DictCode`**(`customer_type`、`customer_settlement_method`),联查 **`DictName`** 且 **`IsDeleted=0`**;**应收财务**指向 **`user`**。**管理端客户资料与重置密码**:OpenAPI **OMS-Customer** 见 **5.1**(请求/响应示例);主/子账号 RBAC 目标接口见 **5.2**。
### 1.5 子账号表 `t_customer_sub_account`
| 字段名 | 字段类型 | 长度 | 主键/外键 | 非空 | 说明 |
| --- | --- | --- | --- | --- | --- |
| Id | BIGINT | - | 主键 | 是 | 自增唯一标识 |
| MainCustomerId | BIGINT | - | 外键(t_customer.Id) | 是 | 主客户ID |
| SubAccountCode | VARCHAR | 32 | - | 是 | 子账号编号,规则:主客户编号+"_"+序号 |
| SubStatus | TINYINT | - | - | 是 | 子账号状态:0-禁用,1-正常 |
| CreateUserId | INT | - | - | 是 | 创建人(主客户/管理员) |
| CreateTime | DATETIME | - | - | 是 | 创建时间 |
| UpdateUserId | INT | - | - | 否 | 更新人 |
| UpdateTime | DATETIME | - | - | 否 | 更新时间 |
| IsDeleted | TINYINT | - | - | 是 | 是否删除:0-否,1-是 |
> 独立登录:`t_auth_account`(`customer_sub` + 本表 `Id`);**`Phone`/`Email` 注册必填**;微信/口令约定同 **2**。角色见 **`t_principal_role`**。
### 1.6 组织表 `organization`
| 字段名 | 字段类型 | 长度 | 主键/外键 | 非空 | 说明 |
| --- | --- | --- | --- | --- | --- |
| Id | BIGINT | - | 主键 | 是 | 自增主键 |
| ParentId | BIGINT | - | - | 否 | 父级组织ID |
| Name | VARCHAR | 128 | - | 是 | 组织名称 |
| Code | VARCHAR | 64 | - | 是 | 组织编码(唯一) |
| Type | VARCHAR | 32 | - | 是 | 组织类型 |
| LeaderId | BIGINT | - | - | 否 | 负责人用户ID |
| Order | INT | - | - | 是 | 排序 |
| Description | VARCHAR | 512 | - | 否 | 描述 |
| IsEnabled | TINYINT(1) | - | - | 是 | 是否启用:0-否,1-是 |
| ConcurrencyStamp | INT | - | - | 是 | 乐观并发版本号(从 0 递增) |
| CreateUserId | INT | - | - | 是 | 创建人 |
| CreateTime | DATETIME(6) | - | - | 是 | 创建时间 |
| UpdateUserId | INT | - | - | 是 | 更新人 |
| UpdateTime | DATETIME(6) | - | - | 是 | 更新时间 |
| IsDeleted | TINYINT(1) | - | - | 是 | 是否删除:0-否,1-是,默认0 |
> **行级数据范围**由业务查询叠加 `OrganizationId` / `org_id`,非 RBAC 权限码。
### 1.7 角色表 `role`
| 字段名 | 字段类型 | 长度 | 主键/外键 | 非空 | 说明 |
| --- | --- | --- | --- | --- | --- |
| Id | BIGINT | - | 主键 | 是 | 自增主键 |
| Name | VARCHAR | 32 | - | 是 | 角色名称 |
| Code | VARCHAR | 64 | - | 是 | 角色编码(唯一);与客户 **`/customer/*`** 入参 **`role`** 字符串同一套编码,长度须一致 |
| AllowedPrincipalTypes | VARCHAR | 128 | - | 是 | 逗号分隔 `user`/`customer`/`customer_sub`;写 **`t_principal_role` 前校验** |
| ConcurrencyStamp | INT | - | - | 是 | 乐观并发版本号(从 0 递增) |
| CreateUserId | INT | - | - | 是 | 创建人 |
| CreateTime | DATETIME(6) | - | - | 是 | 创建时间 |
| UpdateUserId | INT | - | - | 是 | 更新人 |
| UpdateTime | DATETIME(6) | - | - | 是 | 更新时间 |
| IsDeleted | TINYINT(1) | - | - | 是 | 是否删除:0-否,1-是,默认0 |
### 1.8 角色权限关联表 `role_permission`
| 字段名 | 字段类型 | 长度 | 主键/外键 | 非空 | 说明 |
| --- | --- | --- | --- | --- | --- |
| Id | BIGINT | - | 主键 | 是 | 自增主键 |
| RoleId | BIGINT | - | 外键(关联 `role.Id`) | 是 | 角色ID |
| PermissionCode | VARCHAR | 255 | - | 是 | 须 ∈ **`t_permission_definition.Code`**(未删) |
| CreateUserId | INT | - | - | 是 | 创建人 |
| CreateTime | DATETIME(6) | - | - | 是 | 创建时间 |
> 变更 `role_permission` 不逐行刷各主体 `PermissionRevision` 时,须 **短 access TTL(建议 ≤15 分钟)** 或缓存失效(见 **2**)。
### 1.9 权限定义表 `t_permission_definition`
| 字段名 | 字段类型 | 长度 | 主键/外键 | 非空 | 说明 |
| --- | --- | --- | --- | --- | --- |
| Id | BIGINT | - | 主键 | 是 | 自增主键 |
| Code | VARCHAR | 255 | - | 是 | 权限编码(全局唯一) |
| DisplayName | VARCHAR | 128 | - | 是 | 展示名 |
| Module | VARCHAR | 64 | - | 否 | 分组,供权限树 |
| SortOrder | INT | - | - | 是 | 排序,默认 0 |
| IsDeleted | TINYINT | - | - | 是 | 是否删除:0-否,1-是,默认0 |
| CreateUserId | INT | - | - | 是 | 创建人 |
| CreateTime | DATETIME | - | - | 是 | 创建时间 |
| UpdateUserId | INT | - | - | 否 | 更新人 |
| UpdateTime | DATETIME | - | - | 否 | 更新时间 |
> **权威库**;`get-all-permissions` / `get-permission-tree` 基于本表。
#### 1.9.1 权限树与表字段(实现约定)
> **`t_permission_definition` 无 `ParentId` 列** 时,**`GET /rbac/permission/get-permission-tree`**(**4.3.2**)的 **`children`** 由实现生成:常见做法为按 **`Module`** + **`SortOrder`** 聚类,或维护 **独立树配置/物化路径**,须在 OpenAPI 与实现注释中写清来源;**不得以未在 `t_permission_definition` 注册的节点** 作为鉴权依据。
### 1.10 主体角色关联表 `t_principal_role`
> 与 **`t_auth_account`** 主体维度一致;**主体与角色的绑定只维护本表**。扩展新主体类型时仅增加 **`PrincipalType`** 取值,并在 **`role.AllowedPrincipalTypes`**、相关接口与鉴权中一并约定。
| 字段名 | 字段类型 | 长度 | 主键/外键 | 非空 | 说明 |
| --- | --- | --- | --- | --- | --- |
| Id | BIGINT | - | 主键 | 是 | 自增主键 |
| PrincipalType | VARCHAR | 32 | - | 是 | 主体类型,与 `t_auth_account` 一致:`user`(内部用户)/ `customer`(客户主账号)/ `customer_sub`(客户子账号);预留扩展 |
| PrincipalId | BIGINT | - | - | 是 | 主体业务主键:对应 `user.Id`、`t_customer.Id`、`t_customer_sub_account.Id` 等(由 `PrincipalType` 解释,数据库层一般不建跨表 FK) |
| RoleId | BIGINT | - | 外键(关联 `role.Id`) | 是 | 角色 ID |
| IsDeleted | TINYINT | - | - | 是 | 是否删除:0-否,1-是,默认0 |
| CreateUserId | INT | - | - | 是 | 创建人 |
| CreateTime | DATETIME | - | - | 是 | 创建时间 |
| UpdateUserId | INT | - | - | 否 | 更新人 |
| UpdateTime | DATETIME | - | - | 否 | 更新时间 |
> `IsDeleted=0` 时 `PrincipalType+PrincipalId+RoleId` 唯一;`/rbac/user/assign-roles` 等即维护本表对应行。
### 1.11 菜单表 `menu`
| 字段名 | 字段类型 | 长度 | 主键/外键 | 非空 | 说明 |
| --- | --- | --- | --- | --- | --- |
| Id | BIGINT | - | 主键 | 是 | 自增主键 |
| PId | BIGINT | - | - | 是 | 父级菜单ID(根节点为 0) |
| Name | VARCHAR | 64 | - | 是 | 菜单名称 |
| PermissionCode | VARCHAR | 255 | - | 是 | 须与 **`t_permission_definition.Code`** 等长语义一致(建议 **255**);须 ∈ 权限定义且未删;**`IsDeleted=0` 时全局唯一** |
| Type | INT | - | - | 是 | 菜单类型 |
| Icon | VARCHAR | 64 | - | 否 | 图标 |
| Route | VARCHAR | 64 | - | 否 | 路由地址 |
| Component | VARCHAR | 64 | - | 否 | 组件路径 |
| Order | INT | - | - | 是 | 排序 |
| IsHidden | TINYINT(1) | - | - | 是 | 是否隐藏:0-否,1-是 |
| IsCached | TINYINT(1) | - | - | 是 | 是否缓存:0-否,1-是 |
| Description | VARCHAR | 256 | - | 否 | 描述 |
| ConcurrencyStamp | INT | - | - | 是 | 乐观并发版本号(从 0 递增) |
| CreateUserId | INT | - | - | 是 | 创建人 |
| CreateTime | DATETIME(6) | - | - | 是 | 创建时间 |
| UpdateUserId | INT | - | - | 是 | 更新人 |
| UpdateTime | DATETIME(6) | - | - | 是 | 更新时间 |
| IsDeleted | TINYINT(1) | - | - | 是 | 是否删除:0-否,1-是,默认0 |
> **`menu.PermissionCode`** 须在权限定义表中注册;**长度与 `t_permission_definition.Code` 对齐**,避免出现「权限码已注册但菜单列截断」。
### 1.12 子账号权限白名单表 `t_customer_sub_permission`
| 字段名 | 字段类型 | 长度 | 主键/外键 | 非空 | 说明 |
| --- | --- | --- | --- | --- | --- |
| Id | BIGINT | - | 主键 | 是 | 自增唯一标识 |
| SubAccountId | BIGINT | - | 外键(`t_customer_sub_account.Id`) | 是 | 子账号 ID |
| PermissionCode | VARCHAR | 255 | - | 是 | 须 ∈ **`t_permission_definition.Code`**(与 `role_permission` 同源) |
| IsDeleted | TINYINT | - | - | 是 | 是否删除:0-否,1-是,默认0 |
| CreateUserId | INT | - | - | 是 | 创建人 |
| CreateTime | DATETIME | - | - | 是 | 创建时间 |
> 有未删行:最终权限 = 子账号 `t_principal_role` 角色并集 **∩** 白名单;无白名单行:= 角色并集。恒满足 **⊆ 主账号 \(P_{\text{主}}\)**(见 **2.2**)。变更后子账号认证行 **`PermissionRevision`+1**。
### 1.13 统一审计与操作日志表 `t_operation_log`(RBAC 动作 + 业务:字段级 / 快照)
> **定位**:**RBAC 动作**与**业务数据变更**统一写入本表;**不**另建与 **`t_operation_log`** 职责重叠的 RBAC 专用审计表。按行类型使用不同列(**勿在同一业务动作里混用字段级与快照**以免重复):
> - **RBAC 动作行**:一次权限/会话类操作 **1 行**,`Action` 为 **动作码**(非 `ADD`/`UPDATE`/`DELETE`),`DetailJson` / `TargetPrincipal*` / `RoleId` 等承载语义;`FieldName`、逐字段列与快照列通常为 **`NULL`**。
> - **业务·字段级**:`FieldName` + `OldValue` / `NewValue`,便于 SQL 按字段过滤、合规导出「一行一字段」。
> - **业务·快照**:`OldSnapshotJson` / `NewSnapshotJson` 存**整条(或约定范围内的)对象前后态**,**后端不写 diff**;**前端**深度对比展示差异。
> 若既要「前端好展示」又要「库内可按字段检索」,可采用 **快照为主 + 异步任务根据快照生成字段级行**(物化),或仅保留快照、检索走搜索引擎/数仓。
| 字段名 | 字段类型 | 长度 | 主键/外键 | 非空 | 说明 |
| --- | --- | --- | --- | --- | --- |
| Id | BIGINT | - | 主键 | 是 | 自增主键 |
| OpPrincipalType | VARCHAR | 32 | - | 是 | 操作者主体类型:`user` / `customer` / `customer_sub` / **`system`**(定时任务等非登录主体) |
| OpPrincipalId | BIGINT | - | - | 否 | 操作者业务主键;**`system` 时 `NULL`** |
| OpDisplayName | VARCHAR | 64 | - | 是 | 操作者展示名**快照**(防删除后无法展示);**`system` 时固定 `系统自动`** |
| OpTime | DATETIME(6) | - | - | 是 | 记录时间(服务端写入时刻) |
| ModuleCode | VARCHAR | 64 | - | 否 | 模块编码(如 **`rbac`**、`order`、`customer`),便于筛选 |
| TargetType | VARCHAR | 64 | - | 否 | **业务行**:被操作对象类型(全库统一编码)。**RBAC 动作行**可 **`NULL`** 或填 `Principal` / `Role` 等(实现约定) |
| TargetId | VARCHAR | 64 | - | 否 | **业务行**:对象主键字符串化。**RBAC 动作行**可 **`NULL`**(若已用 `TargetPrincipal*` 表达) |
| TargetIdSecondary | VARCHAR | 64 | - | 否 | 可选第二键(子行、明细序号等) |
| TargetPrincipalType | VARCHAR | 32 | - | 否 | **RBAC**:受影响主体类型(如 `user`、`customer_sub`) |
| TargetPrincipalId | BIGINT | - | - | 否 | **RBAC**:受影响主体 Id |
| RoleId | BIGINT | - | - | 否 | **RBAC**:相关角色 Id |
| Action | VARCHAR | 64 | - | 是 | **业务**:**`ADD`** / **`UPDATE`** / **`DELETE`**(大写;批量仍**不合并行**)。**RBAC**:动作码,如 `principal_role_assign`、`role_permission_assign`、`sub_whitelist_assign`、`auth_session_revoke`、`auth_password_change` |
| DetailJson | JSON | - | - | 否 | **RBAC**:请求/变更摘要。**业务行**可 **`NULL`** 或补充元数据 |
| FieldName | VARCHAR | 128 | - | 否 | **业务·字段级**必填(列名或路径);**快照 / RBAC** 行 **`NULL`** |
| OldValue | TEXT | - | - | 否 | **业务·字段级**按 `Action` 规则;其余可 **`NULL`** |
| NewValue | TEXT | - | - | 否 | **业务·字段级**按 `Action` 规则;其余可 **`NULL`** |
| OldSnapshotJson | JSON | - | - | 否 | **业务·快照**按 `Action` 规则;**RBAC** 通常 **`NULL`** |
| NewSnapshotJson | JSON | - | - | 否 | **业务·快照**按 `Action` 规则;**RBAC** 通常 **`NULL`** |
| BatchId | VARCHAR | 64 | - | 否 | 同一批次关联号;**仅关联,不合并日志行** |
| RequestId | VARCHAR | 64 | - | 否 | 请求链路 ID |
| ClientIp | VARCHAR | 64 | - | 否 | 客户端 IP |
| Remark | VARCHAR | 512 | - | 否 | 补充说明;**`system` 操作时建议必填触发原因**(如「定时任务更新价格」) |
#### 1.13.1 记录形态约定
| 形态 | 每行含义 | 必填字段 | 典型场景 |
| --- | --- | --- | --- |
| **RBAC 动作** | 一次管理动作 **1 行** | `Action`(动作码)、`OpPrincipalType`/`OpDisplayName`/`OpTime`;`ModuleCode` 建议 **`rbac`**;`TargetPrincipal*` / `RoleId` / `DetailJson` 按动作填充 | 分配角色、改角色权限、子账号白名单、吊销会话、改密等 |
| **业务·字段级** | 同一 `Target` + `Action` + `OpTime`(及 `BatchId`)下,**每个变化字段 1 行** | `TargetType`、`TargetId`、`Action`∈{ADD,UPDATE,DELETE}、`FieldName`、`OldValue`/`NewValue`(按规则);快照与 RBAC 专用列 **`NULL`** | 报表/审计 SQL 直接查「谁改了金额」 |
| **业务·快照** | 同一 `Target` + `Action` + `OpTime`(及 `BatchId`)下,**每次对象变更 1 行** | `TargetType`、`TargetId`、`Action`、`OldSnapshotJson`/`NewSnapshotJson`(按规则) | **前端 diff**、嵌套结构 |
**快照模式注意**:`UPDATE` 建议在事务内基于**库中完整旧行**与**提交后完整新行**各序列化一份 JSON,避免「只提交部分字段」导致快照缺键、前端 diff 误判为大量删除。JSON **键顺序**不影响语义,但 diff 库可能对顺序敏感——前端对比前可做 **key 排序归一化**(实现约定)。
#### 1.13.2 写入规则(必须遵守)
**A. 业务·字段级模式**
1. **新增(`ADD`)**
- 对**每个需要留痕的字段**写 **1 条**记录(若配置为「仅关键字段」,则只对配置集内字段写)。
- **`OldValue` = `NULL`**,`NewValue` = 录入值(字符串化存储;JSON 可整段序列化)。
2. **修改(`UPDATE`)**
- **仅当字段值相对库中旧值发生变化**才写日志;**无变化不写**。
- **每个变化字段 1 条**记录:`OldValue` = 修改前,`NewValue` = 修改后。
3. **删除(`DELETE`)**
- **`OldValue` = 删除前的值**,**`NewValue` = `NULL`**。
- 可按策略记录**全部字段**或**关键字段**(与 ADD 一致,由模块配置)。
4. **批量操作**
- **逐条、逐字段**写入,**禁止**把多条数据合并成一条摘要;保证 **每条数据、每个字段变更** 均有 **独立日志行**(可用相同 `BatchId` 关联同一事务/任务)。
**B. 业务·快照模式**
1. **新增(`ADD`)**
- **`OldSnapshotJson` = `NULL`**,**`NewSnapshotJson`** = 录入后对象(建议含主键与业务键,便于前端展示)。
- **每个新增对象 1 行**(批量则多行,同一 `BatchId`)。
2. **修改(`UPDATE`)**
- 若整体与库中相比**无任何字段变化**,可不写日志。
- **每次更新 1 行**:`OldSnapshotJson` = 变更前完整对象,`NewSnapshotJson` = 变更后完整对象。
3. **删除(`DELETE`)**
- **每个删除对象 1 行**:`OldSnapshotJson` = 删除前完整对象,**`NewSnapshotJson` = `NULL`**。
4. **批量操作**
- **逐对象**写入,**不合并**;每条变更独立一行,可用相同 `BatchId`。
**C. RBAC 动作行**
1. **一次 API 或后台动作 1 行**(勿与业务字段级/快照混在同一语义行内重复描述同一事件)。
2. **`Action`** 使用已注册动作码;**`ModuleCode`** 建议 **`rbac`**。
3. **`TargetPrincipalType`/`TargetPrincipalId`、`RoleId`、`DetailJson`** 按具体接口填充;无对应实体时列可 **`NULL`**。
**D. 共用规则**
4. **系统自动操作**
- **`OpPrincipalType` = `system`**,**`OpPrincipalId` = `NULL`**,**`OpDisplayName` = `系统自动`**。
- **`Remark` 必填**:写明触发原因(如「定时任务更新价格」「规则引擎自动调账」「MQ 消费重试」)。
**操作者字段**:操作者为内部用户时,使用 **`OpPrincipalType`=`user`**,**`OpPrincipalId`** 填其 **`user.Id`**。本规范所列 RBAC 动作与业务留痕均写入 **`t_operation_log`**,**不**拆分为其它 RBAC 专用审计表。
### 1.14 字典表 `t_dict`
| 字段名 | 字段类型 | 长度 | 主键/外键 | 非空 | 说明 |
| --- | --- | --- | --- | --- | --- |
| Id | BIGINT | - | 主键 | 是 | 自增唯一标识 |
| DictType | VARCHAR | 64 | - | 是 | 字典类型(如 notice_type、warehouse_type、**`user_position`**(岗位)、**`user_education`**(学历)、**`customer_type`**(客户类型)、**`customer_settlement_method`**(客户结算方式)) |
| DictSort | INT | - | - | 是 | 字典排序,值越小越靠前 |
| DictCode | VARCHAR | 64 | - | 是 | 字典编码(同类型下唯一) |
| DictName | VARCHAR | 128 | - | 是 | 字典名称 |
| IsDeleted | TINYINT | - | - | 是 | 是否删除:0-否,1-是,默认0 |
| CreateUserId | INT | - | - | 是 | 创建人 |
| CreateTime | DATETIME | - | - | 是 | 创建时间 |
| UpdateUserId | INT | - | - | 否 | 更新人 |
| UpdateTime | DATETIME | - | - | 否 | 更新时间 |
> **约束**:业务存储、联查与接口校验均针对 **`DictType` + `DictCode`**;须 **`IsDeleted=0`** 方为有效字典项;同一 `DictType` 下有效行 **`DictCode` 唯一**。下拉/列表可按 **`DictSort`** 升序(值越小越靠前)。
**本规范已使用的 DictType**:`user_position`(内部用户岗位)、`user_education`(内部用户学历)、`customer_type`(客户类型)、`customer_settlement_method`(客户结算方式)。
---
## 2 令牌与鉴权约定
> **本章导读**:抓住三条线——**access / refresh** 与 **`PermissionRevision`/`SessionVersion`**;**改角色、改白名单、改密、吊销**;**客户子账号 ⊆ 主账号**。**2.1** 全局约定;**2.2** 客户域风控表;**2.3** 三类主体注册时 **微信绑定** 的两种产品路径(与创建/更新接口对应)。
### 2.1 统一认证与鉴权约定
- 模型分层与链路见 **1.1**;客户主账号/子账号细则见 **2.2**。
- **命名**:`/rbac/user/*` 仅 **`PrincipalType=user`**;客户主/子账号角色与白名单走 **`/customer/role/*`、`/customer/sub-account/*`**(目标接口见 **5.2**);客户资料 CRUD 见 **5.1**。
- 登录:校验 `t_auth_account` → `PrincipalType` + `PrincipalId`。
- **注册**:`Phone`、`Email` **必填**且全局唯一;微信字段可选,仅便捷登录。
- **登录**:请求参数 **`account` = 手机号或邮箱**(与 `Phone` 或 `Email` 匹配)+ 口令和/或已绑微信(实现可约定 Query/Body)。**登录 / 刷新** 响应体与访问令牌中标识主体:**`principalType` + `principalId`**(与 **`t_auth_account`** 一致)。
- **口令、手机/邮箱、微信与展示字段** 仅以 `t_auth_account` 为准。`user.Avatar` 仅为非微信头像。
- **权限码**:`t_permission_definition` 为权威;`role_permission`、`menu`、API 声明须 ∈ 该表。
- **令牌 claims**:含 `permission_rev`、`session_ver`(同库字段);与库不一致则 **401**;或 access **≤15 分钟** 且变更主体绑定后 **`PermissionRevision`+1**。
- **与示例的关系**:**`expiresIn`/`exp`**(如登录响应里 **`7200` 秒**)以**实现与网关配置**为准;上文「**≤15 分钟**」指 **`role_permission` 批量变更** 且不刷各主体 **`PermissionRevision`** 时,靠 **短 TTL** 收敛权限漂移的**策略建议**(**1.8**),**不**表示响应里的秒数必须等于 900。
- **refresh**:内嵌 `session_ver` 与库一致;改密、`revoke-all-sessions`、冻结则 **`SessionVersion`+1**。
- **`PermissionRevision`+1` 时机**:覆盖写该主体 `t_principal_role`、或该 `customer_sub` 的白名单成功后。
- **`role_permission` 批量变更**:不逐行刷 revision 时依赖短 TTL 或全局缓存失效。
- **数据隔离**:账单用 `main_customer_id` / `principal_id`;**行级范围**用 `org_id` / `OrganizationId` 叠加查询。
- **接口域**:`user` 走管理端;`customer` / `customer_sub` 走客户中心。
- **微信字段落库**:一律 **`t_auth_account`**(**1.2**);与注册/绑定的接口分工见 **2.3**。
### 2.2 客户域 RBAC 与子账号权限(模型与风控)
| 项目 | 说明 |
| --- | --- |
| 角色与扩展 | 可绑 `RoleId` 受 **`role.AllowedPrincipalTypes`** 约束。 |
| 主账号 \(P_{\text{主}}\) | `PrincipalType=customer` 的角色 → `role_permission` 并集。 |
| 子账号 \(P_{\text{子}}\) | 角色并集 ∩ 白名单(若有);**必须** \(P_{\text{子}}\subseteq P_{\text{主}}\)。 |
| 保存强校验 | **`assign-roles` / `assign-permissions`** 事务内计算,⊄ 则 **400**;主账号 **收紧** 致子超限:默认 **拒绝** 直至先调整子账号。 |
| 登录与操作 | 写 **`t_operation_log`**;删子账号吊销 token。 |
| 改密 | **`SessionVersion`+1**。 |
### 2.3 主体注册与微信绑定(两种路径)
适用于 **`PrincipalType` = `user` / `customer` / `customer_sub`**:微信仅作**便捷登录与展示**,**不能替代** **`Phone` + `Email`** 开户(**1.2**)。前端一般先走微信 OAuth / 扫码拿到 **`openid`(及可选 `unionid`)、头像、昵称**,再与业务创建/更新配合。
| 路径 | 流程 | 接口 |
| --- | --- | --- |
| **A. 先微信,再开户** | 用户完成微信授权 → 再填写**手机、邮箱、密码**(及主体业务字段)→ **一次性创建**主体与认证行 | **`POST /rbac/user/create`**、**`POST /customer/create`**、**`POST /customer/sub-account/create`**(**4.6.3**、**5.1.5**、**5.2.5**):Body **携带** **`wechatOpenid` / `wechatUnionid`(可选)/ `wechatAvatar` / `wechatNickname`**,写入 **`t_auth_account`** |
| **B. 先开户,后绑微信** | 创建时 **微信字段为空** → 后续用户在微信侧完成授权 → 调用**更新**接口补全绑定 | **`PUT /rbac/user/update`**、**`POST /customer/update`**、**`POST /customer/sub-account/update`**(**4.6.4**、**5.1.6**、**5.2.6**):Body **携带**上述微信字段;服务端更新 **`t_auth_account`**,**`WechatOpenid`/`WechatUnionid` 全局唯一**(**`IsDeleted=0`** 下不得与其它主体重复) |
**实现注意**:路径 **B** 若提供独立 **`bind-wechat`** 接口,仍须写 **`t_auth_account`** 并做 **OpenID 占用校验**;与「更新接口带微信字段」二选一或并存时,在 OpenAPI 标明即可。
---
## 3 接口清单
> **本章导读**:**只关心「有哪些 URL、什么方法」时看本节**。下面 **3.1** 为说明与链接;**3.1.1~3.1.6** 按 **认证 → 权限 → 角色 → 组织 → 用户 → 菜单** 排,与 OpenAPI 标签顺序可能不一致。**3.1.7** 为 **OMS-Customer** 已发布路径;**3.1.8** 为客户域 RBAC **目标路径**(详见 **5.2**)。请求体、错误码、流程见 **4**(RBAC)、**5**(客户域)。
> 下表按业务链路重排;**3.1.1~3.1.6** 仅作分组标题,与实现侧 OpenAPI 中的出现顺序可能不一致。
### 3.1 RBAC 接口路径一览
> 来源:`https://api.nanafa.com/admin/index.html?urls.primaryName=RBAC`
>
> 实际文档地址:`/admin/swagger/RBAC/swagger.json`(接口数以运行时 OpenAPI 为准,含 **`revoke-all-sessions`**)
#### 3.1.1 认证(Auth)
| 接口名称 | 请求方式 | 请求路径 | 说明 |
| --- | --- | --- | --- |
| 登录 | POST | /rbac/auth/login | 用户登录 |
| 刷新令牌 | POST | /rbac/auth/refresh-token | 刷新 token |
| 修改密码 | POST | /rbac/auth/change-password | 当前用户改密(`SessionVersion`+1) |
| 当前用户信息 | GET | /rbac/auth/get-current-user | 获取当前登录用户 |
| 吊销全部会话 | POST | /rbac/auth/revoke-all-sessions | 当前主体所有 refresh 失效(`SessionVersion`+1) |
#### 3.1.2 权限(Permission)
| 接口名称 | 请求方式 | 请求路径 | 说明 |
| --- | --- | --- | --- |
| 全量权限 | GET | /rbac/permission/get-all-permissions | 查询全部权限编码 |
| 权限树 | GET | /rbac/permission/get-permission-tree | 查询权限树 |
| 单权限校验 | POST | /rbac/permission/check-permission | 校验单个权限(**唯一**单码判权入口;**不**再提供 `/rbac/user/has-permission`) |
| 多权限校验 | POST | /rbac/permission/check-permissions | 批量校验权限 |
#### 3.1.3 角色(Role)
| 接口名称 | 请求方式 | 请求路径 | 说明 |
| --- | --- | --- | --- |
| 角色列表 | GET | /rbac/role/get-list | 分页查询角色 |
| 全部角色 | GET | /rbac/role/get-all-roles | 查询全部角色 |
| 角色详情 | GET | /rbac/role/get | 查询角色详情 |
| 创建角色 | POST | /rbac/role/create | 新增角色 |
| 更新角色 | PUT | /rbac/role/update | 编辑角色 |
| 删除角色 | DELETE | /rbac/role/delete | 删除角色 |
| 分配权限 | POST | /rbac/role/assign-permissions | 给角色分配权限 |
| 角色权限 | GET | /rbac/role/get-role-permissions | 查询角色权限 |
| 角色用户数 | GET | /rbac/role/get-role-user-count | 查询角色下用户数 |
| 角色用户列表 | GET | /rbac/role/get-role-users | 查询角色下用户列表 |
#### 3.1.4 组织(Organization)
| 接口名称 | 请求方式 | 请求路径 | 说明 |
| --- | --- | --- | --- |
| 组织列表 | GET | /rbac/organization/get-list | 分页查询组织 |
| 组织树 | GET | /rbac/organization/get-tree | 查询组织树 |
| 组织详情 | GET | /rbac/organization/get | 查询组织详情 |
| 创建组织 | POST | /rbac/organization/create | 新增组织 |
| 更新组织 | PUT | /rbac/organization/update | 编辑组织 |
| 删除组织 | DELETE | /rbac/organization/delete | 删除组织 |
| 移动组织 | POST | /rbac/organization/move | 调整组织节点位置 |
#### 3.1.5 用户(User)
| 接口名称 | 请求方式 | 请求路径 | 说明 |
| --- | --- | --- | --- |
| 用户列表 | GET | /rbac/user/get-list | 分页查询用户 |
| 用户详情 | GET | /rbac/user/get | 查询用户详情 |
| 创建用户 | POST | /rbac/user/create | 新增用户 |
| 更新用户 | PUT | /rbac/user/update | 编辑用户 |
| 删除用户 | DELETE | /rbac/user/delete | 删除单个用户 |
| 批量删除用户 | DELETE | /rbac/user/delete-batch | 批量删除用户 |
| 分配角色 | POST | /rbac/user/assign-roles | 给用户分配角色 |
| 用户角色 | GET | /rbac/user/get-user-roles | 查询用户角色 |
| 用户权限 | GET | /rbac/user/get-user-permissions | 查询用户权限 |
| 角色下用户 | GET | /rbac/user/get-users-by-role | 查询某角色下用户 |
| 更新用户头像 | PUT | /rbac/user/update-avatar | 更新用户头像 |
| 更新用户密码 | PUT | /rbac/user/update-password | 重置/修改用户密码 |
#### 3.1.6 菜单(Menu)
| 接口名称 | 请求方式 | 请求路径 | 说明 |
| --- | --- | --- | --- |
| 菜单详情 | GET | /rbac/menu/get | 查询菜单详情 |
| 菜单树 | GET | /rbac/menu/get-menu-tree | 查询菜单树 |
| 用户菜单 | GET | /rbac/menu/get-user-menus | 查询当前用户可见菜单 |
| 创建菜单 | POST | /rbac/menu/create | 新增菜单 |
| 更新菜单 | PUT | /rbac/menu/update | 编辑菜单 |
| 删除菜单 | DELETE | /rbac/menu/delete | 删除菜单 |
| 移动菜单 | POST | /rbac/menu/move-menu | 移动菜单节点 |
| 更新菜单排序 | PUT | /rbac/menu/update-order | 更新菜单排序 |
#### 3.1.7 客户资料(OMS-Customer)
> 来源:[Swagger UI — OMS-Customer](http://api.nanafa.com/admin/index.html?urls.primaryName=OMS-Customer) · `/admin/swagger/OMS-Customer/swagger.json` — 详细设计见 **5.1**。
| 接口名称 | 请求方式 | 请求路径 | 说明 |
| --- | --- | --- | --- |
| 客户列表 | GET | /customer/list | 分页与筛选 |
| 客户详情 | GET | /customer/get | 按 id |
| 新增客户 | POST | /customer/create | `CreateCustomerInput` |
| 更新客户 | POST | /customer/update | `UpdateCustomerInput` |
| 删除客户 | POST | /customer/delete | `IdInput` |
| 重置客户密码 | POST | /customer/reset-password | `SessionVersion`+1,见 **5.1.8** |
#### 3.1.8 客户域 RBAC(目标)
> 截至 **OMS-Customer v1** 可能未出现在同一 Swagger;路径与 **5.2** 一致。
| 接口名称 | 请求方式 | 请求路径 | 说明 |
| --- | --- | --- | --- |
| 主账号分配角色 | POST | /customer/role/assign | **5.2.2** |
| 主账号角色列表 | GET | /customer/role/list | **5.2.3** |
| 子账号列表 | GET | /customer/sub-account/list | **5.2.4** |
| 创建子账号 | POST | /customer/sub-account/create | **5.2.5** |
| 更新子账号 | POST | /customer/sub-account/update | **5.2.6** |
| 删除子账号 | POST | /customer/sub-account/delete | **5.2.7** |
| 子账号分配角色 | POST | /customer/sub-account/assign-roles | **5.2.8** |
| 子账号配置白名单 | POST | /customer/sub-account/assign-permissions | **5.2.9** |
| 子账号有效权限预览 | GET | /customer/sub-account/effective-permissions | **5.2.10** |
---
## 4 接口详细设计
> **本章导读**:**写代码 / 写用例以本节为主**。结构为 **4.2~4.7** 六大块(与 **3.1.1~3.1.6** 接口分组同序);块内 **`4.x.y.z`** 为接口与子步骤编号(便于搜索),每个接口含 **示例 → 业务逻辑 → 校验与异常**。篇幅长,请 **按路径搜索**(如 `/rbac/role/create`)。
### 4.1 接口详细设计总述
> 下文顺序:**认证 → 权限 → 角色 → 组织 → 用户 → 菜单**,与 **4.2~4.7** 分块一致;接口条目为 **4.2.1**、**4.3.1** 形式;路径以运行时 API 为准。令牌 **`permission_rev`/`session_ver`**、**`PermissionRevision`/`SessionVersion`** 及审计要求见 **2**。**客户域**(客户资料 + 客户侧 RBAC 目标接口)详细设计见 **5**。
#### 4.1.1 标识符与请求参数约定(`principalId` / `userId` / `id`)
为避免 **`t_auth_account.Id`(认证行主键)**、**`PrincipalId`(业务主体主键)**、**`user.Id`** 与各类 Query 同名混用,统一约定如下(**OpenAPI 须与之一致**):
| 符号 / 参数 | 含义 | 典型出现位置 |
| --- | --- | --- |
| **`principalType` + `principalId`**(camelCase) | 与 **`t_auth_account`** 一致:**`principalId` 永远是业务主体主键**(`user.Id` / `t_customer.Id` / `t_customer_sub_account.Id`),**不是** `t_auth_account.Id`。 | 登录/刷新响应(**4.2.1~4.2.2**)、令牌 claims;**`POST /rbac/permission/check-permission`** 等 |
| **`userId`** | **仅**表示 **`user.Id`**(内部员工)。在 **`PrincipalType=user` 时 **`userId` = 该主体的 `PrincipalId`**。 | **`/rbac/user/assign-roles`**、**`get-user-roles`**、**`get-user-permissions`**、**`get-user-menus`** 等 **4.6 / 4.7** 示例 |
| **`id`(裸 Query)** | **随资源而变**,须读接口路径判断目标表:例如 **`/rbac/user/*`** 下多为 **`user.Id`**;**`/rbac/role/*`** 下为 **`role.Id`**;**`/rbac/menu/*`** 下为 **`menu.Id`**;**`/customer/get`** 下为 **`t_customer.Id`**(即客户主账号的 **`PrincipalId`**,但参数名沿用 OpenAPI **`id`**)。 | 各节请求示例 |
| **`customerId` / `subAccountId`** | 分别对应 **`t_customer.Id`**、**`t_customer_sub_account.Id`**;写入 **`t_principal_role` 时即同类型的 `PrincipalId`**。 | **5.2** 客户域 RBAC |
**须写进 OpenAPI 的实现决策(当前规范留歧义处)**:
1. **`GET /rbac/menu/get-user-menus?userId=`**:`userId` 是否**仅允许查询本人**(忽略 Query,以 token 为准)、还是**允许管理员查任意用户**——二者选一并实现,**禁止**默认可查他人而不鉴权。
**库表字段名提示**:各业务表 **`CreateUserId`/`UpdateUserId` 为 INT** 与 **`user.Id` BIGINT** 并存时,以**实现与迁移**为准;新库建议审计列与 **`user.Id` 类型对齐**,避免大 Id 溢出。
### 4.2 认证(Auth)
#### 4.2.1 登录(POST /rbac/auth/login)
##### 4.2.1.1 接口请求示例
**请求 /rbac/auth/login**(POST,Query 参数)
```
?account=13800138000&password=123456&type=0
```
**响应**
```json
{
"accessToken": "eyJhbGciOi...",
"refreshToken": "rt_refresh_token_01",
"tokenType": "Bearer",
"expiresIn": 7200,
"principalType": "user",
"principalId": 1
}
```
##### 4.2.1.2 接口业务逻辑
###### 4.2.1.2.1 核心流程
- `account` 为 **手机号或邮箱**(与 `t_auth_account.Phone` 或 `Email` 匹配);`password` 与/或已绑微信鉴权;成功后签发 `accessToken` 与 `refreshToken`。
- 返回 `LoginResultDto`:**`principalType`、`principalId`** 与 **`t_auth_account`** 一致;令牌 claims 与客户端缓存均以二者为**唯一**主体标识。
###### 4.2.1.2.2 校验与异常
- 校验 **`account` 必填**;**`password` 与「已绑微信」对应的鉴权材料至少满足其一**(与 **4.2.1.2.1** 一致;纯微信登录时 Body/Query 形态由 OpenAPI 约定),**不得**再写「密码必填」以免与微信鉴权矛盾。`type` 默认 `0`(内部管理端);其它取值由部署约定(如客户门户 `type=1`),语义见 **2.2**。
- 鉴权失败或主体状态异常时返回 `ApplicationExceptionResult`(400)。
- 客户主账号 / 子账号登录成功:按主体从 **`t_principal_role`**(及子账号 **`t_customer_sub_permission`** 白名单)聚合 `roles`/`permissions`/`menus`(**2.2**)。**零角色绑定** 时 `permissions` 可为空或仅登录/个人中心最小集(须在 `t_permission_definition` 注册)。
#### 4.2.2 刷新令牌(POST /rbac/auth/refresh-token)
##### 4.2.2.1 接口请求示例
**请求 /rbac/auth/refresh-token**(POST,Query 参数)
```
?refreshToken=rt_refresh_token_01
```
**响应**
```json
{
"accessToken": "eyJhbGciOi...",
"refreshToken": "rt_refresh_token_02",
"tokenType": "Bearer",
"expiresIn": 7200,
"principalType": "user",
"principalId": 1
}
```
##### 4.2.2.2 接口业务逻辑
###### 4.2.2.2.1 核心流程
- 校验 `refreshToken` 有效且内嵌 **`session_ver`** 与 **`t_auth_account.SessionVersion`** 一致,生成并返回新令牌对(可旋转 refresh);**`principalType`/`principalId`** 同 **4.2.1**。
###### 4.2.2.2.2 校验与异常
- `refreshToken` 无效、过期、`session_ver` 不匹配或被吊销时返回 `ApplicationExceptionResult`(400)。
#### 4.2.3 修改密码(POST /rbac/auth/change-password)
##### 4.2.3.1 接口请求示例
**请求 /rbac/auth/change-password**(POST,Query 参数)
```
?oldPassword=old123&newPassword=new123456
```
**响应**
```json
{
"code": 0,
"msg": "success",
"data": null
}
```
##### 4.2.3.2 接口业务逻辑
###### 4.2.3.2.1 核心流程
- 使用当前登录主体校验旧密码,成功后**仅更新当前主体对应 `t_auth_account` 的 `PasswordHash`/`PasswordSalt`**,不写入 `user` 表;**`SessionVersion`+1**;写 **`t_operation_log`(`auth_password_change`)**。
###### 4.2.3.2.2 校验与异常
- 旧密码错误、密码策略不满足或主体不存在时返回 `ApplicationExceptionResult`(400)。
#### 4.2.4 当前用户信息(GET /rbac/auth/get-current-user)
##### 4.2.4.1 接口请求示例
**请求 /rbac/auth/get-current-user**(GET)
> 无请求参数,基于当前登录令牌解析用户身份。
**响应**
```json
{
"id": 1,
"name": "管理员A",
"phone": "13800138000",
"email": "admin@example.com",
"avatar": "https://cdn.example.com/avatar/admin.png",
"organizationId": 10,
"organizationName": "总部",
"roles": [
"Admin"
],
"permissions": [
"rbac:user:get-list",
"rbac:role:get-list"
],
"menus": []
}
```
##### 4.2.4.2 接口业务逻辑
###### 4.2.4.2.1 核心流程
- 根据 token 解析主体,聚合返回基础信息、角色、权限与菜单。
###### 4.2.4.2.2 校验与异常
- token 无效、主体不存在或已禁用时返回 `ApplicationExceptionResult`(400)。
- `principal_type` 为 `customer` / `customer_sub`:按 **2.2** 聚合权限与菜单;子账号响应可带 `mainCustomerId`(与令牌 claim `main_customer_id` 一致)。
#### 4.2.5 吊销全部会话(POST /rbac/auth/revoke-all-sessions)
##### 4.2.5.1 接口请求示例
**请求 /rbac/auth/revoke-all-sessions**(POST)
> 无 Body;基于当前 access token 识别主体。
**响应**
```json
{
"code": 0,
"msg": "success",
"data": null
}
```
##### 4.2.5.2 接口业务逻辑
###### 4.2.5.2.1 核心流程
- 当前主体 **`SessionVersion`+1**,吊销全部 refresh;写 **`t_operation_log`(`auth_session_revoke`)**。
###### 4.2.5.2.2 校验与异常
- token 无效时返回 `ApplicationExceptionResult`(400)。
### 4.3 权限(Permission)
#### 4.3.1 全量权限(GET /rbac/permission/get-all-permissions)
##### 4.3.1.1 接口请求示例
**请求 /rbac/permission/get-all-permissions**(GET)
> 无请求参数。
**响应**
```json
[
"rbac:user:get-list",
"rbac:role:get-list"
]
```
##### 4.3.1.2 接口业务逻辑
###### 4.3.1.2.1 核心流程
- 返回系统全部权限编码列表。
###### 4.3.1.2.2 校验与异常
- 查询异常返回 `ApplicationExceptionResult`(400)。
#### 4.3.2 权限树(GET /rbac/permission/get-permission-tree)
##### 4.3.2.1 接口请求示例
**请求 /rbac/permission/get-permission-tree**(GET)
> 无请求参数。
**响应**
```json
[
{
"permissionCode": "rbac:user",
"children": [
{
"permissionCode": "rbac:user:get-list"
}
]
}
]
```
##### 4.3.2.2 接口业务逻辑
###### 4.3.2.2.1 核心流程
- 返回权限树结构,供角色授权界面使用;树形与 **`t_permission_definition`** 库表字段的对应关系见 **1.9.1**。
###### 4.3.2.2.2 校验与异常
- 数据异常时返回 `ApplicationExceptionResult`(400)。
#### 4.3.3 单权限校验(POST /rbac/permission/check-permission)
> **唯一单码判权接口**:不提供 **`POST /rbac/user/has-permission`**。对内部用户判权使用 **`principalType=user`** 且 **`principalId`=`user.Id`**(见 **4.1.1**)。
##### 4.3.3.1 接口请求示例
**请求 /rbac/permission/check-permission**(POST,Query 参数)
```
?principalType=user&principalId=1&permissionCode=rbac:user:get-list
```
**响应**
```json
true
```
##### 4.3.3.2 接口业务逻辑
###### 4.3.3.2.1 核心流程
- 按 **`principalType` + `principalId`** 解析 **`t_auth_account`** 与 **`t_principal_role`**(及子账号白名单),判断是否具备目标 **`permissionCode`**。
###### 4.3.3.2.2 校验与异常
- 主体不存在、**`permissionCode`** 未注册或权限码为空时返回 `ApplicationExceptionResult`(400)。
#### 4.3.4 多权限校验(POST /rbac/permission/check-permissions)
##### 4.3.4.1 接口请求示例
**请求 /rbac/permission/check-permissions**(POST,Query + Body)
```
?principalType=user&principalId=1
```
```json
[
"rbac:user:get-list",
"rbac:role:get-list"
]
```
**响应**
```json
{
"rbac:user:get-list": true,
"rbac:role:get-list": false
}
```
##### 4.3.4.2 接口业务逻辑
###### 4.3.4.2.1 核心流程
- 批量校验权限并返回映射结果;主体解析同 **4.3.3**。
###### 4.3.4.2.2 校验与异常
- 请求体为空或主体不存在时返回 `ApplicationExceptionResult`(400)。
### 4.4 角色(Role)
#### 4.4.1 角色列表(GET /rbac/role/get-list)
##### 4.4.1.1 接口请求示例
**请求 /rbac/role/get-list**(GET,Query 参数)
```
?Keyword=admin&Page=1&PageSize=20&OrderBy=CreateTime&Descending=true
```
**响应**
```json
{
"total": 1,
"items": [
{
"id": 20,
"name": "管理员",
"code": "Admin",
"permissions": [
"rbac:user:get-list"
]
}
]
}
```
##### 4.4.1.2 接口业务逻辑
###### 4.4.1.2.1 核心流程
- 分页查询角色并返回 `RoleDtoPagedResultDto`。
###### 4.4.1.2.2 校验与异常
- 分页参数非法时返回 `ApplicationExceptionResult`(400)。
#### 4.4.2 全部角色(GET /rbac/role/get-all-roles)
##### 4.4.2.1 接口请求示例
**请求 /rbac/role/get-all-roles**(GET)
> 无请求参数。
**响应**
```json
[
{
"id": 20,
"name": "管理员",
"code": "Admin"
}
]
```
##### 4.4.2.2 接口业务逻辑
###### 4.4.2.2.1 核心流程
- 返回全部可用角色列表。
###### 4.4.2.2.2 校验与异常
- 查询异常返回 `ApplicationExceptionResult`(400)。
#### 4.4.3 角色详情(GET /rbac/role/get)
##### 4.4.3.1 接口请求示例
**请求 /rbac/role/get**(GET,Query 参数)
```
?id=20
```
**响应**
```json
{
"id": 20,
"name": "管理员",
"code": "Admin",
"permissions": [
"rbac:user:get-list"
]
}
```
##### 4.4.3.2 接口业务逻辑
###### 4.4.3.2.1 核心流程
- 根据角色 ID 返回 `RoleDto`。
###### 4.4.3.2.2 校验与异常
- 角色不存在时返回 `ApplicationExceptionResult`(400)。
#### 4.4.4 创建角色(POST /rbac/role/create)
##### 4.4.4.1 接口请求示例
**请求 /rbac/role/create**(POST,Body)
```json
{
"name": "运营",
"code": "Operator",
"allowedPrincipalTypes": "user",
"permissions": [
"rbac:user:get-list",
"rbac:role:get-list"
]
}
```
**响应**
```json
{
"id": 21,
"name": "运营",
"code": "Operator",
"allowedPrincipalTypes": "user",
"permissions": [
"rbac:user:get-list",
"rbac:role:get-list"
]
}
```
##### 4.4.4.2 接口业务逻辑
###### 4.4.4.2.1 核心流程
- 写入 **`role`(含 `AllowedPrincipalTypes`)** 与 **`role_permission`**;权限码须注册;写 **`t_operation_log`**(RBAC 动作行)。
###### 4.4.4.2.2 校验与异常
- `code` 重复、`allowedPrincipalTypes` 非法或权限编码未注册时返回 `ApplicationExceptionResult`(400)。
#### 4.4.5 更新角色(PUT /rbac/role/update)
##### 4.4.5.1 接口请求示例
**请求 /rbac/role/update**(PUT,Query + Body)
```
?id=21
```
```json
{
"name": "运营A"
}
```
**响应**
```json
{
"id": 21,
"name": "运营A",
"code": "Operator"
}
```
##### 4.4.5.2 接口业务逻辑
###### 4.4.5.2.1 核心流程
- 更新角色名称并返回 `RoleDto`。
###### 4.4.5.2.2 校验与异常
- 角色不存在时返回 `ApplicationExceptionResult`(400)。
#### 4.4.6 删除角色(DELETE /rbac/role/delete)
##### 4.4.6.1 接口请求示例
**请求 /rbac/role/delete**(DELETE,Query 参数)
```
?id=21
```
**响应**
```json
{
"code": 0,
"msg": "success",
"data": null
}
```
##### 4.4.6.2 接口业务逻辑
###### 4.4.6.2.1 核心流程
- 删除角色及其关联权限关系。
###### 4.4.6.2.2 校验与异常
- 角色被使用且不允许删除时返回 `ApplicationExceptionResult`(400)。
#### 4.4.7 分配权限(POST /rbac/role/assign-permissions)
##### 4.4.7.1 接口请求示例
**请求 /rbac/role/assign-permissions**(POST,Query + Body)
```
?roleId=21
```
```json
[
"rbac:user:get-list",
"rbac:role:get-list"
]
```
**响应**
```json
{
"code": 0,
"msg": "success",
"data": null
}
```
##### 4.4.7.2 接口业务逻辑
###### 4.4.7.2.1 核心流程
- 校验 **`PermissionCode` ∈ `t_permission_definition`**;覆盖更新 **`role_permission`**;写 **`t_operation_log`(`role_permission_assign`)**。(不逐行刷各主体 `PermissionRevision` 时依赖 **2**所述短 TTL 或缓存失效。)
###### 4.4.7.2.2 校验与异常
- 角色不存在或权限码未注册时返回 `ApplicationExceptionResult`(400)。
#### 4.4.8 角色权限(GET /rbac/role/get-role-permissions)
##### 4.4.8.1 接口请求示例
**请求 /rbac/role/get-role-permissions**(GET,Query 参数)
```
?roleId=21
```
**响应**
```json
[
"rbac:user:get-list",
"rbac:role:get-list"
]
```
##### 4.4.8.2 接口业务逻辑
###### 4.4.8.2.1 核心流程
- 查询角色已授权权限编码列表。
###### 4.4.8.2.2 校验与异常
- 角色不存在时返回 `ApplicationExceptionResult`(400)。
#### 4.4.9 角色用户数(GET /rbac/role/get-role-user-count)
##### 4.4.9.1 接口请求示例
**请求 /rbac/role/get-role-user-count**(GET,Query 参数)
```
?roleId=21
```
**响应**
```json
12
```
##### 4.4.9.2 接口业务逻辑
###### 4.4.9.2.1 核心流程
- 统计角色下用户数。
###### 4.4.9.2.2 校验与异常
- 角色不存在时返回 `ApplicationExceptionResult`(400)。
#### 4.4.10 角色用户列表(GET /rbac/role/get-role-users)
##### 4.4.10.1 接口请求示例
**请求 /rbac/role/get-role-users**(GET,Query 参数)
```
?roleId=21
```
**响应**
```json
[
{
"id": 1,
"name": "管理员A",
"phone": "13800138000"
}
]
```
##### 4.4.10.2 接口业务逻辑
###### 4.4.10.2.1 核心流程
- 返回角色下用户集合。
###### 4.4.10.2.2 校验与异常
- 角色不存在时返回 `ApplicationExceptionResult`(400)。
### 4.5 组织(Organization)
#### 4.5.1 组织列表(GET /rbac/organization/get-list)
##### 4.5.1.1 接口请求示例
**请求 /rbac/organization/get-list**(GET)
> 无请求参数。
**响应**
```json
[
{
"id": 10,
"parentId": null,
"name": "总部",
"code": "HQ",
"type": "Company",
"order": 1,
"isEnabled": true
}
]
```
##### 4.5.1.2 接口业务逻辑
###### 4.5.1.2.1 核心流程
- 查询组织平铺列表。
###### 4.5.1.2.2 校验与异常
- 查询异常返回 `ApplicationExceptionResult`(400)。
#### 4.5.2 组织树(GET /rbac/organization/get-tree)
##### 4.5.2.1 接口请求示例
**请求 /rbac/organization/get-tree**(GET)
> 无请求参数。
**响应**
```json
[
{
"id": 10,
"name": "总部",
"children": []
}
]
```
##### 4.5.2.2 接口业务逻辑
###### 4.5.2.2.1 核心流程
- 按父子关系构建并返回组织树。
###### 4.5.2.2.2 校验与异常
- 数据异常(循环父子关系)时返回 `ApplicationExceptionResult`(400)。
#### 4.5.3 组织详情(GET /rbac/organization/get)
##### 4.5.3.1 接口请求示例
**请求 /rbac/organization/get**(GET,Query 参数)
```
?id=10
```
**响应**
```json
{
"id": 10,
"parentId": null,
"name": "总部",
"code": "HQ",
"type": "Company",
"leaderId": 1,
"leaderName": "管理员A",
"order": 1,
"description": "总部组织",
"isEnabled": true
}
```
##### 4.5.3.2 接口业务逻辑
###### 4.5.3.2.1 核心流程
- 根据组织 ID 查询 `OrganizationDto`。
###### 4.5.3.2.2 校验与异常
- 组织不存在时返回 `ApplicationExceptionResult`(400)。
#### 4.5.4 创建组织(POST /rbac/organization/create)
##### 4.5.4.1 接口请求示例
**请求 /rbac/organization/create**(POST,Body)
```json
{
"parentId": 10,
"name": "运营部",
"code": "OP",
"type": "Department",
"leaderId": 1,
"order": 10,
"description": "运营部门",
"isEnabled": true
}
```
**响应**
```json
{
"id": 11,
"name": "运营部",
"code": "OP",
"type": "Department"
}
```
##### 4.5.4.2 接口业务逻辑
###### 4.5.4.2.1 核心流程
- 新增组织节点并返回 `OrganizationDto`。
###### 4.5.4.2.2 校验与异常
- 编码重复、父节点不存在时返回 `ApplicationExceptionResult`(400)。
#### 4.5.5 更新组织(PUT /rbac/organization/update)
##### 4.5.5.1 接口请求示例
**请求 /rbac/organization/update**(PUT,Query + Body)
```
?id=11
```
```json
{
"name": "运营中心",
"type": "Department",
"leaderId": 1,
"order": 20,
"description": "运营中心",
"isEnabled": true
}
```
**响应**
```json
{
"id": 11,
"name": "运营中心",
"code": "OP"
}
```
##### 4.5.5.2 接口业务逻辑
###### 4.5.5.2.1 核心流程
- 按 ID 更新组织信息。
###### 4.5.5.2.2 校验与异常
- 组织不存在或状态非法时返回 `ApplicationExceptionResult`(400)。
#### 4.5.6 删除组织(DELETE /rbac/organization/delete)
##### 4.5.6.1 接口请求示例
**请求 /rbac/organization/delete**(DELETE,Query 参数)
```
?id=11
```
**响应**
```json
{
"code": 0,
"msg": "success",
"data": null
}
```
##### 4.5.6.2 接口业务逻辑
###### 4.5.6.2.1 核心流程
- 删除组织节点。
###### 4.5.6.2.2 校验与异常
- 有子节点或被用户引用时返回 `ApplicationExceptionResult`(400)。
#### 4.5.7 移动组织(POST /rbac/organization/move)
##### 4.5.7.1 接口请求示例
**请求 /rbac/organization/move**(POST,Query + Body)
```
?id=11
```
```json
{
"newParentId": 10
}
```
**响应**
```json
{
"code": 0,
"msg": "success",
"data": null
}
```
##### 4.5.7.2 接口业务逻辑
###### 4.5.7.2.1 核心流程
- 调整组织父节点,重建树关系。
###### 4.5.7.2.2 校验与异常
- 新父节点非法、移动后形成循环层级时返回 `ApplicationExceptionResult`(400)。
### 4.6 用户(User)
> **命名**:本节 Query 中的 **`userId`** = **`user.Id`**(即 **`PrincipalType=user`** 时的 **`PrincipalId`**),见 **4.1.1**。
> **员工档案与接口**:`user` 表人事字段见 **1.3**。**`POST /rbac/user/create`**、**`PUT /rbac/user/update`** 在姓名、手机、邮箱、组织、角色等基础字段之外,**均可选**携带与 **1.3** 一致的档案属性(`positionCode`、`age`、`idCardNo`、`birthday`、`educationCode`、`address`、`graduateSchool`、`emergencyContact`、`emergencyContactPhone`、`hireDate`、`leaveDate`、`warehouseId`;命名建议 camelCase 与 OpenAPI 一致)。**微信绑定**与 **`t_auth_account`** 的两种路径见 **2.3**:创建时可带 **`wechatOpenid` 等**;仅后续绑定时用 **update** 写入。**`GET /rbac/user/get`** 返回完整 `UserDto`;列表可为 **`UserSummaryDto`**。**`positionCode` / `educationCode`** 响应可附带 **`positionName` / `educationName`**(**`t_dict`**,**`IsDeleted=0`**)。**`idCardNo`** 按权限脱敏;**`warehouseId`** 须校验仓库存在。
#### 4.6.1 用户列表(GET /rbac/user/get-list)
##### 4.6.1.1 接口请求示例
**请求 /rbac/user/get-list**(GET,Query 参数)
```
?Keyword=admin&RoleId=&OrganizationId=&Page=1&PageSize=20&OrderBy=CreateTime&Descending=true
```
**响应**
```json
{
"total": 1,
"items": [
{
"id": 1,
"name": "管理员A",
"phone": "13800138000",
"email": "admin@example.com",
"organizationId": 10,
"organizationName": "总部",
"positionCode": "pos_dev",
"positionName": "开发",
"warehouseId": 100,
"hireDate": "2023-01-15",
"roles": [
"Admin"
]
}
]
}
```
##### 4.6.1.2 接口业务逻辑
###### 4.6.1.2.1 核心流程
- 按关键字、角色、组织与分页参数查询用户,返回 `UserDtoPagedResultDto`;列表项可含 **1.3** 中常用档案列(如岗位、仓库、入职日),敏感列默认不出或按权限裁剪。
###### 4.6.1.2.2 校验与异常
- 分页参数非法或排序字段不支持时返回 `ApplicationExceptionResult`(400)。
#### 4.6.2 用户详情(GET /rbac/user/get)
##### 4.6.2.1 接口请求示例
**请求 /rbac/user/get**(GET,Query 参数)
```
?id=1
```
**响应**
```json
{
"id": 1,
"name": "管理员A",
"phone": "13800138000",
"email": "admin@example.com",
"avatar": "https://cdn.example.com/avatar/admin.png",
"organizationId": 10,
"organizationName": "总部",
"positionCode": "pos_dev",
"positionName": "开发",
"age": 32,
"idCardNo": "110101********1234",
"birthday": "1993-06-01",
"educationCode": "edu_bachelor",
"educationName": "本科",
"address": "北京市朝阳区…",
"graduateSchool": "某某大学",
"emergencyContact": "李四",
"emergencyContactPhone": "13900000000",
"hireDate": "2020-03-01",
"leaveDate": null,
"warehouseId": 100,
"concurrencyStamp": 3,
"roles": [
"Admin"
]
}
```
##### 4.6.2.2 接口业务逻辑
###### 4.6.2.2.1 核心流程
- 根据用户 ID 查询并返回 `UserDto`(**含 1.3 全部员工档案字段**;**`idCardNo`** 等敏感字段按数据权限脱敏或掩码;**`concurrencyStamp`** 供更新时乐观并发可选使用)。
###### 4.6.2.2.2 校验与异常
- `id` 不存在或已删除返回 `ApplicationExceptionResult`(400)。
#### 4.6.3 创建用户(POST /rbac/user/create)
##### 4.6.3.1 接口请求示例
**请求 /rbac/user/create**(POST,Body)
```json
{
"name": "测试用户",
"phone": "13800138001",
"password": "123456",
"email": "test001@example.com",
"avatar": "",
"organizationId": 10,
"roleIds": [20],
"positionCode": "pos_operator",
"age": 28,
"idCardNo": "110101199001011234",
"birthday": "1990-01-01",
"educationCode": "edu_bachelor",
"address": "上海市浦东新区…",
"graduateSchool": "某某学院",
"emergencyContact": "王五",
"emergencyContactPhone": "13800000000",
"hireDate": "2025-05-01",
"leaveDate": null,
"warehouseId": 100,
"wechatOpenid": "oXXXX_先微信授权再开户时可带",
"wechatUnionid": null,
"wechatAvatar": "https://thirdwx.qlogo.cn/...",
"wechatNickname": "微信昵称"
}
```
> 上列 **`positionCode`~`warehouseId`** 均为可选;**`wechatOpenid`~`wechatNickname`** 可选(**路径 A**:与手机邮箱密码同单创建,见 **2.3**)。不传微信字段则 **路径 B** 留待 **update** 绑定。
**响应**
```json
{
"id": 2,
"name": "测试用户",
"phone": "13800138001",
"email": "test001@example.com",
"organizationId": 10,
"organizationName": "总部",
"positionCode": "pos_operator",
"positionName": "运营",
"hireDate": "2025-05-01",
"warehouseId": 100,
"roles": [
"Operator"
]
}
```
##### 4.6.3.2 接口业务逻辑
###### 4.6.3.2.1 核心流程
- 建 `user` + `t_auth_account`(**`Phone`、`Email` 必填**;口令仅存认证表;请求体 `avatar` 落 **`user.Avatar`**);**`wechatOpenid`/`wechatUnionid`/`wechatAvatar`/`wechatNickname`** 若有则写入 **`t_auth_account`**(**路径 A**,**2.3**)。员工档案字段(**1.3**)与请求体 camelCase 一一写入 **`user`**;**`PositionCode`/`EducationCode` 非空**须 **`t_dict`** 有效;**`WarehouseId`/`IdCardNo`** 校验同前;`roleIds` → **`t_principal_role`(user + user.Id)**;返回 `UserDto`。
###### 4.6.3.2.2 校验与异常
- `phone`/`email` 重复、**微信 OpenID/UnionID 已被占用**、组织不存在、角色无效、字典码无效、仓库不存在、身份证号冲突时返回 `ApplicationExceptionResult`(400)。
#### 4.6.4 更新用户(PUT /rbac/user/update)
##### 4.6.4.1 接口请求示例
**请求 /rbac/user/update**(PUT,Query + Body)
```
?id=2
```
```json
{
"name": "测试用户A",
"phone": "13800138001",
"email": "test001a@example.com",
"avatar": "https://cdn.example.com/avatar/test001.png",
"organizationId": 10,
"positionCode": "pos_operator",
"educationCode": "edu_master",
"emergencyContactPhone": "13800000001",
"leaveDate": null,
"warehouseId": 101,
"concurrencyStamp": 3,
"wechatOpenid": "oXXXX_后绑微信时由更新写入",
"wechatUnionid": null,
"wechatAvatar": "https://thirdwx.qlogo.cn/...",
"wechatNickname": "微信昵称"
}
```
> **部分更新**:仅传需要变更的字段即可;未出现的档案字段保持原值。**`concurrencyStamp`** 若传入须与库中一致,否则 **409**。**路径 B**(**2.3**):首次绑定或换绑微信时提交 **`wechatOpenid` 等**;仅改展示可不重复传 OpenID。
**响应**
```json
{
"id": 2,
"name": "测试用户A",
"phone": "13800138001",
"email": "test001a@example.com",
"avatar": "https://cdn.example.com/avatar/test001.png",
"organizationId": 10,
"positionCode": "pos_operator",
"positionName": "运营",
"educationCode": "edu_master",
"educationName": "硕士",
"warehouseId": 101,
"concurrencyStamp": 4
}
```
##### 4.6.4.2 接口业务逻辑
###### 4.6.4.2.1 核心流程
- 按 `id` 更新用户基础信息与员工档案字段(**1.3** 全部,可部分更新),**字典字段**校验同创建;**`phone`/`email`** 变更同步 **`t_auth_account`**;**微信字段**变更同步 **`t_auth_account`**(**路径 B**,**2.3**),**OpenID/UnionID 不得与其它主体冲突**。可选 **`concurrencyStamp`** 与 **`user.ConcurrencyStamp`** 对齐后 **+1**;返回完整 `UserDto`。
###### 4.6.4.2.2 校验与异常
- 用户不存在、邮箱/手机冲突、**微信 OpenID 已被占用**、组织非法、字典码无效、仓库不存在、**并发戳冲突**时返回 `ApplicationExceptionResult`(400/409)。
#### 4.6.5 删除用户(DELETE /rbac/user/delete)
##### 4.6.5.1 接口请求示例
**请求 /rbac/user/delete**(DELETE,Query 参数)
```
?id=2
```
**响应**
```json
{
"code": 0,
"msg": "success",
"data": null
}
```
##### 4.6.5.2 接口业务逻辑
###### 4.6.5.2.1 核心流程
- 逻辑删除指定用户及其关联关系。
###### 4.6.5.2.2 校验与异常
- 用户不存在、不可删除主体(如内置管理员)返回 `ApplicationExceptionResult`(400)。
#### 4.6.6 批量删除用户(DELETE /rbac/user/delete-batch)
##### 4.6.6.1 接口请求示例
**请求 /rbac/user/delete-batch**(DELETE,Query 参数)
```
?ids=2,3
```
**响应**
```json
{
"code": 0,
"msg": "success",
"data": null
}
```
##### 4.6.6.2 接口业务逻辑
###### 4.6.6.2.1 核心流程
- 批量逻辑删除用户。
###### 4.6.6.2.2 校验与异常
- 任一 ID 无效或存在不可删除用户时返回 `ApplicationExceptionResult`(400)。
#### 4.6.7 分配角色(POST /rbac/user/assign-roles)
##### 4.6.7.1 接口请求示例
**请求 /rbac/user/assign-roles**(POST,Query + Body)
```
?userId=2
```
```json
[
20,
21
]
```
**响应**
```json
{
"code": 0,
"msg": "success",
"data": null
}
```
##### 4.6.7.2 接口业务逻辑
###### 4.6.7.2.1 核心流程
- 校验角色 **`AllowedPrincipalTypes`** 含 `user`;覆盖写 **`t_principal_role`**;该用户认证行 **`PermissionRevision`+1**;写 **`t_operation_log`(`principal_role_assign`)**。
###### 4.6.7.2.2 校验与异常
- 用户不存在、角色不适用 `user` 或 Id 非法时返回 `ApplicationExceptionResult`(400)。
#### 4.6.8 用户角色(GET /rbac/user/get-user-roles)
##### 4.6.8.1 接口请求示例
**请求 /rbac/user/get-user-roles**(GET,Query 参数)
```
?userId=2
```
**响应**
```json
[
{
"id": 20,
"name": "管理员",
"code": "Admin"
}
]
```
##### 4.6.8.2 接口业务逻辑
###### 4.6.8.2.1 核心流程
- 查询指定用户角色列表。
###### 4.6.8.2.2 校验与异常
- 用户不存在时返回 `ApplicationExceptionResult`(400)。
#### 4.6.9 用户权限(GET /rbac/user/get-user-permissions)
##### 4.6.9.1 接口请求示例
**请求 /rbac/user/get-user-permissions**(GET,Query 参数)
```
?userId=2
```
**响应**
```json
[
"rbac:user:get-list",
"rbac:role:get-list"
]
```
##### 4.6.9.2 接口业务逻辑
###### 4.6.9.2.1 核心流程
- 聚合用户全部权限编码并返回。
###### 4.6.9.2.2 校验与异常
- 用户不存在或状态异常时返回 `ApplicationExceptionResult`(400)。
#### 4.6.10 角色下用户(GET /rbac/user/get-users-by-role)
##### 4.6.10.1 接口请求示例
**请求 /rbac/user/get-users-by-role**(GET,Query 参数)
```
?roleId=20
```
**响应**
```json
[
{
"id": 1,
"name": "管理员A",
"phone": "13800138000"
}
]
```
##### 4.6.10.2 接口业务逻辑
###### 4.6.10.2.1 核心流程
- 查询某角色下的用户列表。
###### 4.6.10.2.2 校验与异常
- 角色不存在时返回 `ApplicationExceptionResult`(400)。
#### 4.6.11 更新用户头像(PUT /rbac/user/update-avatar)
##### 4.6.11.1 接口请求示例
**请求 /rbac/user/update-avatar**(PUT,Query 参数)
```
?id=2&avatarUrl=https://cdn.example.com/avatar/test001.png
```
**响应**
```json
{
"code": 0,
"msg": "success",
"data": null
}
```
##### 4.6.11.2 接口业务逻辑
###### 4.6.11.2.1 核心流程
- 更新指定用户头像地址。
###### 4.6.11.2.2 校验与异常
- 用户不存在或头像地址非法时返回 `ApplicationExceptionResult`(400)。
#### 4.6.12 更新用户密码(PUT /rbac/user/update-password)
##### 4.6.12.1 接口请求示例
**请求 /rbac/user/update-password**(PUT,Query + Body)
```
?id=2
```
```json
{
"oldPassword": "123456",
"newPassword": "new123456"
}
```
**响应**
```json
{
"code": 0,
"msg": "success",
"data": null
}
```
##### 4.6.12.2 接口业务逻辑
###### 4.6.12.2.1 核心流程
- 按用户 ID 定位 `t_auth_account`(`PrincipalType=user`),校验旧密码后更新 **`PasswordHash`/`PasswordSalt`**,不写入 `user` 表;**`SessionVersion`+1**;写 **`t_operation_log`(`auth_password_change`)**。
###### 4.6.12.2.2 校验与异常
- 原密码错误、密码策略不满足时返回 `ApplicationExceptionResult`(400)。
### 4.7 菜单(Menu)
#### 4.7.1 菜单详情(GET /rbac/menu/get)
##### 4.7.1.1 接口请求示例
**请求 /rbac/menu/get**(GET,Query 参数)
```
?id=30
```
**响应**
```json
{
"id": 30,
"pId": 0,
"name": "系统管理",
"permissionCode": "rbac:system",
"type": 1,
"route": "/system",
"component": "system/index",
"order": 1,
"isHidden": false,
"isCached": true,
"description": null
}
```
##### 4.7.1.2 接口业务逻辑
###### 4.7.1.2.1 核心流程
- 根据菜单 ID 查询 `MenuDto`。
###### 4.7.1.2.2 校验与异常
- 菜单不存在时返回 `ApplicationExceptionResult`(400)。
#### 4.7.2 菜单树(GET /rbac/menu/get-menu-tree)
##### 4.7.2.1 接口请求示例
**请求 /rbac/menu/get-menu-tree**(GET)
> 无请求参数。
**响应**
```json
[
{
"id": 30,
"name": "系统管理",
"children": []
}
]
```
##### 4.7.2.2 接口业务逻辑
###### 4.7.2.2.1 核心流程
- 返回完整菜单树。
###### 4.7.2.2.2 校验与异常
- 查询异常返回 `ApplicationExceptionResult`(400)。
#### 4.7.3 用户菜单(GET /rbac/menu/get-user-menus)
##### 4.7.3.1 接口请求示例
**请求 /rbac/menu/get-user-menus**(GET,Query 参数)
```
?userId=1
```
**响应**
```json
[
{
"id": 30,
"name": "系统管理",
"route": "/system"
}
]
```
##### 4.7.3.2 接口业务逻辑
###### 4.7.3.2.1 核心流程
- 根据 **`userId`(即 `user.Id`,见 4.1.1)** 解析 **`PrincipalType=user` + `PrincipalId`**,再按有效权限码过滤可见菜单。
###### 4.7.3.2.2 校验与异常
- 用户不存在时返回 `ApplicationExceptionResult`(400)。
- **授权**:调用者能否指定**任意** `userId` 须按 **4.1.1** 在 OpenAPI 中写死(仅本人 / 或具备用户管理权限),避免越权枚举他人菜单。
#### 4.7.4 创建菜单(POST /rbac/menu/create)
##### 4.7.4.1 接口请求示例
**请求 /rbac/menu/create**(POST,Body)
```json
{
"pId": 0,
"name": "用户管理",
"permissionCode": "rbac:user:get-list",
"type": 2,
"icon": "User",
"route": "/system/user",
"component": "system/user/index",
"order": 10,
"isHidden": false,
"isCached": true,
"description": "用户管理菜单"
}
```
**响应**
```json
{
"id": 31,
"name": "用户管理",
"permissionCode": "rbac:user:get-list"
}
```
##### 4.7.4.2 接口业务逻辑
###### 4.7.4.2.1 核心流程
- 新增菜单并返回 `MenuDto`;**`permissionCode`** 须已注册于 **`t_permission_definition`** 且未删(**1.9**、**1.11**)。
###### 4.7.4.2.2 校验与异常
- **`permissionCode`** 未注册、与同表 **`IsDeleted=0`** 下行重复、父节点不存在时返回 `ApplicationExceptionResult`(400)。
#### 4.7.5 更新菜单(PUT /rbac/menu/update)
##### 4.7.5.1 接口请求示例
**请求 /rbac/menu/update**(PUT,Query + Body)
```
?id=31
```
```json
{
"name": "用户中心",
"permissionCode": "rbac:user:get-list",
"icon": "User",
"route": "/system/user",
"component": "system/user/index",
"isHidden": false,
"isCached": true,
"description": "用户中心菜单"
}
```
**响应**
```json
{
"id": 31,
"name": "用户中心",
"permissionCode": "rbac:user:get-list"
}
```
##### 4.7.5.2 接口业务逻辑
###### 4.7.5.2.1 核心流程
- 更新菜单信息并返回 `MenuDto`。
###### 4.7.5.2.2 校验与异常
- 菜单不存在、编码冲突时返回 `ApplicationExceptionResult`(400)。
#### 4.7.6 删除菜单(DELETE /rbac/menu/delete)
##### 4.7.6.1 接口请求示例
**请求 /rbac/menu/delete**(DELETE,Query 参数)
```
?id=31
```
**响应**
```json
{
"code": 0,
"msg": "success",
"data": null
}
```
##### 4.7.6.2 接口业务逻辑
###### 4.7.6.2.1 核心流程
- 删除菜单节点。
###### 4.7.6.2.2 校验与异常
- 菜单存在子节点或被引用时返回 `ApplicationExceptionResult`(400)。
#### 4.7.7 移动菜单(POST /rbac/menu/move-menu)
##### 4.7.7.1 接口请求示例
**请求 /rbac/menu/move-menu**(POST,Query 参数)
```
?menuId=31&targetParentId=30
```
**响应**
```json
{
"code": 0,
"msg": "success",
"data": null
}
```
##### 4.7.7.2 接口业务逻辑
###### 4.7.7.2.1 核心流程
- 调整菜单父子层级。
###### 4.7.7.2.2 校验与异常
- 节点非法或形成循环层级时返回 `ApplicationExceptionResult`(400)。
#### 4.7.8 更新菜单排序(PUT /rbac/menu/update-order)
##### 4.7.8.1 接口请求示例
**请求 /rbac/menu/update-order**(PUT,Query 参数)
```
?id=31&order=20
```
**响应**
```json
{
"code": 0,
"msg": "success",
"data": null
}
```
##### 4.7.8.2 接口业务逻辑
###### 4.7.8.2.1 核心流程
- 更新菜单排序值。
###### 4.7.8.2.2 校验与异常
- `order` 非法或菜单不存在时返回 `ApplicationExceptionResult`(400)。
---
## 5 客户域接口详细设计
> **本章导读**:**客户主账号、子账号** 不走 **`/rbac/user/*`**。写法与 **4** 一致:每接口 **请求示例** → **业务逻辑**(核心流程 / 校验与异常)。**5.1** 为管理端 **OMS-Customer** OpenAPI([Swagger UI — OMS-Customer](http://api.nanafa.com/admin/index.html?urls.primaryName=OMS-Customer));**5.2** 为落实 **1.1** / **2.2** / **1.10** / **1.12** 的 **目标接口**(截至 **OMS-Customer v1** 可能尚未出现在同一 Swagger,落地后以 OpenAPI 为准)。须满足 **2.2**(**子账号权限 ⊆ 主账号**)。
### 5.1 客户资料(OMS-Customer)
> **在线文档**:[Swagger UI — OMS-Customer](http://api.nanafa.com/admin/index.html?urls.primaryName=OMS-Customer)
> **机器可读**:`/admin/swagger/OMS-Customer/swagger.json`(`servers[0].url` 一般为 `https://api.nanafa.com/admin`,下列路径相对该 base;**以部署环境为准**)。
> **鉴权**:`Authorization: Bearer {token}`。
#### 5.1.1 接口路径速查
| 接口名称 | 请求方式 | 路径(相对 admin base) | 说明 |
| --- | --- | --- | --- |
| 客户列表 | GET | /customer/list | Query:`Page`、`PageSize`、`Keyword`、`Status`、`CustomerGroup`、`Salesman`、`CustomerService` |
| 客户详情 | GET | /customer/get | Query:`id`(long) |
| 新增客户 | POST | /customer/create | Body:`CreateCustomerInput` |
| 更新客户 | POST | /customer/update | Body:`UpdateCustomerInput` |
| 删除客户 | POST | /customer/delete | Body:`IdInput` |
| 重置客户密码 | POST | /customer/reset-password | Body:`ResetCustomerPasswordInput` |
#### 5.1.2 与 **1.4 `t_customer` / 1.2 `t_auth_account`** 及 DTO 约定
- **主键参数名**:**`/customer/get`** 等 OMS-Customer 路径使用 Query **`id`** 表示 **`t_customer.Id`**;**5.2** 客户 RBAC 使用 **`customerId`**。二者**同值**(即客户主账号的 **`PrincipalId`**),异名为兼容既有 OpenAPI 与 RBAC 草案,**勿与 `user.Id` 混淆**(见 **4.1.1**)。
- **`t_customer`**:创建/更新 Body 与列表、详情响应以 **5.1.3~5.1.6** JSON 为准;**`settlementMethodCode` / `customerTypeCode`** 非空须 **`t_dict`** 有效(**1.14**);**`receivableFinanceUserId`** 非空须指向未删 **`user`**。
- **`t_auth_account`**:**手机、邮箱、口令(仅创建)、微信**。**管理端代客重置口令** 走 **`/customer/reset-password`**(**5.1.8**)。若产品对客户主账号 / 子账号开放 **`/rbac/auth/change-password`**(**4.2.3**),则 **自助改密** 与之相同机制(`SessionVersion`+1,见 **2**),**不要**把「改密」理解成仅有 **5.1.8** 一条路。**微信**:**路径 A** 在 **`/customer/create`** 一次写入;**路径 B** 在 **`/customer/update`** 补写(**2.3**)。
- **`CustomerStatus`**:`0` 禁用、`1` 正常、`2` 冻结。
- **`role`(字符串)**:写入 **`t_principal_role`** 的映射由实现约定,或与 **`roleIds`**(**5.2.2**)统一。
#### 5.1.3 客户列表(GET /customer/list)
##### 5.1.3.1 接口请求示例
**请求 /customer/list**(GET,Query 参数)
```
?Page=1&PageSize=30&Keyword=&Status=1&CustomerGroup=0&Salesman=&CustomerService=
```
**响应**
```json
{
"items": [
{
"id": 1001,
"customerCode": "W2601",
"customerName": "示例客户A",
"companyName": "示例科技有限公司",
"businessLicenseNo": "91110000MA01234567",
"businessLicenseImageUrl": "https://cdn.example.com/license/w2601.jpg",
"legalRepresentative": "张三",
"website": "https://example.com",
"settlementMethodCode": "settle_monthly",
"settlementMethodName": "月结",
"customerTypeCode": "type_enterprise",
"customerTypeName": "企业客户",
"appId": "app_oms_001",
"receivableFinanceUserId": 88,
"wechatAvatar": null,
"wechatNickname": null,
"email": "a@example.com",
"phone": "13800138000",
"balance": 0,
"totalQuota": 100000,
"usedQuota": 0,
"status": 1,
"remark": null,
"salesman": "zhangsan",
"customerService": "lisi",
"customerGroup": 0,
"role": "CustomerDefault",
"lastLoginIp": "10.0.0.1",
"lastLoginTime": "2026-05-08T10:00:00Z",
"createTime": "2026-01-01T08:00:00Z",
"updateTime": "2026-05-08T10:00:00Z"
}
],
"total": 1
}
```
##### 5.1.3.2 接口业务逻辑
###### 5.1.3.2.1 核心流程
- 按 **`Keyword`**(客户编号/名称/手机等,实现约定)、**`Status`**、**`CustomerGroup`**、**`Salesman`**、**`CustomerService`** 过滤 **`t_customer`**(**`IsDeleted=0`**)。
- 分页:**`Page`** 从 1 开始,**`PageSize`** 与 **3** 中 TablePro 约定对齐时可默认 30。
- 列表项返回 **1.4** 业务列;**`settlementMethodCode`/`customerTypeCode`** 联 **`t_dict`** 填充 **`settlementMethodName`/`customerTypeName`**。
- 联查 **`t_auth_account`** 展示 **`phone`/`email`/微信/最后登录**;**`role`** 可由 **`t_principal_role` + `role.Code`** 聚合为首角色或主角色(与实现一致即可,须在 OpenAPI 说明)。
###### 5.1.3.2.2 校验与异常
- 分页参数非法、排序字段不支持时返回 `ApplicationExceptionResult`(400)。
- 未授权或 token 无效时 **401**(与网关一致)。
#### 5.1.4 客户详情(GET /customer/get)
##### 5.1.4.1 接口请求示例
**请求 /customer/get**(GET,Query 参数)
```
?id=1001
```
**响应**
```json
{
"id": 1001,
"customerCode": "W2601",
"customerName": "示例客户A",
"companyName": "示例科技有限公司",
"businessLicenseNo": "91110000MA01234567",
"businessLicenseImageUrl": "https://cdn.example.com/license/w2601.jpg",
"legalRepresentative": "张三",
"website": "https://example.com",
"settlementMethodCode": "settle_monthly",
"settlementMethodName": "月结",
"customerTypeCode": "type_enterprise",
"customerTypeName": "企业客户",
"appId": "app_oms_001",
"receivableFinanceUserId": 88,
"wechatAvatar": null,
"wechatNickname": null,
"email": "a@example.com",
"phone": "13800138000",
"balance": 0,
"totalQuota": 100000,
"usedQuota": 0,
"status": 1,
"remark": null,
"salesman": "zhangsan",
"customerService": "lisi",
"customerGroup": 0,
"role": "CustomerDefault",
"lastLoginIp": "10.0.0.1",
"lastLoginTime": "2026-05-08T10:00:00Z",
"createTime": "2026-01-01T08:00:00Z",
"createBy": "admin",
"updateTime": "2026-05-08T10:00:00Z",
"updateBy": "admin"
}
```
##### 5.1.4.2 接口业务逻辑
###### 5.1.4.2.1 核心流程
- 按 **`id`** 加载 **`t_customer`**;不存在或已删则异常。
- 拼接 **`t_auth_account`**(`customer` + `PrincipalId=id`)展示联系方式与登录信息。
- 返回 **1.4** 业务列;**`settlementMethodCode`/`customerTypeCode`** 联 **`t_dict`** 填充 **`settlementMethodName`/`customerTypeName`**(见 **5.1.4.1** 响应示例)。
###### 5.1.4.2.2 校验与异常
- **`id`** 缺失、客户不存在或已删除时返回 `ApplicationExceptionResult`(400)。
#### 5.1.5 新增客户(POST /customer/create)
##### 5.1.5.1 接口请求示例
**请求 /customer/create**(POST,Body:`CreateCustomerInput`)
```json
{
"customerCode": "W2602",
"customerName": "新客户B",
"companyName": "新客户B有限公司",
"businessLicenseNo": "91310115MA0ABCDEF0",
"businessLicenseImageUrl": "https://cdn.example.com/license/w2602.jpg",
"legalRepresentative": "李四",
"website": "https://newb.example.com",
"settlementMethodCode": "settle_prepaid",
"customerTypeCode": "type_enterprise",
"appId": "app_oms_002",
"receivableFinanceUserId": 88,
"phone": "13800138002",
"email": "b@example.com",
"password": "Initial#123",
"wechatOpenid": "oXXXX_先微信再开户",
"wechatUnionid": null,
"wechatAvatar": "https://thirdwx.qlogo.cn/...",
"wechatNickname": "客户微信昵称",
"balance": 0,
"totalQuota": 50000,
"usedQuota": 0,
"status": 1,
"remark": "线下签约",
"salesman": "zhangsan",
"customerService": "lisi",
"customerGroup": 0,
"role": "CustomerDefault"
}
```
**响应**
> OpenAPI 200 无 schema;实现可返回新建 **`id`** 或 **`CustomerOutput`**,建议与 **`/customer/get`** 对齐便于前端刷新。
```json
{
"id": 1002
}
```
##### 5.1.5.2 接口业务逻辑
###### 5.1.5.2.1 核心流程
- 校验 **`customerCode`** 唯一(**`IsDeleted=0`**)、**`phone`/`email`** 全局唯一(**1.2**);**`settlementMethodCode`/`customerTypeCode`**、**`receivableFinanceUserId`** 校验见 **5.1.2**。
- 插入 **`t_customer`**(**1.4** 业务列与 Body 一致,含工商、字典码、**`AppId`**、**`ReceivableFinanceUserId`** 等)。
- 创建 **`t_auth_account`**:`PrincipalType=customer`,`PrincipalId=新客户Id`,写入 **`PasswordHash`/`PasswordSalt`**、**`Phone`/`Email`**、**微信列**(**路径 A** 见 **2.3**,Body 可含 **`wechatOpenid` 等**);**`Status`** 与客户状态策略一致(禁用客户则认证行禁用)。
- **`role`**:按 **5.1.2** 约定写入 **`t_principal_role`**(仅 `customer` 主体),角色 **`AllowedPrincipalTypes`** 须含 **`customer`**。
- 写 **`t_operation_log`**(业务 **`ADD`** 快照或字段级,模块 **`customer`**)。
- **`PermissionRevision`**:若创建即绑定角色,主账号认证行 **`PermissionRevision`+1**(**1.1.6**)。
###### 5.1.5.2.2 校验与异常
- 必填缺失、编号/手机/邮箱冲突、**微信 OpenID/UnionID 冲突**、密码策略不满足、字典码无效、应收财务用户不存在、角色编码不存在或不允许 `customer` 主体时返回 `ApplicationExceptionResult`(400)。
#### 5.1.6 更新客户(POST /customer/update)
##### 5.1.6.1 接口请求示例
**请求 /customer/update**(POST,Body:`UpdateCustomerInput`)
```json
{
"id": 1002,
"customerCode": "W2602",
"customerName": "新客户B(更名)",
"companyName": "新客户B有限公司",
"businessLicenseNo": "91310115MA0ABCDEF0",
"businessLicenseImageUrl": "https://cdn.example.com/license/w2602.jpg",
"legalRepresentative": "李四",
"website": "https://newb.example.com",
"settlementMethodCode": "settle_monthly",
"customerTypeCode": "type_enterprise",
"appId": "app_oms_002",
"receivableFinanceUserId": 90,
"phone": "13800138002",
"email": "b-new@example.com",
"wechatOpenid": "oXXXX_后绑微信",
"wechatUnionid": null,
"wechatAvatar": "https://thirdwx.qlogo.cn/...",
"wechatNickname": "客户微信昵称",
"balance": 1000,
"totalQuota": 80000,
"usedQuota": 0,
"status": 1,
"remark": "已续约",
"salesman": "zhangsan",
"customerService": "wangwu",
"customerGroup": 1,
"role": "CustomerVIP"
}
```
**响应**
```json
{
"code": 0,
"msg": "success",
"data": null
}
```
##### 5.1.6.2 接口业务逻辑
###### 5.1.6.2.1 核心流程
- 加载客户与认证行;**部分字段**未在 Body 出现时可保持原值(实现约定 PATCH 语义或全量替换,须在 OpenAPI 写清;上例为常见「可编辑字段全量提交」)。
- 更新 **`t_customer`**;**`phone`/`email`/微信** 同步 **`t_auth_account`**(**路径 B** 补绑微信见 **2.3**),**OpenID/UnionID** 唯一性校验。
- **`role`** 变更:覆盖更新 **`t_principal_role`**(`customer` 主体),成功后 **`PermissionRevision`+1**。
- 若 **`status`** 变为禁用/冻结:可同步认证行 **`Status`**,是否立即 **`SessionVersion`+1** 由产品决定(建议冻结时 **+1** 踢下线)。
- 写 **`t_operation_log`**(**`UPDATE`**)。
###### 5.1.6.2.2 校验与异常
- 客户不存在、编号/手机/邮箱冲突、**微信 OpenID 已被占用**、额度逻辑非法(如 **`usedQuota` > `totalQuota`**)、字典码无效、应收财务用户不存在、角色非法时返回 `ApplicationExceptionResult`(400)。
#### 5.1.7 删除客户(POST /customer/delete)
##### 5.1.7.1 接口请求示例
**请求 /customer/delete**(POST,Body:`IdInput`)
```json
{
"id": 1002
}
```
**响应**
```json
{
"code": 0,
"msg": "success",
"data": null
}
```
##### 5.1.7.2 接口业务逻辑
###### 5.1.7.2.1 核心流程
- 逻辑删除 **`t_customer`**(**`IsDeleted=1`**);逻辑删除或禁用 **`t_auth_account`**;清理/失效 refresh(**`SessionVersion`+1** 或等价吊销)。
- 软删 **`t_principal_role`** 相关行(或保留历史由 **`IsDeleted`** 表达,与实现统一即可)。
- 若存在 **`t_customer_sub_account`**:须先业务校验(禁止删主客户或级联策略由产品定)。
- 写 **`t_operation_log`**(**`DELETE`** 或 RBAC 相关动作码)。
###### 5.1.7.2.2 校验与异常
- 客户不存在、存在未处理子账号或业务禁止删除时返回 `ApplicationExceptionResult`(400)。
#### 5.1.8 重置客户密码(POST /customer/reset-password)
##### 5.1.8.1 接口请求示例
**请求 /customer/reset-password**(POST,Body:`ResetCustomerPasswordInput`)
```json
{
"id": 1001,
"newPassword": "Reset#2026"
}
```
**响应**
```json
{
"code": 0,
"msg": "success",
"data": null
}
```
##### 5.1.8.2 接口业务逻辑
###### 5.1.8.2.1 核心流程
- 定位 **`t_auth_account`**(`customer` + **`PrincipalId=id`**)。
- 更新 **`PasswordHash`/`PasswordSalt`**;**`SessionVersion`+1**,使已有 refresh 失效(见 **2**)。
- 写 **`t_operation_log`**(**`auth_password_change`** 或管理端重置专用动作码,`TargetPrincipalType=customer`)。
###### 5.1.8.2.2 校验与异常
- 客户不存在、认证行不存在、新密码不满足策略时返回 `ApplicationExceptionResult`(400)。
- 操作者须具备客户管理权限(PermissionCode 由实现注册在 **`t_permission_definition`**)。
### 5.2 客户域 RBAC(主账号 / 子账号 / 白名单)— 目标接口
> **说明**:下列路径在 **OMS-Customer v1** OpenAPI 中可能尚未出现;**请求/响应字段以未来 OpenAPI 为准**,本节为与 **1.1**、**2.2** 对齐的 **规范草案**。子账号 **`MainCustomerId`**、白名单 **`t_customer_sub_permission`** 见 **1.5**、**1.12**。
#### 5.2.1 接口路径速查
| 接口名称 | 请求方式 | 请求路径 | 说明 |
| --- | --- | --- | --- |
| 主账号分配角色 | POST | /customer/role/assign | 覆盖写主账号 **`t_principal_role`**;**`PermissionRevision`+1**;子账号权限 ⊆ 主校验 |
| 主账号角色列表 | GET | /customer/role/list | 主账号已绑角色 |
| 子账号列表 | GET | /customer/sub-account/list | 某主客户下子账号分页 |
| 创建子账号 | POST | /customer/sub-account/create | **`t_customer_sub_account` + `t_auth_account`** |
| 更新子账号 | POST | /customer/sub-account/update | 状态/认证信息等 |
| 删除子账号 | POST | /customer/sub-account/delete | 软删 + 吊销登录态 |
| 子账号分配角色 | POST | /customer/sub-account/assign-roles | **\(P_{\text{子}}\subseteq P_{\text{主}}\)** |
| 子账号配置白名单 | POST | /customer/sub-account/assign-permissions | 覆盖 **`t_customer_sub_permission`** |
| 子账号有效权限预览 | GET | /customer/sub-account/effective-permissions | 聚合权限码 |
#### 5.2.2 主账号分配角色(POST /customer/role/assign)
##### 5.2.2.1 接口请求示例
**请求 /customer/role/assign**(POST,Body)
```json
{
"customerId": 1001,
"roleIds": [200, 201]
}
```
**响应**
```json
{
"code": 0,
"msg": "success",
"data": null
}
```
##### 5.2.2.2 接口业务逻辑
###### 5.2.2.2.1 核心流程
- 校验客户存在且 **`PrincipalType=customer`** 对应认证行有效。
- 校验每个 **`roleId`**:**`role.AllowedPrincipalTypes`** 含 **`customer`**,且 **`role`** 未删除。
- **事务内**:计算主账号新权限并集 \(P_{\text{主}}^{\text{new}}\);对所有 **`t_customer_sub_account`**(`MainCustomerId=customerId`,未删)计算当前 \(P_{\text{子}}\),须满足 **\(P_{\text{子}}\subseteq P_{\text{主}}^{\text{new}}\)**(**2.2**);否则整单回滚 **400**。
- 覆盖写 **`t_principal_role`**(`PrincipalType=customer`,`PrincipalId=customerId`)。
- 主账号 **`t_auth_account.PermissionRevision`+1**。
- **`t_operation_log`**:`Action=principal_role_assign`,`TargetPrincipalType=customer`,`TargetPrincipalId=customerId`,`DetailJson` 含 **`roleIds`**。
###### 5.2.2.2.2 校验与异常
- 客户不存在、角色非法、子账号超限、空 **`roleIds`** 是否允许(产品定)时返回 `ApplicationExceptionResult`(400)。
#### 5.2.3 主账号角色列表(GET /customer/role/list)
##### 5.2.3.1 接口请求示例
**请求 /customer/role/list**(GET,Query)
```
?customerId=1001
```
**响应**
```json
[
{
"id": 200,
"name": "客户默认角色",
"code": "CustomerDefault"
}
]
```
##### 5.2.3.2 接口业务逻辑
###### 5.2.3.2.1 核心流程
- 查 **`t_principal_role`**(`customer` + **`PrincipalId=customerId`**,`IsDeleted=0`),联 **`role`** 表返回 id/name/code。
###### 5.2.3.2.2 校验与异常
- **`customerId`** 无效时返回 `ApplicationExceptionResult`(400)。
#### 5.2.4 子账号列表(GET /customer/sub-account/list)
##### 5.2.4.1 接口请求示例
**请求 /customer/sub-account/list**(GET,Query)
```
?mainCustomerId=1001&Page=1&PageSize=30&Keyword=
```
**响应**
```json
{
"total": 2,
"items": [
{
"id": 5001,
"mainCustomerId": 1001,
"subAccountCode": "W2601_01",
"subStatus": 1,
"phone": "13900139001",
"email": "sub1@example.com",
"createTime": "2026-03-01T08:00:00Z"
}
]
}
```
##### 5.2.4.2 接口业务逻辑
###### 5.2.4.2.1 核心流程
- 过滤 **`t_customer_sub_account`**:`MainCustomerId=mainCustomerId`,**`IsDeleted=0`**;**`Keyword`** 可匹配编号/手机(联认证表)。
- 返回分页 **`total` + `items`**(与 TablePro 约定一致)。
###### 5.2.4.2.2 校验与异常
- 主客户不存在、分页参数非法时返回 `ApplicationExceptionResult`(400)。
#### 5.2.5 创建子账号(POST /customer/sub-account/create)
##### 5.2.5.1 接口请求示例
**请求 /customer/sub-account/create**(POST,Body)
```json
{
"mainCustomerId": 1001,
"subAccountCode": "W2601_02",
"phone": "13900139002",
"email": "sub2@example.com",
"password": "Sub#123456",
"wechatOpenid": null,
"wechatUnionid": null,
"wechatAvatar": null,
"wechatNickname": null,
"roleIds": [210]
}
```
> **`subAccountCode`** 可省略,由规则「主客户编号 + `_` + 序号」生成(**1.5**)。**`wechatOpenid`/`wechatUnionid`/`wechatAvatar`/`wechatNickname`**:**路径 A** 可与本单创建一并提交(**2.3**);**路径 B** 留 **`/customer/sub-account/update`**。
**响应**
```json
{
"id": 5002,
"subAccountCode": "W2601_02"
}
```
##### 5.2.5.2 接口业务逻辑
###### 5.2.5.2.1 核心流程
- 校验主客户存在;**`phone`/`email`** 在 **`t_auth_account`** 全局唯一。
- 插入 **`t_customer_sub_account`**(**`SubStatus`** 默认正常)。
- 创建 **`t_auth_account`**:`PrincipalType=customer_sub`,`PrincipalId=子账号Id`,口令、联系方式与可选 **微信字段**(**1.2**)写入。
- **`roleIds`**:写入 **`t_principal_role`**(`customer_sub`),角色须 **`AllowedPrincipalTypes`** 含 **`customer_sub`**;**事务内**校验 **\(P_{\text{子}}\subseteq P_{\text{主}}\)**(**1.1.3**、**2.2**)。
- 子账号认证行 **`PermissionRevision`** 初值与后续 **+1** 规则同 **1.1.6**。
- **`t_operation_log`**:子账号创建(业务 **`ADD`** 或 RBAC 组合动作)。
###### 5.2.5.2.2 校验与异常
- 主客户不存在、编号重复、手机邮箱冲突、**微信 OpenID/UnionID 冲突**、角色非法、**子权限超出主账号**时返回 `ApplicationExceptionResult`(400)。
#### 5.2.6 更新子账号(POST /customer/sub-account/update)
##### 5.2.6.1 接口请求示例
**请求 /customer/sub-account/update**(POST,Body)
```json
{
"id": 5002,
"subStatus": 0,
"phone": "13900139022",
"email": "sub2-new@example.com",
"wechatOpenid": "oXXXX_后绑微信",
"wechatUnionid": null,
"wechatAvatar": "https://thirdwx.qlogo.cn/...",
"wechatNickname": "子账号微信昵称"
}
```
**响应**
```json
{
"code": 0,
"msg": "success",
"data": null
}
```
##### 5.2.6.2 接口业务逻辑
###### 5.2.6.2.1 核心流程
- 更新 **`t_customer_sub_account`**(如 **`SubStatus`**)。
- **`phone`/`email`/微信** 变更同步 **`t_auth_account`**(**路径 B** 见 **2.3**),**OpenID/UnionID** 唯一;若禁用子账号可 **`SessionVersion`+1**。
- 若变更影响权限边界(如后续版本增加字段),须重新校验 **\(P_{\text{子}}\subseteq P_{\text{主}}\)**。
- **`t_operation_log`**(**`UPDATE`**)。
###### 5.2.6.2.2 校验与异常
- 子账号不存在、越权(非该主客户下)、联系方式冲突、**微信 OpenID 已被占用**时返回 `ApplicationExceptionResult`(400)。
#### 5.2.7 删除子账号(POST /customer/sub-account/delete)
##### 5.2.7.1 接口请求示例
**请求 /customer/sub-account/delete**(POST,Body)
```json
{
"id": 5002
}
```
**响应**
```json
{
"code": 0,
"msg": "success",
"data": null
}
```
##### 5.2.7.2 接口业务逻辑
###### 5.2.7.2.1 核心流程
- 软删 **`t_customer_sub_account`**;软删/禁用认证行;**`SessionVersion`+1** 吊销 token。
- 软删 **`t_principal_role`**、**`t_customer_sub_permission`** 相关行(约定与 **1.12** 一致)。
- **`t_operation_log`**。
###### 5.2.7.2.2 校验与异常
- 子账号不存在或已删除时返回 `ApplicationExceptionResult`(400)。
#### 5.2.8 子账号分配角色(POST /customer/sub-account/assign-roles)
##### 5.2.8.1 接口请求示例
**请求 /customer/sub-account/assign-roles**(POST,Query + Body)
```
?subAccountId=5001
```
```json
[210, 211]
```
**响应**
```json
{
"code": 0,
"msg": "success",
"data": null
}
```
##### 5.2.8.2 接口业务逻辑
###### 5.2.8.2.1 核心流程
- 解析 **`subAccountId`** 归属 **`MainCustomerId`**,加载主账号有效权限 \(P_{\text{主}}\)。
- **事务内**:由 **`roleIds`** 得 \(P_{\text{role}}\);若有白名单 **`t_customer_sub_permission`** 未删行,则 \(P = P_{\text{role}} \cap P_{\text{whitelist}}\),否则 \(P = P_{\text{role}}\)(**1.1.3**)。校验 **\(P \subseteq P_{\text{主}}\)**。
- 覆盖写 **`t_principal_role`**(`customer_sub` + **`PrincipalId=subAccountId`**)。
- 子账号 **`PermissionRevision`+1**。
- **`t_operation_log`**:`principal_role_assign`,`TargetPrincipalType=customer_sub`,`TargetPrincipalId=subAccountId`。
###### 5.2.8.2.2 校验与异常
- 子账号不存在、角色不适用 **`customer_sub`**、**权限超出主账号**时返回 `ApplicationExceptionResult`(400)。
#### 5.2.9 子账号配置白名单(POST /customer/sub-account/assign-permissions)
##### 5.2.9.1 接口请求示例
**请求 /customer/sub-account/assign-permissions**(POST,Body)
```json
{
"subAccountId": 5001,
"permissionCodes": [
"order:list",
"order:create"
]
}
```
**响应**
```json
{
"code": 0,
"msg": "success",
"data": null
}
```
##### 5.2.9.2 接口业务逻辑
###### 5.2.9.2.1 核心流程
- 每个 **`permissionCodes`** 元素须 ∈ **`t_permission_definition.Code`** 且未删除。
- **事务内**:令白名单集合为 \(W\);角色并集 \(P_{\text{role}}\);最终 \(P = P_{\text{role}} \cap W\)(若 **`permissionCodes` 为空** 是否表示「无白名单」= 仅用角色并集,产品须二选一并写清 OpenAPI;推荐:**空数组 = 清除白名单行**,回退角色并集)。
- 校验 **\(P \subseteq P_{\text{主}}\)**。
- 覆盖写 **`t_customer_sub_permission`**(软删旧行 + 插入新行,或等价单表维护)。
- 子账号 **`PermissionRevision`+1**。
- **`t_operation_log`**:`sub_whitelist_assign`,`DetailJson` 含码表。
###### 5.2.9.2.2 校验与异常
- 未注册权限码、**超出主账号权限**、子账号不存在时返回 `ApplicationExceptionResult`(400)。
#### 5.2.10 子账号有效权限预览(GET /customer/sub-account/effective-permissions)
##### 5.2.10.1 接口请求示例
**请求 /customer/sub-account/effective-permissions**(GET,Query)
```
?subAccountId=5001
```
**响应**
```json
{
"rolePermissionCodes": ["order:list", "order:create", "order:export"],
"afterWhitelist": ["order:list", "order:create"],
"mainCustomerMax": ["order:list", "order:create", "order:export", "finance:view"]
}
```
> **`afterWhitelist`** 为最终用于 API 鉴权的集合 \(P\);字段命名实现可改为 `effectivePermissionCodes`。
##### 5.2.10.2 接口业务逻辑
###### 5.2.10.2.1 核心流程
- 计算 \(P_{\text{role}}\)、白名单交、\(P_{\text{主}}\),返回便于联调与排错的三元组(或最小返回 **`effectivePermissionCodes`**)。
###### 5.2.10.2.2 校验与异常
- 子账号不存在、调用者非主客户管理员时返回 `ApplicationExceptionResult`(400)。