# 统一权限管理系统(Permission System Server)
集中式多产品权限管理平台后端服务。为多个产品提供统一的用户认证、权限管理、角色管理能力,产品通过 HTTP API 或 gRPC 接入。
## 核心特性
- **多产品隔离** — 权限、角色、成员按产品(`productCode`)隔离,一个账号可跨产品通用
- **灵活的权限模型** — 角色权限 + 用户级 ALLOW/DENY 覆盖,细粒度控制
- **研发部门自动授权** — 研发部门(`deptType=DEV`)的成员加入产品后自动拥有全部权限
- **双协议** — 同时提供 HTTP REST API(管理 UI)和 gRPC(产品后端高性能调用)
- **自动权限同步** — 产品启动时通过 API 自动上报权限列表,系统自动新增/更新/禁用
- **JWT 本地验证** — 登录获取 JWT,产品后端可本地验证,无需每次请求回调权限系统
- **登录端隔离** — 产品端(`/auth/login`)和管理后台(`/auth/adminLogin`)独立登录接口,超管仅能通过管理后台登录
## 系统架构
```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[获取用户权限] --> 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`),该账号是该产品的 `SUPER_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", "password": "123456", "nickname": "张三", "deptId": 1}
# 返回 userId=10
# 将张三添加为 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", "password": "123456", "nickname": "李四", "deptId": 2}
# 返回 userId=11
POST /api/member/add {"productCode": "crm", "userId": 11, "memberType": "MEMBER"}
POST /api/user/bindRoles {"userId": 11, "roleIds": [2]} # 分配"普通销售"角色
```
李四登录 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", "password": "123456", "nickname": "外包王五"}
# deptId 不传,不归属任何部门
POST /api/member/add {"productCode": "crm", "userId": 12, "memberType": "MEMBER"}
POST /api/user/bindRoles {"userId": 12, "roleIds": [3]} # 分配"客服"角色(只读)
```
### 权限配置决策树
```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["③ 冻结检查
Status != 1 → 403"]
L3 --> L4["④ Logic 层
操作权限控制"]
L4 --> BIZ[业务逻辑]
```
| 层级 | 组件 | 职责 |
| ------ | ------ | ------ |
| 第一层 | JWT 中间件 | 解析 access token、验证签名、校验 `tokenType` |
| 第二层 | UserDetailsLoader | 从 Redis 缓存或 DB 加载用户完整信息(含部门、角色、权限) |
| 第三层 | 用户状态检查 | 冻结账号(`status ≠ 1`)直接返回 403 |
| 第四层 | access.go | 按接口类型检查超管/产品管理员/部门层级/权限级别 |
### 接口操作权限矩阵
| 接口 | 权限要求 | 额外检查 |
| ------ | ---------- | ---------- |
| **产品管理** | | |
| 创建产品 | 仅超级管理员 | — |
| 更新产品 | 仅超级管理员 | — |
| **部门管理** | | |
| 创建部门 | 仅超级管理员 | — |
| 更新部门 | 仅超级管理员 | — |
| 删除部门 | 仅超级管理员 | 有子部门时拒绝 |
| **角色管理** | | |
| 创建角色 | 超管 或 产品管理员 | — |
| 更新角色 | 超管 或 产品管理员 | — |
| 删除角色 | 超管 或 产品管理员 | 级联删除关联数据 |
| 绑定角色权限 | 超管 或 产品管理员 | — |
| **用户管理** | | |
| 创建用户 | 超管 或 产品管理员 | — |
| 更新用户信息 | 仅本人 或 超管 | — |
| 冻结/解冻用户 | 通过 `CheckManageAccess` | 不可冻结自己和超管 |
| 绑定角色 | 通过 `CheckManageAccess` | — |
| 设置权限覆盖 | 通过 `CheckManageAccess` | — |
| **成员管理** | | |
| 添加成员 | 通过 `CheckManageAccess` + `CheckMemberTypeAssignment` | 不可分配同级或更高类型 |
| 更新成员 | 通过 `CheckManageAccess` + `CheckMemberTypeAssignment` | — |
| 移除成员 | 通过 `CheckManageAccess` | 级联清理角色/权限绑定 |
| **查询类接口** | | |
| 产品/部门/角色/用户/成员列表与详情 | 已登录即可 | — |
| 用户信息 (userInfo) | 已登录即可 | 返回当前登录用户自己的信息 |
| **公开接口** | | |
| 产品端登录 (login) | 无需鉴权 | 超级管理员被拒绝,productCode 必传 |
| 管理后台登录 (adminLogin) | 无需鉴权 | 需验证 managementKey |
| 刷新令牌 / 同步权限 | 无需鉴权 | 同步权限通过 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 |
| `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 | 该部门下所有用户的缓存 |
---
## 快速开始
### 环境要求
- 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": "客户关系管理"}
```
响应中包含 `appKey`、`appSecret`、`adminUser`、`adminPassword`,**请立即保存,后续不再展示**。
### 阶段二:产品启动时同步权限
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"`
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"`。
#### 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 天,刷新时返回原始 refreshToken(不重新签发),过期后必须重新登录。这确保了 token 有固定的生命周期。
### 接入检查清单
- [ ] 已获取产品的 `appKey` 和 `appSecret`
- [ ] 产品启动时调用 `/api/perm/sync` 同步了全部权限
- [ ] 后端配置了与权限系统相同的 `AccessSecret`
- [ ] 登录接口正确调用权限系统并返回 token
- [ ] JWT 鉴权中间件已加入受保护路由,并验证 `tokenType == "access"`
- [ ] 业务接口中根据 `claims.Perms` 做了权限校验
- [ ] 前端在 `accessToken` 过期时能自动调用 `refreshToken` 续期
---
## 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/auth/login — 产品端登录
**供产品后端调用**,超级管理员无法通过此接口登录。
| 字段 | 类型 | 必填 | 说明 |
| ------ | ------ | ------ | ------ |
| username | string | 是 | 登录名 |
| password | string | 是 | 密码 |
| productCode | string | 是 | 产品编码 |
**响应 data:**
| 字段 | 类型 | 说明 |
| ------ | ------ | ------ |
| accessToken | string | 访问令牌 |
| refreshToken | string | 刷新令牌 |
| expires | int64 | accessToken 过期时间(Unix 时间戳,秒) |
| userInfo | object | 用户信息(含 perms 权限码数组) |
#### POST /api/auth/adminLogin — 管理后台登录
**仅供权限系统管理后台使用**,需要传入配置的 `managementKey` 进行身份验证。
| 字段 | 类型 | 必填 | 说明 |
| ------ | ------ | ------ | ------ |
| username | string | 是 | 登录名 |
| password | string | 是 | 密码 |
| managementKey | string | 是 | 管理端密钥(配置文件中的 `Auth.ManagementKey`) |
**响应 data:** 与产品端登录接口相同。登录后不携带产品上下文,token 中 `productCode` 和 `perms` 为空。
#### POST /api/auth/refreshToken — 刷新令牌
通过 `Authorization: Bearer {refreshToken}` 请求头传入 refresh token。
| 字段 | 类型 | 必填 | 说明 |
| ------ | ------ | ------ | ------ |
| Authorization | header | 是 | `Bearer {refreshToken}` |
| productCode | string | 否 | 切换产品上下文时传入(Body) |
**响应 data:** 与登录接口相同。注意返回的 `refreshToken` 是原始值(不重新签发),refresh token 有固定有效期,过期后需重新登录。
#### POST /api/perm/sync — 同步产品权限
通过 appKey/appSecret 认证,产品后端上报全量权限列表。
| 字段 | 类型 | 必填 | 说明 |
| ------ | ------ | ------ | ------ |
| 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/userInfo — 获取当前用户信息
无请求参数。**响应 data:** `UserInfo` 对象。
#### POST /api/auth/changePassword — 修改密码
| 字段 | 类型 | 必填 | 说明 |
| ------ | ------ | ------ | ------ |
| oldPassword | string | 是 | 原密码 |
| newPassword | string | 是 | 新密码(6-72 字符,不能与旧密码相同) |
### 产品管理(仅超级管理员)
#### POST /api/product/create — 创建产品
| 字段 | 类型 | 必填 | 说明 |
| ------ | ------ | ------ | ------ |
| code | string | 是 | 产品编码(全局唯一) |
| name | string | 是 | 产品名称 |
| remark | string | 否 | 备注 |
**响应 data:** `{"id", "code", "appKey", "appSecret", "adminUser", "adminPassword"}`
#### POST /api/product/update — 更新产品
| 字段 | 类型 | 必填 | 说明 |
| ------ | ------ | ------ | ------ |
| 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 — 产品详情
| 字段 | 类型 | 必填 | 说明 |
| ------ | ------ | ------ | ------ |
| id | int64 | 是 | 产品 ID |
### 部门管理(仅超级管理员)
#### POST /api/dept/create — 创建部门
| 字段 | 类型 | 必填 | 说明 |
| ------ | ------ | ------ | ------ |
| parentId | int64 | 是 | 父部门 ID,0 表示顶级部门 |
| name | string | 是 | 部门名称 |
| sort | int64 | 否 | 排序值 |
| deptType | string | 否 | `NORMAL`(默认)或 `DEV`(研发部门) |
| remark | string | 否 | 备注 |
**响应 data:** `{"id": 1}`
#### POST /api/dept/update — 更新部门
| 字段 | 类型 | 必填 | 说明 |
| ------ | ------ | ------ | ------ |
| id | int64 | 是 | 部门 ID |
| name | string | 是 | 名称 |
| sort | int64 | 否 | 排序值 |
| deptType | string | 否 | `NORMAL` 或 `DEV` |
| remark | string | 否 | 备注 |
| status | int64 | 否 | 状态 |
#### POST /api/dept/delete — 删除部门
存在子部门时无法删除。
| 字段 | 类型 | 必填 | 说明 |
| ------ | ------ | ------ | ------ |
| id | int64 | 是 | 部门 ID |
#### POST /api/dept/tree — 部门树
无请求参数。返回完整的部门树形结构(含 `children` 嵌套和 `deptType`)。
### 权限管理
#### POST /api/perm/list — 权限列表
| 字段 | 类型 | 必填 | 说明 |
| ------ | ------ | ------ | ------ |
| productCode | string | 是 | 产品编码 |
| page | int64 | 否 | 页码 |
| pageSize | int64 | 否 | 每页条数 |
**响应 data:** `{"total": N, "list": [PermItem...]}`
### 角色管理(超级管理员 或 产品管理员)
#### POST /api/role/create — 创建角色
| 字段 | 类型 | 必填 | 说明 |
| ------ | ------ | ------ | ------ |
| productCode | string | 是 | 所属产品编码 |
| name | string | 是 | 角色名(产品内唯一) |
| remark | string | 否 | 备注 |
| permsLevel | int64 | 是 | 权限等级 |
**响应 data:** `{"id": 1}`
#### POST /api/role/update — 更新角色
| 字段 | 类型 | 必填 | 说明 |
| ------ | ------ | ------ | ------ |
| id | int64 | 是 | 角色 ID |
| name | string | 是 | 角色名 |
| remark | string | 否 | 备注 |
| permsLevel | int64 | 是 | 权限等级 |
| status | int64 | 否 | 状态 |
#### POST /api/role/delete — 删除角色
级联删除角色关联的权限绑定和用户绑定。
| 字段 | 类型 | 必填 | 说明 |
| ------ | ------ | ------ | ------ |
| id | int64 | 是 | 角色 ID |
#### POST /api/role/list — 角色列表
| 字段 | 类型 | 必填 | 说明 |
| ------ | ------ | ------ | ------ |
| productCode | string | 是 | 产品编码 |
| page | int64 | 否 | 页码 |
| pageSize | int64 | 否 | 每页条数 |
#### POST /api/role/detail — 角色详情
返回角色信息及绑定的权限 ID 列表(`permIds`)。
| 字段 | 类型 | 必填 | 说明 |
| ------ | ------ | ------ | ------ |
| id | int64 | 是 | 角色 ID |
#### POST /api/role/bindPerms — 绑定角色权限
全量替换该角色的权限。
| 字段 | 类型 | 必填 | 说明 |
| ------ | ------ | ------ | ------ |
| roleId | int64 | 是 | 角色 ID |
| permIds | []int64 | 是 | 权限 ID 列表(空数组清空绑定) |
### 用户管理
#### POST /api/user/create — 创建用户(超级管理员 或 产品管理员)
| 字段 | 类型 | 必填 | 说明 |
| ------ | ------ | ------ | ------ |
| username | string | 是 | 登录名(唯一) |
| password | string | 是 | 密码(6-72 字符) |
| nickname | string | 否 | 昵称 |
| email | string | 否 | 邮箱(需合法格式) |
| phone | string | 否 | 手机号(7-15 位数字,可含 `+` 前缀) |
| remark | string | 否 | 备注 |
| deptId | int64 | 否 | 部门 ID |
**响应 data:** `{"id": 1}`
#### POST /api/user/update — 更新用户(仅本人 或 超级管理员)
支持字段清空:传 `""` 清空字符串字段,传 `0` 清空 deptId,不传字段则不更新。
| 字段 | 类型 | 必填 | 说明 |
| ------ | ------ | ------ | ------ |
| id | int64 | 是 | 用户 ID |
| nickname | *string | 否 | 昵称 |
| email | *string | 否 | 邮箱 |
| phone | *string | 否 | 手机号 |
| remark | *string | 否 | 备注 |
| deptId | *int64 | 否 | 部门 ID(传 0 取消部门) |
| status | int64 | 否 | 状态 |
#### POST /api/user/list — 用户列表
| 字段 | 类型 | 必填 | 说明 |
| ------ | ------ | ------ | ------ |
| productCode | string | 否 | 产品编码(传入则附带成员类型) |
| page | int64 | 否 | 页码 |
| pageSize | int64 | 否 | 每页条数 |
#### POST /api/user/detail — 用户详情
返回用户信息及绑定的角色 ID 列表(`roleIds`)。
| 字段 | 类型 | 必填 | 说明 |
| ------ | ------ | ------ | ------ |
| id | int64 | 是 | 用户 ID |
#### POST /api/user/bindRoles — 绑定用户角色(需管理权限)
需通过 `CheckManageAccess` 权限检查。全量替换该用户的角色。
| 字段 | 类型 | 必填 | 说明 |
| ------ | ------ | ------ | ------ |
| userId | int64 | 是 | 用户 ID |
| roleIds | []int64 | 是 | 角色 ID 列表 |
#### POST /api/user/setPerms — 设置用户权限覆盖(需管理权限)
需通过 `CheckManageAccess` 权限检查。全量替换用户级别的 ALLOW/DENY 权限覆盖。
| 字段 | 类型 | 必填 | 说明 |
| ------ | ------ | ------ | ------ |
| userId | int64 | 是 | 用户 ID |
| perms | array | 是 | 权限覆盖列表 |
| perms[].permId | int64 | 是 | 权限 ID |
| perms[].effect | string | 是 | `ALLOW` 或 `DENY` |
#### POST /api/user/updateStatus — 更新用户状态(需管理权限)
需通过 `CheckManageAccess` 权限检查。不允许冻结自己和超级管理员。
| 字段 | 类型 | 必填 | 说明 |
| ------ | ------ | ------ | ------ |
| id | int64 | 是 | 用户 ID |
| status | int64 | 是 | 1=正常 2=冻结 |
### 产品成员管理(需管理权限)
#### POST /api/member/add — 添加产品成员
需通过 `CheckManageAccess` + `CheckMemberTypeAssignment` 权限检查。不可分配与自己同级或更高级的成员类型。
| 字段 | 类型 | 必填 | 说明 |
| ------ | ------ | ------ | ------ |
| productCode | string | 是 | 产品编码 |
| userId | int64 | 是 | 用户 ID |
| memberType | string | 是 | `ADMIN` / `DEVELOPER` / `MEMBER` |
**响应 data:** `{"id": 1}`
#### POST /api/member/update — 更新成员
需通过 `CheckManageAccess` + `CheckMemberTypeAssignment` 权限检查。
| 字段 | 类型 | 必填 | 说明 |
| ------ | ------ | ------ | ------ |
| id | int64 | 是 | 成员记录 ID |
| memberType | string | 是 | 成员类型 |
| status | int64 | 否 | 状态 |
#### POST /api/member/remove — 移除成员
需通过 `CheckManageAccess` 权限检查。级联清理该成员在该产品下的角色绑定和权限覆盖。
| 字段 | 类型 | 必填 | 说明 |
| ------ | ------ | ------ | ------ |
| id | int64 | 是 | 成员记录 ID |
#### POST /api/member/list — 成员列表
| 字段 | 类型 | 必填 | 说明 |
| ------ | ------ | ------ | ------ |
| productCode | string | 是 | 产品编码 |
| page | int64 | 否 | 页码 |
| pageSize | int64 | 否 | 每页条数 |
**响应 data:** `{"total": N, "list": [MemberItem...]}`
---
## gRPC 接口文档
gRPC 服务定义见 `pb/perm.proto`,默认监听 `:10002`。
| 方法 | 说明 | 使用场景 |
| ------ | ------ | ---------- |
| `SyncPermissions` | 同步产品权限列表 | 产品启动时调用 |
| `Login` | 产品端登录 | 产品后端代理用户登录(productCode 必传,超管被拒绝) |
| `RefreshToken` | 刷新令牌 | accessToken 过期续期 |
| `VerifyToken` | 验证令牌 | 产品后端验证用户 token(可选,推荐本地 JWT 验证) |
| `GetUserPerms` | 获取用户权限 | 实时查询用户最新权限 |
所有 gRPC 错误使用标准 `status.Error(codes.Xxx, msg)` 格式。
Go 项目可直接引用 `permclient` 包:
```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})
```
---
## 项目结构
```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 # 测试用例设计文档(433 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 验证 + 状态检查 + 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` 接口验证) | — |
> **生产环境部署前,务必修改 `AccessSecret`、`RefreshSecret` 和 `ManagementKey` 为安全的随机字符串,并确保产品后端的本地验证密钥与 `AccessSecret` 一致。`ManagementKey` 仅管理后台前端持有,不可泄露给产品端。**
---
## 测试
### 测试概览
| 指标 | 数值 |
| ------ | ------ |
| 测试用例总数 | 499 |
| 已覆盖 TC | 498(99.8%) |
| 测试函数 | 662 |
| 测试子用例 | 744 |
| 测试包 | 23 |
| 通过率 | 99.7% |
测试覆盖范围:
- **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` | 测试用例设计文档(499 个 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 连接