# 统一权限管理系统(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", "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["③ 多维状态检查
用户/产品/成员状态"] L3 --> L4["④ Logic 层
操作权限控制"] 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) | 已登录即可 | 返回当前登录用户自己的信息 | | **认证接口** | | | | 用户注销 (logout) | 已登录即可 | 递增 tokenVersion,所有已签发令牌即时失效 | | **公开接口** | | | | 产品端登录 (login) | 无需鉴权 | 超级管理员被拒绝,productCode 必传 | | 管理后台登录 (adminLogin) | 无需鉴权 | 需验证 managementKey,限流在 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, 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": "客户关系管理"} ``` 响应中包含 `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"` 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/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:** 与登录接口相同。采用令牌轮转策略:每次刷新都会递增 `tokenVersion`,返回全新的 accessToken 和 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/logout — 用户注销 无请求参数。递增当前用户的 `tokenVersion`,使所有已签发的 access/refresh 令牌立即失效,并清除用户缓存。 **响应 data:** `null` #### 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 — 更新角色 非超级管理员不可降低角色的 `permsLevel`(即不可将数值改大)。 | 字段 | 类型 | 必填 | 说明 | | ------ | ------ | ------ | ------ | | 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` 权限检查。降级产品最后一个 ADMIN 时会被拒绝。 | 字段 | 类型 | 必填 | 说明 | | ------ | ------ | ------ | ------ | | id | int64 | 是 | 成员记录 ID | | memberType | string | 是 | 成员类型 | | status | int64 | 否 | 状态 | #### POST /api/member/remove — 移除成员 需通过 `CheckManageAccess` 权限检查。不可移除产品最后一个 ADMIN。级联清理该成员在该产品下的角色绑定和权限覆盖。 | 字段 | 类型 | 必填 | 说明 | | ------ | ------ | ------ | ------ | | 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` | 同步产品权限列表 | 产品启动时调用,通过 appKey/appSecret 认证 | | `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/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` | > **生产环境部署前,务必修改 `AccessSecret`、`RefreshSecret` 和 `ManagementKey` 为安全的随机字符串,并确保产品后端的本地验证密钥与 `AccessSecret` 一致。`ManagementKey` 仅管理后台前端持有,不可泄露给产品端。** ### 反向代理部署 当服务部署在 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 --- ## 测试 ### 测试概览 | 指标 | 数值 | | ------ | ------ | | 测试用例总数 | 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 连接