# 统一权限管理系统(Permission System Server)
集中式多产品权限管理平台后端服务。为多个产品提供统一的用户认证、权限管理、角色管理能力,产品通过 HTTP API 或 gRPC 接入。
## 核心特性
- **多产品隔离** — 权限、角色、成员按产品(`productCode`)隔离,一个账号可跨产品通用
- **灵活的权限模型** — 角色权限 + 用户级 ALLOW/DENY 覆盖,细粒度控制
- **研发部门自动授权** — 研发部门(`deptType=DEV`)的成员加入产品后自动拥有全部权限
- **双协议** — 同时提供 HTTP REST API(管理 UI)和 gRPC(产品后端高性能调用)
- **自动权限同步** — 产品启动时通过 API 自动上报权限列表,系统自动新增/更新/禁用
- **JWT 本地验证** — 登录获取 JWT,产品后端可本地验证,无需每次请求回调权限系统
- **登录端隔离** — 产品端(`/auth/login`)和管理后台(`/auth/adminLogin`)独立登录接口,超管仅能通过管理后台登录
- **令牌轮转** — refreshToken 刷新时自动递增 tokenVersion,旧令牌即时失效,防止令牌被盗后的无限续期
- **主动注销** — `/auth/logout` 接口递增 tokenVersion 使所有已签发令牌立即失效
- **产品级熔断** — 禁用产品后所有在线成员的令牌即时失效,HTTP/gRPC 双通道同步拦截
- **最后管理员保护** — 移除或降级产品最后一个 ADMIN 时自动拒绝,防止产品进入无管理状态
## 系统架构
```mermaid
graph TB
subgraph 权限系统
HTTP[HTTP API :10001]
GRPC[gRPC Server :10002]
DB[(MySQL)]
CACHE[(Redis Cache)]
HTTP --> DB
HTTP --> CACHE
GRPC --> DB
GRPC --> CACHE
end
UI[管理后台 UI] -->|REST API| HTTP
ProductA[产品 A 后端] -->|gRPC| GRPC
ProductB[产品 B 后端] -->|gRPC| GRPC
ProductC[产品 C 后端] -->|HTTP| HTTP
```
---
## 核心概念与实体关系
### 实体关系总览
```mermaid
erDiagram
sys_product ||--o{ sys_perm : "productCode → 产品拥有权限"
sys_product ||--o{ sys_role : "productCode → 产品拥有角色"
sys_product ||--o{ sys_product_member : "productCode → 产品拥有成员"
sys_user ||--o{ sys_product_member : "userId → 用户加入产品"
sys_user ||--o{ sys_user_role : "userId → 用户绑定角色"
sys_user ||--o{ sys_user_perm : "userId → 用户自定义权限"
sys_user }o--o| sys_dept : "deptId → 用户所属部门"
sys_dept }o--o| sys_dept : "parentId → 父子部门"
sys_role ||--o{ sys_role_perm : "roleId → 角色绑定权限"
sys_role ||--o{ sys_user_role : "roleId → 角色分配给用户"
sys_perm ||--o{ sys_role_perm : "permId → 权限绑定到角色"
sys_perm ||--o{ sys_user_perm : "permId → 权限直接授予用户"
sys_product {
bigint id PK
varchar code UK "产品编码"
varchar name "产品名称"
varchar appKey UK "接入密钥"
varchar appSecret "签名密钥"
tinyint status "1启用 2禁用"
}
sys_user {
bigint id PK
varchar username UK "登录名"
varchar password "bcrypt密码"
varchar nickname "昵称"
bigint deptId FK "部门ID"
tinyint isSuperAdmin "1是 2否"
tinyint mustChangePassword "1是 2否"
tinyint status "1正常 2冻结"
}
sys_dept {
bigint id PK
bigint parentId "父部门ID"
varchar name "部门名称"
varchar path "层级路径"
varchar deptType "NORMAL普通 DEV研发"
int sort "排序值"
tinyint status "1启用 2禁用"
}
sys_perm {
bigint id PK
varchar productCode "所属产品"
varchar name "权限名"
varchar code UK "权限code"
tinyint status "1启用 2禁用"
}
sys_role {
bigint id PK
varchar productCode "所属产品"
varchar name UK "角色名"
int permsLevel "权限等级"
tinyint status "1启用 2禁用"
}
sys_product_member {
bigint id PK
varchar productCode "产品编码"
bigint userId FK "用户ID"
varchar memberType "成员类型"
tinyint status "1启用 2禁用"
}
sys_role_perm {
bigint id PK
bigint roleId FK
bigint permId FK
}
sys_user_role {
bigint id PK
bigint userId FK
bigint roleId FK
}
sys_user_perm {
bigint id PK
bigint userId FK
bigint permId FK
varchar effect "ALLOW或DENY"
}
```
### 五大核心实体
| 实体 | 说明 | 隔离范围 |
| ------ | ------ | ---------- |
| **产品 (Product)** | 接入权限系统的业务系统(如 CRM、OA),每个产品有独立的 `appKey`/`appSecret` | 全局 |
| **部门 (Dept)** | 组织架构树形结构,支持无限层级嵌套。`deptType=DEV` 的研发部门有特殊权限逻辑 | 全局 |
| **用户 (User)** | 全局账号,通过 `deptId` 归属部门,通过成员关系加入不同产品 | 全局 |
| **角色 (Role)** | 产品级别的权限集合,同一角色名在不同产品中互不干扰 | 产品级 |
| **权限 (Perm)** | 产品级别的最小权限单元,由产品后端通过 `SyncPerms` 自动上报 | 产品级 |
### 实体间关系详解
```text
全局组织架构 产品权限体系(按产品隔离)
┌─────────────────┐ ┌────────────────────────────────────┐
│ 部门 (Dept) │ │ 产品 (Product) │
│ ├── 研发部(DEV) │ │ ┌─────────┐ ┌──────────┐ │
│ ├── 市场部 │ │ │ 权限列表 │ │ 角色列表 │ │
│ └── 财务部 │ │ │ Perm │ │ Role │ │
└───────┬─────────┘ │ └────┬────┘ └────┬─────┘ │
│ deptId │ │ │ │
┌───────┴──────┐ │ ┌────┴────────────┴────┐ │
│ 用户 (User) │─────────────│ │ 角色-权限绑定 RolePerm │ │
│ │ 成员关系 │ └─────────────────────┘ │
│ │──────────────│ │
│ │ │ ┌───────────────────────┐ │
│ │──────────────│ │ 用户-角色 UserRole │ │
│ │ │ └───────────────────────┘ │
│ │──────────────│ ┌───────────────────────┐ │
│ │ │ │ 用户-权限覆盖 UserPerm │ │
└──────────────┘ │ │ ALLOW / DENY │ │
│ └───────────────────────┘ │
└────────────────────────────────────┘
```
#### 关键关系
1. **用户 → 产品**:通过 `sys_product_member` 建立,一个用户可以是多个产品的成员
2. **用户 → 部门**:通过 `sys_user.deptId` 关联,一个用户只属于一个部门
3. **角色 → 权限**:通过 `sys_role_perm` 绑定,一个角色可绑定多个权限
4. **用户 → 角色**:通过 `sys_user_role` 分配,一个用户可拥有多个角色
5. **用户 → 权限覆盖**:通过 `sys_user_perm` 直接授予/拒绝,优先级高于角色权限
### 成员类型与权限层级
| 优先级 | 类型 | 权限范围 | 来源 |
| -------- | ------ | ---------- | ------ |
| 1 | `SUPER_ADMIN` | **所有产品**的全部权限 | `sys_user.isSuperAdmin = 1` |
| 2 | `DEVELOPER` | **该产品**全部权限 | `sys_product_member.memberType` |
| 3 | `ADMIN` | **该产品**全部权限 | `sys_product_member.memberType` |
| 4 | 研发部门成员 | **该产品**全部权限 | `sys_dept.deptType = DEV` + 是产品成员 |
| 5 | `MEMBER` | 角色权限 ∪ ALLOW - DENY | 角色绑定 + 用户级覆盖计算 |
### 权限计算流程
```mermaid
flowchart TD
START[获取用户权限] --> CHK_PROD{产品已启用?}
CHK_PROD -->|否| EMPTY_PROD[返回空权限]
CHK_PROD -->|是| IS_SUPER{是超级管理员?}
IS_SUPER -->|是| ALL_PERMS[返回该产品全部权限码]
IS_SUPER -->|否| FIND_MEMBER[查询 sys_product_member]
FIND_MEMBER --> HAS_MEMBER{是活跃成员?}
HAS_MEMBER -->|否| EMPTY[返回空权限]
HAS_MEMBER -->|是| CHECK_TYPE{成员类型?}
CHECK_TYPE -->|DEVELOPER / ADMIN| ALL_PERMS
CHECK_TYPE -->|MEMBER| CHECK_DEPT{所属部门是研发部门?}
CHECK_DEPT -->|是| ALL_PERMS
CHECK_DEPT -->|否| CALC[计算权限]
CALC --> ROLE_PERMS[查询用户角色绑定的权限 ID]
ROLE_PERMS --> USER_ALLOW[查询用户级 ALLOW 权限 ID]
USER_ALLOW --> USER_DENY[查询用户级 DENY 权限 ID]
USER_DENY --> MERGE["合并: (角色权限 ∪ ALLOW) - DENY"]
MERGE --> FILTER[过滤已禁用的权限]
FILTER --> CODES[转换为权限 code 列表返回]
```
---
## 实际业务配置指南
以下以一个典型的中小公司为例,说明如何配置和使用权限系统。
### 场景假设
公司有**研发部**(30 人)、**市场部**(10 人)、**运营部**(15 人),负责维护 3 个产品:**CRM 系统**、**OA 系统**、**电商后台**。研发人员需要在产品间流动。
### 第一步:搭建组织架构
```mermaid
graph TD
ROOT[公司] --> DEV[研发部 deptType=DEV]
ROOT --> MKT[市场部 deptType=NORMAL]
ROOT --> OPS[运营部 deptType=NORMAL]
DEV --> FE[前端组]
DEV --> BE[后端组]
DEV --> QA[测试组]
```
```bash
# 创建顶级部门
POST /api/dept/create {"parentId": 0, "name": "研发部", "deptType": "DEV", "sort": 1}
# 返回 id=1
POST /api/dept/create {"parentId": 0, "name": "市场部", "sort": 2}
# deptType 不传默认为 NORMAL,返回 id=2
POST /api/dept/create {"parentId": 0, "name": "运营部", "sort": 3}
# 返回 id=3
# 创建研发部子部门
POST /api/dept/create {"parentId": 1, "name": "前端组", "deptType": "DEV", "sort": 1}
POST /api/dept/create {"parentId": 1, "name": "后端组", "deptType": "DEV", "sort": 2}
POST /api/dept/create {"parentId": 1, "name": "测试组", "deptType": "DEV", "sort": 3}
```
> **研发部门的特殊能力**:`deptType=DEV` 的部门中的成员,只要被添加到某个产品下,就自动拥有该产品的全部权限。当从产品成员中移除时,权限立即收回。
### 第二步:注册产品
```bash
POST /api/product/create {"code": "crm", "name": "CRM 系统"}
# 返回 appKey, appSecret, adminUser, adminPassword
POST /api/product/create {"code": "oa", "name": "OA 系统"}
POST /api/product/create {"code": "mall", "name": "电商后台"}
```
每个产品创建后会自动生成一个初始管理员账号(如 `admin_crm`),该账号是该产品的 `ADMIN` 级别成员。
### 第三步:产品上报权限
各产品后端在启动时自动调用 `POST /api/perm/sync` 上报权限列表。以 CRM 为例:
```json
{
"appKey": "...", "appSecret": "...",
"perms": [
{"code": "customer:list", "name": "查看客户列表"},
{"code": "customer:create", "name": "创建客户"},
{"code": "customer:update", "name": "编辑客户"},
{"code": "customer:delete", "name": "删除客户"},
{"code": "order:list", "name": "查看订单"},
{"code": "order:create", "name": "创建订单"},
{"code": "report:export", "name": "导出报表"}
]
}
```
权限由产品代码自动管理,管理员无需手动创建。
### 第四步:创建角色并绑定权限
```bash
# CRM 产品下创建角色
POST /api/role/create {"productCode": "crm", "name": "销售经理", "permsLevel": 10}
POST /api/role/create {"productCode": "crm", "name": "普通销售", "permsLevel": 20}
POST /api/role/create {"productCode": "crm", "name": "客服", "permsLevel": 30}
# 为角色绑定权限
POST /api/role/bindPerms {"roleId": 1, "permIds": [1,2,3,4,5,6,7]} # 销售经理:全部
POST /api/role/bindPerms {"roleId": 2, "permIds": [1,2,5,6]} # 普通销售:查看+创建
POST /api/role/bindPerms {"roleId": 3, "permIds": [1,5]} # 客服:仅查看
```
### 第五步:创建用户并配置权限
**场景 A:研发人员张三** — 属于研发部,需要参与 CRM 和 OA 的开发
```bash
# 创建用户,归属研发部
POST /api/user/create {"username": "zhangsan", "nickname": "张三", "deptId": 1}
# 返回 userId=10, credentialsTicket(用于领取初始密码)
# 将张三添加为 CRM 和 OA 的成员(MEMBER 类型即可)
POST /api/member/add {"productCode": "crm", "userId": 10, "memberType": "MEMBER"}
POST /api/member/add {"productCode": "oa", "userId": 10, "memberType": "MEMBER"}
# 因为张三属于研发部(DEV),他登录 CRM 或 OA 时自动拥有全部权限
# 无需为他分配角色!
```
**场景 B:一段时间后张三不再负责 OA** — 只需移除成员关系
```bash
POST /api/member/remove {"id": <张三在OA中的成员记录ID>}
# OA 的权限立即收回,CRM 的权限不受影响
```
**场景 C:市场部李四** — 需要使用 CRM,分配销售角色
```bash
POST /api/user/create {"username": "lisi", "nickname": "李四", "deptId": 2}
# 返回 userId=11, credentialsTicket
POST /api/member/add {"productCode": "crm", "userId": 11, "memberType": "MEMBER"}
POST /api/user/bindRoles {"userId": 11, "roleIds": [2], "productCode": "crm"} # 分配"普通销售"角色
```
李四登录 CRM 后拥有的权限 = 普通销售角色的权限 = `[customer:list, customer:create, order:list, order:create]`。
**场景 D:给李四额外权限** — 在角色基础上微调
```bash
# 额外给李四"导出报表"权限,但禁止他"创建订单"
POST /api/user/setPerms {
"userId": 11,
"perms": [
{"permId": 7, "effect": "ALLOW"},
{"permId": 6, "effect": "DENY"}
]
}
```
最终李四的权限 = 角色 `[1,2,5,6]` ∪ ALLOW `[7]` - DENY `[6]` = `[customer:list, customer:create, order:list, report:export]`
**场景 E:外部临时协作人员** — 不属于任何部门
```bash
POST /api/user/create {"username": "temp_wang", "nickname": "外包王五"}
# deptId 不传,不归属任何部门
POST /api/member/add {"productCode": "crm", "userId": 12, "memberType": "MEMBER"}
POST /api/user/bindRoles {"userId": 12, "roleIds": [3], "productCode": "crm"} # 分配"客服"角色(只读)
```
### 权限配置决策树
```mermaid
flowchart TD
Q1{该用户是研发人员?}
Q1 -->|是| A1[归属研发部门 deptType=DEV]
A1 --> A2["添加为产品成员(MEMBER)即可
自动拥有全部权限"]
Q1 -->|否| Q2{该用户是产品管理员?}
Q2 -->|是| A3["添加为产品成员(ADMIN)
自动拥有全部权限"]
Q2 -->|否| Q3{需要精细权限控制?}
Q3 -->|是| A4["添加为产品成员(MEMBER)"]
A4 --> A5[分配角色 + 可选 ALLOW/DENY 覆盖]
Q3 -->|否| A6[不添加为成员 = 无权限]
```
---
## 操作权限控制(Access Control)
系统内置一套集中式操作权限管控机制,对所有管理类接口实施多维度的访问控制,防止越权操作。
### 安全架构:四层纵深防护
```mermaid
flowchart LR
REQ[HTTP 请求] --> L1["① JWT 中间件
解析 token / 验证签名"]
L1 --> L2["② UserDetailsLoader
加载用户实时状态"]
L2 --> L3["③ 多维状态检查
用户/产品/成员状态"]
L3 --> L4["④ Logic 层
操作权限控制"]
L4 --> BIZ[业务逻辑]
```
| 层级 | 组件 | 职责 |
| ------ | ------ | ------ |
| 第一层 | JWT 中间件 | 解析 access token、验证签名、校验 `tokenType`、校验 `tokenVersion` |
| 第二层 | UserDetailsLoader | 从 Redis 缓存或 DB 加载用户完整信息(含部门、角色、权限、产品状态) |
| 第三层 | 多维状态检查 | 用户冻结(`status ≠ 1`)→ 403;产品禁用(`productStatus ≠ 1`)→ 403;成员无效(`memberType` 为空)→ 403 |
| 第四层 | access.go | 按接口类型检查超管/产品管理员/部门层级/权限级别 |
### 接口操作权限矩阵
| 接口 | 权限要求 | 额外检查 |
| ------ | ---------- | ---------- |
| **产品管理** | | |
| 创建产品 | 仅超级管理员 | — |
| 更新产品 | 仅超级管理员 | — |
| **部门管理** | | |
| 创建部门 | 仅超级管理员 | — |
| 更新部门 | 仅超级管理员 | — |
| 删除部门 | 仅超级管理员 | 有子部门时拒绝 |
| **角色管理** | | |
| 创建角色 | 超管 或 产品管理员 | 产品必须启用 |
| 更新角色 | 超管 或 产品管理员 | 非超管不可降低 permsLevel |
| 删除角色 | 超管 或 产品管理员 | 级联删除关联数据 |
| 绑定角色权限 | 超管 或 产品管理员 | — |
| **用户管理** | | |
| 创建用户 | 超管 或 产品管理员 | 服务端生成密码,ticket 下发 |
| 重置密码 | 通过 `CheckManageAccess` | 不可重置超管密码 |
| 获取用户凭证 | 超管 或 产品管理员 | ticket 一次性消费 |
| 更新用户信息 | 仅本人 或 超管 | — |
| 冻结/解冻用户 | 通过 `CheckManageAccess` | 不可冻结自己和超管 |
| 绑定角色 | 通过 `CheckManageAccess` | 目标用户的成员状态必须启用 |
| 设置权限覆盖 | 通过 `CheckManageAccess` | 目标用户的成员状态必须启用;产品必须启用 |
| 查看权限覆盖 | 本人自查无需额外检查;查他人需通过 `CheckManageAccess` | userId 枚举防护:非超管非本人时先检查产品管理员身份 |
| **成员管理** | | |
| 添加成员 | 通过 `CheckManageAccess` + `CheckMemberTypeAssignment` | 不可分配同级或更高类型;产品必须启用 |
| 更新成员 | 通过 `CheckManageAccess` + `CheckMemberTypeAssignment` | 不可降级最后一个 ADMIN |
| 移除成员 | 通过 `CheckManageAccess` | 不可移除最后一个 ADMIN;级联清理角色/权限绑定 |
| **查询类接口** | | |
| 产品/部门/角色/用户/成员列表与详情 | 已登录即可 | — |
| 用户信息 (userInfo) | 已登录即可 | 返回当前登录用户自己的信息 |
| **认证接口** | | |
| 修改自身信息 (updateInfo) | 已登录即可 | 仅允许修改昵称/头像/邮箱/手机,userId 从 JWT 获取 |
| 用户注销 (logout) | 已登录即可 | 递增 tokenVersion,所有已签发令牌即时失效 |
| **公开接口** | | |
| 产品端登录 (login) | 无需鉴权 | cap.js 已启用时拒绝(必须走 login/cap);未启用时需携带图片验证码;超级管理员被拒绝,productCode 必传 |
| 产品端 cap.js 登录 (auth/login/cap) | 无需鉴权 | cap.js 未启用时拒绝(必须走 login);提交 cap token 完成人机验证后登录 |
| 管理后台登录 (adminLogin) | 无需鉴权 | cap.js 已启用时拒绝(必须走 adminLogin/cap);未启用时需携带图片验证码;需验证 managementKey |
| 管理后台 cap.js 登录 (adminLogin/cap) | 无需鉴权 | cap.js 未启用时拒绝(必须走 adminLogin);提交 cap token 完成人机验证后登录 |
| 获取图片验证码 (captcha/get) | 无需鉴权 | cap.js 未启用时前端调用此接口展示图片验证码 |
| 获取 cap.js 端点 (capjs/endpoint) | 无需鉴权 | 返回 cap.js 端点 URL;空串代表 cap.js 未启用,前端降级为图片验证码 |
| 刷新令牌 | 无需鉴权 | 令牌轮转:旧令牌即时失效,返回全新令牌对;产品禁用时拒绝 |
| 同步权限 | 无需鉴权 | 通过 appKey/appSecret 认证 |
### CheckManageAccess — 用户管理权限检查
`CheckManageAccess` 是管理类操作的核心权限检查函数,执行多维度判定:
```mermaid
flowchart TD
START[CheckManageAccess] --> SA{是超级管理员?}
SA -->|是| PASS[✅ 通过]
SA -->|否| SELF{操作自己?}
SELF -->|是| PASS
SELF -->|否| DEPT[部门层级检查]
DEPT --> IS_ADMIN{操作者是 ADMIN?}
IS_ADMIN -->|是| LEVEL[权限级别检查]
IS_ADMIN -->|否| HAS_DEPT{操作者有部门?}
HAS_DEPT -->|是| TARGET_DEPT{目标在操作者
本部门或子部门?}
HAS_DEPT -->|否| DENY1[❌ 未归属部门]
TARGET_DEPT -->|是| LEVEL
TARGET_DEPT -->|否| DENY2[❌ 跨部门拒绝]
LEVEL --> CMP_TYPE{比较 memberType 优先级}
CMP_TYPE -->|操作者更高| PASS
CMP_TYPE -->|目标更高| DENY3[❌ 权限不足]
CMP_TYPE -->|同级| CMP_PERM{比较 permsLevel}
CMP_PERM -->|操作者更小| PASS
CMP_PERM -->|相等或更大| DENY4[❌ 同级或更低]
```
#### 检查维度说明
1. **超管豁免**:`SUPER_ADMIN` 不受任何限制
2. **自我豁免**:操作自己的记录总是允许
3. **部门层级**:操作者只能管理本部门及下级子部门的用户(`ADMIN` 和超管豁免此检查)
4. **权限级别**:先比 `memberType` 优先级,同级再比 `permsLevel` 数值
- `memberType` 优先级:`SUPER_ADMIN(0) > ADMIN(1) > DEVELOPER(2) > MEMBER(3)`
- `permsLevel`:数值越小权限越高(如 10 > 20),无角色默认为 `MaxInt64`(最低)
### UserDetailsLoader — 用户信息缓存
`UserDetailsLoader` 是一个集中式的用户信息加载与缓存组件,为中间件、登录、用户信息查询等多个场景提供统一的数据来源。
#### 加载的完整数据
| 数据来源 | 加载的字段 |
| ---------- | ----------- |
| `sys_user` | userId, username, nickname, avatar, email, phone, remark, isSuperAdmin, mustChangePassword, status |
| `sys_dept` | deptId, deptName, deptPath, deptType |
| `sys_product` | productCode, productName, productStatus |
| `sys_product_member` | memberType |
| `sys_role` (当前产品) | roles[], minPermsLevel |
| 计算后的权限 | perms[] (权限 code 集合) |
#### 缓存策略
- **存储**:Redis JSON,key 格式 `{prefix}:ud:{userId}:{productCode}`
- **TTL**:300 秒(5 分钟)自然过期
- **主动失效**:所有写操作均触发对应的缓存清除
| 操作 | 失效方法 | 失效范围 |
| ------ | ---------- | ---------- |
| 更新用户信息 / 冻结解冻 / 修改密码 / 注销 | `Clean(userId)` | 该用户所有产品缓存 |
| 设置用户权限覆盖 / 添加成员 / 更新成员 | `Del(userId, productCode)` | 该用户在指定产品的缓存 |
| 更新角色 / 删除角色 / 绑定角色权限 | `BatchDel(userIds, productCode)` | 受影响用户在指定产品的缓存 |
| 更新产品 / 同步权限 | `CleanByProduct(productCode)` | 该产品下所有用户的缓存 |
| 更新部门 | `Clean(uid)` × N | 该部门下所有用户的缓存 |
| 刷新令牌 | `Clean(userId)` | 轮转时递增 tokenVersion 后清除缓存 |
---
## 快速开始
### 环境要求
- Go 1.21+
- MySQL 8.0+
- Redis 6.0+
- goctl 1.10+
### 部署步骤
```bash
# 1. 创建数据库并导入表结构
mysql -u root -p -e "CREATE DATABASE perms_system DEFAULT CHARACTER SET utf8mb4;"
mysql -u root -p perms_system < perm.sql
# 2. 修改配置(数据库、Redis、JWT 密钥等)
vim etc/perm-api.yaml
# 3. 安装依赖并启动
go mod tidy
go run perm.go
# 服务启动后:
# - HTTP API: http://localhost:10001
# - gRPC: localhost:10002
```
### 初始化超级管理员
首次部署后,需手动在数据库中插入超级管理员账号:
```sql
INSERT INTO sys_user (username, password, nickname, isSuperAdmin, status, createTime, updateTime)
VALUES ('superadmin', '$2a$10$这里替换为bcrypt加密后的密码', '超级管理员', 1, 1, UNIX_TIMESTAMP(), UNIX_TIMESTAMP());
```
可使用任意 bcrypt 工具生成密码哈希,或在 Go 中执行:
```go
hash, _ := bcrypt.GenerateFromPassword([]byte("your-password"), bcrypt.DefaultCost)
fmt.Println(string(hash))
```
---
## 产品接入指南
本节以 "CRM 系统"(编码 `crm`)为例,完整演示接入流程。
### 接入全景图
```mermaid
sequenceDiagram
participant SA as 超级管理员
participant PS as 权限系统
participant CRM as CRM 后端
participant U as 终端用户
rect rgb(230, 245, 255)
Note over SA, PS: 阶段一:注册产品
SA ->> PS: POST /api/product/create
PS -->> SA: appKey + appSecret + 初始管理员
end
rect rgb(230, 255, 230)
Note over CRM, PS: 阶段二:CRM 启动 → 同步权限
CRM ->> PS: POST /api/perm/sync (appKey + appSecret + 权限列表)
PS -->> CRM: {added, updated, disabled}
end
rect rgb(255, 245, 230)
Note over SA, PS: 阶段三:管理员配置角色与用户
SA ->> PS: 创建角色、绑定权限、创建用户、分配角色
end
rect rgb(245, 230, 255)
Note over U, CRM: 阶段四:用户登录与鉴权
U ->> CRM: 提交用户名密码
CRM ->> PS: POST /api/auth/login 或 gRPC Login
PS -->> CRM: accessToken + refreshToken + perms[]
CRM -->> U: 返回 token
U ->> CRM: 业务请求 + Bearer token
CRM ->> CRM: 本地 JWT 解析,检查 perms[]
CRM -->> U: 返回业务数据
end
```
### 阶段一:注册产品
超级管理员登录后创建产品:
```bash
POST /api/product/create
{"code": "crm", "name": "CRM 系统", "remark": "客户关系管理", "adminDeptId": 1}
```
响应中包含 `credentialsTicket`(一次性凭证票据,5 分钟内有效),请立即用该 ticket 调用 `/api/product/fetchInitialCredentials` 领取 `appKey`、`appSecret`、`adminUser`、`adminPassword`。ticket 一次性消费,过期或已使用后无法重新获取。
### 阶段二:产品启动时同步权限
CRM 后端在启动阶段上报全部权限列表。**此接口无需 JWT,通过 appKey + appSecret 认证**。
```bash
POST /api/perm/sync
{
"appKey": "a1b2c3d4e5f6...",
"appSecret": "x9y8z7w6v5u4...",
"perms": [
{"code": "customer:list", "name": "查看客户列表"},
{"code": "customer:create", "name": "创建客户"},
{"code": "customer:update", "name": "编辑客户"},
{"code": "customer:delete", "name": "删除客户"},
{"code": "order:list", "name": "查看订单列表"},
{"code": "order:create", "name": "创建订单"},
{"code": "report:export", "name": "导出报表"}
]
}
```
#### Go 代码示例(放在 main 启动流程中)
```go
var permsList = []map[string]string{
{"code": "customer:list", "name": "查看客户列表"},
{"code": "customer:create", "name": "创建客户"},
// ...
}
func syncPermsOnStartup(permSystemURL, appKey, appSecret string) {
body, _ := json.Marshal(map[string]interface{}{
"appKey": appKey, "appSecret": appSecret, "perms": permsList,
})
resp, err := http.Post(permSystemURL+"/api/perm/sync", "application/json", bytes.NewReader(body))
if err != nil {
log.Fatalf("同步权限失败: %v", err)
}
defer resp.Body.Close()
log.Println("权限同步完成")
}
```
### 阶段三:管理员配置角色与用户
详见上文「实际业务配置指南」。
### 阶段四:用户登录与鉴权
#### 1. 用户登录
##### 方式 A:HTTP API
```bash
POST /api/auth/login
{"username": "zhangsan", "password": "123456", "productCode": "crm"}
```
##### 方式 B:gRPC SDK(推荐 Go 项目)
```go
import "perms-system-server/permclient"
client, err := permclient.NewPermClient("perm-system:10002")
resp, err := client.Login(ctx, "crm", "zhangsan", "123456")
// resp.AccessToken, resp.RefreshToken, resp.Perms
```
#### 2. 本地 JWT 验证(推荐)
产品后端配置与权限系统相同的 `AccessSecret`,本地解析 JWT 即可获取用户信息和权限列表,无需每次回调。
```go
type Claims struct {
TokenType string `json:"tokenType"`
TokenVersion int64 `json:"tokenVersion"`
UserId int64 `json:"userId"`
Username string `json:"username"`
ProductCode string `json:"productCode"`
MemberType string `json:"memberType"`
Perms []string `json:"perms"`
jwt.RegisteredClaims
}
```
> - `tokenType` 字段区分 `"access"` 和 `"refresh"`,验证时应检查 `tokenType == "access"`
> - `tokenVersion` 用于令牌吊销:用户注销或刷新令牌时版本递增,旧版本令牌即时失效
#### 3. 业务接口中检查权限
```go
func CreateOrderHandler(w http.ResponseWriter, r *http.Request) {
claims := middleware.GetClaims(r.Context())
if !hasPermission(claims.Perms, "order:create") {
// 返回 403
return
}
// 执行业务逻辑...
}
```
#### 4. Token 续期
```bash
POST /api/auth/refreshToken
Authorization: Bearer eyJhbGciOi... # refreshToken 放在 header 中
Content-Type: application/json
{"productCode": "crm"}
```
> `refreshToken` 有效期默认 7 天。系统采用令牌轮转策略:每次刷新都返回全新的 accessToken 和 refreshToken,旧令牌即时失效(通过递增 `tokenVersion` 实现)。过期后必须重新登录。
### 接入检查清单
- [ ] 已获取产品的 `appKey` 和 `appSecret`
- [ ] 产品启动时调用 `/api/perm/sync` 同步了全部权限
- [ ] 后端配置了与权限系统相同的 `AccessSecret`
- [ ] 登录接口正确调用权限系统并返回 token
- [ ] JWT 鉴权中间件已加入受保护路由,并验证 `tokenType == "access"`
- [ ] 业务接口中根据 `claims.Perms` 做了权限校验
- [ ] 前端在 `accessToken` 过期时能自动调用 `refreshToken` 续期(注意:刷新后旧令牌失效,需立即替换)
- [ ] 前端实现注销功能(调用 `/api/auth/logout`),确保令牌在服务端即时吊销
---
## HTTP API 接口文档
所有接口基础路径为 `/api`,请求方式统一为 **POST**,参数通过 **JSON Body** 传递。需鉴权接口须携带 `Authorization: Bearer {accessToken}`。
### 统一响应格式
所有 HTTP 接口均返回统一的 JSON 结构:
```json
{
"code": 0,
"msg": "ok",
"data": { ... }
}
```
| 字段 | 类型 | 说明 |
| ------ | ------ | ------ |
| `code` | int | 业务状态码,`0` 表示成功,非零表示失败 |
| `msg` | string | 状态描述 |
| `data` | object/null | 业务数据,失败时无此字段 |
### 错误码一览
| Code | 语义 | 典型场景 |
| ------ | ------ | ---------- |
| `0` | 成功 | 所有正常响应 |
| `400` | 请求不合法 | 参数缺失/格式错误、原密码错误、存在子部门无法删除等 |
| `401` | 未认证 | 未登录、token 无效/过期/类型错误、用户名密码错误 |
| `403` | 无权限 | 账号已冻结、产品已禁用、非超管操作产品/部门、非管理员操作角色/用户、跨部门/越级管理 |
| `404` | 资源不存在 | 用户/产品/角色/部门/成员不存在 |
| `409` | 资源冲突 | 用户名已存在、产品编码已存在、角色名重复、成员重复添加 |
| `500` | 系统错误 | 数据库异常等未预期的内部错误(不暴露具体信息) |
### 公开接口(无需鉴权)
#### POST /api/captcha/get — 获取图片验证码
生成一张数字图片验证码,返回验证码 ID 和 Base64 编码的图片数据。仅在 cap.js **未启用**时使用,前端应先调用 `/api/capjs/endpoint` 判断当前验证方式。
| 字段 | 类型 | 必填 | 说明 |
| ------ | ------ | ------ | ------ |
| width | int | 否 | 图片宽度(像素),默认 240 |
| height | int | 否 | 图片高度(像素),默认 80 |
**响应 data:** `{"id": "xxx", "base64image": "data:image/png;base64,..."}`
#### POST /api/capjs/endpoint — 获取 cap.js 端点 URL
返回 cap.js 人机验证服务的前端接入地址。前端应在展示登录页前调用此接口,据返回值决定显示图片验证码还是 cap.js 挂件。
无请求参数。
**响应 data:** `{"data": "https://cap.example.com/sitekey/"}` 或 `{"data": ""}` (空串表示 cap.js 未启用,降级为图片验证码)
#### POST /api/auth/login — 产品端登录
产品终端用户的统一登录入口。用户通过用户名密码 + 产品编码登录指定产品,登录成功后获得 JWT 令牌对和该用户在此产品下的权限列表。
**调用场景:**
- **产品前端登录页**:用户在 CRM、OA 等业务系统的登录页面输入账号密码,前端调用此接口获取 token
- **产品后端代理登录**:产品后端收到用户凭据后通过 HTTP 或 gRPC 调用此接口,将返回的 token 下发给客户端
- **移动端 / 小程序登录**:移动客户端直接调用此接口完成产品侧身份认证
**安全约束:**
- 超级管理员账号无法通过此接口登录(必须走 `/auth/adminLogin`),防止超管凭据在产品端暴露
- 产品必须处于启用状态,否则拒绝登录
- 用户必须是该产品的有效成员(`status=1`),且账号未冻结
- 受 IP 维度限流保护,防止暴力破解;仅对已存在的用户名消耗限流配额
- **验证机制互斥**:cap.js 启用时此接口拒绝请求(返回 400),必须走 `/auth/login/cap`;cap.js 未启用时必须携带图片验证码(captchaId + captchaCode)
| 字段 | 类型 | 必填 | 说明 |
| ------ | ------ | ------ | ------ |
| username | string | 是 | 登录名 |
| password | string | 是 | 密码 |
| productCode | string | 是 | 产品编码 |
| captchaId | string | 条件必填 | 图片验证码 ID(cap.js 未启用时必填) |
| captchaCode | string | 条件必填 | 图片验证码答案(cap.js 未启用时必填) |
#### 响应 data
| 字段 | 类型 | 说明 |
| ------ | ------ | ------ |
| accessToken | string | 访问令牌(用于后续 API 调用,默认 2 小时有效) |
| refreshToken | string | 刷新令牌(用于续期,默认 7 天有效) |
| expires | int64 | accessToken 过期时间(Unix 时间戳,秒) |
| userInfo | object | 用户信息(含 `perms` 权限码数组,前端据此控制菜单/按钮显隐) |
#### POST /api/auth/login/cap — 产品端 cap.js 登录
产品端使用 cap.js 人机验证令牌登录。前端从 cap.js 挂件获得 `capToken` 后调用此接口,服务端向 cap.js 服务端验证令牌后执行与 `/auth/login` 相同的业务逻辑。
**前提条件:** 服务端必须配置并启用 cap.js(`Capjs.Enable=1`),否则返回 400。
**安全约束:** 与 `/auth/login` 相同,另外 cap.js 未启用时此接口直接拒绝请求,防止绕过验证。
| 字段 | 类型 | 必填 | 说明 |
| ------ | ------ | ------ | ------ |
| username | string | 是 | 登录名 |
| password | string | 是 | 密码 |
| productCode | string | 是 | 产品编码 |
| capToken | string | 是 | cap.js 前端挂件返回的验证令牌 |
**响应 data:** 与 `/auth/login` 相同。
#### POST /api/auth/adminLogin — 管理后台登录
权限管理系统自身管理后台的登录入口。仅限超级管理员使用,必须额外提供 `managementKey` 验证身份。
**调用场景:**
- **管理后台前端登录**:超级管理员在权限系统管理 UI 的登录页面输入账号密码 + managementKey
- **运维脚本自动化**:运维人员通过脚本调用此接口获取超管 token,执行批量管理操作(如创建产品、批量建用户)
**安全约束:**
- 必须传入与服务端配置一致的 `managementKey`,该密钥仅管理后台前端持有,不可泄露给产品端
- `managementKey` 校验在限流之前执行,无效密钥不会消耗限流配额,防止 DoS 攻击
- 受 IP 维度限流保护
- **验证机制互斥**:cap.js 启用时此接口拒绝请求(返回 400),必须走 `/auth/adminLogin/cap`;cap.js 未启用时必须携带图片验证码(captchaId + captchaCode)
| 字段 | 类型 | 必填 | 说明 |
| ------ | ------ | ------ | ------ |
| username | string | 是 | 登录名 |
| password | string | 是 | 密码 |
| managementKey | string | 是 | 管理端密钥(配置文件中的 `Auth.ManagementKey`) |
| captchaId | string | 条件必填 | 图片验证码 ID(cap.js 未启用时必填) |
| captchaCode | string | 条件必填 | 图片验证码答案(cap.js 未启用时必填) |
**响应 data:** 与产品端登录接口相同。登录后不携带产品上下文,token 中 `productCode` 和 `perms` 为空。超管拥有所有产品的全部权限,不需要在 token 中枚举。
#### POST /api/auth/adminLogin/cap — 管理后台 cap.js 登录
管理后台使用 cap.js 人机验证令牌登录。与 `/auth/adminLogin` 业务逻辑相同,区别在于以 cap.js 令牌代替图片验证码完成人机验证。
**前提条件:** 服务端必须配置并启用 cap.js(`Capjs.Enable=1`),否则返回 400。
| 字段 | 类型 | 必填 | 说明 |
| ------ | ------ | ------ | ------ |
| username | string | 是 | 登录名 |
| password | string | 是 | 密码 |
| managementKey | string | 是 | 管理端密钥 |
| capToken | string | 是 | cap.js 前端挂件返回的验证令牌 |
**响应 data:** 与 `/auth/adminLogin` 相同。
#### POST /api/auth/refreshToken — 刷新令牌
使用有效的 refreshToken 换取全新的令牌对。采用**令牌轮转**策略,每次刷新后旧令牌即时失效,确保单会话安全。
**调用场景:**
- **accessToken 即将过期时自动续期**:前端在 accessToken 到期前(如提前 5 分钟)调用此接口,无感刷新用户会话
- **accessToken 已过期后恢复会话**:前端收到 401 响应后,用 refreshToken 尝试换取新令牌,成功则重试原请求,失败则跳转登录
- **切换产品上下文**:管理后台超管登录后,通过传入不同的 `productCode` 切换到特定产品的权限视角,获取该产品下的权限列表
**安全约束:**
- 通过 `Authorization: Bearer {refreshToken}` 请求头传入,必须是 `tokenType=refresh` 的令牌
- 每次刷新递增 `tokenVersion`,旧的 access/refresh 令牌即时失效——即使令牌被窃取,攻击者也只能用一次
- 产品禁用时拒绝刷新,立即切断该产品下所有用户的会话
- refreshToken 有固定有效期(默认 7 天),过期后必须重新登录
| 字段 | 类型 | 必填 | 说明 |
| ------ | ------ | ------ | ------ |
| Authorization | header | 是 | `Bearer {refreshToken}` |
| productCode | string | 否 | 切换产品上下文时传入(Body) |
**响应 data:** 与登录接口相同,包含全新的 accessToken + refreshToken + userInfo。
#### POST /api/perm/sync — 同步产品权限
产品后端全量上报权限列表,系统自动对比差异执行新增、更新和禁用操作。这是产品权限声明的唯一入口,管理员无需手动创建权限。
**调用场景:**
- **产品服务启动时自动同步**:产品后端在 main 函数中调用此接口,确保代码中新增或重命名的权限点自动注册到权限系统。推荐放在服务启动阶段(非请求链路),失败时 `log.Fatal` 终止启动
- **CI/CD 部署流程中调用**:在产品部署脚本中调用此接口,确保新版本的权限定义在服务上线前就已同步
- **权限定义热更新**:产品无需重启即可通过调用此接口更新权限列表(如通过管理接口触发)
**处理逻辑:**
- 列表中已存在的权限:按 `code` 匹配,更新 `name` 和 `remark`
- 列表中新增的权限:自动创建
- 数据库中有但列表中没有的权限:自动禁用(`status=2`),不删除——已绑定到角色的记录保留但权限计算时过滤
**安全约束:** 通过 `appKey`/`appSecret` 认证(非 JWT),无需登录,受 IP 维度限流保护
| 字段 | 类型 | 必填 | 说明 |
| ------ | ------ | ------ | ------ |
| appKey | string | 是 | 产品接入密钥 |
| appSecret | string | 是 | 产品签名密钥 |
| perms | array | 是 | 权限列表(全量,不在列表中的已有权限将被禁用) |
| perms[].code | string | 是 | 权限码(如 `user:create`,产品内唯一) |
| perms[].name | string | 是 | 权限名(用于管理后台展示) |
| perms[].remark | string | 否 | 备注 |
**响应 data:** `{"added": 3, "updated": 1, "disabled": 0}`
### 认证接口(需鉴权)
#### POST /api/auth/logout — 用户注销
主动吊销当前用户的所有已签发令牌,使其在所有设备/浏览器上立即失效。
**调用场景:**
- **用户主动退出登录**:前端「退出登录」按钮点击后调用此接口,确保服务端即时吊销令牌(而非仅清理客户端本地存储)
- **安全事件应急处置**:用户发现账号异常(如收到异地登录提醒),通过注销接口使所有设备上的令牌立即失效
- **管理后台切换账号前**:超管在管理后台切换到其他账号前,先注销当前会话
**处理逻辑:** 递增当前用户的 `tokenVersion`,所有持有旧版本的 access/refresh 令牌在下次使用时都会被拒绝;同时清除 UserDetailsLoader 缓存。
无请求参数。**响应 data:** `null`
#### POST /api/auth/userInfo — 获取当前用户信息
返回当前登录用户的完整信息,包括基本资料、部门归属、产品成员身份和权限列表。数据来自 UserDetailsLoader 缓存。
**调用场景:**
- **前端页面初始化**:用户刷新页面或首次加载时,前端调用此接口获取用户信息,渲染头像、昵称、菜单权限等
- **前端权限刷新**:当管理员调整了用户的角色/权限后,前端可主动调用此接口重新获取最新权限列表,无需重新登录
- **移动端个人中心**:展示用户头像、昵称、部门等个人资料
- **前端权限守卫**:路由切换前调用此接口确认用户仍有有效会话及对应权限
无请求参数。**响应 data:** `UserInfo` 对象(含 userId、username、nickname、avatar、email、phone、deptName、memberType、perms 等)。
#### POST /api/auth/changePassword — 修改密码
用户修改自己的登录密码。修改成功后递增 `tokenVersion`,所有已签发令牌即时失效,用户需重新登录。
**调用场景:**
- **用户主动修改密码**:用户在个人设置页面修改密码
- **首次登录强制改密**:管理员创建用户时设置了初始密码,用户首次登录后系统提示修改密码(前端检查 `mustChangePassword` 标志)
- **密码泄露后重置**:用户怀疑密码泄露,通过修改密码使所有已签发令牌失效
**安全约束:** 必须验证原密码正确后才能修改;新密码需 6-72 字符且不能与旧密码相同。
| 字段 | 类型 | 必填 | 说明 |
| ------ | ------ | ------ | ------ |
| oldPassword | string | 是 | 原密码 |
| newPassword | string | 是 | 新密码(6-72 字符,不能与旧密码相同) |
#### POST /api/auth/updateInfo — 修改自身信息
已登录用户修改自己的昵称、头像、邮箱、手机号。userId 从 JWT 令牌中提取,不接受客户端传入,防止越权修改他人信息。
**调用场景:**
- **个人资料编辑**:用户在个人设置页面修改昵称、头像、邮箱或手机号
- **首次完善资料**:管理员创建用户后,用户首次登录补充个人信息
- **头像更新**:用户上传新头像后调用此接口保存头像 URL
**安全约束:**
- userId 从 JWT 获取,禁止客户端指定(防 IDOR)
- 仅允许修改 nickname、avatar、email、phone 四个安全字段
- 不允许修改 username(登录凭证)、deptId、status、remark 等敏感字段
- 使用乐观锁(`updateTime` CAS)防止并发覆盖
- 至少需要传入一个字段,全部为空时返回 400
| 字段 | 类型 | 必填 | 说明 |
| ------ | ------ | ------ | ------ |
| nickname | string | 否 | 昵称(上限 64 字符) |
| avatar | string | 否 | 头像 URL(上限 255 字符) |
| email | string | 否 | 邮箱(上限 64 字符) |
| phone | string | 否 | 手机号(上限 32 字符) |
**响应:** 无 data(成功时返回标准 `{"code":0,"msg":"ok"}` 结构)。若信息被其他会话并发修改,返回 409 冲突错误。
### 产品管理(仅超级管理员)
#### POST /api/product/create — 创建产品
为一个新的业务系统注册接入权限系统。创建后自动生成 `appKey`/`appSecret`(用于权限同步认证)和初始 ADMIN 管理员账号。
**调用场景:**
- **新业务系统接入**:公司新开发了一个 CRM/OA/电商后台等系统,超管在管理后台创建产品,获取接入凭证交给产品开发团队
- **多租户场景拓展**:SaaS 平台新增一个租户,通过创建产品实现权限隔离
**注意事项:** 响应中直接返回 `appKey` 和 `adminUser`,但 `appSecret` 与初始 `adminPassword` 不在响应体中——响应携带一次性 `credentialsTicket`(5 分钟有效),需立即调用 `/api/product/fetchInitialCredentials` 领取完整凭证。ticket 一次消费即失效,过期或已使用后无法重新获取。
| 字段 | 类型 | 必填 | 说明 |
| ------ | ------ | ------ | ------ |
| code | string | 是 | 产品编码(全局唯一,仅允许字母/数字/下划线/中划线,不能以数字开头,上限 64 字符) |
| name | string | 是 | 产品名称 |
| remark | string | 否 | 备注 |
| adminDeptId | int64 | 是 | 初始管理员账号所属部门 ID(归属研发部门的管理员将自动拥有全部权限) |
**响应 data:** `{"id", "code", "appKey", "adminUser", "credentialsTicket", "credentialsExpiresAt"}`
#### POST /api/product/update — 更新产品
修改产品的名称、备注或启用/禁用状态。
**调用场景:**
- **修改产品信息**:产品改名(如从「客户系统」改为「CRM 系统」)、补充备注说明
- **禁用产品**:项目下线或暂停运营时,将产品状态设为禁用(`status=2`)。禁用后该产品下所有在线用户的令牌即时失效(JWT 中间件和 gRPC 接口均拦截),任何登录/刷新请求都将被拒绝
- **重新启用产品**:暂停后恢复运营,将产品状态改回启用(`status=1`)
**副作用:** 更新操作会清除该产品下所有用户的 UserDetailsLoader 缓存(`CleanByProduct`),确保状态变更即时生效。
| 字段 | 类型 | 必填 | 说明 |
| ------ | ------ | ------ | ------ |
| id | int64 | 是 | 产品 ID |
| name | string | 是 | 产品名称 |
| remark | string | 否 | 备注 |
| status | int64 | 否 | 1=启用 2=禁用 |
#### POST /api/product/list — 产品列表
分页查询所有已注册的产品。
**调用场景:**
- **管理后台产品管理页面**:超管查看所有接入的产品列表,进行管理操作
- **创建角色/成员时选择产品**:管理后台在创建角色、添加成员等操作时,需要先选择目标产品,此接口提供产品下拉列表数据
- **运维监控面板**:展示当前系统管理的产品总数和各产品状态
| 字段 | 类型 | 必填 | 说明 |
| ------ | ------ | ------ | ------ |
| page | int64 | 否 | 页码,默认 1 |
| pageSize | int64 | 否 | 每页条数,默认 20,上限 100 |
**响应 data:** `{"total": N, "list": [ProductItem...]}`
#### POST /api/product/detail — 产品详情
查询单个产品的完整信息(含 `appKey`,不含 `appSecret`)。
**调用场景:**
- **产品详情/编辑页面**:管理后台进入产品编辑页时,先调用此接口获取当前数据回填表单
- **查看产品接入凭证**:超管需要查看某产品的 `appKey`(`appSecret` 仅创建时返回,此接口不包含)
| 字段 | 类型 | 必填 | 说明 |
| ------ | ------ | ------ | ------ |
| id | int64 | 是 | 产品 ID |
#### POST /api/product/fetchInitialCredentials — 获取产品初始凭证
安全地获取产品创建时生成的初始凭证(appKey、appSecret、初始管理员账号密码)。采用一次性 ticket 机制,凭证仅可获取一次。
**调用场景:**
- **产品创建后获取凭证**:管理后台创建产品成功后,前端使用返回的 ticket 调用此接口获取完整凭证并展示给超管。ticket 使用后立即销毁,无法重复获取
**安全约束:**
- 仅超级管理员可调用
- ticket 存储在 Redis 中,TTL 为 5 分钟,过期后无法获取
- ticket 采用原子性 GetDel 操作,确保一次性消费,防止重放攻击
| 字段 | 类型 | 必填 | 说明 |
| ------ | ------ | ------ | ------ |
| ticket | string | 是 | 创建产品时返回的一次性凭证票据 |
**响应 data:** `{"appKey": "...", "appSecret": "...", "adminUser": "...", "adminPassword": "..."}`
### 部门管理(仅超级管理员)
#### POST /api/dept/create — 创建部门
在组织架构中新增一个部门节点。部门是全局概念,不隶属于任何产品。
**调用场景:**
- **搭建组织架构**:公司初始化时创建一级部门(研发部、市场部、运营部),再创建子部门(前端组、后端组、测试组)
- **组织调整新增部门**:公司扩张新增业务线或团队时,在对应的上级部门下创建新部门
- **创建研发部门**:设置 `deptType=DEV` 的部门中的成员加入任何产品后自动拥有全部权限,适用于需要跨产品调试的研发团队
| 字段 | 类型 | 必填 | 说明 |
| ------ | ------ | ------ | ------ |
| parentId | int64 | 是 | 父部门 ID,0 表示顶级部门 |
| name | string | 是 | 部门名称 |
| sort | int64 | 否 | 排序值(同级部门间的排列顺序,数值越小越靠前) |
| deptType | string | 否 | `NORMAL`(默认)或 `DEV`(研发部门,成员在产品中自动拥有全部权限) |
| remark | string | 否 | 备注 |
**响应 data:** `{"id": 1}`
#### POST /api/dept/update — 更新部门
修改部门名称、排序、类型或启禁用状态。
**调用场景:**
- **部门更名**:组织调整时修改部门名称(如「技术部」改为「研发中心」)
- **调整排序**:变更部门在同级中的显示顺序
- **变更部门类型**:将普通部门升级为研发部门(`NORMAL` → `DEV`),或将研发部门降级为普通部门。类型变更立即影响该部门下所有用户在各产品中的权限
- **禁用部门**:部门撤销时禁用,保留历史数据
**副作用:** 更新操作会清除该部门下所有用户的 UserDetailsLoader 缓存,确保部门类型或状态变更即时反映到权限计算中。
| 字段 | 类型 | 必填 | 说明 |
| ------ | ------ | ------ | ------ |
| id | int64 | 是 | 部门 ID |
| name | string | 是 | 名称 |
| sort | int64 | 否 | 排序值 |
| deptType | string | 否 | `NORMAL` 或 `DEV` |
| remark | string | 否 | 备注 |
| status | int64 | 否 | 状态 |
#### POST /api/dept/delete — 删除部门
永久删除一个部门。存在子部门或关联用户时拒绝删除,必须先迁移或删除子部门及部门下的用户。
**调用场景:**
- **清理空部门**:组织架构调整后,将人员迁移到新部门后删除空的旧部门
- **撤销误创建的部门**:部门创建有误时及时删除
**约束:** 存在子部门或关联用户时返回 400 错误,防止意外删除整个子树。删除前应先通过部门树接口确认该部门无子节点且无用户归属。
| 字段 | 类型 | 必填 | 说明 |
| ------ | ------ | ------ | ------ |
| id | int64 | 是 | 部门 ID |
#### POST /api/dept/tree — 部门树
返回完整的部门树形结构,包含所有层级的嵌套 `children` 和 `deptType` 标识。
**调用场景:**
- **管理后台组织架构页面**:以树形控件展示公司完整的部门层级结构,支持展开/折叠
- **用户创建/编辑时选择部门**:表单中的部门选择器(树形下拉)需要此接口提供数据源
- **权限检查的辅助参考**:管理员查看部门层级关系,了解谁能管理谁(部门层级影响管理权限范围)
无请求参数。
### 权限管理
#### POST /api/perm/list — 权限列表
分页查询指定产品下的所有权限定义。权限由产品后端通过 `/perm/sync` 自动上报,管理员无法手动创建或删除权限。
**调用场景:**
- **角色绑定权限时选择权限**:管理后台在为角色分配权限时,需要先展示该产品下所有可用的权限列表(通常以穿梭框或复选列表形式)
- **用户权限覆盖时选择权限**:管理后台在为用户设置 ALLOW/DENY 覆盖时,需要展示权限列表供选择
- **权限审计**:管理员查看某产品当前注册了哪些权限,确认是否与代码中的定义一致
- **排查权限同步结果**:产品部署后确认新增的权限是否已正确同步到系统中
| 字段 | 类型 | 必填 | 说明 |
| ------ | ------ | ------ | ------ |
| productCode | string | 是 | 产品编码 |
| page | int64 | 否 | 页码 |
| pageSize | int64 | 否 | 每页条数 |
**响应 data:** `{"total": N, "list": [PermItem...]}`(含 id、code、name、remark、status)
### 角色管理(超级管理员 或 产品管理员)
#### POST /api/role/create — 创建角色
在指定产品下创建一个新角色。角色是产品级概念,同名角色在不同产品中互不干扰。
**调用场景:**
- **按岗位建立角色体系**:产品管理员根据业务需求创建角色,如「销售经理」「普通销售」「客服」「财务审批」
- **按操作范围划分角色**:创建「只读角色」「编辑角色」「管理角色」等不同操作级别的角色
- **新功能上线配套角色**:产品新增功能模块时,创建对应的角色(如新增报表模块时创建「报表管理员」角色)
**安全约束:** 产品必须处于启用状态;`permsLevel` 数值越小权限越高,用于同级成员间的管理权限比较。
| 字段 | 类型 | 必填 | 说明 |
| ------ | ------ | ------ | ------ |
| productCode | string | 是 | 所属产品编码 |
| name | string | 是 | 角色名(产品内唯一) |
| remark | string | 否 | 备注 |
| permsLevel | int64 | 是 | 权限等级(数值越小权限越高,用于管理权限判定) |
**响应 data:** `{"id": 1}`
#### POST /api/role/update — 更新角色
修改角色名称、备注、权限等级或启禁用状态。
**调用场景:**
- **调整角色名称和描述**:角色职责变化后更新名称和备注
- **调整权限等级**:将角色的 `permsLevel` 调高或调低,影响该角色用户在管理权限检查中的级别
- **禁用角色**:角色不再使用时禁用(`status=2`),已绑定该角色的用户在权限计算时将跳过该角色的权限
**安全约束:** 非超级管理员不可降低角色的 `permsLevel`(即不可将数值改大),防止产品管理员通过降低角色级别来提升自己的相对权限。
**副作用:** 更新后自动清除所有绑定该角色的用户在该产品下的 UserDetailsLoader 缓存。
| 字段 | 类型 | 必填 | 说明 |
| ------ | ------ | ------ | ------ |
| id | int64 | 是 | 角色 ID |
| name | string | 是 | 角色名 |
| remark | string | 否 | 备注 |
| permsLevel | int64 | 是 | 权限等级 |
| status | int64 | 否 | 状态 |
#### POST /api/role/delete — 删除角色
永久删除一个角色,级联清理该角色的所有权限绑定(`sys_role_perm`)和用户绑定(`sys_user_role`)。
**调用场景:**
- **清理废弃角色**:业务重构后旧角色不再需要,彻底删除以保持角色列表清洁
- **合并角色**:将多个相似角色合并为一个时,先将用户迁移到新角色,再删除旧角色
**副作用:** 删除前先查询所有绑定该角色的用户 ID 列表,删除后批量清除这些用户在该产品下的缓存,确保权限变更即时生效。
| 字段 | 类型 | 必填 | 说明 |
| ------ | ------ | ------ | ------ |
| id | int64 | 是 | 角色 ID |
#### POST /api/role/list — 角色列表
分页查询指定产品下的所有角色。
**调用场景:**
- **管理后台角色管理页面**:展示产品下的角色列表,供管理员查看、编辑、删除
- **用户绑定角色时选择角色**:管理后台在为用户分配角色时,需要展示该产品下的角色列表(通常以多选框或穿梭框形式)
- **权限审计**:查看某产品下有哪些角色及其权限等级分布
| 字段 | 类型 | 必填 | 说明 |
| ------ | ------ | ------ | ------ |
| productCode | string | 是 | 产品编码 |
| page | int64 | 否 | 页码 |
| pageSize | int64 | 否 | 每页条数 |
#### POST /api/role/detail — 角色详情
查询单个角色的完整信息及其绑定的权限 ID 列表。
**调用场景:**
- **角色编辑页面数据回填**:管理后台进入角色编辑页时,调用此接口获取角色信息和已绑定的权限列表回填表单
- **查看角色权限配置**:管理员查看某角色具体拥有哪些权限,进行审计或排查
| 字段 | 类型 | 必填 | 说明 |
| ------ | ------ | ------ | ------ |
| id | int64 | 是 | 角色 ID |
**响应 data:** 角色信息 + `permIds`(该角色绑定的权限 ID 数组)。
#### POST /api/role/bindPerms — 绑定角色权限
全量替换该角色绑定的权限列表。传入空数组清空所有绑定。
**调用场景:**
- **初始化角色权限**:创建角色后为其配置初始权限集合
- **调整角色权限范围**:业务变化时增减角色权限(如销售角色新增「导出报表」权限)
- **权限收缩**:产品下线某功能时,从相关角色中移除对应权限
- **清空角色权限**:传入空 `permIds` 数组可清除该角色的所有权限绑定
**副作用:** 操作后自动清除所有绑定该角色的用户在该产品下的缓存,确保权限变更即时反映到在线用户。
| 字段 | 类型 | 必填 | 说明 |
| ------ | ------ | ------ | ------ |
| roleId | int64 | 是 | 角色 ID |
| permIds | []int64 | 是 | 权限 ID 列表(全量替换,空数组清空绑定) |
### 用户管理
#### POST /api/user/create — 创建用户(超级管理员 或 产品管理员)
创建一个全局用户账号。服务端自动生成符合强度要求的随机密码,通过一次性凭证票据(ticket)安全下发,避免密码明文出现在请求/响应体中。
**调用场景:**
- **新员工入职**:HR 或管理员为新员工创建账号并归属到对应部门,创建成功后通过 ticket 领取初始密码交给员工
- **批量建立账号**:运维脚本遍历员工花名册,批量调用此接口创建用户账号
- **外部协作人员**:为外包或合作伙伴创建账号(可不设 `deptId`),后续通过成员管理授予特定产品访问权
**注意事项:** 创建用户后还需通过 `/member/add` 将其加入产品才能登录使用该产品。响应中携带 `credentialsTicket`(5 分钟有效),需立即调用 `/api/user/fetchCredentials` 领取用户名和初始密码。ticket 一次消费即失效。用户首次登录后会被强制要求修改密码(`mustChangePassword=1`)。
| 字段 | 类型 | 必填 | 说明 |
| ------ | ------ | ------ | ------ |
| username | string | 是 | 登录名(全局唯一) |
| nickname | string | 否 | 昵称 |
| email | string | 否 | 邮箱(需合法格式) |
| phone | string | 否 | 手机号(7-15 位数字,可含 `+` 前缀) |
| remark | string | 否 | 备注 |
| deptId | int64 | 否 | 部门 ID(归属研发部门的用户加入产品后自动获得全部权限) |
**响应 data:** `{"id": 1, "credentialsTicket": "...", "credentialsExpiresAt": 1716000000}`
#### POST /api/user/resetPassword — 重置用户密码(超级管理员 或 产品管理员)
管理员为指定用户重置密码。服务端生成新的随机强密码,通过一次性凭证票据安全下发。重置后旧令牌即时失效(tokenVersion 递增),用户下次登录需使用新密码且会被强制要求修改密码。
**调用场景:**
- **用户忘记密码**:用户无法自行修改密码时,管理员为其重置
- **安全事件处置**:发现账号异常时,管理员重置密码使旧凭据失效
- **新员工初始密码遗失**:创建用户时的 ticket 过期或已消费,管理员通过重置密码重新生成
**安全约束:**
- 不允许重置超级管理员的密码(超管只能通过 `/auth/changePassword` 自行修改)
- 需通过 `CheckManageAccess` 权限检查(防止越权重置高级别用户)
- 重置后递增 `tokenVersion`,所有已签发令牌即时失效
| 字段 | 类型 | 必填 | 说明 |
| ------ | ------ | ------ | ------ |
| userId | int64 | 是 | 目标用户 ID |
**响应 data:** `{"credentialsTicket": "...", "credentialsExpiresAt": 1716000000}`
#### POST /api/user/fetchCredentials — 获取用户凭证(超级管理员 或 产品管理员)
安全地获取用户创建或密码重置时生成的凭证(用户名 + 密码)。采用一次性 ticket 机制,凭证仅可获取一次。
**调用场景:**
- **创建用户后获取凭证**:管理后台创建用户成功后,前端使用返回的 ticket 调用此接口获取初始密码并展示给管理员
- **重置密码后获取凭证**:管理员重置用户密码后,前端使用返回的 ticket 获取新密码
**安全约束:**
- 仅超级管理员或产品管理员可调用
- ticket 存储在 Redis 中,TTL 为 5 分钟,过期后无法获取
- ticket 采用原子性 GetDel 操作,确保一次性消费,防止重放攻击
| 字段 | 类型 | 必填 | 说明 |
| ------ | ------ | ------ | ------ |
| ticket | string | 是 | 创建用户或重置密码时返回的一次性凭证票据 |
**响应 data:** `{"username": "zhangsan", "password": "Ax7#kL9m..."}`
#### POST /api/user/update — 更新用户(仅本人 或 超级管理员)
修改用户个人信息(昵称、邮箱、手机、部门等)。使用乐观锁防止并发更新冲突。
**调用场景:**
- **用户修改个人资料**:用户在个人设置页面更新昵称、邮箱、手机号
- **超管调整用户部门**:组织架构调整时,超管将用户从一个部门转移到另一个部门(修改 `deptId`)。部门变更会影响权限——从研发部门转出的用户将失去自动全权限特权
- **超管补充用户信息**:为缺失信息的用户补充邮箱、手机等联系方式
- **取消部门归属**:传 `deptId=0` 将用户从部门中移出
**字段更新规则:** 支持字段清空——传 `""` 清空字符串字段,传 `0` 清空 deptId,不传字段则不更新。
**副作用:** 更新后清除该用户所有产品的 UserDetailsLoader 缓存。
| 字段 | 类型 | 必填 | 说明 |
| ------ | ------ | ------ | ------ |
| id | int64 | 是 | 用户 ID |
| nickname | *string | 否 | 昵称 |
| email | *string | 否 | 邮箱 |
| phone | *string | 否 | 手机号 |
| remark | *string | 否 | 备注 |
| deptId | *int64 | 否 | 部门 ID(传 0 取消部门归属) |
| status | int64 | 否 | 状态 |
#### POST /api/user/list — 用户列表
分页查询用户列表,支持按用户名、昵称、状态、部门筛选,可选传入产品编码以附带成员身份信息。
**调用场景:**
- **管理后台用户管理页面**:展示所有用户列表,含基本信息和状态,支持多条件组合筛选
- **按产品筛选成员视角**:传入 `productCode` 时,列表中每个用户会附带其在该产品中的 `memberType`(ADMIN/DEVELOPER/MEMBER/空),方便管理员了解哪些用户已经是该产品的成员
- **添加成员时选择用户**:管理后台在添加产品成员时,先查询用户列表让管理员选择要添加的用户
| 字段 | 类型 | 必填 | 说明 |
| ------ | ------ | ------ | ------ |
| productCode | string | 否 | 产品编码(传入则每个用户附带在该产品中的成员类型) |
| username | string | 否 | 用户名(模糊匹配) |
| nickname | string | 否 | 昵称(模糊匹配) |
| status | int64 | 否 | 状态筛选(1=启用,2=禁用) |
| deptId | *int64 | 否 | 部门 ID(0 表示无部门) |
| page | int64 | 否 | 页码 |
| pageSize | int64 | 否 | 每页条数 |
#### POST /api/user/detail — 用户详情
查询单个用户的完整信息及其在当前产品下绑定的角色 ID 列表。
**调用场景:**
- **用户编辑页面数据回填**:管理后台进入用户编辑页时,调用此接口获取用户信息回填表单
- **查看用户角色配置**:管理员查看某用户当前绑定了哪些角色,评估权限是否合理
- **用户权限排查**:当用户反馈无法访问某功能时,管理员通过此接口查看用户的角色绑定情况
| 字段 | 类型 | 必填 | 说明 |
| ------ | ------ | ------ | ------ |
| id | int64 | 是 | 用户 ID |
| productCode | string | 否 | 产品编码;仅 SUPER_ADMIN 有效,传入时返回该用户在指定产品下绑定的角色 ID;不传时返回该用户在所有产品下的全量角色 ID。非超管始终使用 JWT 中的产品上下文 |
**响应 data:** 用户信息 + `roleIds`(超管不传 productCode 时为跨所有产品的角色 ID;否则为指定产品下的角色 ID)。
#### POST /api/user/bindRoles — 绑定用户角色(需管理权限)
全量替换该用户在当前产品下的角色绑定。需通过 `CheckManageAccess` 权限检查。
**调用场景:**
- **为新成员分配角色**:用户被添加为产品成员后,管理员为其分配一个或多个角色
- **调整用户职责**:用户岗位变动时,更换其角色(如从「普通销售」升级为「销售经理」)
- **多角色组合**:为用户同时分配多个角色以组合权限(如同时拥有「订单管理」和「报表查看」角色)
- **清空角色**:传入空 `roleIds` 数组清除用户的所有角色绑定
**安全约束:** 操作者需要对目标用户有管理权限(通过 `CheckManageAccess` 校验超管/部门层级/权限级别);目标用户的成员状态必须启用。
**副作用:** 操作后清除目标用户在该产品下的 UserDetailsLoader 缓存。
| 字段 | 类型 | 必填 | 说明 |
| ------ | ------ | ------ | ------ |
| userId | int64 | 是 | 用户 ID |
| roleIds | []int64 | 是 | 角色 ID 列表(全量替换) |
| productCode | string | 条件必填 | 产品编码;仅 SUPER_ADMIN 有效,超管必须显式传入(为空时返回 400),非超管忽略此字段始终使用 JWT 中的产品上下文 |
#### POST /api/user/setPerms — 设置用户权限覆盖(需管理权限)
全量替换用户在当前产品下的个性化 ALLOW/DENY 权限覆盖。这是在角色权限基础上的微调机制。需通过 `CheckManageAccess` 权限检查。
**调用场景:**
- **给个别用户额外授权**:用户的角色不包含某权限,但业务上需要临时或长期授予(如给李四额外授予「导出报表」权限),使用 `ALLOW`
- **给个别用户限制权限**:用户的角色包含某权限,但需要对该用户个别禁止(如禁止实习生「删除客户」),使用 `DENY`
- **组合 ALLOW 和 DENY**:同时为用户增加某些权限并限制某些权限,实现精细化控制
- **清空所有覆盖**:传入空 `perms` 数组清除所有个性化覆盖,用户权限完全由角色决定
**权限计算:** 最终权限 = (角色权限 ∪ ALLOW) - DENY。DENY 优先级最高。
**安全约束:** 目标用户的成员状态必须启用;产品必须处于启用状态。
| 字段 | 类型 | 必填 | 说明 |
| ------ | ------ | ------ | ------ |
| userId | int64 | 是 | 用户 ID |
| perms | array | 是 | 权限覆盖列表(全量替换,空数组清除所有覆盖) |
| perms[].permId | int64 | 是 | 权限 ID |
| perms[].effect | string | 是 | `ALLOW`(额外授予)或 `DENY`(强制拒绝) |
#### POST /api/user/userPerms — 查看用户权限覆盖
查询指定用户在当前产品下已配置的个性化 ALLOW/DENY 覆盖记录。供管理后台在打开「设置权限」抽屉时回填当前状态,使编辑页能正确展示现有的授权/拒绝项。
**调用场景:**
- **管理后台权限设置面板初始化**:打开某用户的「权限覆盖」设置抽屉时,前端调用此接口获取该用户已有的 ALLOW/DENY 记录,结合角色继承权限计算出「当前有效选中项」,回填权限树的勾选状态
- **权限配置审计**:管理员查看某用户有哪些独立于角色的额外授权或强制拒绝项,评估权限是否合理
**访问控制:**
- 用户查询自己的覆盖记录:无需管理员权限,直接允许
- 非超管查询他人记录:需先通过产品管理员身份检查(`RequireProductAdminFor`),再通过 `CheckManageAccess` 校验是否有权管理目标用户,防止普通成员枚举其他用户 ID
| 字段 | 类型 | 必填 | 说明 |
| ------ | ------ | ------ | ------ |
| userId | int64 | 是 | 目标用户 ID |
> `productCode` 从 JWT 中间件注入的产品上下文获取,无需在请求体传入。
**响应 data:** `{"perms": [{"permId": 1, "effect": "ALLOW"}, {"permId": 5, "effect": "DENY"}, ...]}`
仅返回状态为启用(`status=1`)的权限项的覆盖记录。前端须结合用户的角色继承权限 (`/user/detail` 中的 `roleIds` → `/role/detail` 中的 `permIds`) 计算最终有效选中状态:有效 = 角色继承 ∪ ALLOW - DENY。
#### POST /api/user/updateStatus — 更新用户状态(需管理权限)
冻结或解冻用户账号。冻结后用户立即无法访问任何产品。需通过 `CheckManageAccess` 权限检查。
**调用场景:**
- **员工离职冻结**:员工离职后冻结其账号,即时切断所有产品的访问权限(无需逐个产品移除成员)
- **安全事件处置**:发现账号被盗用或存在违规行为时,紧急冻结账号
- **临时停权**:员工休长假或处于考察期时临时冻结
- **解冻恢复**:冻结原因消除后恢复账号,用户无需重新配置角色和权限
**安全约束:** 不允许冻结自己;不允许冻结超级管理员;需要对目标用户有管理权限。
**副作用:** 状态变更后递增该用户的 `tokenVersion`(使所有已签发令牌失效)并清除缓存。
| 字段 | 类型 | 必填 | 说明 |
| ------ | ------ | ------ | ------ |
| id | int64 | 是 | 用户 ID |
| status | int64 | 是 | 1=正常 2=冻结 |
### 产品成员管理(需管理权限)
#### POST /api/member/add — 添加产品成员
将一个已有用户添加为指定产品的成员,并指定成员类型。用户成为产品成员后才能登录该产品。
**调用场景:**
- **新员工加入产品**:创建用户后,将其添加为 CRM/OA 等产品的成员,使其可以登录使用
- **研发人员跨产品参与**:研发人员需要参与另一个产品的开发,将其添加为该产品的 MEMBER(因为归属研发部门,自动获得全部权限)
- **提升为产品管理员**:将用户添加为 ADMIN 类型,使其拥有该产品下的角色/用户管理权限
- **设置产品开发者**:将用户添加为 DEVELOPER 类型,使其自动拥有该产品的全部功能权限但不具备管理权限
**安全约束:**
- 产品必须处于启用状态
- 操作者不可分配与自己同级或更高级的成员类型(如 ADMIN 不能将他人设为 ADMIN)
- 需通过 `CheckManageAccess` + `CheckMemberTypeAssignment` 权限检查
| 字段 | 类型 | 必填 | 说明 |
| ------ | ------ | ------ | ------ |
| productCode | string | 是 | 产品编码 |
| userId | int64 | 是 | 用户 ID |
| memberType | string | 是 | `ADMIN`(产品管理员)/ `DEVELOPER`(开发者,全权限)/ `MEMBER`(普通成员,按角色授权) |
**响应 data:** `{"id": 1}`
#### POST /api/member/update — 更新成员
修改成员类型或启禁用成员状态。
**调用场景:**
- **成员升级**:将普通 MEMBER 提升为 DEVELOPER 或 ADMIN(如项目负责人变动)
- **成员降级**:将 ADMIN 或 DEVELOPER 降级为 MEMBER(如不再负责管理工作)
- **禁用成员**:临时禁止某成员访问产品,但保留成员关系和角色绑定(恢复时权限不变)
- **恢复成员**:将禁用的成员重新启用
**安全约束:**
- 不可降级产品最后一个活跃 ADMIN——系统保证每个产品至少有一个 ADMIN,防止产品进入无管理状态
- 操作者不可将成员提升到与自己同级或更高的类型
- 需通过 `CheckManageAccess` + `CheckMemberTypeAssignment` 权限检查
**副作用:** 更新后清除该用户在该产品下的 UserDetailsLoader 缓存。若操作属于"降权"(`ADMIN`/`DEVELOPER` → `MEMBER`,或启用 → 禁用),还会递增该用户的 `tokenVersion` 并失效底层用户缓存,使其已签发的令牌在下次请求时被强制踢出,须重新登录。升权路径(`MEMBER` → `ADMIN` 等)不触发强制重登。
| 字段 | 类型 | 必填 | 说明 |
| ------ | ------ | ------ | ------ |
| id | int64 | 是 | 成员记录 ID |
| memberType | string | 否 | 成员类型(不传则不修改) |
| status | int64 | 否 | 状态(不传则不修改);两字段均不传时返回 400 |
#### POST /api/member/remove — 移除成员
将成员从产品中移除。移除后用户无法再登录该产品,其在该产品下的所有角色绑定和权限覆盖被级联清理。
**调用场景:**
- **员工不再参与某产品**:研发人员调岗不再负责 CRM 项目,从 CRM 产品成员中移除
- **外部协作结束**:外包人员合同到期,从产品中移除其成员资格
- **权限收紧**:安全审计后移除不应有权限的人员
**安全约束:**
- 不可移除产品最后一个活跃 ADMIN——防止产品进入无管理状态
- 需通过 `CheckManageAccess` 权限检查
**级联清理:** 在事务内同时清理 `sys_user_role`(该用户在该产品下的角色绑定)和 `sys_user_perm`(该用户在该产品下的权限覆盖),然后删除成员记录并清除缓存。
| 字段 | 类型 | 必填 | 说明 |
| ------ | ------ | ------ | ------ |
| id | int64 | 是 | 成员记录 ID |
#### POST /api/member/list — 成员列表
分页查询指定产品下的所有成员,含用户基本信息和成员类型/状态。
**调用场景:**
- **管理后台成员管理页面**:展示产品下的所有成员列表,供管理员查看、编辑、移除
- **成员权限概览**:管理员查看产品有哪些成员、各自是什么类型(ADMIN/DEVELOPER/MEMBER),评估权限分配是否合理
- **成员数量统计**:了解产品的使用人数和成员构成
| 字段 | 类型 | 必填 | 说明 |
| ------ | ------ | ------ | ------ |
| productCode | string | 是 | 产品编码 |
| page | int64 | 否 | 页码 |
| pageSize | int64 | 否 | 每页条数 |
**响应 data:** `{"total": N, "list": [MemberItem...]}`(含 userId、username、nickname、memberType、status 等)
#### POST /api/member/userProducts — 查询用户加入的产品列表
查询指定用户作为成员加入的所有产品,含成员类型和状态。
**调用场景:**
- **管理后台分配角色/权限前选择产品**:超管在「分配角色」或「设置权限」抽屉中,需要先选择产品,此接口提供该用户实际加入的产品列表,避免选择无效产品
**访问控制:**
- 超级管理员:可查任意用户
- 本人:可查自己
- 其他已登录用户:返回 403,防止枚举他人产品归属(IDOR)
| 字段 | 类型 | 必填 | 说明 |
| ------ | ------ | ------ | ------ |
| userId | int64 | 是 | 目标用户 ID |
**响应 data:** `{"list": [{"productCode", "productName", "memberType", "status"}]}`
### 文件上传(需鉴权)
#### POST /api/minio/upload — 上传文件
将文件上传到 MinIO 对象存储,返回访问 URL 和文件路径。支持按 `fileType` 分类存储到不同目录,自动去重(基于 MD5)。
**调用场景:**
- **用户头像上传**:用户在个人设置页面上传头像图片,`fileType` 传 `avatar`
- **其他业务文件**:根据配置的 `fileType` 上传不同类型的文件到对应目录
**请求格式:** `multipart/form-data`(非 JSON),最大 32MB
| 字段 | 类型 | 必填 | 说明 |
| ------ | ------ | ------ | ------ |
| file | file | 是 | 上传的文件 |
| fileType | string | 是 | 文件类型标识(需在服务端 Minio 配置中预定义,如 `avatar`) |
**安全约束:**
- `fileType` 必须在服务端配置的允许列表中,未配置的类型拒绝上传
- 可按 `fileType` 配置允许的 Content-Type 白名单(如头像仅允许 `image/jpeg`、`image/png`)
- 基于 MD5 去重:相同文件不会重复存储,直接返回已有文件的信息
**响应 data:** `{"url": "https://...", "path": "avatar/2026/05/13/xxx.png", "md5": "...", "size": 102400}`
---
## gRPC 接口文档
gRPC 服务定义见 `pb/perm.proto`,默认监听 `:10002`。所有 gRPC 错误使用标准 `status.Error(codes.Xxx, msg)` 格式。
gRPC 接口主要面向**产品后端服务间调用**,相比 HTTP 接口具有更高性能和更强的类型安全。推荐 Go 项目通过 `permclient` 包直接调用。
### SyncPermissions — 同步权限声明
产品后端在启动阶段全量上报权限列表,与 HTTP 的 `/perm/sync` 功能等价。
**调用场景:**
- **Go 微服务启动时自动同步**:在 `main` 函数中通过 gRPC 调用同步权限,相比 HTTP 调用减少序列化开销
- **服务网格内部调用**:在 Kubernetes 等容器编排环境中,服务间直接通过 gRPC 通信,无需经过 HTTP 网关
**认证方式:** 通过 `appKey`/`appSecret` 认证(与 HTTP 接口一致)。
### Login — 产品端登录
产品后端代理终端用户进行身份认证,返回 JWT 令牌对和权限列表。
**调用场景:**
- **Go 后端代理登录**:产品的 Go 后端收到用户登录请求后,通过 gRPC 调用权限系统进行身份验证,将返回的令牌下发给前端。相比 HTTP 调用延迟更低
- **服务间认证委托**:微服务架构中,网关服务通过 gRPC 调用权限系统完成用户认证
**安全约束:** `productCode` 必传;超级管理员被拒绝(必须走 adminLogin)。
### RefreshToken — 刷新令牌(轮转)
使用 refreshToken 换取全新令牌对,采用令牌轮转策略。
**调用场景:**
- **Go 后端代理续期**:产品后端代替前端执行令牌刷新(如 BFF 模式下,后端持有 refreshToken 并管理令牌生命周期)
- **长连接会话保活**:WebSocket 或长连接服务在 accessToken 即将过期时,通过 gRPC 刷新令牌维持会话
**安全约束:** 每次刷新递增 `tokenVersion`,旧令牌即时失效;产品禁用时返回 `codes.PermissionDenied`。
### VerifyToken — 验证令牌
服务端验证 accessToken 的有效性,返回用户信息和权限列表。
**调用场景:**
- **不支持本地 JWT 验证的语言/框架**:非 Go 语言的产品后端(如 Python、Node.js)可能未配置 JWT 密钥,通过 gRPC 调用权限系统进行服务端验证
- **需要实时权限的场景**:本地 JWT 验证只能拿到签发时的权限快照;如果需要实时反映权限变更(如管理员刚修改了用户角色),可通过此接口获取最新权限
- **关键操作的二次验证**:对敏感操作(如资金操作、数据删除),在本地 JWT 验证基础上再通过此接口做一次服务端校验,确保令牌未被吊销
**推荐实践:** 常规场景推荐本地 JWT 验证(零网络开销);仅在上述特殊场景使用此接口。
**安全约束:** 产品禁用时返回 `codes.PermissionDenied`。
### GetUserPerms — 获取用户权限
实时查询指定用户在指定产品下的最新权限码列表。
**调用场景:**
- **实时权限网关**:API 网关在每次请求时通过 gRPC 查询用户最新权限,实现实时权限控制(适用于权限变更需要立即生效的高安全场景)
- **后台任务权限校验**:异步任务/定时任务执行时,需要校验发起用户是否仍有执行权限
- **权限缓存刷新**:产品后端自行维护权限缓存,定期通过此接口刷新,确保与权限系统保持一致
- **管理后台权限预览**:管理员查看某用户在某产品下的实时权限,用于排查和审计
### Go SDK 使用示例
```go
import "perms-system-server/permclient"
client, err := permclient.NewPermClient("perm-system:10002")
resp, err := client.SyncPermissions(ctx, &pb.SyncPermissionsReq{...})
resp, err := client.Login(ctx, &pb.LoginReq{...})
resp, err := client.VerifyToken(ctx, &pb.VerifyTokenReq{AccessToken: token})
resp, err := client.GetUserPerms(ctx, &pb.GetUserPermsReq{...})
```
---
## 项目结构
```text
server/
├── perm.go # 入口(同时启动 HTTP + gRPC)
├── perm.api # HTTP API 定义(go-zero 标准)
├── perm.sql # 数据库 DDL
├── gen-api.sh # API 代码生成脚本
├── gen-model.sh # Model 代码生成脚本
├── run-test.sh # 测试运行脚本
├── test-design.md # 测试用例设计文档(1065 TC)
├── test-report.md # 测试报告
├── etc/
│ └── perm-api.yaml # 服务配置
├── pb/
│ ├── perm.proto # gRPC 接口定义
│ ├── perm.pb.go # protoc 生成
│ └── perm_grpc.pb.go # protoc 生成
├── permclient/
│ └── permclient.go # gRPC 客户端 SDK
├── cli/goctl/ # 自定义 goctl 模板
│ ├── api/handler.tpl # Handler 模板(含参数校验 → 400 处理)
│ └── model/ # Model 模板(含 *WithTx 事务查询方法)
└── internal/
├── config/config.go # 配置结构体
├── consts/consts.go # 全局常量(Status/MemberType/DeptType/TokenType 等)
├── response/response.go # 统一响应 + 错误码
├── util/ # 工具函数(分页、校验、JWT)
├── loaders/
│ └── userDetailsLoader.go # 用户详情加载器(Redis 缓存 + DB 回源)
├── middleware/
│ └── jwtauthMiddleware.go # JWT 鉴权中间件(token/tokenVersion 验证 + 用户/产品/成员状态检查 + UserDetails 注入)
├── svc/serviceContext.go # 依赖注入容器
├── server/permserver.go # gRPC 服务实现
├── types/types.go # 请求/响应结构体(goctl 生成)
├── handler/ # HTTP handler(按模块分组,goctl 生成)
│ ├── routes.go
│ ├── pub/ # 公开接口(登录/刷新令牌/同步权限)
│ ├── auth/ # 认证接口(用户信息/修改密码/注销)
│ ├── product/ # 产品管理
│ ├── dept/ # 部门管理
│ ├── perm/ # 权限管理
│ ├── role/ # 角色管理
│ ├── user/ # 用户管理
│ └── member/ # 产品成员
├── logic/ # 业务逻辑层(按模块分组)
│ ├── auth/
│ │ ├── jwt.go # JWT 生成/解析(含 tokenType 区分)
│ │ ├── perms.go # 权限计算核心逻辑
│ │ └── access.go # 集中式操作权限控制(超管/管理员/部门/级别检查)
│ ├── pub/ # 登录、刷新、同步
│ ├── product/ # 产品 CRUD
│ ├── dept/ # 部门 CRUD + 树
│ ├── perm/ # 权限列表
│ ├── role/ # 角色 CRUD + 绑定权限 + 级联删除
│ ├── user/ # 用户 CRUD + 绑定角色/权限
│ └── member/ # 成员管理 + 级联清理
└── model/ # 数据模型层(每张表独立目录)
├── models.go # Models 聚合结构体(统一初始化所有 Model 实例)
├── product/ # sys_product
├── dept/ # sys_dept
├── perm/ # sys_perm
├── role/ # sys_role
├── roleperm/ # sys_role_perm
├── user/ # sys_user
├── userperm/ # sys_user_perm
├── userrole/ # sys_user_role
└── productmember/ # sys_product_member
```
---
## 配置说明
配置文件位于 `etc/perm-api.yaml`:
| 配置项 | 说明 | 默认值 |
| -------- | ------ | -------- |
| `Host` / `Port` | HTTP 监听地址 | `0.0.0.0:10001` |
| `RpcServerConf.ListenOn` | gRPC 监听地址 | `0.0.0.0:10002` |
| `MySQL.DataSource` | MySQL 连接串 | — |
| `CacheRedis[].Host` | Redis 地址 | `127.0.0.1:6379` |
| `Auth.AccessSecret` | JWT accessToken 签名密钥 | — |
| `Auth.AccessExpire` | accessToken 有效期(秒) | `7200`(2h) |
| `Auth.RefreshSecret` | JWT refreshToken 签名密钥 | — |
| `Auth.RefreshExpire` | refreshToken 有效期(秒) | `604800`(7d) |
| `Auth.ManagementKey` | 管理后台登录密钥(`/auth/adminLogin` 接口验证) | — |
| `BehindProxy` | 是否部署在反向代理(Nginx 等)后面 | `false` |
| `Capjs.Enable` | cap.js 人机验证开关(`1` 启用,`0` 或不配置则使用图片验证码) | `0` |
| `Capjs.EndpointURL` | cap.js 服务地址(如 `https://cap.example.com`) | — |
| `Capjs.Key` | cap.js site key(前端挂件加载和 siteverify 路径使用) | — |
| `Capjs.Secret` | cap.js site secret(服务端 siteverify 验证使用,不可暴露给前端) | — |
> **生产环境部署前,务必修改 `AccessSecret`、`RefreshSecret` 和 `ManagementKey` 为安全的随机字符串,并确保产品后端的本地验证密钥与 `AccessSecret` 一致。`ManagementKey` 仅管理后台前端持有,不可泄露给产品端。启用 cap.js 时,`Capjs.Secret` 同样属于服务端私密配置,不得暴露给前端。**
### 反向代理部署
当服务部署在 Nginx 等反向代理后面时,需要开启 `BehindProxy` 配置以正确获取客户端真实 IP(用于登录限流等安全策略):
#### 1. 配置文件设置
```yaml
BehindProxy: true
```
#### 2. Nginx 配置要求
必须在 Nginx 中正确设置 `X-Real-IP` 并清除客户端可伪造的 `X-Forwarded-For`:
```nginx
server {
location /api/ {
proxy_pass http://127.0.0.1:10001;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For "";
proxy_set_header Host $host;
}
}
```
#### 安全说明
- `BehindProxy: false`(默认)— 仅使用 TCP 连接的 `RemoteAddr` 作为客户端 IP,适合服务直接暴露或在无法信任代理头的场景
- `BehindProxy: true` — 信任 Nginx 设置的 `X-Real-IP` 头获取真实客户端 IP,**必须**确保 Nginx 正确配置且外部无法绕过 Nginx 直连后端
- gRPC 接口不受此配置影响,始终通过 TCP 对端地址获取 IP
---
## 测试
### 测试概览
| 指标 | 数值 |
| ------ | ------ |
| 测试用例总数 | 1065 |
| 已覆盖 TC | 1065(100%) |
| 测试函数 | 1170 |
| 测试子用例 | 1309 |
| 测试包 | 28 |
| 通过率 | 100% |
测试覆盖范围:
- **REST API 测试** — 全部 HTTP 接口的正常路径、参数边界、错误路径
- **gRPC 接口测试** — 全部 5 个 RPC 方法的正常/异常场景
- **中间件测试** — JWT 鉴权、冻结账号拦截、UserDetails 注入、统一响应
- **访问控制测试** — access.go 全部函数的正面/负面测试
- **UserDetailsLoader 测试** — 缓存命中/miss、Clean/Del/BatchDel/CleanByProduct、权限计算
- **Logic 层访问控制负面测试** — 非超管/非管理员操作被拒绝
- **Model 层测试** — 9 个 Model 的 CRUD、事务、批量操作、唯一索引方法、自定义方法
- **Logic 层测试** — 业务逻辑单元测试(含 mock 测试)
- **工具函数测试** — 分页、邮箱/手机校验、JWT 生成解析
### 测试文档
| 文件 | 说明 |
| ------ | ------ |
| `test-design.md` | 测试用例设计文档(1065 个 TC,含测试场景、输入数据、预期结果) |
| `test-report.md` | 测试执行报告(含各包耗时、TC 明细、源码审计结果) |
### 运行测试
项目提供 `run-test.sh` 脚本,自动完成以下流程:
1. **连通性检查** — 验证 MySQL 和 Redis 是否可达,不可达则提前终止
2. **编译检查** — 先执行 `go build ./...`,编译失败则不跑测试
3. **执行测试** — 运行 `go test`,结束后汇总通过/失败的包数和耗时
```bash
# 运行全部测试
./run-test.sh
# 运行指定包的测试
./run-test.sh ./internal/logic/auth/...
./run-test.sh ./internal/model/...
# 查看帮助
./run-test.sh -h
```
#### 环境变量
| 变量 | 默认值 | 说明 |
| ------ | -------- | ------ |
| `TEST_VERBOSE` | 空(关闭) | 设为 `1` 开启详细输出(`-v`) |
| `TEST_RUN` | 空(全部) | 只运行匹配的测试函数名 |
| `TEST_TIMEOUT` | `600s` | 单个包的超时时间 |
| `TEST_COUNT` | `1` | 每个测试运行次数 |
```bash
# 组合使用示例
TEST_VERBOSE=1 TEST_RUN="TestLogin" ./run-test.sh ./internal/logic/pub/...
```
### 测试文件组织
测试文件遵循 Go 标准命名,与被测文件同目录:
```text
internal/logic/auth/
├── jwt.go # 源码
├── jwt_test.go # 测试
├── perms.go # 源码
├── perms_test.go # 测试
└── perms_mock_test.go # Mock 测试
internal/model/product/
├── sysProductModel.go # 自定义 Model
├── sysProductModel_gen.go # 生成的 Model
└── sysProductModel_test.go # 测试
```
---
## 开发指南
本项目严格遵循 go-zero 标准开发流程:
```mermaid
flowchart LR
A[编辑 perm.api] --> B["./gen-api.sh"]
B --> C[生成 handler + types + routes]
C --> D[在 logic 中填充业务逻辑]
D --> E[go build 验证]
F[编辑 perm.sql] --> G["./gen-model.sh"]
G --> H[生成 model 到对应子目录]
H --> I[在 sysXxxModel.go 中扩展自定义查询]
```
### 新增 HTTP 接口
```bash
# 1. 在 perm.api 中添加类型和路由定义
# 2. 重新生成(已有 logic 文件不会被覆盖)
./gen-api.sh
# 3. 在生成的 logic 文件中填写业务逻辑
```
### 新增数据表 Model
```bash
# 1. 在 perm.sql 中添加建表语句
# 2. 生成 model(使用自定义模板,包含 *WithTx 事务查询方法)
./gen-model.sh
# 3. 在生成的 sysXxxModel.go 中添加自定义查询方法
```
### 自定义 goctl 模板
项目使用自定义模板(`cli/goctl/`),主要增强:
- **Handler 模板** — 参数解析错误自动包装为 `400 Bad Request`(而非 500)
- **Model 模板** — 所有 `FindOne` 和 `FindOneByXxx` 方法均提供 `*WithTx` 事务变体,确保事务内查询使用 session 连接