# RBAC 与统一认证 — 产品需求文档(PRD) > **代码基线**:`C:\Users\wilso\Desktop\SimpleAdmin.Api-master`(后端)+ `E:\oms0414`(前端 OpenAPI 客户端) > **RBAC 内核**:NuGet 包 `Fake.Rbac.Application` / `Fake.Rbac.Infrastructure` **v8.0.5.11** > **API 前缀**:`/rbac/*`(RBAC 管理)、`/customer/*`(OMS 客户资料,含 RBAC 联动) --- ## 导读 ### 这份文档解决什么问题? 系统当前实现要搞清楚四件事: 1. **谁能登录** — 内部用户(管理端)与客户(OMS 端)共用 RBAC `user` 表;客户登录凭证存 `t_customer`,通过 `user.userid` 与 RBAC 关联。 2. **登录之后凭什么进业务** — JWT Access Token + Refresh Token;Claims 含 `mapId`(OMS 整型用户编号)与 `loginType`(0=内部用户,1=客户)。 3. **能操作哪些功能** — 用户有效权限 = 其所有角色在 `role_permission` 中权限码的**并集**;子客户通过**专属角色** `CustomerSub_{userId}` 收窄权限;菜单按权限码裁剪展示,**不替代** API 鉴权。 4. **客户与 RBAC 如何联动** — 创建客户时同步创建 RBAC 用户、分配 `Customer` 角色;创建子客户时创建专属角色并校验权限不超过主客户。 ### 建议阅读路径 | 角色 | 建议路径 | | --- | --- | | 后端 / DBA | **2 概念模型** → **3 表结构** → **8 接口详细设计** | | 前端 / BFF | **4 认证与令牌** → **7 接口速查** → **9 前端集成** | | 产品 / 测试 | **2** → **5 权限模型** → **7** → **8.7 客户接口** | ### 章节一览 | 章 | 内容 | | --- | --- | | **1** | 系统架构 | | **2** | 概念模型 | | **3** | 数据表结构 | | **4** | 认证与令牌 | | **5** | 权限模型 | | **6** | 枚举与常量 | | **7** | 接口路径速查 | | **8** | 接口详细设计 | | **9** | 前端集成 | | **10** | 种子数据与默认账号 | --- ## 1 系统架构 ### 1.1 整体分层 ``` SimpleAdmin.Api(宿主) ├── Fake.Rbac.Application # User/Role/Menu/Permission/Organization 服务 + DTO + JWT ├── Fake.Rbac.Infrastructure # FakeRbacDbContext、EF 配置、仓储、种子数据 ├── Fake.Rbac.Domain # 聚合根、AccountManager、权限定义 Provider └── Oms.Application # CustomerService:客户资料 + RBAC 用户/角色生命周期 ``` - **API 生成方式**:继承 `ApplicationService` 的服务由 Fake 框架自动映射为 Controller(`ApplicationServices2Controller`)。 - **RBAC 路由根路径**:`rbac`(见 `SimpleAdminApiModule.cs`)。 - **自定义认证**:宿主项目 `AuthService` **替换**包内默认 `AuthService`,补充 OMS 双端登录。 - **数据库**:RBAC 表由 EF Core 管理(MySQL);OMS 业务表由 SqlSugar 管理(`db.sql`)。 ### 1.2 双 ID 体系(关键) | 标识 | 类型 | 用途 | | --- | --- | --- | | `user.Id` | `GUID` | RBAC 主键;JWT `sub`;服务层用户标识 | | `user.userid` | `INT` 自增 | OMS 桥接 ID;`t_customer.UserId` 外键;JWT Claim `mapId` | - 内部 API 操作用户时传 **GUID**(`id` 查询参数)。 - OMS 业务侧(订单、客户上下文)用 **`CurrentUser.GetId()`** 读取 `mapId`。 ### 1.3 中间件管道 ``` UseForwardedHeaders → UsePathBase → UseFakeSwagger → UseFakeExceptionHandling → UseCors → UseStaticFiles → UseRouting → UseAuthentication → UseAuthorization → MapControllers ``` - 当前**无**细粒度权限拦截器;接口层主要校验 **Bearer Token 是否有效**(`[Authorize]`)。 - 细粒度 `[Authorize(Permission = "...")]` 在包内标记为待实现。 --- ## 2 概念模型 ### 2.1 三层分工 | 层次 | 回答的问题 | 核心对象 | | --- | --- | --- | | **身份(Authentication)** | 你是谁?凭证是否有效? | `user`(内部用户)/ `t_customer`(客户登录凭证) | | **授权(Authorization)** | 你被允许执行哪些 PermissionCode? | `user_role` → `role` → `role_permission` | | **展示(UI)** | 控制台显示哪些菜单? | `menu`(每节点绑定 `PermissionCode`,按有效权限裁剪) | ### 2.2 有效权限码集合 P 的计算 记用户绑定的所有角色,其 `role_permission.PermissionCode` 并集为 **P**: - **内部用户**:`P = ⋃ role_permission`(该用户所有角色)。 - **客户主账号**:创建时分配 `Customer` 角色;`P` 由该角色(及后续分配的其他角色)决定。 - **子客户**:创建时新建角色 `CustomerSub_{userId}`,创建入参 `permissions[]` 直接写入该角色的 `role_permission`;保存前校验 **子客户权限 ⊆ 主客户当前权限**。 > **注意**:当前实现**没有** `customer_sub_permission` 白名单表;子客户权限通过**专属角色**表达,而非与角色权限求交。 ### 2.3 登录主体类型 | `OmsLoginType` | 值 | 说明 | | --- | --- | --- | | `User` | 0 | 内部用户;校验 RBAC `user.Account` + 哈希密码 | | `Customer` | 1 | 客户;校验 `t_customer` 手机/邮箱/客户编号 + MD5 密码,再映射到 RBAC GUID | ### 2.4 关联关系图 ```mermaid flowchart LR subgraph RBAC U[user] UR[user_role] R[role] RP[role_permission] M[menu] O[organization] end subgraph OMS C[t_customer] end U -->|userid INT| C U --> UR --> R --> RP RP --> P["有效权限码 P"] P --> API["API 鉴权(当前主要为登录态)"] P --> M U -->|OrganizationId| O ``` --- ## 3 数据表结构 > RBAC 表名在 MySQL 中为**小写**;由 `Fake.Rbac.Infrastructure` EF Migration 创建。 > OMS 客户表在 `db.sql` 中定义,表名保留 `t_` 前缀。 ### 3.1 `user` — RBAC 用户表 | 字段名 | 类型 | 约束 | 说明 | | --- | --- | --- | --- | | `Id` | CHAR(36) | PK | GUID 主键 | | `userid` | INT | 自增,唯一 | OMS 桥接编号,对应 `t_customer.UserId` | | `Name` | VARCHAR | NOT NULL | 显示名称 | | `Account` | VARCHAR | NOT NULL,唯一索引 `IX_user_Account` | 登录账号 | | `Password` | VARCHAR | NOT NULL | 密码哈希(`EncryptPassword` 值对象) | | `Salt` | VARCHAR | NOT NULL | 密码盐 | | `Email` | VARCHAR | 可空,唯一索引 `IX_user_Email` | 邮箱 | | `Avatar` | VARCHAR | 可空 | 头像 URL | | `OrganizationId` | CHAR(36) | 可空,索引 `IX_user_OrganizationId` | 所属组织 | | `ConcurrencyStamp` | CHAR(36) | NOT NULL | 乐观并发 | | `CreateUserId` | INT | NOT NULL | 创建人 | | `CreateTime` | DATETIME | NOT NULL | 创建时间 | | `UpdateUserId` | INT | 可空 | 更新人 | | `UpdateTime` | DATETIME | 可空 | 更新时间 | | `IsDeleted` | TINYINT | NOT NULL,默认 0 | 软删除 | **说明**: - 内部用户:账号、密码、姓名、邮箱均存此表。 - 客户:RBAC 用户行由 `CustomerService` 创建(`Account` 默认用手机号),登录密码以 `t_customer.Password`(MD5)为准。 ### 3.2 `role` — 角色表 | 字段名 | 类型 | 约束 | 说明 | | --- | --- | --- | --- | | `Id` | CHAR(36) | PK | GUID 主键 | | `Name` | VARCHAR | NOT NULL | 角色名称,如「客户」「子客户-xxx」 | | `Code` | VARCHAR | NOT NULL,唯一索引 `IX_role_Code` | 角色编码,如 `Customer`、`CustomerSub_123` | | `ConcurrencyStamp` | CHAR(36) | NOT NULL | 乐观并发 | | `CreateUserId` / `CreateTime` | — | 审计 | | | `UpdateUserId` / `UpdateTime` | — | 可空 | | | `IsDeleted` | TINYINT | 默认 0 | 软删除 | **内置角色**: | Code | Name | 来源 | | --- | --- | --- | | `Customer` | 客户 | `CustomerRoleSeedContributor` 启动种子 | | `CustomerSub_{userId}` | 子客户-{名称} | `CustomerService.CreateSubCustomerAsync` 动态创建 | ### 3.3 `user_role` — 用户角色关联表 | 字段名 | 类型 | 约束 | 说明 | | --- | --- | --- | --- | | `Id` | CHAR(36) | PK | | | `UserId` | CHAR(36) | FK → `user.Id` | | | `RoleId` | CHAR(36) | FK → `role.Id` | | | `CreateUserId` | INT | | 审计 | | `CreateTime` | DATETIME | | 审计 | 唯一索引:`IX_user_role_UserId_RoleId` ### 3.4 `role_permission` — 角色权限关联表 | 字段名 | 类型 | 约束 | 说明 | | --- | --- | --- | --- | | `Id` | CHAR(36) | PK | | | `RoleId` | CHAR(36) | FK → `role.Id` | | | `PermissionCode` | VARCHAR | NOT NULL | 权限码字符串 | | `CreateUserId` | INT | | 审计 | | `CreateTime` | DATETIME | | 审计 | 唯一索引:`IX_role_permission_RoleId_PermissionCode` > **无** `permission_definition` 数据库表。权限码权威定义在代码 `PermissionDefinitionProvider` 中,分配时以字符串存入本表。 ### 3.5 `menu` — 菜单表 | 字段名 | 类型 | 约束 | 说明 | | --- | --- | --- | --- | | `Id` | CHAR(36) | PK | | | `PId` | CHAR(36) | NOT NULL | 父菜单 ID;`00000000-0000-0000-0000-000000000000` 表示根 | | `Name` | VARCHAR | NOT NULL | 菜单名称 | | `PermissionCode` | VARCHAR | 可空,索引 `IX_menu_PermissionCode` | 关联权限码 | | `Type` | INT | NOT NULL | `MenuType`:1=菜单,2=按钮 | | `Icon` | VARCHAR | 可空 | 图标 | | `Route` | VARCHAR | 可空 | 前端路由 path | | `Component` | VARCHAR | 可空 | 前端组件路径 | | `Order` | INT | NOT NULL | 排序 | | `IsHidden` | TINYINT | 默认 0 | 是否隐藏 | | `IsCached` | TINYINT | 默认 0 | 是否缓存(keep-alive) | | `Description` | VARCHAR | 可空 | 描述 | | 审计 + `IsDeleted` | — | | 标准软删除 | **用户可见菜单算法**(`AuthService` / `MenuService`): 1. 取用户有效权限码集合 `P`。 2. 选出 `menu.PermissionCode ∈ P` 的节点。 3. 递归补齐所有祖先节点(保证树结构完整)。 4. 按 `Order` 构建树返回。 ### 3.6 `organization` — 组织表 | 字段名 | 类型 | 约束 | 说明 | | --- | --- | --- | --- | | `Id` | CHAR(36) | PK | | | `ParentId` | CHAR(36) | 可空 | 父组织;空或零 GUID 为根 | | `Name` | VARCHAR | NOT NULL | 组织名称 | | `Code` | VARCHAR | NOT NULL,唯一索引 `IX_organization_Code` | 组织编码 | | `Type` | INT | NOT NULL | `OrganizationType`:1/2/3 | | `LeaderId` | CHAR(36) | 可空 | 负责人用户 GUID | | `Order` | INT | | 排序 | | `Description` | VARCHAR | 可空 | 描述 | | `IsEnabled` | TINYINT | 默认 1 | 是否启用 | | 审计 + `IsDeleted` | — | | | **说明**:组织仅关联内部用户 `user.OrganizationId`;客户不挂组织树。组织**不是**权限码,用于人事与数据范围等业务查询。 ### 3.7 `t_customer` — OMS 客户表(桥接 RBAC) | 字段名 | 类型 | 约束 | 说明 | | --- | --- | --- | --- | | `Id` | INT | PK,自增 | 客户主键 | | `ParentCustomerId` | INT | 可空 | NULL=主客户;有值=子客户 | | `UserId` | INT | NOT NULL,唯一 `uk_cus_user` | 关联 `user.userid` | | `CustomerCode` | VARCHAR(32) | NOT NULL,唯一 | 客户编号 | | `CustomerName` | VARCHAR(128) | NOT NULL | 客户名称 | | `Phone` | VARCHAR(16) | NOT NULL,唯一 | 登录账号(之一) | | `Email` | VARCHAR(64) | 可空,唯一 | 登录账号(之一) | | `Password` | VARCHAR(64) | NOT NULL | MD5 加密,客户登录用 | | `WechatAvatar` / `WechatNickname` | VARCHAR | 可空 | 微信资料 | | `WechatOpenid` | VARCHAR(64) | 可空,唯一 | 微信 OpenID | | `Balance` / `TotalQuota` / `UsedQuota` | DECIMAL(18,2) | | 账户额度 | | `Status` | TINYINT | 默认 1 | 0=禁用,1=正常,2=冻结 | | `CustomerGroup` | TINYINT | 可空 | 0=普通,1~5=VIP1~VIP5 | | `Salesman` / `CustomerService` | VARCHAR(32) | 可空 | 销售/客服 | | `LastLoginIp` / `LastLoginTime` | — | 可空 | 最后登录 | | `Remark` | VARCHAR(512) | 可空 | 备注 | | 审计 + `IsDeleted` + `ConcurrencyStamp` | — | | | **主/子客户判定**: - `ParentCustomerId IS NULL` → 主客户,可调用 `/customer/sub/create` 创建子客户。 - `ParentCustomerId IS NOT NULL` → 子客户;登录时同时校验子客户与主客户状态。 --- ## 4 认证与令牌 ### 4.1 JWT 配置项 | 配置键 | 说明 | 示例 | | --- | --- | --- | | `Jwt:SecretKey` | 签名密钥 | 生产从 Consul 加载 | | `Jwt:Issuer` | 签发者 | | | `Jwt:Audience` | 受众 | | | `Jwt:ExpiryInMinutes` | Access Token 有效期(分钟) | 60 | | `Jwt:RefreshExpiryInDays` | Refresh Token 有效期(天) | 7 | | `Jwt:ClockSkewMinutes` | 时钟偏移容忍(分钟) | 5 | ### 4.2 JWT Claims | Claim | 类型 | 说明 | | --- | --- | --- | | 标准 RBAC Claims | — | 由 `JwtService.GenerateClaimsByUserIdAsync` 生成(用户 ID、角色等) | | `mapId` | int 字符串 | `user.userid`,OMS 业务上下文用 | | `loginType` | int 字符串 | `0`=内部用户,`1`=客户 | 自定义实现:`SimpleJwtService` 在生成 Claims 时注入 `mapId`;`AuthService` 在客户登录时覆盖 `loginType=1`。 ### 4.3 登录流程 #### 4.3.1 内部用户(`type=0`,管理端 `/admin`) > **不支持微信登录**。管理端仅提供账号 + 密码方式,登录页不展示微信扫码入口。 1. `POST /rbac/auth/login`,body:`{ account, password, type: 0 }` 2. `AccountManager.ValidateCredentialsAsync` 校验 `user.Account` + 哈希密码。 3. 生成 Access Token + Refresh Token。 4. 前端调用 `GET /rbac/auth/get-current-user` 拉取用户信息、角色、权限、菜单。 #### 4.3.2 客户(`type=1`,客户端 `/login`) > 客户端支持 **账号密码** 与 **微信扫码** 两种方式(见 [4.3.4](#434-客户微信扫码登录仅客户端))。 1. `POST /rbac/auth/login`,body:`{ account, password, type: 1 }` 2. 按手机 → 邮箱 → 客户编号顺序查找 `t_customer`。 3. 校验客户状态(子客户还需校验主客户状态)。 4. 比对 `t_customer.Password`(MD5)。 5. 通过 `user.userid` 查 RBAC GUID,生成 Token(`loginType=1`)。 6. 更新 `LastLoginIp`、`LastLoginTime`。 #### 4.3.3 刷新令牌 1. `POST /rbac/auth/refresh-token`,body:`{ refreshToken }` 2. 校验 Refresh Token 有效性,解析 `loginType`。 3. 客户刷新:通过 `mapId` 重新查 GUID 并签发新 Token。 4. 内部用户刷新:通过 GUID 查用户并签发新 Token。 #### 4.3.4 客户微信扫码登录(仅客户端) ##### 适用范围 | 登录页 | 路由 | 账号密码 | 微信扫码 | | --- | --- | --- | --- | | 管理端 | `/admin`(`adminLogin`) | ✅ | ❌ **不支持** | | 客户端 | `/login`(`login`) | ✅ | ✅ | 前端实现:`login/index.vue` 根据路由 `loginPortal` 判定主体;仅 `OmsLoginType.Customer` 时渲染 `WechatLogin` 组件及密码页底部的微信入口按钮。 ##### 现状与依赖 | 层级 | 状态 | | --- | --- | | 数据表 | `t_customer` 已有 `WechatOpenid`(唯一)、`WechatAvatar`、`WechatNickname` | | 仓储 | `ICustomerRepository.GetByWechatOpenidAsync` 已实现 | | 后端认证 | **未实现** 微信 OAuth / 扫码登录接口;`AuthService` 仅支持密码登录 | | 前端 | `wechat-login.vue` 为占位 UI(假二维码 + 120s 倒计时),待对接后端 | ##### 微信开放平台配置(网站应用扫码登录) 采用 [微信开放平台 · 网站应用 · 微信登录](https://developers.weixin.qq.com/doc/oplatform/Website_App/WeChat_Login/Wechat_Login.html) 方案: | 配置项 | 说明 | | --- | --- | | `AppID` / `AppSecret` | 网站应用凭证,存 Consul / 环境变量,**禁止**写入前端 | | 授权回调域 | 与 OMS 客户门户同域,如 `oms.example.com` | | 业务 scope | `snsapi_login`(扫码确认后换取 OpenID) | ##### 端到端流程 ```mermaid sequenceDiagram participant U as 客户浏览器 participant F as OMS 前端 /login participant B as SimpleAdmin.Api participant W as 微信开放平台 U->>F: 打开客户端登录页,切换「微信扫码」 F->>B: GET /rbac/auth/wechat/qrcode B->>B: 生成 scene/ticket,写入 Redis(TTL 120s) B->>W: 拼装 oauth2/authorize URL(state=ticket) B-->>F: { ticket, qrUrl, expiresIn } F->>F: 渲染二维码(qrUrl) loop 每 2s 轮询,最长 120s F->>B: GET /rbac/auth/wechat/poll?ticket=xxx alt 未扫码 / 已扫码未确认 B-->>F: { status: pending } else 用户取消 B-->>F: { status: cancelled } else 扫码成功 B-->>F: { status: success, accessToken, refreshToken, expiresIn } F->>F: 写入 Token,跳转 /user/profile end end Note over W,B: 用户扫码确认后,微信回调 B 的 redirect_uri W->>B: GET /rbac/auth/wechat/callback?code=xxx&state=ticket B->>W: code 换 access_token + openid B->>B: GetByWechatOpenidAsync(openid) alt OpenID 已绑定客户 B->>B: 校验客户/主客户状态,签发 JWT(loginType=1) B->>B: 更新 LastLoginIp/Time、WechatAvatar/Nickname B->>B: 将 ticket 标记为 success + Token else OpenID 未绑定 B->>B: ticket 标记为 unbound end ``` **步骤说明**: 1. **申请二维码**:前端调用 `GET /rbac/auth/wechat/qrcode`,后端生成唯一 `ticket`(即 OAuth `state`),返回可嵌入 `` 的 `qrUrl`(微信 `showqrcode` 或自建二维码图)。 2. **用户扫码**:微信 App 打开授权页,用户确认登录。 3. **服务端回调**:微信重定向 `GET /rbac/auth/wechat/callback?code&state`;后端用 `code` 换 `openid`(及昵称、头像)。 4. **客户匹配**:`openid` → `t_customer.WechatOpenid`;命中则走与密码登录相同的 Token 签发逻辑(`mapId = user.userid`,`loginType = 1`)。 5. **前端轮询**:`poll` 接口在 ticket 变为 `success` 时返回 Token 对;前端复用现有 `rbac-user` store 落盘与跳转逻辑。 6. **刷新二维码**:ticket 过期或 `cancelled` 时,前端重新调用 `qrcode` 接口。 ##### 待实现 API 契约 **1. 获取扫码 ticket** ``` GET /rbac/auth/wechat/qrcode ``` 响应: ```json { "ticket": "a1b2c3d4-...", "qrUrl": "https://open.weixin.qq.com/connect/qrconnect?appid=...&redirect_uri=...&response_type=code&scope=snsapi_login&state=a1b2c3d4-...#wechat_redirect", "expiresIn": 120 } ``` **2. 轮询扫码结果** ``` GET /rbac/auth/wechat/poll?ticket={ticket} ``` | status | 含义 | 前端行为 | | --- | --- | --- | | `pending` | 等待扫码或确认 | 继续轮询 | | `cancelled` | 用户取消授权 | 提示后展示刷新按钮 | | `unbound` | OpenID 未绑定任何客户 | 提示「请联系管理员绑定微信」 | | `disabled` | 客户或主客户已禁用/冻结 | 提示账号不可用 | | `expired` | ticket 超时 | 自动刷新二维码 | | `success` | 登录成功 | 取 `accessToken` / `refreshToken` 完成登录 | `success` 时响应体与 `POST /rbac/auth/login` 一致(含 `accessToken`、`refreshToken`、`expiresIn`)。 **3. 微信 OAuth 回调(浏览器重定向,非前端直接调用)** ``` GET /rbac/auth/wechat/callback?code={code}&state={ticket} ``` - 成功:更新 Redis ticket 状态,返回简单 HTML「登录成功,请关闭此页」或 302 回 `/login?wechat=ok`。 - 失败:ticket 标记 `unbound` / `error`,记录日志。 ##### 绑定与账号规则 | 规则 | 说明 | | --- | --- | | 绑定主体 | **仅客户**(`t_customer`);内部 `user` 表不存 `WechatOpenid` | | 绑定时机 | 管理员在「客户管理」创建/编辑客户时写入 `WechatOpenid`;或客户首次密码登录后在个人中心「绑定微信」(二期) | | 唯一性 | 一个 `WechatOpenid` 只能绑定一个客户;解绑需管理员操作 | | 子客户 | 子客户可有独立 `WechatOpenid`;登录时同样校验子客户与主客户 `Status` | | 资料同步 | 扫码成功后可选更新 `WechatAvatar`、`WechatNickname`(以微信侧为准) | | 无密码登录 | 微信登录**不校验** `t_customer.Password`;禁用客户不得仅凭历史 OpenID 登录 | ##### 安全要求 - `AppSecret` 仅存服务端;`code` 换 `openid` 必须在后端完成。 - `ticket` / `state` 一次性使用,Redis TTL ≤ 120s,防重放。 - 回调域与 `redirect_uri` 严格匹配微信开放平台配置。 - 轮询接口限流(建议每 IP 每 ticket ≤ 1 次/2s)。 - 日志记录 `openid` 哈希或末四位,避免明文落盘。 ##### 前端集成要点(`oms0414`) | 文件 | 职责 | | --- | --- | | `views/login/index.vue` | 仅 `login` 路由展示 `WechatLogin`;`adminLogin` 强制 `activeKey=pwd` | | `views/login/components/pwd-login/index.vue` | 仅 `OmsLoginType.Customer` 显示底部微信图标 | | `views/login/components/wechat-login.vue` | 对接 `qrcode` + `poll`;二维码用 `qrUrl` 或 `qrcode` 库渲染 | | `store/modules/rbac-user` | `success` 后复用 `setToken` + `fetchCurrentUser` + 跳转 `/user/profile` | 建议新增 `src/api/rbac/wechat-auth.ts` 封装上述两个 GET 接口。 ##### 实施阶段建议 | 阶段 | 内容 | | --- | --- | | P0(当前) | 管理端隐藏微信入口;客户端保留占位 UI;本文档定稿 | | P1 | 后端实现 `qrcode` / `callback` / `poll` + Redis;管理端录入 `WechatOpenid` | | P2 | 前端 `wechat-login.vue` 实装轮询与错误态;个人中心「绑定/解绑微信」 | ### 4.4 前端 Token 策略(`rbac-user` store) - Access Token 存 `localStorage`(`rbac_access_token`)。 - 到期前 5 分钟触发刷新(`expiresIn - 300s`)。 - 1 小时无用户交互则停止自动续期(`KEEP_ALIVE_IDLE_MS`)。 - 401 / 刷新失败 → 清除本地状态并跳转登录页。 ### 4.5 密码策略 | 主体 | 存储 | 修改接口 | | --- | --- | --- | | 内部用户 | RBAC 哈希 + 盐 | `POST /rbac/auth/change-password`(本人)/ `PUT /rbac/user/update-password`(管理员重置) | | 客户 | `t_customer.Password` MD5 | `POST /rbac/auth/change-password`(本人)/ `POST /customer/reset-password`(管理员重置) | 客户改密:`AccountManager.ValidatePasswordStrength` 校验新密码强度。 --- ## 5 权限模型 ### 5.1 权限定义方式 - 权限码在 **代码** 中通过 `RbacPermissionDefinitionProvider` 静态注册。 - 运行时通过 `GET /rbac/permission/get-all-permissions` 或 `get-permission-tree` 获取目录。 - 角色分配时调用 `POST /rbac/role/assign-permissions`,权限码写入 `role_permission`。 - **不存在**权限定义数据库表。 ### 5.2 权限码命名规范 **格式**:`{Module}.{Resource}.{Action}`(PascalCase,点分隔) #### RBAC 管理模块(包内定义) | 权限码 | 含义 | | --- | --- | | `Rbac.Users` | 用户模块 | | `Rbac.Users.Query` | 查询用户 | | `Rbac.Users.Create` | 创建用户 | | `Rbac.Users.Update` | 更新用户 | | `Rbac.Users.Delete` | 删除用户 | | `Rbac.Users.AssignRoles` | 分配角色 | | `Rbac.Users.ResetPassword` | 重置密码 | | `Rbac.Roles` / `.Query` / `.Create` / `.Update` / `.Delete` / `.AssignPermissions` | 角色 CRUD + 分配权限 | | `Rbac.Menus` / `.Query` / `.Create` / `.Update` / `.Delete` / `.Move` | 菜单 CRUD + 移动 | | `Rbac.Organizations` / `.Query` / `.Create` / `.Update` / `.Delete` / `.Move` | 组织 CRUD + 移动 | #### 业务模块(前端路由已使用,须在 Provider 中注册后方能分配) | 模块前缀 | 示例 | | --- | --- | | `Api.*` | `Api.Interfaces.Query`、`Api.Applications.Create` | | `Track.*` | `Track.Orders.Query`、`Track.Kpi.Query` | | `Home.*` | `Home.Monitor` | | `System.*` | `System.BatchEmail.Use` | 完整列表以 `GET /rbac/permission/get-permission-tree` 返回为准。 ### 5.3 鉴权现状 | 机制 | 状态 | | --- | --- | | `[Authorize]`(需登录) | ✅ 已启用 | | `[Authorize(Permission = "...")]` 细粒度 | ❌ 包内待实现 | | `POST /rbac/permission/check-permission` | ✅ API 可用,供显式校验 | | `IUserService.HasPermissionAsync` | ✅ 服务方法可用 | | 前端 `hasPermission()` | ⚠️ 当前临时返回 `true`(`src/utils/rbac-permission.ts`) | ### 5.4 菜单类型 | `MenuType` | 值 | 说明 | | --- | --- | --- | | Menu | 1 | 页面菜单 | | Button | 2 | 按钮权限 | --- ## 6 枚举与常量 ### 6.1 OMS 集成(代码内) | 名称 | 值 | 文件 | | --- | --- | --- | | `OmsLoginType.User` | 0 | `AuthRequestDtos.cs` | | `OmsLoginType.Customer` | 1 | `AuthRequestDtos.cs` | | `SimpleOmsClaimTypes.MapId` | `"mapId"` | `SimpleOmsClaimTypes.cs` | | `SimpleOmsClaimTypes.LoginType` | `"loginType"` | `SimpleOmsClaimTypes.cs` | | `CustomerRoleCode` | `"Customer"` | `CustomerRoleSeedContributor.cs` | | `CustomerRoleName` | `"客户"` | `CustomerRoleSeedContributor.cs` | | 子客户角色 Code | `CustomerSub_{userId}` | `CustomerService.cs` | | `CustomerStatus.Disabled` | 0 | | | `CustomerStatus.Normal` | 1 | | | `CustomerStatus.Frozen` | 2 | | | `CustomerGroup.Normal` | 0 | | | `CustomerGroup.Vip1~Vip5` | 1~5 | | | `OrganizationType` | 1 / 2 / 3 | OpenAPI 生成 | | `MenuType.Menu` | 1 | | | `MenuType.Button` | 2 | | --- ## 7 接口路径速查 ### 7.1 认证 `/rbac/auth/*` | 方法 | 路径 | 鉴权 | 说明 | | --- | --- | --- | --- | | POST | `/rbac/auth/login` | 匿名 | 用户/客户登录 | | POST | `/rbac/auth/refresh-token` | 匿名 | 刷新令牌 | | GET | `/rbac/auth/get-current-user` | Bearer | 当前用户信息(含权限、菜单) | | POST | `/rbac/auth/change-password` | Bearer | 当前用户改密 | | PUT | `/rbac/auth/update-profile` | Bearer(仅内部用户) | 更新姓名、邮箱 | | POST | `/rbac/auth/upload-avatar` | Bearer(仅内部用户) | 上传头像 | ### 7.2 用户 `/rbac/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-password` | 重置密码 | | PUT | `/rbac/user/update-avatar` | 更新头像 | ### 7.3 角色 `/rbac/role/*` | 方法 | 路径 | 说明 | | --- | --- | --- | | GET | `/rbac/role/get-list` | 分页列表 | | GET | `/rbac/role/get-all-roles` | 全部角色(简表) | | GET | `/rbac/role/get` | 详情 | | POST | `/rbac/role/create` | 创建(可带初始 permissions) | | PUT | `/rbac/role/update` | 更新名称 | | DELETE | `/rbac/role/delete` | 删除 | | POST | `/rbac/role/assign-permissions` | 分配权限码 | | GET | `/rbac/role/get-role-permissions` | 角色权限码列表 | | GET | `/rbac/role/get-role-users` | 角色下用户 | | GET | `/rbac/role/get-role-user-count` | 角色用户数量 | ### 7.4 菜单 `/rbac/menu/*` | 方法 | 路径 | 说明 | | --- | --- | --- | | GET | `/rbac/menu/get-menu-tree` | 完整菜单树 | | GET | `/rbac/menu/get-user-menus` | 指定用户可见菜单 | | GET | `/rbac/menu/get` | 菜单详情 | | POST | `/rbac/menu/create` | 创建 | | PUT | `/rbac/menu/update` | 更新 | | PUT | `/rbac/menu/update-order` | 更新排序 | | POST | `/rbac/menu/move-menu` | 移动节点 | | DELETE | `/rbac/menu/delete` | 删除 | ### 7.5 权限 `/rbac/permission/*` | 方法 | 路径 | 说明 | | --- | --- | --- | | GET | `/rbac/permission/get-all-permissions` | 全部权限定义(树形) | | GET | `/rbac/permission/get-permission-tree` | 按分组返回权限树 | | POST | `/rbac/permission/check-permission` | 单权限校验 | | POST | `/rbac/permission/check-permissions` | 批量权限校验 | ### 7.6 组织 `/rbac/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` | 移动节点 | ### 7.7 客户(OMS)`/customer/*` | 方法 | 路径 | 说明 | | --- | --- | --- | | GET | `/customer/list` | 客户分页列表 | | GET | `/customer/get` | 客户详情 | | POST | `/customer/create` | 创建主客户(联动 RBAC) | | POST | `/customer/sub/create` | 主客户创建子客户(联动 RBAC + 专属角色) | | POST | `/customer/update` | 更新客户资料 | | POST | `/customer/delete` | 删除客户 | | POST | `/customer/reset-password` | 重置客户密码 | --- ## 8 接口详细设计 > 请求/响应字段以 OpenAPI 生成客户端 `E:\oms0414\src\api\rbac\generated` 为准。 > 分页接口统一返回 `{ total, items }`;单条删除/变更成功通常无 body 或返回更新后 DTO。 ### 8.1 认证接口 #### 8.1.1 `POST /rbac/auth/login` **请求体 `OmsLoginInput`** ```json { "account": "admin", "password": "123456", "type": 0 } ``` | 字段 | 类型 | 必填 | 说明 | | --- | --- | --- | --- | | account | string | 是 | 内部用户=账号;客户=手机/邮箱/客户编号 | | password | string | 是 | 明文密码 | | type | int | 否,默认 0 | 0=内部用户,1=客户 | **响应 `LoginResultDto`** ```json { "accessToken": "eyJ...", "refreshToken": "eyJ...", "tokenType": "Bearer", "expiresIn": 3600, "userId": "3fa85f64-5717-4562-b3fc-2c963f66afa6" } ``` **业务逻辑** 1. 按 `type` 分流到 `LoginAdminAsync` 或 `LoginCustomerAsync`。 2. 客户登录失败统一提示「客户账号或密码错误」(不暴露具体原因)。 3. 子客户登录额外校验主客户未禁用/冻结。 **异常** | 场景 | 错误信息 | | --- | --- | | 不支持的 type | 不支持的登录类型 | | 客户凭证错误 | 客户账号或密码错误 | | 客户已禁用/冻结 | 客户已禁用 / 客户已冻结 | | 子客户已禁用/冻结 | 子客户已禁用 / 子客户已冻结 | | 未关联 RBAC 用户 | 客户未关联用户 | #### 8.1.2 `POST /rbac/auth/refresh-token` **请求体** ```json { "refreshToken": "eyJ..." } ``` **响应**:同 `LoginResultDto`。 #### 8.1.3 `GET /rbac/auth/get-current-user` **响应 `UserInfoDto`** ```json { "id": "guid", "name": "管理员", "account": "admin", "email": "admin@example.com", "avatar": "/uploads/avatars/xxx.jpg", "organizationId": "guid", "organizationName": "总公司", "roles": [{ "id": "guid", "name": "超级管理员", "code": "Admin" }], "permissions": ["Rbac.Users.Query", "Rbac.Roles.Query"], "menus": [{ "id": "guid", "pId": "00000000-0000-0000-0000-000000000000", "name": "系统管理", "permissionCode": "Rbac.Users", "type": 1, "route": "/system", "children": [] }], "createdAt": "2026-01-01T00:00:00Z", "createdBy": "system", "updatedAt": null, "updatedBy": null } ``` #### 8.1.4 `POST /rbac/auth/change-password` **请求体 `OmsChangePasswordInput`** ```json { "oldPassword": "123456", "newPassword": "654321" } ``` - 内部用户:走 `AccountManager.UpdatePasswordAsync`。 - 客户:校验 `t_customer` MD5 旧密码后更新。 #### 8.1.5 `PUT /rbac/auth/update-profile` **请求体 `OmsUpdateProfileInput`**:`{ name?, email? }` - 仅内部用户可用;客户调用返回「当前登录主体暂不支持此操作」。 #### 8.1.6 `POST /rbac/auth/upload-avatar` - Content-Type:`multipart/form-data`,字段 `file`。 - 支持 jpg/jpeg/png/gif/webp,最大 10MB,自动缩放至 200px。 - 返回头像 URL 字符串,如 `/uploads/avatars/{userId}_{timestamp}.jpg`。 --- ### 8.2 用户接口 #### 8.2.1 `GET /rbac/user/get-list` **Query 参数** | 参数 | 类型 | 说明 | | --- | --- | --- | | Keyword | string | 搜索名称或账号 | | RoleId | guid | 按角色筛选 | | Page | int | 页码,从 1 开始 | | PageSize | int | 每页条数 | | OrderBy | string | 排序字段 | | Descending | bool | 是否降序 | **响应 `UserDtoPagedResultDto`** ```json { "total": 100, "items": [ { "id": "guid", "name": "张三", "account": "zhangsan", "email": "zs@example.com", "avatar": null, "roles": [{ "id": "guid", "name": "运营", "code": "Operator" }], "createdAt": "...", "createdBy": "...", "updatedAt": null, "updatedBy": null } ] } ``` #### 8.2.2 `POST /rbac/user/create` **请求体 `UserCreateDto`** ```json { "name": "张三", "account": "zhangsan", "password": "123456", "email": "zs@example.com", "avatar": null, "roleIds": ["role-guid-1"] } ``` #### 8.2.3 `PUT /rbac/user/update` - Query:`id`(用户 GUID) - Body `UserUpdateDto`:`{ name?, email?, avatar? }` #### 8.2.4 `POST /rbac/user/assign-roles` - Query:`userId`(GUID) - Body:`string[]`(角色 GUID 列表) #### 8.2.5 `PUT /rbac/user/update-password` - Query:`id` - Body `UpdatePasswordDto`:`{ oldPassword, newPassword }` #### 8.2.6 `DELETE /rbac/user/delete` / `delete-batch` - 单删 Query:`id` - 批删 Query:`ids`(数组) --- ### 8.3 角色接口 #### 8.3.1 `POST /rbac/role/create` **请求体 `RoleCreateDto`** ```json { "name": "运营专员", "code": "Operator", "permissions": ["Api.Interfaces.Query", "Api.Logs.Query"] } ``` #### 8.3.2 `PUT /rbac/role/update` - Query:`id` - Body `RoleUpdateDto`:`{ name? }`(仅可改名称) #### 8.3.3 `POST /rbac/role/assign-permissions` - Query:`roleId` - Body:`string[]`(权限码列表,全量覆盖式分配) #### 8.3.4 `GET /rbac/role/get-role-permissions` - Query:`roleId` - 响应:`string[]` --- ### 8.4 菜单接口 #### 8.4.1 `POST /rbac/menu/create` **请求体 `MenuCreateDto`** ```json { "pId": "00000000-0000-0000-0000-000000000000", "name": "用户管理", "permissionCode": "Rbac.Users.Query", "type": 1, "icon": "icon-user", "route": "/system/users", "component": "views/system/users/index", "order": 10, "isHidden": false, "isCached": true, "description": null } ``` #### 8.4.2 `PUT /rbac/menu/update` - Query:`id` - Body `MenuUpdateDto`:`{ name?, permissionCode?, icon?, route?, component?, isHidden?, isCached?, description?, order? }` #### 8.4.3 `POST /rbac/menu/move-menu` - Query:`menuId`、`targetParentId` --- ### 8.5 权限接口 #### 8.5.1 `GET /rbac/permission/get-permission-tree` **响应 `PermissionGroupDto[]`** ```json [ { "code": "Rbac", "name": "权限管理", "permissions": [ { "code": "Rbac.Users", "name": "用户管理", "parentCode": null, "description": null, "children": [ { "code": "Rbac.Users.Query", "name": "查询", "parentCode": "Rbac.Users", "children": [] } ] } ] } ] ``` #### 8.5.2 `POST /rbac/permission/check-permissions` - Query:`userId` - Body:`string[]`(待检查的权限码) - 响应:`Record`,如 `{ "Rbac.Users.Query": true, "Rbac.Users.Delete": false }` --- ### 8.6 组织接口 #### 8.6.1 `POST /rbac/organization/create` **请求体 `OrganizationCreateDto`** ```json { "parentId": null, "name": "技术部", "code": "TECH", "type": 1, "leaderId": null, "order": 1, "description": null, "isEnabled": true } ``` #### 8.6.2 `POST /rbac/organization/move` - Query:`id` - Body `OrganizationMoveDto`:目标父节点信息 --- ### 8.7 客户接口(OMS + RBAC 联动) #### 8.7.1 `POST /customer/create` — 创建主客户 **请求体 `CreateCustomerInput`(主要字段)** | 字段 | 类型 | 必填 | 说明 | | --- | --- | --- | --- | | customerCode | string(32) | 是 | 客户编号,全局唯一 | | customerName | string(128) | 是 | 客户名称 | | phone | string(16) | 是 | 手机号,全局唯一 | | email | string(64) | 否 | 邮箱,全局唯一 | | password | string | 是,≥6 位 | 登录密码(OMS 侧 MD5 存储) | | status | int | 否,默认 1 | 0/1/2 | | balance / totalQuota / usedQuota | decimal | 否 | 额度 | | salesman / customerService | string | 否 | 销售/客服 | | customerGroup | int | 否 | 客户分组 | **核心流程** 1. 校验客户编号、手机、邮箱唯一性。 2. `UserService.CreateAsync`:创建 RBAC 用户(`Account=phone`,`Name=customerName`)。 3. `UserService.AssignRolesAsync`:分配 `Customer` 角色。 4. 读取 `user.userid` 写入 `t_customer.UserId`。 5. 插入 `t_customer`(`ParentCustomerId=null`)。 6. 任一步失败则回滚已创建的 RBAC 用户。 #### 8.7.2 `POST /customer/sub/create` — 创建子客户 **请求体**:`CreateSubCustomerInput` = `CreateCustomerInput` + `permissions: string[]` **核心流程** 1. 当前登录主体必须是**主客户**(`ParentCustomerId IS NULL`)。 2. 校验主客户状态正常。 3. 校验 `permissions` 中每个码 ⊆ 主客户当前权限;否则「子客户权限不能超出当前客户权限」。 4. 创建 RBAC 用户。 5. 创建角色 `CustomerSub_{userId}`,名称 `子客户-{customerName}`,写入 `permissions`。 6. 分配该专属角色给用户。 7. 插入 `t_customer`(`ParentCustomerId=主客户Id`)。 8. 失败时回滚角色和用户。 #### 8.7.3 `GET /customer/list` **Query `CustomerPaginatedInput`** | 参数 | 说明 | | --- | --- | | page | 页码,默认 1 | | pageSize | 每页条数,默认 20 | | keyword | 匹配编号/名称/手机/邮箱 | | status | 状态筛选 | | customerGroup | 分组筛选 | | salesman / customerService | 销售/客服筛选 | **响应**:`{ items: CustomerPaginatedOutput[], total: number }` #### 8.7.4 `POST /customer/reset-password` - Body:`{ id, newPassword }` - 直接更新 `t_customer.Password`(MD5),不影响 RBAC 密码。 --- ## 9 前端集成 ### 9.1 API 客户端 - 路径:`src/api/rbac/generated/`(由 OpenAPI 自动生成)。 - 统一前缀:`/rbac/*`;Bearer Token 由 `OpenAPI.TOKEN` 注入。 ### 9.2 状态管理(`src/store/modules/rbac-user.ts`) | 状态 | 说明 | | --- | --- | | `accessToken` / `refreshToken` | JWT | | `userInfo` | `UserInfoDto`,含 roles、permissions、menus | | `loginPortalType` | 0=管理端,1=客户端 | | `permissions` | 计算属性,来自 `userInfo.permissions` | | `menus` | 计算属性,来自 `userInfo.menus` | **登录调用**:`AuthService.postRbacAuthLogin({ account, password, type })` **拉用户信息**:`AuthService.getRbacAuthGetCurrentUser()` ### 9.3 路由权限(`meta.permission`) 前端路由在 `src/router/routes/newModules/` 配置 `meta.permission`,示例: | 路由模块 | permission 示例 | | --- | --- | | 系统管理 | `Rbac.Users.Query`、`Rbac.Roles.Query` | | 接口管理 | `Api.Interfaces.Query` | | 轨迹中心 | `Track.Orders.Query` | | 首页 | `Home.Monitor` | 路由守卫 `src/router/guard/permission.ts` 读取 `meta.permission` 并调用 `hasPermission()`。 ### 9.4 权限校验函数现状 `src/utils/rbac-permission.ts` 中 `hasPermission` / `hasAnyPermission` / `hasAllPermissions` **当前固定返回 `true`**(临时放开菜单权限)。 恢复时应改回: ```typescript return useRbacUserStore().permissions.includes(permission) ``` ### 9.5 动态菜单 - 登录后 `get-current-user` 返回的 `menus` 即为服务端按权限裁剪后的菜单树。 - 亦可调用 `MenuService.getRbacMenuGetUserMenus(userId)` 单独获取。 --- ## 10 种子数据与默认账号 ### 10.1 RBAC 种子(Fake.Rbac.Infrastructure) - 自动创建默认管理员:账号 `admin`,密码 `123456`,角色「超级管理员」。 - 初始化系统管理菜单(组织、用户、角色、菜单等)。 ### 10.2 OMS 客户角色种子(`CustomerRoleSeedContributor`) - 启动时若不存在则创建角色:`Code=Customer`,`Name=客户`,`Permissions=[]`。 - 客户主账号创建时分配此角色;具体业务权限需后续通过 `assign-permissions` 配置。 ### 10.3 生产环境注意 - 部署后立即修改默认管理员密码。 - JWT `SecretKey` 通过 Consul(`idwms/admin`)或本地配置注入,禁止硬编码弱密钥。 --- ## 附录 A 关键源码文件索引 | 用途 | 路径 | | --- | --- | | 宿主模块 / RBAC 注册 | `SimpleAdmin.Api/SimpleAdminApiModule.cs` | | 自定义认证(双端登录) | `SimpleAdmin.Api/Auth/AuthService.cs` | | 认证 DTO / 登录类型 | `SimpleAdmin.Api/Auth/AuthRequestDtos.cs` | | JWT Claims 扩展 | `SimpleAdmin.Api/SimpleJwtService.cs` | | OMS Claim 类型 | `SimpleAdmin.Api/SimpleOmsClaimTypes.cs` | | 客户角色种子 | `SimpleAdmin.Api/Auth/CustomerRoleSeedContributor.cs` | | 客户 ↔ RBAC 联动 | `Oms.Application/Services/Customers/CustomerService.cs` | | 子客户权限入参 | `Oms.Application/Dtos/Customers/CreateSubCustomerInput.cs` | | OMS 客户表 SQL | `db.sql` → `t_customer` | | 前端 OpenAPI 客户端 | `oms0414/src/api/rbac/generated/` | | 前端用户 Store | `oms0414/src/store/modules/rbac-user.ts` | | 前端权限工具 | `oms0414/src/utils/rbac-permission.ts` | ## 附录 B Swagger 分组 | 分组名 | 内容 | | --- | --- | | RBAC | 认证、用户、角色、菜单、权限、组织 | | OMS-Customer | 客户资料 CRUD + 子客户创建 | 本地调试可访问 Fake Swagger UI 查看完整 OpenAPI 契约;前端 `src/api/rbac/generated` 应与 Swagger 保持同步重新生成。