Няма описание

BaiLuoYan 1a87ae868b feat: 新增更新自身用户信息的接口 /auth/updateInfo преди 4 дни
cli c52b2cbd3f feat: 权限系统代码首次提交 преди 1 месец
etc 81f970b9cf fix: 修复rpc服务名称未配置导致服务无法启动的bug преди 4 дни
internal 1a87ae868b feat: 新增更新自身用户信息的接口 /auth/updateInfo преди 4 дни
pb 228ee889a9 feat: 静态代码审计,修复逻辑bug和安全漏洞 преди 4 седмици
permclient 9a4ad6dc05 feat: 静态代码审计,修复逻辑bug和安全漏洞 преди 4 седмици
.gitignore 3a7f161433 perf: gitigone преди 4 дни
.gitmodules c52b2cbd3f feat: 权限系统代码首次提交 преди 1 месец
README.md 1a87ae868b feat: 新增更新自身用户信息的接口 /auth/updateInfo преди 4 дни
audit-report.md fa46acc11e feat: 静态代码审计,修复逻辑bug和安全漏洞 преди 3 седмици
gen-api.sh c52b2cbd3f feat: 权限系统代码首次提交 преди 1 месец
gen-model.sh fe615b3d0f fix: 修复gen-model.sh中的示例 преди 6 дни
go.mod 0f6d4b1cb8 perf: 登录逻辑添加人机验证和图片验证码验证 преди 5 дни
go.sum 0f6d4b1cb8 perf: 登录逻辑添加人机验证和图片验证码验证 преди 5 дни
perm.api 1a87ae868b feat: 新增更新自身用户信息的接口 /auth/updateInfo преди 4 дни
perm.go b481ee3d26 feat: 静态代码审计,修复逻辑bug和安全漏洞 преди 4 седмици
perm.sql b481ee3d26 feat: 静态代码审计,修复逻辑bug和安全漏洞 преди 4 седмици
run-test.sh c52b2cbd3f feat: 权限系统代码首次提交 преди 1 месец
test-design.md fa46acc11e feat: 静态代码审计,修复逻辑bug和安全漏洞 преди 3 седмици
test-report.md fa46acc11e feat: 静态代码审计,修复逻辑bug和安全漏洞 преди 3 седмици

README.md

统一权限管理系统(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 时自动拒绝,防止产品进入无管理状态

系统架构

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

核心概念与实体关系

实体关系总览

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 自动上报 产品级

实体间关系详解

全局组织架构                   产品权限体系(按产品隔离)
┌─────────────────┐           ┌────────────────────────────────────┐
│  部门 (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 角色绑定 + 用户级覆盖计算

权限计算流程

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 系统电商后台。研发人员需要在产品间流动。

第一步:搭建组织架构

graph TD
    ROOT[公司] --> DEV[研发部 deptType=DEV]
    ROOT --> MKT[市场部 deptType=NORMAL]
    ROOT --> OPS[运营部 deptType=NORMAL]
    DEV --> FE[前端组]
    DEV --> BE[后端组]
    DEV --> QA[测试组]
# 创建顶级部门
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 的部门中的成员,只要被添加到某个产品下,就自动拥有该产品的全部权限。当从产品成员中移除时,权限立即收回。

第二步:注册产品

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 为例:

{
  "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": "导出报表"}
  ]
}

权限由产品代码自动管理,管理员无需手动创建。

第四步:创建角色并绑定权限

# 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 的开发

# 创建用户,归属研发部
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 — 只需移除成员关系

POST /api/member/remove  {"id": <张三在OA中的成员记录ID>}
# OA 的权限立即收回,CRM 的权限不受影响

场景 C:市场部李四 — 需要使用 CRM,分配销售角色

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:给李四额外权限 — 在角色基础上微调

# 额外给李四"导出报表"权限,但禁止他"创建订单"
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:外部临时协作人员 — 不属于任何部门

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]}  # 分配"客服"角色(只读)

权限配置决策树

flowchart TD
    Q1{该用户是研发人员?}
    Q1 -->|是| A1[归属研发部门 deptType=DEV]
    A1 --> A2["添加为产品成员(MEMBER)即可<br>自动拥有全部权限"]
    Q1 -->|否| Q2{该用户是产品管理员?}
    Q2 -->|是| A3["添加为产品成员(ADMIN)<br>自动拥有全部权限"]
    Q2 -->|否| Q3{需要精细权限控制?}
    Q3 -->|是| A4["添加为产品成员(MEMBER)"]
    A4 --> A5[分配角色 + 可选 ALLOW/DENY 覆盖]
    Q3 -->|否| A6[不添加为成员 = 无权限]

操作权限控制(Access Control)

系统内置一套集中式操作权限管控机制,对所有管理类接口实施多维度的访问控制,防止越权操作。

安全架构:四层纵深防护

flowchart LR
    REQ[HTTP 请求] --> L1["① JWT 中间件<br>解析 token / 验证签名"]
    L1 --> L2["② UserDetailsLoader<br>加载用户实时状态"]
    L2 --> L3["③ 多维状态检查<br>用户/产品/成员状态"]
    L3 --> L4["④ Logic 层<br>操作权限控制"]
    L4 --> BIZ[业务逻辑]
层级 组件 职责
第一层 JWT 中间件 解析 access token、验证签名、校验 tokenType、校验 tokenVersion
第二层 UserDetailsLoader 从 Redis 缓存或 DB 加载用户完整信息(含部门、角色、权限、产品状态)
第三层 多维状态检查 用户冻结(status ≠ 1)→ 403;产品禁用(productStatus ≠ 1)→ 403;成员无效(memberType 为空)→ 403
第四层 access.go 按接口类型检查超管/产品管理员/部门层级/权限级别

接口操作权限矩阵

接口 权限要求 额外检查
产品管理
创建产品 仅超级管理员
更新产品 仅超级管理员
部门管理
创建部门 仅超级管理员
更新部门 仅超级管理员
删除部门 仅超级管理员 有子部门时拒绝
角色管理
创建角色 超管 或 产品管理员 产品必须启用
更新角色 超管 或 产品管理员 非超管不可降低 permsLevel
删除角色 超管 或 产品管理员 级联删除关联数据
绑定角色权限 超管 或 产品管理员
用户管理
创建用户 超管 或 产品管理员
更新用户信息 仅本人 或 超管
冻结/解冻用户 通过 CheckManageAccess 不可冻结自己和超管
绑定角色 通过 CheckManageAccess 目标用户的成员状态必须启用
设置权限覆盖 通过 CheckManageAccess 目标用户的成员状态必须启用;产品必须启用
成员管理
添加成员 通过 CheckManageAccess + CheckMemberTypeAssignment 不可分配同级或更高类型;产品必须启用
更新成员 通过 CheckManageAccess + CheckMemberTypeAssignment 不可降级最后一个 ADMIN
移除成员 通过 CheckManageAccess 不可移除最后一个 ADMIN;级联清理角色/权限绑定
查询类接口
产品/部门/角色/用户/成员列表与详情 已登录即可
用户信息 (userInfo) 已登录即可 返回当前登录用户自己的信息
认证接口
修改自身信息 (updateInfo) 已登录即可 仅允许修改昵称/头像/邮箱/手机,userId 从 JWT 获取
用户注销 (logout) 已登录即可 递增 tokenVersion,所有已签发令牌即时失效
公开接口
产品端登录 (login) 无需鉴权 cap.js 未启用时需携带图片验证码;超级管理员被拒绝,productCode 必传
产品端 cap.js 登录 (user/login/cap) 无需鉴权 cap.js 必须已启用;提交 cap token 完成人机验证后登录
管理后台登录 (adminLogin) 无需鉴权 cap.js 未启用时需携带图片验证码;需验证 managementKey
管理后台 cap.js 登录 (adminLogin/cap) 无需鉴权 cap.js 必须已启用;提交 cap token 完成人机验证后登录
获取图片验证码 (captcha/get) 无需鉴权 cap.js 未启用时前端调用此接口展示图片验证码
获取 cap.js 端点 (capjs/endpoint) 无需鉴权 返回 cap.js 端点 URL;空串代表 cap.js 未启用,前端降级为图片验证码
刷新令牌 无需鉴权 令牌轮转:旧令牌即时失效,返回全新令牌对;产品禁用时拒绝
同步权限 无需鉴权 通过 appKey/appSecret 认证

CheckManageAccess — 用户管理权限检查

CheckManageAccess 是管理类操作的核心权限检查函数,执行多维度判定:

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{目标在操作者<br>本部门或子部门?}
    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+

部署步骤

# 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

初始化超级管理员

首次部署后,需手动在数据库中插入超级管理员账号:

INSERT INTO sys_user (username, password, nickname, isSuperAdmin, status, createTime, updateTime)
VALUES ('superadmin', '$2a$10$这里替换为bcrypt加密后的密码', '超级管理员', 1, 1, UNIX_TIMESTAMP(), UNIX_TIMESTAMP());

可使用任意 bcrypt 工具生成密码哈希,或在 Go 中执行:

hash, _ := bcrypt.GenerateFromPassword([]byte("your-password"), bcrypt.DefaultCost)
fmt.Println(string(hash))

产品接入指南

本节以 "CRM 系统"(编码 crm)为例,完整演示接入流程。

接入全景图

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

阶段一:注册产品

超级管理员登录后创建产品:

POST /api/product/create
{"code": "crm", "name": "CRM 系统", "remark": "客户关系管理"}

响应中包含 appKeyappSecretadminUseradminPassword请立即保存,后续不再展示

阶段二:产品启动时同步权限

CRM 后端在启动阶段上报全部权限列表。此接口无需 JWT,通过 appKey + appSecret 认证

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 启动流程中)

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
POST /api/auth/login
{"username": "zhangsan", "password": "123456", "productCode": "crm"}
方式 B:gRPC SDK(推荐 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 即可获取用户信息和权限列表,无需每次回调。

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. 业务接口中检查权限

func CreateOrderHandler(w http.ResponseWriter, r *http.Request) {
    claims := middleware.GetClaims(r.Context())
    if !hasPermission(claims.Perms, "order:create") {
        // 返回 403
        return
    }
    // 执行业务逻辑...
}

4. Token 续期

POST /api/auth/refreshToken
Authorization: Bearer eyJhbGciOi...   # refreshToken 放在 header 中
Content-Type: application/json

{"productCode": "crm"}

refreshToken 有效期默认 7 天。系统采用令牌轮转策略:每次刷新都返回全新的 accessToken 和 refreshToken,旧令牌即时失效(通过递增 tokenVersion 实现)。过期后必须重新登录。

接入检查清单

  • 已获取产品的 appKeyappSecret
  • 产品启动时调用 /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 结构:

{
  "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 启用时无需图片验证码,直接使用 /auth/login;cap.js 未启用时必须携带图片验证码,或改用 /user/login/cap(但此时 cap.js 未启用该路径也会被拒绝)
字段 类型 必填 说明
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/user/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 启用时无需图片验证码;cap.js 未启用时必须携带图片验证码,或改用 /auth/adminLogin/cap
字段 类型 必填 说明
username string 登录名
password string 密码
managementKey string 管理端密钥(配置文件中的 Auth.ManagementKey
captchaId string 条件必填 图片验证码 ID(cap.js 未启用时必填)
captchaCode string 条件必填 图片验证码答案(cap.js 未启用时必填)

响应 data: 与产品端登录接口相同。登录后不携带产品上下文,token 中 productCodeperms 为空。超管拥有所有产品的全部权限,不需要在 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 匹配,更新 nameremark
  • 列表中新增的权限:自动创建
  • 数据库中有但列表中没有的权限:自动禁用(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 平台新增一个租户,通过创建产品实现权限隔离

注意事项: 响应中的 appKeyappSecretadminUseradminPassword 仅在创建时返回一次,请立即保存。

字段 类型 必填 说明
code string 产品编码(全局唯一,仅允许字母/数字/下划线/中划线,不能以数字开头,上限 64 字符)
name string 产品名称
remark string 备注

响应 data: {"id", "code", "appKey", "appSecret", "adminUser", "adminPassword"}

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)。

调用场景:

  • 产品详情/编辑页面:管理后台进入产品编辑页时,先调用此接口获取当前数据回填表单
  • 查看产品接入凭证:超管需要查看某产品的 appKeyappSecret 仅创建时返回,此接口不包含)
字段 类型 必填 说明
id int64 产品 ID

部门管理(仅超级管理员)

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 — 更新部门

修改部门名称、排序、类型或启禁用状态。

调用场景:

  • 部门更名:组织调整时修改部门名称(如「技术部」改为「研发中心」)
  • 调整排序:变更部门在同级中的显示顺序
  • 变更部门类型:将普通部门升级为研发部门(NORMALDEV),或将研发部门降级为普通部门。类型变更立即影响该部门下所有用户在各产品中的权限
  • 禁用部门:部门撤销时禁用,保留历史数据

副作用: 更新操作会清除该部门下所有用户的 UserDetailsLoader 缓存,确保部门类型或状态变更即时反映到权限计算中。

字段 类型 必填 说明
id int64 部门 ID
name string 名称
sort int64 排序值
deptType string NORMALDEV
remark string 备注
status int64 状态

POST /api/dept/delete — 删除部门

永久删除一个部门。存在子部门时拒绝删除,必须先删除或迁移子部门。

调用场景:

  • 清理空部门:组织架构调整后,将人员迁移到新部门后删除空的旧部门
  • 撤销误创建的部门:部门创建有误时及时删除

约束: 存在子部门时返回 400 错误,防止意外删除整个子树。删除前应先通过部门树接口确认该部门无子节点。

字段 类型 必填 说明
id int64 部门 ID

POST /api/dept/tree — 部门树

返回完整的部门树形结构,包含所有层级的嵌套 childrendeptType 标识。

调用场景:

  • 管理后台组织架构页面:以树形控件展示公司完整的部门层级结构,支持展开/折叠
  • 用户创建/编辑时选择部门:表单中的部门选择器(树形下拉)需要此接口提供数据源
  • 权限检查的辅助参考:管理员查看部门层级关系,了解谁能管理谁(部门层级影响管理权限范围)

无请求参数。

权限管理

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 — 创建用户(超级管理员 或 产品管理员)

创建一个全局用户账号。用户是全局概念,创建后可通过「添加成员」接口加入多个产品。

调用场景:

  • 新员工入职:HR 或管理员为新员工创建账号,设置初始密码,并归属到对应部门
  • 批量建立账号:运维脚本遍历员工花名册,批量调用此接口创建用户账号
  • 外部协作人员:为外包或合作伙伴创建账号(可不设 deptId),后续通过成员管理授予特定产品访问权

注意事项: 创建用户后还需通过 /member/add 将其加入产品才能登录使用该产品。

字段 类型 必填 说明
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 — 更新用户(仅本人 或 超级管理员)

修改用户个人信息(昵称、邮箱、手机、部门等)。使用乐观锁防止并发更新冲突。

调用场景:

  • 用户修改个人资料:用户在个人设置页面更新昵称、邮箱、手机号
  • 超管调整用户部门:组织架构调整时,超管将用户从一个部门转移到另一个部门(修改 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 产品编码(传入则每个用户附带在该产品中的成员类型)
page int64 页码
pageSize int64 每页条数

POST /api/user/detail — 用户详情

查询单个用户的完整信息及其在当前产品下绑定的角色 ID 列表。

调用场景:

  • 用户编辑页面数据回填:管理后台进入用户编辑页时,调用此接口获取用户信息回填表单
  • 查看用户角色配置:管理员查看某用户当前绑定了哪些角色,评估权限是否合理
  • 用户权限排查:当用户反馈无法访问某功能时,管理员通过此接口查看用户的角色绑定情况
字段 类型 必填 说明
id int64 用户 ID

响应 data: 用户信息 + roleIds(该用户在当前产品下绑定的角色 ID 数组)。

POST /api/user/bindRoles — 绑定用户角色(需管理权限)

全量替换该用户在当前产品下的角色绑定。需通过 CheckManageAccess 权限检查。

调用场景:

  • 为新成员分配角色:用户被添加为产品成员后,管理员为其分配一个或多个角色
  • 调整用户职责:用户岗位变动时,更换其角色(如从「普通销售」升级为「销售经理」)
  • 多角色组合:为用户同时分配多个角色以组合权限(如同时拥有「订单管理」和「报表查看」角色)
  • 清空角色:传入空 roleIds 数组清除用户的所有角色绑定

安全约束: 操作者需要对目标用户有管理权限(通过 CheckManageAccess 校验超管/部门层级/权限级别);目标用户的成员状态必须启用。

副作用: 操作后清除目标用户在该产品下的 UserDetailsLoader 缓存。

字段 类型 必填 说明
userId int64 用户 ID
roleIds []int64 角色 ID 列表(全量替换)

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/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 缓存。

字段 类型 必填 说明
id int64 成员记录 ID
memberType string 成员类型
status int64 状态

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 等)


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 使用示例

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{...})

项目结构

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/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 验证使用,不可暴露给前端)

生产环境部署前,务必修改 AccessSecretRefreshSecretManagementKey 为安全的随机字符串,并确保产品后端的本地验证密钥与 AccessSecret 一致。ManagementKey 仅管理后台前端持有,不可泄露给产品端。启用 cap.js 时,Capjs.Secret 同样属于服务端私密配置,不得暴露给前端。

反向代理部署

当服务部署在 Nginx 等反向代理后面时,需要开启 BehindProxy 配置以正确获取客户端真实 IP(用于登录限流等安全策略):

1. 配置文件设置

BehindProxy: true

2. Nginx 配置要求

必须在 Nginx 中正确设置 X-Real-IP 并清除客户端可伪造的 X-Forwarded-For

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

测试

测试概览

指标 数值
测试用例总数 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,结束后汇总通过/失败的包数和耗时
# 运行全部测试
./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 每个测试运行次数
# 组合使用示例
TEST_VERBOSE=1 TEST_RUN="TestLogin" ./run-test.sh ./internal/logic/pub/...

测试文件组织

测试文件遵循 Go 标准命名,与被测文件同目录:

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 标准开发流程:

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 接口

# 1. 在 perm.api 中添加类型和路由定义
# 2. 重新生成(已有 logic 文件不会被覆盖)
./gen-api.sh
# 3. 在生成的 logic 文件中填写业务逻辑

新增数据表 Model

# 1. 在 perm.sql 中添加建表语句
# 2. 生成 model(使用自定义模板,包含 *WithTx 事务查询方法)
./gen-model.sh
# 3. 在生成的 sysXxxModel.go 中添加自定义查询方法

自定义 goctl 模板

项目使用自定义模板(cli/goctl/),主要增强:

  • Handler 模板 — 参数解析错误自动包装为 400 Bad Request(而非 500)
  • Model 模板 — 所有 FindOneFindOneByXxx 方法均提供 *WithTx 事务变体,确保事务内查询使用 session 连接