BaiLuoYan 1 месяц назад
Родитель
Сommit
3e40294a9f
3 измененных файлов с 178 добавлено и 191 удалено
  1. 55 69
      README.md
  2. 64 64
      test-design.md
  3. 59 58
      test-report.md

+ 55 - 69
README.md

@@ -126,7 +126,7 @@ erDiagram
 ### 五大核心实体
 
 | 实体 | 说明 | 隔离范围 |
-|------|------|----------|
+| ------ | ------ | ---------- |
 | **产品 (Product)** | 接入权限系统的业务系统(如 CRM、OA),每个产品有独立的 `appKey`/`appSecret` | 全局 |
 | **部门 (Dept)** | 组织架构树形结构,支持无限层级嵌套。`deptType=DEV` 的研发部门有特殊权限逻辑 | 全局 |
 | **用户 (User)** | 全局账号,通过 `deptId` 归属部门,通过成员关系加入不同产品 | 全局 |
@@ -135,7 +135,7 @@ erDiagram
 
 ### 实体间关系详解
 
-```
+```text
 全局组织架构                   产品权限体系(按产品隔离)
 ┌─────────────────┐           ┌────────────────────────────────────┐
 │  部门 (Dept)     │           │  产品 (Product)                    │
@@ -169,7 +169,7 @@ erDiagram
 ### 成员类型与权限层级
 
 | 优先级 | 类型 | 权限范围 | 来源 |
-|--------|------|----------|------|
+| -------- | ------ | ---------- | ------ |
 | 1 | `SUPER_ADMIN` | **所有产品**的全部权限 | `sys_user.isSuperAdmin = 1` |
 | 2 | `DEVELOPER` | **该产品**全部权限 | `sys_product_member.memberType` |
 | 3 | `ADMIN` | **该产品**全部权限 | `sys_product_member.memberType` |
@@ -380,7 +380,7 @@ flowchart LR
 ```
 
 | 层级 | 组件 | 职责 |
-|------|------|------|
+| ------ | ------ | ------ |
 | 第一层 | JWT 中间件 | 解析 access token、验证签名、校验 `tokenType` |
 | 第二层 | UserDetailsLoader | 从 Redis 缓存或 DB 加载用户完整信息(含部门、角色、权限) |
 | 第三层 | 用户状态检查 | 冻结账号(`status ≠ 1`)直接返回 403 |
@@ -389,33 +389,33 @@ flowchart LR
 ### 接口操作权限矩阵
 
 | 接口 | 权限要求 | 额外检查 |
-|------|----------|----------|
-| **产品管理** |||
+| ------ | ---------- | ---------- |
+| **产品管理** | | |
 | 创建产品 | 仅超级管理员 | — |
 | 更新产品 | 仅超级管理员 | — |
-| **部门管理** |||
+| **部门管理** | | |
 | 创建部门 | 仅超级管理员 | — |
 | 更新部门 | 仅超级管理员 | — |
 | 删除部门 | 仅超级管理员 | 有子部门时拒绝 |
-| **角色管理** |||
+| **角色管理** | | |
 | 创建角色 | 超管 或 产品管理员 | — |
 | 更新角色 | 超管 或 产品管理员 | — |
 | 删除角色 | 超管 或 产品管理员 | 级联删除关联数据 |
 | 绑定角色权限 | 超管 或 产品管理员 | — |
-| **用户管理** |||
+| **用户管理** | | |
 | 创建用户 | 超管 或 产品管理员 | — |
 | 更新用户信息 | 仅本人 或 超管 | — |
 | 冻结/解冻用户 | 通过 `CheckManageAccess` | 不可冻结自己和超管 |
 | 绑定角色 | 通过 `CheckManageAccess` | — |
 | 设置权限覆盖 | 通过 `CheckManageAccess` | — |
-| **成员管理** |||
+| **成员管理** | | |
 | 添加成员 | 通过 `CheckManageAccess` + `CheckMemberTypeAssignment` | 不可分配同级或更高类型 |
 | 更新成员 | 通过 `CheckManageAccess` + `CheckMemberTypeAssignment` | — |
 | 移除成员 | 通过 `CheckManageAccess` | 级联清理角色/权限绑定 |
-| **查询类接口** |||
+| **查询类接口** | | |
 | 产品/部门/角色/用户/成员列表与详情 | 已登录即可 | — |
 | 用户信息 (userInfo) | 已登录即可 | 返回当前登录用户自己的信息 |
-| **公开接口** |||
+| **公开接口** | | |
 | 登录 / 刷新令牌 / 同步权限 | 无需鉴权 | 同步权限通过 appKey/appSecret 认证 |
 
 ### CheckManageAccess — 用户管理权限检查
@@ -460,7 +460,7 @@ flowchart TD
 **加载的完整数据**:
 
 | 数据来源 | 加载的字段 |
-|----------|-----------|
+| ---------- | ----------- |
 | `sys_user` | userId, username, nickname, avatar, email, phone, remark, isSuperAdmin, mustChangePassword, status |
 | `sys_dept` | deptId, deptName, deptPath, deptType |
 | `sys_product` | productCode, productName |
@@ -475,7 +475,7 @@ flowchart TD
 - **主动失效**:所有写操作均触发对应的缓存清除
 
 | 操作 | 失效方法 | 失效范围 |
-|------|----------|----------|
+| ------ | ---------- | ---------- |
 | 更新用户信息 / 冻结解冻 / 修改密码 | `Clean(userId)` | 该用户所有产品缓存 |
 | 设置用户权限覆盖 / 添加成员 / 更新成员 | `Del(userId, productCode)` | 该用户在指定产品的缓存 |
 | 更新角色 / 删除角色 / 绑定角色权限 | `BatchDel(userIds, productCode)` | 受影响用户在指定产品的缓存 |
@@ -634,14 +634,14 @@ func syncPermsOnStartup(permSystemURL, appKey, appSecret string) {
 
 #### 1. 用户登录
 
-**方式 A:HTTP API**
+##### 方式 A:HTTP API
 
 ```bash
 POST /api/auth/login
 {"username": "zhangsan", "password": "123456", "productCode": "crm"}
 ```
 
-**方式 B:gRPC SDK(推荐 Go 项目)**
+##### 方式 B:gRPC SDK(推荐 Go 项目)
 
 ```go
 import "perms-system-server/permclient"
@@ -723,7 +723,7 @@ Content-Type: application/json
 ```
 
 | 字段 | 类型 | 说明 |
-|------|------|------|
+| ------ | ------ | ------ |
 | `code` | int | 业务状态码,`0` 表示成功,非零表示失败 |
 | `msg` | string | 状态描述 |
 | `data` | object/null | 业务数据,失败时无此字段 |
@@ -731,7 +731,7 @@ Content-Type: application/json
 ### 错误码一览
 
 | Code | 语义 | 典型场景 |
-|------|------|----------|
+| ------ | ------ | ---------- |
 | `0` | 成功 | 所有正常响应 |
 | `400` | 请求不合法 | 参数缺失/格式错误、原密码错误、存在子部门无法删除等 |
 | `401` | 未认证 | 未登录、token 无效/过期/类型错误、用户名密码错误 |
@@ -745,7 +745,7 @@ Content-Type: application/json
 #### POST /api/auth/login — 用户登录
 
 | 字段 | 类型 | 必填 | 说明 |
-|------|------|------|------|
+| ------ | ------ | ------ | ------ |
 | username | string | 是 | 登录名 |
 | password | string | 是 | 密码 |
 | productCode | string | 否 | 产品编码,传入则返回该产品的权限列表 |
@@ -753,7 +753,7 @@ Content-Type: application/json
 **响应 data:**
 
 | 字段 | 类型 | 说明 |
-|------|------|------|
+| ------ | ------ | ------ |
 | accessToken | string | 访问令牌 |
 | refreshToken | string | 刷新令牌 |
 | expires | int64 | accessToken 过期时间(Unix 时间戳,秒) |
@@ -764,7 +764,7 @@ Content-Type: application/json
 通过 `Authorization: Bearer {refreshToken}` 请求头传入 refresh token。
 
 | 字段 | 类型 | 必填 | 说明 |
-|------|------|------|------|
+| ------ | ------ | ------ | ------ |
 | Authorization | header | 是 | `Bearer {refreshToken}` |
 | productCode | string | 否 | 切换产品上下文时传入(Body) |
 
@@ -775,7 +775,7 @@ Content-Type: application/json
 通过 appKey/appSecret 认证,产品后端上报全量权限列表。
 
 | 字段 | 类型 | 必填 | 说明 |
-|------|------|------|------|
+| ------ | ------ | ------ | ------ |
 | appKey | string | 是 | 产品接入密钥 |
 | appSecret | string | 是 | 产品签名密钥 |
 | perms | array | 是 | 权限列表 |
@@ -794,7 +794,7 @@ Content-Type: application/json
 #### POST /api/auth/changePassword — 修改密码
 
 | 字段 | 类型 | 必填 | 说明 |
-|------|------|------|------|
+| ------ | ------ | ------ | ------ |
 | oldPassword | string | 是 | 原密码 |
 | newPassword | string | 是 | 新密码(6-72 字符,不能与旧密码相同) |
 
@@ -803,7 +803,7 @@ Content-Type: application/json
 #### POST /api/product/create — 创建产品
 
 | 字段 | 类型 | 必填 | 说明 |
-|------|------|------|------|
+| ------ | ------ | ------ | ------ |
 | code | string | 是 | 产品编码(全局唯一) |
 | name | string | 是 | 产品名称 |
 | remark | string | 否 | 备注 |
@@ -813,7 +813,7 @@ Content-Type: application/json
 #### POST /api/product/update — 更新产品
 
 | 字段 | 类型 | 必填 | 说明 |
-|------|------|------|------|
+| ------ | ------ | ------ | ------ |
 | id | int64 | 是 | 产品 ID |
 | name | string | 是 | 产品名称 |
 | remark | string | 否 | 备注 |
@@ -822,7 +822,7 @@ Content-Type: application/json
 #### POST /api/product/list — 产品列表
 
 | 字段 | 类型 | 必填 | 说明 |
-|------|------|------|------|
+| ------ | ------ | ------ | ------ |
 | page | int64 | 否 | 页码,默认 1 |
 | pageSize | int64 | 否 | 每页条数,默认 20,上限 100 |
 
@@ -831,7 +831,7 @@ Content-Type: application/json
 #### POST /api/product/detail — 产品详情
 
 | 字段 | 类型 | 必填 | 说明 |
-|------|------|------|------|
+| ------ | ------ | ------ | ------ |
 | id | int64 | 是 | 产品 ID |
 
 ### 部门管理(仅超级管理员)
@@ -839,7 +839,7 @@ Content-Type: application/json
 #### POST /api/dept/create — 创建部门
 
 | 字段 | 类型 | 必填 | 说明 |
-|------|------|------|------|
+| ------ | ------ | ------ | ------ |
 | parentId | int64 | 是 | 父部门 ID,0 表示顶级部门 |
 | name | string | 是 | 部门名称 |
 | sort | int64 | 否 | 排序值 |
@@ -851,7 +851,7 @@ Content-Type: application/json
 #### POST /api/dept/update — 更新部门
 
 | 字段 | 类型 | 必填 | 说明 |
-|------|------|------|------|
+| ------ | ------ | ------ | ------ |
 | id | int64 | 是 | 部门 ID |
 | name | string | 是 | 名称 |
 | sort | int64 | 否 | 排序值 |
@@ -864,7 +864,7 @@ Content-Type: application/json
 存在子部门时无法删除。
 
 | 字段 | 类型 | 必填 | 说明 |
-|------|------|------|------|
+| ------ | ------ | ------ | ------ |
 | id | int64 | 是 | 部门 ID |
 
 #### POST /api/dept/tree — 部门树
@@ -876,7 +876,7 @@ Content-Type: application/json
 #### POST /api/perm/list — 权限列表
 
 | 字段 | 类型 | 必填 | 说明 |
-|------|------|------|------|
+| ------ | ------ | ------ | ------ |
 | productCode | string | 是 | 产品编码 |
 | page | int64 | 否 | 页码 |
 | pageSize | int64 | 否 | 每页条数 |
@@ -888,7 +888,7 @@ Content-Type: application/json
 #### POST /api/role/create — 创建角色
 
 | 字段 | 类型 | 必填 | 说明 |
-|------|------|------|------|
+| ------ | ------ | ------ | ------ |
 | productCode | string | 是 | 所属产品编码 |
 | name | string | 是 | 角色名(产品内唯一) |
 | remark | string | 否 | 备注 |
@@ -899,7 +899,7 @@ Content-Type: application/json
 #### POST /api/role/update — 更新角色
 
 | 字段 | 类型 | 必填 | 说明 |
-|------|------|------|------|
+| ------ | ------ | ------ | ------ |
 | id | int64 | 是 | 角色 ID |
 | name | string | 是 | 角色名 |
 | remark | string | 否 | 备注 |
@@ -911,13 +911,13 @@ Content-Type: application/json
 级联删除角色关联的权限绑定和用户绑定。
 
 | 字段 | 类型 | 必填 | 说明 |
-|------|------|------|------|
+| ------ | ------ | ------ | ------ |
 | id | int64 | 是 | 角色 ID |
 
 #### POST /api/role/list — 角色列表
 
 | 字段 | 类型 | 必填 | 说明 |
-|------|------|------|------|
+| ------ | ------ | ------ | ------ |
 | productCode | string | 是 | 产品编码 |
 | page | int64 | 否 | 页码 |
 | pageSize | int64 | 否 | 每页条数 |
@@ -927,7 +927,7 @@ Content-Type: application/json
 返回角色信息及绑定的权限 ID 列表(`permIds`)。
 
 | 字段 | 类型 | 必填 | 说明 |
-|------|------|------|------|
+| ------ | ------ | ------ | ------ |
 | id | int64 | 是 | 角色 ID |
 
 #### POST /api/role/bindPerms — 绑定角色权限
@@ -935,7 +935,7 @@ Content-Type: application/json
 全量替换该角色的权限。
 
 | 字段 | 类型 | 必填 | 说明 |
-|------|------|------|------|
+| ------ | ------ | ------ | ------ |
 | roleId | int64 | 是 | 角色 ID |
 | permIds | []int64 | 是 | 权限 ID 列表(空数组清空绑定) |
 
@@ -944,7 +944,7 @@ Content-Type: application/json
 #### POST /api/user/create — 创建用户(超级管理员 或 产品管理员)
 
 | 字段 | 类型 | 必填 | 说明 |
-|------|------|------|------|
+| ------ | ------ | ------ | ------ |
 | username | string | 是 | 登录名(唯一) |
 | password | string | 是 | 密码(6-72 字符) |
 | nickname | string | 否 | 昵称 |
@@ -960,7 +960,7 @@ Content-Type: application/json
 支持字段清空:传 `""` 清空字符串字段,传 `0` 清空 deptId,不传字段则不更新。
 
 | 字段 | 类型 | 必填 | 说明 |
-|------|------|------|------|
+| ------ | ------ | ------ | ------ |
 | id | int64 | 是 | 用户 ID |
 | nickname | *string | 否 | 昵称 |
 | email | *string | 否 | 邮箱 |
@@ -972,7 +972,7 @@ Content-Type: application/json
 #### POST /api/user/list — 用户列表
 
 | 字段 | 类型 | 必填 | 说明 |
-|------|------|------|------|
+| ------ | ------ | ------ | ------ |
 | productCode | string | 否 | 产品编码(传入则附带成员类型) |
 | page | int64 | 否 | 页码 |
 | pageSize | int64 | 否 | 每页条数 |
@@ -982,7 +982,7 @@ Content-Type: application/json
 返回用户信息及绑定的角色 ID 列表(`roleIds`)。
 
 | 字段 | 类型 | 必填 | 说明 |
-|------|------|------|------|
+| ------ | ------ | ------ | ------ |
 | id | int64 | 是 | 用户 ID |
 
 #### POST /api/user/bindRoles — 绑定用户角色(需管理权限)
@@ -990,7 +990,7 @@ Content-Type: application/json
 需通过 `CheckManageAccess` 权限检查。全量替换该用户的角色。
 
 | 字段 | 类型 | 必填 | 说明 |
-|------|------|------|------|
+| ------ | ------ | ------ | ------ |
 | userId | int64 | 是 | 用户 ID |
 | roleIds | []int64 | 是 | 角色 ID 列表 |
 
@@ -999,7 +999,7 @@ Content-Type: application/json
 需通过 `CheckManageAccess` 权限检查。全量替换用户级别的 ALLOW/DENY 权限覆盖。
 
 | 字段 | 类型 | 必填 | 说明 |
-|------|------|------|------|
+| ------ | ------ | ------ | ------ |
 | userId | int64 | 是 | 用户 ID |
 | perms | array | 是 | 权限覆盖列表 |
 | perms[].permId | int64 | 是 | 权限 ID |
@@ -1010,7 +1010,7 @@ Content-Type: application/json
 需通过 `CheckManageAccess` 权限检查。不允许冻结自己和超级管理员。
 
 | 字段 | 类型 | 必填 | 说明 |
-|------|------|------|------|
+| ------ | ------ | ------ | ------ |
 | id | int64 | 是 | 用户 ID |
 | status | int64 | 是 | 1=正常 2=冻结 |
 
@@ -1021,7 +1021,7 @@ Content-Type: application/json
 需通过 `CheckManageAccess` + `CheckMemberTypeAssignment` 权限检查。不可分配与自己同级或更高级的成员类型。
 
 | 字段 | 类型 | 必填 | 说明 |
-|------|------|------|------|
+| ------ | ------ | ------ | ------ |
 | productCode | string | 是 | 产品编码 |
 | userId | int64 | 是 | 用户 ID |
 | memberType | string | 是 | `ADMIN` / `DEVELOPER` / `MEMBER` |
@@ -1033,7 +1033,7 @@ Content-Type: application/json
 需通过 `CheckManageAccess` + `CheckMemberTypeAssignment` 权限检查。
 
 | 字段 | 类型 | 必填 | 说明 |
-|------|------|------|------|
+| ------ | ------ | ------ | ------ |
 | id | int64 | 是 | 成员记录 ID |
 | memberType | string | 是 | 成员类型 |
 | status | int64 | 否 | 状态 |
@@ -1043,13 +1043,13 @@ Content-Type: application/json
 需通过 `CheckManageAccess` 权限检查。级联清理该成员在该产品下的角色绑定和权限覆盖。
 
 | 字段 | 类型 | 必填 | 说明 |
-|------|------|------|------|
+| ------ | ------ | ------ | ------ |
 | id | int64 | 是 | 成员记录 ID |
 
 #### POST /api/member/list — 成员列表
 
 | 字段 | 类型 | 必填 | 说明 |
-|------|------|------|------|
+| ------ | ------ | ------ | ------ |
 | productCode | string | 是 | 产品编码 |
 | page | int64 | 否 | 页码 |
 | pageSize | int64 | 否 | 每页条数 |
@@ -1063,7 +1063,7 @@ Content-Type: application/json
 gRPC 服务定义见 `pb/perm.proto`,默认监听 `:10002`。
 
 | 方法 | 说明 | 使用场景 |
-|------|------|----------|
+| ------ | ------ | ---------- |
 | `SyncPermissions` | 同步产品权限列表 | 产品启动时调用 |
 | `Login` | 用户登录 | 产品后端代理用户登录 |
 | `RefreshToken` | 刷新令牌 | accessToken 过期续期 |
@@ -1088,7 +1088,7 @@ resp, err := client.VerifyToken(ctx, &pb.VerifyTokenReq{AccessToken: token})
 
 ## 项目结构
 
-```
+```text
 server/
 ├── perm.go                           # 入口(同时启动 HTTP + gRPC)
 ├── perm.api                          # HTTP API 定义(go-zero 标准)
@@ -1163,7 +1163,7 @@ server/
 配置文件位于 `etc/perm-api.yaml`:
 
 | 配置项 | 说明 | 默认值 |
-|--------|------|--------|
+| -------- | ------ | -------- |
 | `Host` / `Port` | HTTP 监听地址 | `0.0.0.0:10001` |
 | `RpcServerConf.ListenOn` | gRPC 监听地址 | `0.0.0.0:10002` |
 | `MySQL.DataSource` | MySQL 连接串 | — |
@@ -1182,7 +1182,7 @@ server/
 ### 测试概览
 
 | 指标 | 数值 |
-|------|------|
+| ------ | ------ |
 | 测试用例总数 | 499 |
 | 已覆盖 TC | 498(99.8%) |
 | 测试函数 | 662 |
@@ -1205,7 +1205,7 @@ server/
 ### 测试文档
 
 | 文件 | 说明 |
-|------|------|
+| ------ | ------ |
 | `test-design.md` | 测试用例设计文档(499 个 TC,含测试场景、输入数据、预期结果) |
 | `test-report.md` | 测试执行报告(含各包耗时、TC 明细、源码审计结果) |
 
@@ -1232,7 +1232,7 @@ server/
 **环境变量**:
 
 | 变量 | 默认值 | 说明 |
-|------|--------|------|
+| ------ | -------- | ------ |
 | `TEST_VERBOSE` | 空(关闭) | 设为 `1` 开启详细输出(`-v`) |
 | `TEST_RUN` | 空(全部) | 只运行匹配的测试函数名 |
 | `TEST_TIMEOUT` | `600s` | 单个包的超时时间 |
@@ -1247,7 +1247,7 @@ TEST_VERBOSE=1 TEST_RUN="TestLogin" ./run-test.sh ./internal/logic/pub/...
 
 测试文件遵循 Go 标准命名,与被测文件同目录:
 
-```
+```text
 internal/logic/auth/
 ├── jwt.go                    # 源码
 ├── jwt_test.go               # 测试
@@ -1303,17 +1303,3 @@ flowchart LR
 
 - **Handler 模板** — 参数解析错误自动包装为 `400 Bad Request`(而非 500)
 - **Model 模板** — 所有 `FindOne` 和 `FindOneByXxx` 方法均提供 `*WithTx` 事务变体,确保事务内查询使用 session 连接
-
-### 代码规范
-
-- 所有魔数已替换为 `internal/consts/consts.go` 中的命名常量
-- 自定义 SQL 中的 MySQL 关键字统一使用大写
-- 多步写入操作均在 `TransactCtx` 中,事务内查询使用 `*WithTx` 方法
-- 删除角色/移除成员时有完整的级联清理
-- 列表接口统一 `NormalizePage`,`pageSize` 上限 100
-- 输入校验涵盖邮箱/手机格式、密码长度、状态值合法性、实体存在性
-- JWT token 通过 `tokenType` 字段区分 `access`/`refresh`,防止混用
-- 管理类接口统一通过 `access.go` 进行操作权限控制
-- 用户完整信息通过 `UserDetailsLoader` 集中加载,Redis 缓存 + 主动失效
-- 所有写操作均触发对应的缓存失效(`Clean`/`Del`/`BatchDel`/`CleanByProduct`)
-- `refreshToken` 不重新签发,具有固定有效期

+ 64 - 64
test-design.md

@@ -9,7 +9,7 @@
 
 ### 1.1 整体调用链路
 
-```
+```text
 HTTP Client                                  gRPC Client
      │                                            │
      ▼                                            ▼
@@ -40,7 +40,7 @@ MySQL (InnoDB) + Redis Cache
 共 9 个 Model,每个包含:
 
 | 层级 | 方法类别 | 数量/模型 | 来源 |
-|:---|:---|:---|:---|
+| :--- | :--- | :--- | :--- |
 | _gen.go 基础 CRUD | Insert, InsertWithTx, FindOne, FindOneWithTx, Update, UpdateWithTx, Delete, DeleteWithTx | 8 | 自定义模板 |
 | _gen.go 批量操作 | BatchInsert, BatchInsertWithTx, BatchUpdate, BatchUpdateWithTx, BatchDelete, BatchDeleteWithTx | 6 | 自定义模板 |
 | _gen.go 唯一索引查询 | FindOneBy{UniqueField}, FindOneBy{UniqueField}WithTx (因表而异) | 0~2 组 | 自定义模板 |
@@ -49,7 +49,7 @@ MySQL (InnoDB) + Redis Cache
 
 ### 1.3 权限计算逻辑链路
 
-```
+```text
 输入: userId + deptId + productCode + isSuperAdmin(bool)
     ├─ isSuperAdmin=true → 产品全部启用权限 + "SUPER_ADMIN"
@@ -69,7 +69,7 @@ MySQL (InnoDB) + Redis Cache
 ### 2.1 登录 `POST /api/auth/login`
 
 | TC编号 | 接口/方法 | 测试场景 | 输入参数 (JSON) | 预期结果 | 测试类型 | 优先级 | 覆盖说明 |
-|:---|:---|:---|:---|:---|:---|:---|:---|
+| :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- |
 | TC-0001 | POST /api/auth/login | 正常登录-不带productCode | `{"username":"admin","password":"123456"}` | code=0, accessToken/refreshToken/userInfo | 正常路径 | P0 | loginLogic全路径 |
 | TC-0002 | POST /api/auth/login | 正常登录-带productCode | `{"username":"user1","password":"123456","productCode":"test"}` | code=0, perms含用户可用权限 | 正常路径 | P0 | GetUserPerms(false) MEMBER分支 |
 | TC-0003 | POST /api/auth/login | 超管登录+productCode | `{"username":"super","password":"x","productCode":"p1"}` | memberType="SUPER_ADMIN", perms全量 | 分支覆盖 | P0 | GetUserPerms(true) |
@@ -86,7 +86,7 @@ MySQL (InnoDB) + Redis Cache
 ### 2.2 刷新Token `POST /api/auth/refreshToken`
 
 | TC编号 | 接口/方法 | 测试场景 | 输入参数 (JSON) | 预期结果 | 测试类型 | 优先级 | 覆盖说明 |
-|:---|:---|:---|:---|:---|:---|:---|:---|
+| :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- |
 | TC-0013 | POST /api/auth/refreshToken | 正常刷新 | Header `Authorization: Bearer <refreshToken>` | code=0, 新accessToken, refreshToken原样返回(不重新生成) | 正常路径 | P0 | refreshTokenLogic全路径 |
 | TC-0014 | POST /api/auth/refreshToken | 不带productCode(回退) | Header Authorization, 无productCode | 使用claims.ProductCode | 分支覆盖 | P1 | productCode=""回退 |
 | TC-0015 | POST /api/auth/refreshToken | token无效 | Header `Authorization: Bearer invalid` | code=401 | 异常路径 | P0 | ParseRefreshToken失败 |
@@ -97,7 +97,7 @@ MySQL (InnoDB) + Redis Cache
 ### 2.3 同步权限 `POST /api/perm/sync`
 
 | TC编号 | 接口/方法 | 测试场景 | 输入参数 (JSON) | 预期结果 | 测试类型 | 优先级 | 覆盖说明 |
-|:---|:---|:---|:---|:---|:---|:---|:---|
+| :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- |
 | TC-0019 | POST /api/perm/sync | 全部新增 | `{"appKey":"ak","appSecret":"as","perms":[{"code":"x","name":"y"}]}` | code=0, added=1, updated=0, disabled=0 | 正常路径 | P0 | toInsert→BatchInsert |
 | TC-0020 | POST /api/perm/sync | 更新已有(名称变更) | 已存在code但name不同 | updated=1 | 正常路径 | P0 | toUpdate→BatchUpdate |
 | TC-0021 | POST /api/perm/sync | 无变化 | 已存在且name/remark/status均相同 | added=0, updated=0 | 分支覆盖 | P1 | 跳过更新 |
@@ -113,7 +113,7 @@ MySQL (InnoDB) + Redis Cache
 ### 2.4 获取用户信息 `POST /api/auth/userInfo`
 
 | TC编号 | 接口/方法 | 测试场景 | 输入参数 (JSON) | 预期结果 | 测试类型 | 优先级 | 覆盖说明 |
-|:---|:---|:---|:---|:---|:---|:---|:---|
+| :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- |
 | TC-0030 | POST /api/auth/userInfo | 正常获取-含productCode | Bearer token (含productCode) | code=0, 完整UserInfo+实时perms | 正常路径 | P0 | userInfoLogic全路径 |
 | TC-0031 | POST /api/auth/userInfo | 不含productCode | Bearer token (无productCode) | perms为空 | 分支覆盖 | P1 | productCode="" |
 | TC-0032 | POST /api/auth/userInfo | 未登录 | 无Authorization头 | code=401, "未登录" | 异常路径 | P0 | middleware拦截 |
@@ -123,7 +123,7 @@ MySQL (InnoDB) + Redis Cache
 ### 2.5 修改密码 `POST /api/auth/changePassword`
 
 | TC编号 | 接口/方法 | 测试场景 | 输入参数 (JSON) | 预期结果 | 测试类型 | 优先级 | 覆盖说明 |
-|:---|:---|:---|:---|:---|:---|:---|:---|
+| :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- |
 | TC-0035 | POST /api/auth/changePassword | 正常修改 | `{"oldPassword":"123456","newPassword":"654321"}` | code=0 | 正常路径 | P0 | changePasswordLogic全路径 |
 | TC-0036 | POST /api/auth/changePassword | mustChangePassword重置 | 正常修改后 | DB中mustChangePassword=2 | 功能验证 | P0 | user.MustChangePassword=2 |
 | TC-0037 | POST /api/auth/changePassword | 原密码错误 | `{"oldPassword":"wrong","newPassword":"newpwd"}` | code=400, "原密码错误" | 异常路径 | P0 | bcrypt失败 |
@@ -138,7 +138,7 @@ MySQL (InnoDB) + Redis Cache
 ### 2.6 创建产品 `POST /api/product/create`
 
 | TC编号 | 接口/方法 | 测试场景 | 输入参数 (JSON) | 预期结果 | 测试类型 | 优先级 | 覆盖说明 |
-|:---|:---|:---|:---|:---|:---|:---|:---|
+| :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- |
 | TC-0045 | POST /api/product/create | 正常创建 | `{"code":"new","name":"新产品"}` | code=0, id/appKey/appSecret/adminUser/adminPassword | 正常路径 | P0 | TransactCtx全路径 |
 | TC-0046 | POST /api/product/create | 事务回滚-用户创建失败 | 模拟InsertWithTx User失败 | 返回错误, DB无新产品 | 事务验证 | P0 | TransactCtx回滚 |
 | TC-0047 | POST /api/product/create | 事务回滚-成员创建失败 | 模拟InsertWithTx Member失败 | 产品和用户均回滚 | 事务验证 | P0 | TransactCtx回滚 |
@@ -148,7 +148,7 @@ MySQL (InnoDB) + Redis Cache
 ### 2.7 产品更新/列表/详情
 
 | TC编号 | 接口/方法 | 测试场景 | 输入参数 (JSON) | 预期结果 | 测试类型 | 优先级 | 覆盖说明 |
-|:---|:---|:---|:---|:---|:---|:---|:---|
+| :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- |
 | TC-0050 | POST /api/product/update | 正常更新 | `{"id":1,"name":"新名","status":1}` | code=0 | 正常路径 | P0 | updateProductLogic |
 | TC-0051 | POST /api/product/update | 不存在 | `{"id":9999,"name":"x"}` | code=404 | 异常路径 | P0 | FindOne失败 |
 | TC-0052 | POST /api/product/update | 不传status | `{"id":1,"name":"x"}` | status不变 | 分支覆盖 | P1 | Status>0 |
@@ -163,7 +163,7 @@ MySQL (InnoDB) + Redis Cache
 ### 2.8 创建部门 `POST /api/dept/create`
 
 | TC编号 | 接口/方法 | 测试场景 | 输入参数 (JSON) | 预期结果 | 测试类型 | 优先级 | 覆盖说明 |
-|:---|:---|:---|:---|:---|:---|:---|:---|
+| :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- |
 | TC-0060 | POST /api/dept/create | 创建顶级部门 | `{"parentId":0,"name":"总部"}` | code=0, path="/{id}/" | 正常路径 | P0 | TransactCtx, parentPath="/" |
 | TC-0061 | POST /api/dept/create | 创建子部门 | `{"parentId":1,"name":"技术部"}` | code=0, path=parent.path+id+"/" | 正常路径 | P0 | parentId>0分支 |
 | TC-0062 | POST /api/dept/create | 父部门不存在 | `{"parentId":9999,"name":"x"}` | code=404, "父部门不存在" | 异常路径 | P0 | FindOneWithTx失败 |
@@ -178,7 +178,7 @@ MySQL (InnoDB) + Redis Cache
 ### 2.9 部门更新/删除/树
 
 | TC编号 | 接口/方法 | 测试场景 | 输入参数 (JSON) | 预期结果 | 测试类型 | 优先级 | 覆盖说明 |
-|:---|:---|:---|:---|:---|:---|:---|:---|
+| :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- |
 | TC-0070 | POST /api/dept/update | 正常更新 | `{"id":1,"name":"新名","sort":5}` | code=0 | 正常路径 | P0 | updateDeptLogic |
 | TC-0071 | POST /api/dept/update | 不存在 | `{"id":9999,"name":"x"}` | code=404 | 异常路径 | P0 | FindOne失败 |
 | TC-0072 | POST /api/dept/update | DeptType NORMAL→DEV | `{"id":1,"deptType":"DEV"}` | DB deptType="DEV" | 正常路径 | P0 | DeptType合法值更新 |
@@ -193,7 +193,7 @@ MySQL (InnoDB) + Redis Cache
 ### 2.10 权限列表 `POST /api/perm/list`
 
 | TC编号 | 接口/方法 | 测试场景 | 输入参数 (JSON) | 预期结果 | 测试类型 | 优先级 | 覆盖说明 |
-|:---|:---|:---|:---|:---|:---|:---|:---|
+| :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- |
 | TC-0080 | POST /api/perm/list | 正常查询 | `{"productCode":"p1","page":1,"pageSize":10}` | code=0, total/list | 正常路径 | P0 | permListLogic |
 | TC-0081 | POST /api/perm/list | 默认分页 | `{"productCode":"p1"}` | page=1, pageSize=20 | 分支覆盖 | P1 | NormalizePage |
 | TC-0082 | POST /api/perm/list | pageSize超过上限 | `{"productCode":"p1","pageSize":200}` | 实际pageSize=100 | 边界 | P0 | NormalizePage cap |
@@ -202,7 +202,7 @@ MySQL (InnoDB) + Redis Cache
 ### 2.11 角色管理
 
 | TC编号 | 接口/方法 | 测试场景 | 输入参数 (JSON) | 预期结果 | 测试类型 | 优先级 | 覆盖说明 |
-|:---|:---|:---|:---|:---|:---|:---|:---|
+| :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- |
 | TC-0084 | POST /api/role/create | 正常创建 | `{"productCode":"p1","name":"管理员","permsLevel":1}` | code=0, id>0 | 正常路径 | P0 | createRoleLogic |
 | TC-0085 | POST /api/role/create | 重复角色名 | 同产品同名 | code=409, "该产品下角色名已存在" | 业务约束 | P0 | Duplicate entry→ErrConflict |
 | TC-0086 | POST /api/role/create | 并发同名创建 | 两请求同时 | 一成功一冲突409 | 并发 | P1 | 唯一索引+1062捕获 |
@@ -216,7 +216,7 @@ MySQL (InnoDB) + Redis Cache
 ### 2.12 删除角色 `POST /api/role/delete`
 
 | TC编号 | 接口/方法 | 测试场景 | 输入参数 (JSON) | 预期结果 | 测试类型 | 优先级 | 覆盖说明 |
-|:---|:---|:---|:---|:---|:---|:---|:---|
+| :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- |
 | TC-0093 | POST /api/role/delete | 正常删除+级联 | `{"id":5}` (含权限/用户绑定) | code=0, role_perm/user_role同步清理 | 正常+事务 | P0 | TransactCtx全路径 |
 | TC-0094 | POST /api/role/delete | 事务回滚 | 模拟DeleteWithTx失败 | 级联删除回滚 | 事务验证 | P0 | TransactCtx |
 | TC-0095 | POST /api/role/delete | 无关联数据 | 新角色无绑定 | code=0 | 分支覆盖 | P1 | 删0条 |
@@ -224,7 +224,7 @@ MySQL (InnoDB) + Redis Cache
 ### 2.13 绑定角色权限 `POST /api/role/bindPerms`
 
 | TC编号 | 接口/方法 | 测试场景 | 输入参数 (JSON) | 预期结果 | 测试类型 | 优先级 | 覆盖说明 |
-|:---|:---|:---|:---|:---|:---|:---|:---|
+| :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- |
 | TC-0096 | POST /api/role/bindPerms | 正常绑定 | `{"roleId":1,"permIds":[1,2,3]}` | code=0 | 正常路径 | P0 | TransactCtx |
 | TC-0097 | POST /api/role/bindPerms | 角色不存在 | `{"roleId":9999,"permIds":[1]}` | code=404, "角色不存在" | 存在性校验 | P0 | FindOne预检 |
 | TC-0098 | POST /api/role/bindPerms | 清空权限 | `{"roleId":1,"permIds":[]}` | code=0, 全清空 | 分支覆盖 | P1 | len==0→return |
@@ -234,7 +234,7 @@ MySQL (InnoDB) + Redis Cache
 ### 2.14 创建用户 `POST /api/user/create`
 
 | TC编号 | 接口/方法 | 测试场景 | 输入参数 (JSON) | 预期结果 | 测试类型 | 优先级 | 覆盖说明 |
-|:---|:---|:---|:---|:---|:---|:---|:---|
+| :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- |
 | TC-0101 | POST /api/user/create | 正常创建 | `{"username":"new","password":"123456"}` | code=0, id>0 | 正常路径 | P0 | createUserLogic |
 | TC-0102 | POST /api/user/create | 用户名已存在(预检) | `{"username":"existing","password":"x"}` | code=409, "用户名已存在" | 异常路径 | P0 | FindOneByUsername成功 |
 | TC-0103 | POST /api/user/create | 带完整可选字段 | 含nickname/email/phone/remark/deptId | code=0 | 正常路径 | P1 | 各字段赋值 |
@@ -250,7 +250,7 @@ MySQL (InnoDB) + Redis Cache
 ### 2.15 用户更新 `POST /api/user/update` (指针类型+DeptId可清零)
 
 | TC编号 | 接口/方法 | 测试场景 | 输入参数 (JSON) | 预期结果 | 测试类型 | 优先级 | 覆盖说明 |
-|:---|:---|:---|:---|:---|:---|:---|:---|
+| :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- |
 | TC-0112 | POST /api/user/update | 正常更新 | `{"id":1,"nickname":"n","email":"[email protected]"}` | code=0 | 正常路径 | P0 | updateUserLogic |
 | TC-0113 | POST /api/user/update | 不存在 | `{"id":9999}` | code=404 | 异常路径 | P0 | FindOne失败 |
 | TC-0114 | POST /api/user/update | 仅传id | `{"id":1}` | 仅updateTime变 | 分支覆盖 | P1 | 所有指针nil |
@@ -261,14 +261,14 @@ MySQL (InnoDB) + Redis Cache
 | TC-0119 | POST /api/user/update | 非法phone格式 | `{"id":1,"phone":"12345"}` | code=400, "手机号格式不正确" | 输入校验 | P0 | util.IsValidPhone |
 | TC-0120 | POST /api/user/update | 合法phone | `{"id":1,"phone":"+8613800138000"}` | code=0 | 正常路径 | P1 | IsValidPhone通过 |
 | TC-0121 | POST /api/user/update | 不传email(nil) | `{"id":1,"nickname":"x"}` | email不变 | 分支覆盖 | P1 | req.Email==nil |
-| TC-0122 | POST /api/user/update | DeptId设为0(取消部门) | `{"id":1,"deptId":0}` | DB deptId→0 | 功能 | P0 | *int64, *req.DeptId=0 |
+| TC-0122 | POST /api/user/update | DeptId设为0(取消部门) | `{"id":1,"deptId":0}` | DB deptId→0 | 功能 | P0 | \*int64, \*req.DeptId=0 |
 | TC-0123 | POST /api/user/update | DeptId设为正值 | `{"id":1,"deptId":5}` | DB deptId→5 | 正常路径 | P0 | *int64指针 |
 | TC-0124 | POST /api/user/update | DeptId不传(nil) | `{"id":1,"nickname":"x"}` | deptId不变 | 分支覆盖 | P1 | req.DeptId==nil |
 
 ### 2.16 用户列表/详情/状态 及其他用户操作
 
 | TC编号 | 接口/方法 | 测试场景 | 输入参数 (JSON) | 预期结果 | 测试类型 | 优先级 | 覆盖说明 |
-|:---|:---|:---|:---|:---|:---|:---|:---|
+| :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- |
 | TC-0125 | POST /api/user/list | 含productCode | `{"productCode":"p1","page":1,"pageSize":10}` | 每用户含memberType(批量查) | 正常路径 | P0 | FindMapByProductCodeUserIds |
 | TC-0126 | POST /api/user/list | 不含productCode | `{"page":1}` | memberType全空,不调批量查 | 分支覆盖 | P1 | productCode="" |
 | TC-0127 | POST /api/user/list | pageSize超过上限 | `{"pageSize":500}` | 实际pageSize=100 | 边界 | P0 | NormalizePage cap |
@@ -294,7 +294,7 @@ MySQL (InnoDB) + Redis Cache
 ### 2.17 成员管理
 
 | TC编号 | 接口/方法 | 测试场景 | 输入参数 (JSON) | 预期结果 | 测试类型 | 优先级 | 覆盖说明 |
-|:---|:---|:---|:---|:---|:---|:---|:---|
+| :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- |
 | TC-0146 | POST /api/member/add | 正常添加 | `{"productCode":"p1","userId":1,"memberType":"MEMBER"}` | code=0, id>0 | 正常路径 | P0 | addMemberLogic |
 | TC-0147 | POST /api/member/add | 产品不存在 | `{"productCode":"notexist",...}` | code=404, "产品不存在" | 存在性校验 | P0 | FindOneByCode预检 |
 | TC-0148 | POST /api/member/add | 用户不存在 | `{"userId":9999,...}` | code=404, "用户不存在" | 存在性校验 | P0 | FindOne预检 |
@@ -318,7 +318,7 @@ MySQL (InnoDB) + Redis Cache
 ### 3.1 gRPC SyncPermissions
 
 | TC编号 | 接口/方法 | 测试场景 | 输入 | 预期结果 | 测试类型 | 优先级 | 覆盖说明 |
-|:---|:---|:---|:---|:---|:---|:---|:---|
+| :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- |
 | TC-0161 | SyncPermissions | 正常同步 | valid req | added/updated/disabled计数正确 | 正常路径 | P0 | permserver.go SyncPermissions |
 | TC-0162 | SyncPermissions | appKey无效 | invalid appKey | codes.Unauthenticated | 异常路径 | P0 | status.Error |
 | TC-0163 | SyncPermissions | appSecret错误 | wrong secret | codes.Unauthenticated | 异常路径 | P0 | status.Error |
@@ -328,7 +328,7 @@ MySQL (InnoDB) + Redis Cache
 ### 3.2 gRPC Login / RefreshToken / VerifyToken / GetUserPerms
 
 | TC编号 | 接口/方法 | 测试场景 | 输入 | 预期结果 | 测试类型 | 优先级 | 覆盖说明 |
-|:---|:---|:---|:---|:---|:---|:---|:---|
+| :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- |
 | TC-0166 | Login | 正常登录(无productCode) | valid credentials | token对+userInfo | 正常路径 | P0 | permserver.go Login |
 | TC-0167 | Login | 用户不存在 | wrong username | codes.Unauthenticated | 异常路径 | P0 | status.Error |
 | TC-0168 | Login | 密码错误 | wrong password | codes.Unauthenticated | 异常路径 | P0 | status.Error |
@@ -353,7 +353,7 @@ MySQL (InnoDB) + Redis Cache
 ## 四、JWT中间件 / 统一响应测试用例
 
 | TC编号 | 测试场景 | 输入 | 预期结果 | 类型 | 优先级 | 覆盖说明 |
-|:---|:---|:---|:---|:---|:---|:---|
+| :--- | :--- | :--- | :--- | :--- | :--- | :--- |
 | TC-0184 | 正常Bearer token | `Authorization: Bearer {valid}` | 通过, ctx注入5个值 | 正常路径 | P0 | middleware全路径 |
 | TC-0185 | 无Authorization头 | 无Header | code=401, "未登录" | 异常 | P0 | authHeader=="" |
 | TC-0186 | 无Bearer前缀 | `Authorization: xxx` | code=401, "token格式错误" | 异常 | P0 | TrimPrefix相等 |
@@ -373,7 +373,7 @@ MySQL (InnoDB) + Redis Cache
 ### 5.1 NormalizePage
 
 | TC编号 | 测试场景 | 输入 | 预期结果 | 类型 | 优先级 | 覆盖说明 |
-|:---|:---|:---|:---|:---|:---|:---|
+| :--- | :--- | :--- | :--- | :--- | :--- | :--- |
 | TC-0194 | 正常值 | page=2, pageSize=10 | (2, 10) | 正常路径 | P0 | 无修正 |
 | TC-0195 | page<=0 | page=0, pageSize=10 | (1, 10) | 边界 | P0 | page<=0→1 |
 | TC-0196 | page=-1 | page=-1, pageSize=10 | (1, 10) | 边界 | P0 | page<=0→1 |
@@ -386,7 +386,7 @@ MySQL (InnoDB) + Redis Cache
 ### 5.2 IsValidEmail
 
 | TC编号 | 测试场景 | 输入 | 预期结果 | 类型 | 优先级 | 覆盖说明 |
-|:---|:---|:---|:---|:---|:---|:---|
+| :--- | :--- | :--- | :--- | :--- | :--- | :--- |
 | TC-0202 | 正常邮箱 | `[email protected]` | true | 正常路径 | P0 | 标准格式 |
 | TC-0203 | 含点号 | `[email protected]` | true | 正常路径 | P1 | 允许点号 |
 | TC-0204 | 含加号 | `[email protected]` | true | 正常路径 | P1 | 允许加号 |
@@ -398,7 +398,7 @@ MySQL (InnoDB) + Redis Cache
 ### 5.3 IsValidPhone
 
 | TC编号 | 测试场景 | 输入 | 预期结果 | 类型 | 优先级 | 覆盖说明 |
-|:---|:---|:---|:---|:---|:---|:---|
+| :--- | :--- | :--- | :--- | :--- | :--- | :--- |
 | TC-0209 | 国内手机号 | `13800138000` | true | 正常路径 | P0 | 11位数字 |
 | TC-0210 | 带+国际码 | `+8613800138000` | true | 正常路径 | P0 | +前缀 |
 | TC-0211 | 太短(6位) | `123456` | false | 边界 | P0 | <7位 |
@@ -417,7 +417,7 @@ MySQL (InnoDB) + Redis Cache
 ### 6.1 auth/jwt.go — GenerateAccessToken
 
 | TC编号 | 测试场景 | 输入 | 预期结果 | 类型 | 优先级 | 覆盖说明 |
-|:---|:---|:---|:---|:---|:---|:---|
+| :--- | :--- | :--- | :--- | :--- | :--- | :--- |
 | TC-0217 | 正常生成 | secret="s", expire=3600, userId=1, username="u", productCode="p", memberType="M", perms=["a"] | 返回非空token, err=nil | 正常路径 | P0 | jwt.NewWithClaims(HS256) |
 | TC-0218 | 解析token验证claims | 上述token | ParseWithClaims可解析出正确userId/username/productCode/memberType/perms | 功能验证 | P0 | claims完整性 |
 | TC-0219 | 空secret | secret="" | 仍能生成token(空key签名) | 边界 | P2 | HS256 允许空key |
@@ -427,7 +427,7 @@ MySQL (InnoDB) + Redis Cache
 ### 6.2 auth/jwt.go — GenerateRefreshToken
 
 | TC编号 | 测试场景 | 输入 | 预期结果 | 类型 | 优先级 | 覆盖说明 |
-|:---|:---|:---|:---|:---|:---|:---|
+| :--- | :--- | :--- | :--- | :--- | :--- | :--- |
 | TC-0222 | 正常生成 | secret="s", expire=86400, userId=1, productCode="p" | 返回非空token | 正常路径 | P0 | RefreshClaims |
 | TC-0223 | 解析验证 | 上述token | ParseRefreshToken解析出userId=1, productCode="p" | 功能验证 | P0 | 往返一致 |
 | TC-0224 | productCode为空 | productCode="" | 生成成功, 解析后productCode="" | 边界 | P1 | 空字符串 |
@@ -435,7 +435,7 @@ MySQL (InnoDB) + Redis Cache
 ### 6.3 auth/jwt.go — ParseRefreshToken
 
 | TC编号 | 测试场景 | 输入 | 预期结果 | 类型 | 优先级 | 覆盖说明 |
-|:---|:---|:---|:---|:---|:---|:---|
+| :--- | :--- | :--- | :--- | :--- | :--- | :--- |
 | TC-0225 | 正常解析 | 有效token+正确secret | 返回RefreshClaims, err=nil | 正常路径 | P0 | token.Valid |
 | TC-0226 | 错误secret | 有效token+错误secret | err!=nil | 异常路径 | P0 | 签名验证失败 |
 | TC-0227 | 无效token字符串 | "invalid-token" | err!=nil | 异常路径 | P0 | 解析失败 |
@@ -450,7 +450,7 @@ MySQL (InnoDB) + Redis Cache
 > **v7 变更**: GetUserPerms 新增 `deptId` 参数,MEMBER 类型增加 DEV 部门全权限分支。
 
 | TC编号 | 测试场景 | Mock设置 | 预期结果 | 类型 | 优先级 | 覆盖说明 |
-|:---|:---|:---|:---|:---|:---|:---|
+| :--- | :--- | :--- | :--- | :--- | :--- | :--- |
 | TC-0231 | 超管 | isSuperAdmin=true, deptId=0, FindAllCodesByProductCode返回["a","b"] | perms=["a","b"], memberType="SUPER_ADMIN" | 正常路径 | P0 | isSuperAdmin分支 |
 | TC-0232 | 超管+查询失败 | isSuperAdmin=true, deptId=0, FindAllCodesByProductCode返回err | err透传, perms=nil | 异常路径 | P0 | err分支 |
 | TC-0233 | 非产品成员 | deptId=0, FindOneByProductCodeUserId返回ErrNotFound | perms=nil, memberType="" | 分支覆盖 | P0 | ErrNotFound |
@@ -482,7 +482,7 @@ MySQL (InnoDB) + Redis Cache
 ### 6.5 middleware — 辅助函数单元测试
 
 | TC编号 | 测试场景 | 输入 | 预期结果 | 类型 | 优先级 | 覆盖说明 |
-|:---|:---|:---|:---|:---|:---|:---|
+| :--- | :--- | :--- | :--- | :--- | :--- | :--- |
 | TC-0258 | GetUserId-正常 | ctx含userId=100 | 100 | 正常路径 | P0 | 类型断言成功 |
 | TC-0259 | GetUserId-空ctx | 空ctx | 0 | 边界 | P0 | 断言失败→零值 |
 | TC-0260 | GetUsername-正常 | ctx含username="admin" | "admin" | 正常路径 | P0 | 类型断言 |
@@ -505,7 +505,7 @@ MySQL (InnoDB) + Redis Cache
 > 适用: SysUser, SysProduct, SysPerm, SysDept, SysRole, SysRolePerm, SysUserPerm, SysUserRole, SysProductMember
 
 | TC编号 | 方法 | 测试场景 | 预期结果 | 类型 | 优先级 | 覆盖说明 |
-|:---|:---|:---|:---|:---|:---|:---|
+| :--- | :--- | :--- | :--- | :--- | :--- | :--- |
 | TC-0267 | **Insert** | 正常插入 | 返回Result+nil, DB有新记录 | 正常路径 | P0 | ExecCtx+缓存key清理 |
 | TC-0268 | **Insert** | 唯一索引冲突 | 返回DB错误(1062) | 异常路径 | P0 | MySQL uk |
 | TC-0269 | **Insert** | 缓存key生成正确 | 验证清理的缓存key包含主键和唯一索引 | 功能验证 | P0 | cacheSys*Prefix |
@@ -531,7 +531,7 @@ MySQL (InnoDB) + Redis Cache
 ### 7.2 批量插入方法
 
 | TC编号 | 方法 | 测试场景 | 预期结果 | 类型 | 优先级 | 覆盖说明 |
-|:---|:---|:---|:---|:---|:---|:---|
+| :--- | :--- | :--- | :--- | :--- | :--- | :--- |
 | TC-0288 | **BatchInsert** | 空列表 | 直接返回nil, 不执行SQL | 边界 | P0 | len==0 early return |
 | TC-0289 | **BatchInsert** | 单条记录 | 生成1组VALUES, 执行成功 | 正常路径 | P0 | 单条 |
 | TC-0290 | **BatchInsert** | 多条记录(3条) | 生成3组VALUES, SQL正确, 缓存key全清理 | 正常路径 | P0 | 多条+缓存 |
@@ -544,7 +544,7 @@ MySQL (InnoDB) + Redis Cache
 ### 7.3 批量更新方法
 
 | TC编号 | 方法 | 测试场景 | 预期结果 | 类型 | 优先级 | 覆盖说明 |
-|:---|:---|:---|:---|:---|:---|:---|
+| :--- | :--- | :--- | :--- | :--- | :--- | :--- |
 | TC-0296 | **BatchUpdate** | 空列表 | 直接返回nil | 边界 | P0 | len==0 early return |
 | TC-0297 | **BatchUpdate** | 单条记录 | CASE-WHEN SQL正确, 更新成功 | 正常路径 | P0 | buildBatchUpdateQuery 单条 |
 | TC-0298 | **BatchUpdate** | 多条记录(3条) | CASE-WHEN生成3个WHEN子句, 旧缓存key全清理 | 正常路径 | P0 | buildBatchUpdateQuery 多条 |
@@ -558,7 +558,7 @@ MySQL (InnoDB) + Redis Cache
 ### 7.4 批量删除方法
 
 | TC编号 | 方法 | 测试场景 | 预期结果 | 类型 | 优先级 | 覆盖说明 |
-|:---|:---|:---|:---|:---|:---|:---|
+| :--- | :--- | :--- | :--- | :--- | :--- | :--- |
 | TC-0305 | **BatchDelete** | 空ids | 直接返回nil | 边界 | P0 | len==0 early return |
 | TC-0306 | **BatchDelete** | 单个id | DELETE WHERE id IN (?), 缓存清理 | 正常路径 | P0 | 单条 |
 | TC-0307 | **BatchDelete** | 多个id(3个) | 3个占位符, 旧数据查询→缓存key全清理 | 正常路径 | P0 | findListByPrimaryKeys |
@@ -571,7 +571,7 @@ MySQL (InnoDB) + Redis Cache
 > **v7 新增**: 每个 FindOneBy{UniqueField} 均新增对应 FindOneBy{UniqueField}WithTx 方法,通过 session 直查(无缓存)。
 
 | TC编号 | Model | 方法 | 测试场景 | 预期结果 | 优先级 | 覆盖说明 |
-|:---|:---|:---|:---|:---|:---|:---|
+| :--- | :--- | :--- | :--- | :--- | :--- | :--- |
 | TC-0311 | SysUser | FindOneByUsername | 正常查询 | 返回用户, 缓存写入 (索引缓存→主键缓存双层) | P0 | QueryRowIndexCtx |
 | TC-0312 | SysUser | FindOneByUsername | 不存在 | 返回ErrNotFound | P0 | sqlc.ErrNotFound |
 | TC-0313 | SysUser | FindOneByUsernameWithTx | 事务内正常查询 | 返回用户, 使用session直查 | P0 | session.QueryRowCtx |
@@ -612,7 +612,7 @@ MySQL (InnoDB) + Redis Cache
 ### 7.6 内部辅助方法
 
 | TC编号 | 方法 | 测试场景 | 预期结果 | 类型 | 优先级 | 覆盖说明 |
-|:---|:---|:---|:---|:---|:---|:---|
+| :--- | :--- | :--- | :--- | :--- | :--- | :--- |
 | TC-0347 | **findListByPrimaryKeys** | 空ids | 返回空slice, 不执行SQL | 边界 | P0 | len==0 |
 | TC-0348 | **findListByPrimaryKeys** | 正常ids | 返回匹配记录(无缓存) | 正常路径 | P0 | QueryRowsNoCacheCtx |
 | TC-0349 | **findListByPrimaryKeys** | 部分不存在 | 仅返回存在的记录 | 边界 | P1 | IN查询 |
@@ -624,7 +624,7 @@ MySQL (InnoDB) + Redis Cache
 ### 7.7 缓存key与前缀初始化
 
 | TC编号 | 测试场景 | 输入 | 预期结果 | 类型 | 优先级 | 覆盖说明 |
-|:---|:---|:---|:---|:---|:---|:---|
+| :--- | :--- | :--- | :--- | :--- | :--- | :--- |
 | TC-0354 | cachePrefix为空 | cachePrefix="" | 使用默认前缀 (如 "cache:sysUser:id:") | 分支覆盖 | P0 | if cachePrefix!="" 未进入 |
 | TC-0355 | cachePrefix非空 | cachePrefix="test" | 前缀变为 "test:cache:sysUser:id:" | 分支覆盖 | P0 | if cachePrefix!="" 进入 |
 | TC-0356 | 多唯一索引前缀(SysProduct) | cachePrefix="test" | 3个缓存前缀均更新: id/appKey/code | 功能验证 | P0 | 3个变量均修改 |
@@ -636,7 +636,7 @@ MySQL (InnoDB) + Redis Cache
 ### 8.1 SysUserModel
 
 | TC编号 | 方法 | 测试场景 | 输入 | 预期结果 | 类型 | 优先级 | 覆盖说明 |
-|:---|:---|:---|:---|:---|:---|:---|:---|
+| :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- |
 | TC-0357 | FindListByPage | 正常分页 | page=1, pageSize=10, DB有20条 | 返回10条+total=20 | 正常路径 | P0 | count+limit offset |
 | TC-0358 | FindListByPage | 第二页 | page=2, pageSize=10 | offset=10, 返回后10条 | 正常路径 | P0 | (page-1)*pageSize |
 | TC-0359 | FindListByPage | 空表 | 无数据 | total=0, list为空 | 边界 | P0 | count=0 |
@@ -654,7 +654,7 @@ MySQL (InnoDB) + Redis Cache
 ### 8.2 SysProductModel
 
 | TC编号 | 方法 | 测试场景 | 输入 | 预期结果 | 类型 | 优先级 | 覆盖说明 |
-|:---|:---|:---|:---|:---|:---|:---|:---|
+| :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- |
 | TC-0370 | FindList | 正常分页 | page=1, pageSize=10 | 返回list+total | 正常路径 | P0 | count+limit |
 | TC-0371 | FindList | 空表 | 无数据 | total=0, list空 | 边界 | P0 | |
 | TC-0372 | FindList | count失败 | DB异常 | 返回err | 异常路径 | P1 | 第一个err |
@@ -662,7 +662,7 @@ MySQL (InnoDB) + Redis Cache
 ### 8.3 SysPermModel
 
 | TC编号 | 方法 | 测试场景 | 输入 | 预期结果 | 类型 | 优先级 | 覆盖说明 |
-|:---|:---|:---|:---|:---|:---|:---|:---|
+| :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- |
 | TC-0373 | FindListByProductCode | 正常分页 | productCode="p1", page=1, pageSize=10 | list+total | 正常路径 | P0 | WHERE productCode=? |
 | TC-0374 | FindListByProductCode | 不存在的productCode | "notexist" | total=0, list空 | 边界 | P1 | |
 | TC-0375 | FindAllByProductCode | 正常查询(仅status=1) | DB有status=1和status=2 | 仅返回status=1 | 正常路径 | P0 | AND status=1 |
@@ -682,7 +682,7 @@ MySQL (InnoDB) + Redis Cache
 ### 8.4 SysDeptModel
 
 | TC编号 | 方法 | 测试场景 | 输入 | 预期结果 | 类型 | 优先级 | 覆盖说明 |
-|:---|:---|:---|:---|:---|:---|:---|:---|
+| :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- |
 | TC-0388 | FindAll | 正常查询 | DB有5条 | 返回5条, 按sort asc排序 | 正常路径 | P0 | ORDER BY sort, id |
 | TC-0389 | FindAll | 空表 | 无数据 | 空slice | 边界 | P0 | |
 | TC-0390 | FindByParentId | 正常查询 | parentId=1 | 返回子部门列表 | 正常路径 | P0 | WHERE parentId=? |
@@ -694,7 +694,7 @@ MySQL (InnoDB) + Redis Cache
 ### 8.5 SysRoleModel
 
 | TC编号 | 方法 | 测试场景 | 输入 | 预期结果 | 类型 | 优先级 | 覆盖说明 |
-|:---|:---|:---|:---|:---|:---|:---|:---|
+| :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- |
 | TC-0395 | FindListByProductCode | 正常分页 | productCode="p1" | 按permsLevel asc排序 | 正常路径 | P0 | ORDER BY permsLevel, id |
 | TC-0396 | FindListByProductCode | 空结果 | 无匹配 | total=0 | 边界 | P1 | |
 | TC-0397 | FindByIds | 正常 | ids=[1,2] | 返回2条 | 正常路径 | P0 | IN查询 |
@@ -703,7 +703,7 @@ MySQL (InnoDB) + Redis Cache
 ### 8.6 SysRolePermModel
 
 | TC编号 | 方法 | 测试场景 | 输入 | 预期结果 | 类型 | 优先级 | 覆盖说明 |
-|:---|:---|:---|:---|:---|:---|:---|:---|
+| :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- |
 | TC-0399 | FindPermIdsByRoleId | 正常查询 | roleId=1, DB有3条 | 返回3个permId | 正常路径 | P0 | SELECT permId WHERE roleId=? |
 | TC-0400 | FindPermIdsByRoleId | 无绑定 | roleId=999 | 空slice | 边界 | P1 | |
 | TC-0401 | FindPermIdsByRoleIds | 正常查询 | roleIds=[1,2] | 返回去重后的permId | 正常路径 | P0 | DISTINCT + IN |
@@ -716,7 +716,7 @@ MySQL (InnoDB) + Redis Cache
 ### 8.7 SysUserPermModel
 
 | TC编号 | 方法 | 测试场景 | 输入 | 预期结果 | 类型 | 优先级 | 覆盖说明 |
-|:---|:---|:---|:---|:---|:---|:---|:---|
+| :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- |
 | TC-0407 | FindByUserId | 正常查询 | userId=1 | 返回该用户所有权限配置 | 正常路径 | P0 | WHERE userId=? |
 | TC-0408 | FindByUserId | 无记录 | userId=999 | 空slice | 边界 | P1 | |
 | TC-0409 | FindPermIdsByUserIdAndEffect | ALLOW | userId=1, effect="ALLOW" | 返回ALLOW的permIds | 正常路径 | P0 | AND effect=? |
@@ -731,7 +731,7 @@ MySQL (InnoDB) + Redis Cache
 ### 8.8 SysUserRoleModel
 
 | TC编号 | 方法 | 测试场景 | 输入 | 预期结果 | 类型 | 优先级 | 覆盖说明 |
-|:---|:---|:---|:---|:---|:---|:---|:---|
+| :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- |
 | TC-0417 | FindRoleIdsByUserId | 正常查询 | userId=1, DB有3条 | 返回3个roleId | 正常路径 | P0 | SELECT roleId |
 | TC-0418 | FindRoleIdsByUserId | 无绑定 | userId=999 | 空slice | 边界 | P1 | |
 | TC-0419 | FindByUserId | 正常查询 | userId=1 | 返回完整SysUserRole列表 | 正常路径 | P0 | SELECT * |
@@ -745,7 +745,7 @@ MySQL (InnoDB) + Redis Cache
 ### 8.9 SysProductMemberModel
 
 | TC编号 | 方法 | 测试场景 | 输入 | 预期结果 | 类型 | 优先级 | 覆盖说明 |
-|:---|:---|:---|:---|:---|:---|:---|:---|
+| :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- |
 | TC-0426 | FindListByProductCode | 正常分页 | productCode="p1" | list+total | 正常路径 | P0 | WHERE productCode=? |
 | TC-0427 | FindListByProductCode | 空结果 | 无匹配 | total=0 | 边界 | P1 | |
 | TC-0428 | FindByUserId | 正常查询 | userId=1 | 返回用户所有产品成员身份 | 正常路径 | P0 | WHERE userId=? |
@@ -760,7 +760,7 @@ MySQL (InnoDB) + Redis Cache
 ### 9.1 RequireSuperAdmin
 
 | TC编号 | 测试场景 | 输入 | 预期结果 | 类型 | 优先级 | 覆盖说明 |
-|:---|:---|:---|:---|:---|:---|:---|
+| :--- | :--- | :--- | :--- | :--- | :--- | :--- |
 | TC-0435 | 超管通过 | ctx含SuperAdmin UserDetails | nil (允许) | 正常路径 | P0 | caller.IsSuperAdmin |
 | TC-0436 | 非超管拒绝 | ctx含ADMIN UserDetails | 403 "仅超级管理员" | 异常路径 | P0 | !IsSuperAdmin |
 | TC-0437 | MEMBER拒绝 | ctx含MEMBER UserDetails | 403 "仅超级管理员" | 异常路径 | P0 | |
@@ -769,7 +769,7 @@ MySQL (InnoDB) + Redis Cache
 ### 9.2 RequireProductAdmin
 
 | TC编号 | 测试场景 | 输入 | 预期结果 | 类型 | 优先级 | 覆盖说明 |
-|:---|:---|:---|:---|:---|:---|:---|
+| :--- | :--- | :--- | :--- | :--- | :--- | :--- |
 | TC-0439 | 超管通过 | ctx含SuperAdmin | nil | 正常路径 | P0 | IsSuperAdmin |
 | TC-0440 | ADMIN通过 | ctx含ADMIN | nil | 正常路径 | P0 | MemberType==ADMIN |
 | TC-0441 | DEVELOPER拒绝 | ctx含DEVELOPER | 403 | 异常路径 | P0 | 非Admin |
@@ -779,7 +779,7 @@ MySQL (InnoDB) + Redis Cache
 ### 9.3 CheckMemberTypeAssignment
 
 | TC编号 | 测试场景 | 输入 | 预期结果 | 类型 | 优先级 | 覆盖说明 |
-|:---|:---|:---|:---|:---|:---|:---|
+| :--- | :--- | :--- | :--- | :--- | :--- | :--- |
 | TC-0444 | 超管可分配任何类型 | caller=SuperAdmin, assigned=ADMIN | nil | 正常路径 | P0 | IsSuperAdmin豁免 |
 | TC-0445 | ADMIN分配DEVELOPER | caller=ADMIN, assigned=DEVELOPER | nil | 正常路径 | P0 | callerPri(1) < assignPri(2) |
 | TC-0446 | ADMIN分配ADMIN(同级拒绝) | caller=ADMIN, assigned=ADMIN | 403 | 深度业务 | P0 | callerPri >= assignPri |
@@ -790,7 +790,7 @@ MySQL (InnoDB) + Redis Cache
 ### 9.4 CheckManageAccess
 
 | TC编号 | 测试场景 | 输入 | 预期结果 | 类型 | 优先级 | 覆盖说明 |
-|:---|:---|:---|:---|:---|:---|:---|
+| :--- | :--- | :--- | :--- | :--- | :--- | :--- |
 | TC-0450 | 超管可管理任何人 | caller=SuperAdmin | nil | 正常路径 | P0 | IsSuperAdmin豁免 |
 | TC-0451 | 操作自己 | caller.UserId==targetUserId | nil | 正常路径 | P0 | self豁免 |
 | TC-0452 | ADMIN跳过部门检查 | caller=ADMIN | nil (直接比级别) | 深度业务 | P0 | checkDeptHierarchy ADMIN豁免 |
@@ -802,7 +802,7 @@ MySQL (InnoDB) + Redis Cache
 ### 9.5 memberTypePriority
 
 | TC编号 | 测试场景 | 输入 | 预期结果 | 类型 | 优先级 | 覆盖说明 |
-|:---|:---|:---|:---|:---|:---|:---|
+| :--- | :--- | :--- | :--- | :--- | :--- | :--- |
 | TC-0457 | 各类型优先级正确 | 全部4种+未知 | SA=0,A=1,D=2,M=3,unknown=MaxInt32 | 白盒 | P0 | switch分支 |
 
 ## 十、UserDetailsLoader (loaders/userDetailsLoader.go)
@@ -810,7 +810,7 @@ MySQL (InnoDB) + Redis Cache
 ### 10.1 Load / 缓存
 
 | TC编号 | 测试场景 | 输入 | 预期结果 | 类型 | 优先级 | 覆盖说明 |
-|:---|:---|:---|:---|:---|:---|:---|
+| :--- | :--- | :--- | :--- | :--- | :--- | :--- |
 | TC-0458 | DB加载(缓存miss) | 有效userId+productCode | 返回完整UserDetails | 正常路径 | P0 | loadFromDB全链路 |
 | TC-0459 | 缓存命中 | 第二次Load同key | 从Redis返回,不查DB | 正常路径 | P0 | GetCtx hit |
 | TC-0460 | 用户不存在 | userId=999999 | 返回零值UserDetails(Status=0) | 边界 | P0 | loadUser失败 |
@@ -819,7 +819,7 @@ MySQL (InnoDB) + Redis Cache
 ### 10.2 缓存失效
 
 | TC编号 | 测试场景 | 输入 | 预期结果 | 类型 | 优先级 | 覆盖说明 |
-|:---|:---|:---|:---|:---|:---|:---|
+| :--- | :--- | :--- | :--- | :--- | :--- | :--- |
 | TC-0462 | Del删除指定缓存 | Del(uid, pc) | 缓存被删除,下次Load查DB | 正常路径 | P0 | DelCtx |
 | TC-0463 | Clean清除用户所有产品缓存 | Clean(uid) | 该用户所有key被删 | 正常路径 | P0 | KEYS pattern |
 | TC-0464 | CleanByProduct清除产品所有用户 | CleanByProduct(pc) | 该产品所有key被删 | 正常路径 | P0 | KEYS pattern |
@@ -829,7 +829,7 @@ MySQL (InnoDB) + Redis Cache
 ### 10.3 loadPerms权限计算
 
 | TC编号 | 测试场景 | 输入 | 预期结果 | 类型 | 优先级 | 覆盖说明 |
-|:---|:---|:---|:---|:---|:---|:---|
+| :--- | :--- | :--- | :--- | :--- | :--- | :--- |
 | TC-0467 | 超管全量权限 | IsSuperAdmin=true | Perms=全部启用的权限码 | 正常路径 | P0 | 超管分支 |
 | TC-0468 | ADMIN全量权限 | MemberType=ADMIN | Perms=全量 | 正常路径 | P0 | ADMIN分支 |
 | TC-0469 | DEVELOPER全量权限 | MemberType=DEVELOPER | Perms=全量 | 正常路径 | P0 | DEVELOPER分支 |
@@ -839,7 +839,7 @@ MySQL (InnoDB) + Redis Cache
 ### 10.4 loadRoles + MinPermsLevel
 
 | TC编号 | 测试场景 | 输入 | 预期结果 | 类型 | 优先级 | 覆盖说明 |
-|:---|:---|:---|:---|:---|:---|:---|
+| :--- | :--- | :--- | :--- | :--- | :--- | :--- |
 | TC-0472 | 多角色取最小permsLevel | 用户有level=10和level=5的角色 | MinPermsLevel=5 | 正常路径 | P0 | min计算 |
 | TC-0473 | 无角色 | 用户无角色 | MinPermsLevel=MaxInt64 | 边界 | P0 | 默认值 |
 | TC-0474 | 角色跨产品过滤 | 角色在不同产品 | 仅加载当前产品角色 | 深度业务 | P0 | productCode过滤 |
@@ -848,14 +848,14 @@ MySQL (InnoDB) + Redis Cache
 ### 10.5 loadMembership
 
 | TC编号 | 测试场景 | 输入 | 预期结果 | 类型 | 优先级 | 覆盖说明 |
-|:---|:---|:---|:---|:---|:---|:---|
+| :--- | :--- | :--- | :--- | :--- | :--- | :--- |
 | TC-0476 | 超管自动设置SUPER_ADMIN | IsSuperAdmin=true | MemberType=SUPER_ADMIN, 不查DB | 正常路径 | P0 | 早期return |
 | TC-0477 | 非成员MemberType为空 | 用户非该产品成员 | MemberType="" | 边界 | P0 | ErrNotFound |
 
 ## 十一、中间件 — 冻结账号拦截
 
 | TC编号 | 测试场景 | 输入 | 预期结果 | 类型 | 优先级 | 覆盖说明 |
-|:---|:---|:---|:---|:---|:---|:---|
+| :--- | :--- | :--- | :--- | :--- | :--- | :--- |
 | TC-0478 | 冻结用户被403 | 有效token但Status=2 | code=403 "账号已被冻结" | 安全 | P0 | ud.Status!=Enabled |
 | TC-0479 | 用户不存在(Status=0) | token中userId不存在 | code=403 "账号已被冻结" | 安全 | P0 | loadUser失败→Status=0 |
 | TC-0480 | UserDetails注入context | 正常请求 | GetUserDetails(ctx)非nil | 正常路径 | P0 | WithUserDetails |
@@ -863,7 +863,7 @@ MySQL (InnoDB) + Redis Cache
 ## 十二、Logic层 — 访问控制负面测试
 
 | TC编号 | 测试场景 | 输入 | 预期结果 | 类型 | 优先级 | 覆盖说明 |
-|:---|:---|:---|:---|:---|:---|:---|
+| :--- | :--- | :--- | :--- | :--- | :--- | :--- |
 | TC-0481 | createDept非超管拒绝 | ctx=ADMIN | 403 "仅超级管理员" | 安全 | P0 | RequireSuperAdmin |
 | TC-0482 | updateDept非超管拒绝 | ctx=ADMIN | 403 "仅超级管理员" | 安全 | P0 | RequireSuperAdmin |
 | TC-0483 | deleteDept非超管拒绝 | ctx=ADMIN | 403 "仅超级管理员" | 安全 | P0 | RequireSuperAdmin |
@@ -881,28 +881,28 @@ MySQL (InnoDB) + Redis Cache
 ### 13.1 SysRoleModel
 
 | TC编号 | 测试场景 | 输入 | 预期结果 | 类型 | 优先级 | 覆盖说明 |
-|:---|:---|:---|:---|:---|:---|:---|
+| :--- | :--- | :--- | :--- | :--- | :--- | :--- |
 | TC-0492 | FindMinPermsLevelByUserIdAndProductCode正常 | 有角色用户 | 返回最小permsLevel | 正常路径 | P0 | MIN聚合 |
 | TC-0493 | FindMinPermsLevelByUserIdAndProductCode无角色 | 无角色用户 | error(ErrNotFound) | 边界 | P0 | IFNULL返回-1→level<0→ErrNotFound |
 
 ### 13.2 SysPermModel
 
 | TC编号 | 测试场景 | 输入 | 预期结果 | 类型 | 优先级 | 覆盖说明 |
-|:---|:---|:---|:---|:---|:---|:---|
+| :--- | :--- | :--- | :--- | :--- | :--- | :--- |
 | TC-0494 | FindAllCodesByProductCode正常 | 有权限产品 | 返回code列表(仅status=1) | 正常路径 | P0 | WHERE status=1 |
 | TC-0495 | FindAllCodesByProductCode无权限 | 无权限产品 | 空slice | 边界 | P1 | |
 
 ### 13.3 SysUserModel
 
 | TC编号 | 测试场景 | 输入 | 预期结果 | 类型 | 优先级 | 覆盖说明 |
-|:---|:---|:---|:---|:---|:---|:---|
+| :--- | :--- | :--- | :--- | :--- | :--- | :--- |
 | TC-0496 | FindIdsByDeptId正常 | 有用户的部门 | 返回id列表 | 正常路径 | P0 | WHERE deptId=? |
 | TC-0497 | FindIdsByDeptId空部门 | 无用户部门 | 空slice | 边界 | P1 | |
 
 ### 13.4 SysUserRoleModel
 
 | TC编号 | 测试场景 | 输入 | 预期结果 | 类型 | 优先级 | 覆盖说明 |
-|:---|:---|:---|:---|:---|:---|:---|
+| :--- | :--- | :--- | :--- | :--- | :--- | :--- |
 | TC-0498 | FindUserIdsByRoleId正常 | 有绑定的角色 | 返回userId列表 | 正常路径 | P0 | WHERE roleId=? |
 | TC-0499 | FindUserIdsByRoleId无绑定 | 无绑定角色 | 空slice | 边界 | P1 | |
 

+ 59 - 58
test-report.md

@@ -9,7 +9,7 @@
 ## 一、测试执行总览
 
 | 指标 | 数值 |
-|:---|:---|
+| :--- | :--- |
 | 测试用例总数 (test-design.md) | 499 |
 | 已覆盖 TC 数 | 498 |
 | 未实现 TC 数 | 1 (TC-0189, 不可达防御分支, t.Skip) |
@@ -21,6 +21,7 @@
 | ⏭️ 跳过 | **1** (TC-0189 — 防御性不可达分支) |
 
 > **第七轮变更说明**:
+>
 > - **Expires 字段变更** ✅:`ExpiresIn`(过期秒数) → `Expires`(过期 unix 时间戳),REST/gRPC 同步更新
 > - 更新了 TC-0001 (loginLogic)、TC-0013 (refreshTokenLogic)、TC-0165/TC-0172 (gRPC) 的断言逻辑
 > - 至此 BUG-001 ~ BUG-004 及 WARN-001 全部修复,WARN-002 / WARN-003 为设计取舍不修改
@@ -29,7 +30,7 @@
 ### 1.1 各包测试耗时
 
 | 测试包 | 状态 | 耗时 |
-|:---|:---|:---|
+| :--- | :--- | :--- |
 | handler/pub | ✅ ok | 2.475s |
 | loaders | ✅ ok | 3.104s |
 | logic/auth | ✅ ok | 8.033s |
@@ -61,7 +62,7 @@
 ### 2.1 REST API (TC-0001 ~ TC-0160)
 
 | TC编号 | 测试场景 | 测试结果 |
-|:---|:---|:---|
+| :--- | :--- | :--- |
 | TC-0001 | 正常登录-不带productCode | ✅ pass |
 | TC-0002 | 正常登录-带productCode | ✅ pass |
 | TC-0003 | 超管登录+productCode | ✅ pass |
@@ -226,7 +227,7 @@
 ### 2.2 gRPC 接口 (TC-0161 ~ TC-0183)
 
 | TC编号 | 测试场景 | 测试结果 |
-|:---|:---|:---|
+| :--- | :--- | :--- |
 | TC-0161 | 正常同步 | ✅ pass |
 | TC-0162 | appKey无效 | ✅ pass |
 | TC-0163 | appSecret错误 | ✅ pass |
@@ -254,7 +255,7 @@
 ### 2.3 中间件 / 统一响应 / util (TC-0184 ~ TC-0266, TC-0434, TC-0478 ~ TC-0480)
 
 | TC编号 | 测试场景 | 测试结果 |
-|:---|:---|:---|
+| :--- | :--- | :--- |
 | TC-0184 | 正常Bearer token | ✅ pass |
 | TC-0185 | 无Authorization头 | ✅ pass |
 | TC-0186 | 无Bearer前缀 | ✅ pass |
@@ -350,7 +351,7 @@
 **CRUD / 事务 / 批量方法 (TC-0267 ~ TC-0310):**
 
 | TC编号 | 测试场景 | 测试结果 |
-|:---|:---|:---|
+| :--- | :--- | :--- |
 | TC-0267 | 正常插入 | ✅ pass |
 | TC-0268 | 唯一索引冲突 | ✅ pass |
 | TC-0269 | 缓存key生成正确 | ✅ pass |
@@ -399,7 +400,7 @@
 **唯一索引方法明细 (TC-0311 ~ TC-0346):**
 
 | TC编号 | Model | 方法 | 测试场景 | 测试结果 |
-|:---|:---|:---|:---|:---|
+| :--- | :--- | :--- | :--- | :--- |
 | TC-0311 | SysUser | FindOneByUsername | 正常查询 | ✅ pass |
 | TC-0312 | SysUser | FindOneByUsername | 不存在 | ✅ pass |
 | TC-0313 | SysUser | FindOneByUsernameWithTx | 事务内正常查询 | ✅ pass |
@@ -440,7 +441,7 @@
 **内部辅助方法 (TC-0347 ~ TC-0356):**
 
 | TC编号 | 测试场景 | 测试结果 | 未实现原因 |
-|:---|:---|:---|:---|
+| :--- | :--- | :--- | :--- |
 | TC-0347 | 空ids | ✅ pass | role 同包测试可访问私有函数 |
 | TC-0348 | 正常ids | ✅ pass | role 同包测试 |
 | TC-0349 | 部分不存在 | ✅ pass | role 同包测试 |
@@ -455,7 +456,7 @@
 ### 2.5 Model 层自定义方法 (TC-0357 ~ TC-0433)
 
 | TC编号 | 测试场景 | 测试结果 |
-|:---|:---|:---|
+| :--- | :--- | :--- |
 | TC-0357 | 正常分页 | ✅ pass |
 | TC-0358 | 第二页 | ✅ pass |
 | TC-0359 | 空表 | ✅ pass |
@@ -537,7 +538,7 @@
 ### 2.6 访问控制 access.go (TC-0435 ~ TC-0457) 🆕
 
 | TC编号 | 测试场景 | 测试结果 |
-|:---|:---|:---|
+| :--- | :--- | :--- |
 | TC-0435 | RequireSuperAdmin-超管通过 | ✅ pass |
 | TC-0436 | RequireSuperAdmin-ADMIN拒绝 | ✅ pass |
 | TC-0437 | RequireSuperAdmin-MEMBER拒绝 | ✅ pass |
@@ -565,7 +566,7 @@
 ### 2.7 UserDetailsLoader (TC-0458 ~ TC-0477) 🆕
 
 | TC编号 | 测试场景 | 测试结果 |
-|:---|:---|:---|
+| :--- | :--- | :--- |
 | TC-0458 | Load-DB加载(缓存miss) | ✅ pass |
 | TC-0459 | Load-缓存命中 | ✅ pass |
 | TC-0460 | Load-用户不存在 | ✅ pass |
@@ -590,7 +591,7 @@
 ### 2.8 Logic 层访问控制负面测试 (TC-0481 ~ TC-0491) 🆕
 
 | TC编号 | 测试场景 | 测试结果 |
-|:---|:---|:---|
+| :--- | :--- | :--- |
 | TC-0481 | createDept-非超管拒绝 | ✅ pass |
 | TC-0482 | updateDept-非超管拒绝 | ✅ pass |
 | TC-0483 | deleteDept-非超管拒绝 | ✅ pass |
@@ -606,7 +607,7 @@
 ### 2.9 Model 层新增方法 (TC-0492 ~ TC-0499) 🆕
 
 | TC编号 | 测试场景 | 测试结果 |
-|:---|:---|:---|
+| :--- | :--- | :--- |
 | TC-0492 | FindMinPermsLevelByUserIdAndProductCode-正常 | ✅ pass(BUG-003 已修复) |
 | TC-0493 | FindMinPermsLevelByUserIdAndProductCode-无角色 | ✅ pass(BUG-003 已修复) |
 | TC-0494 | FindAllCodesByProductCode-正常 | ✅ pass |
@@ -623,59 +624,59 @@
 ### 3.1 历史修复确认 (v1→v7)
 
 | # | 原始隐患 | 修复轮次 | 最终状态 |
-|:---|:---|:---|:---|
-| BindRoles/BindPerms/SetPerms 非事务 | v2 | ✅ TransactCtx + *WithTx |
-| DeleteRole 无级联删除 | v2 | ✅ 事务内三步级联 |
-| RemoveMember 无级联清理 | v2→v3 | ✅ v2增加ForProduct; v3升级事务 |
-| UpdateUserStatus 无校验 | v2 | ✅ status∈{1,2}+自我保护+超管保护 |
-| SyncPerms N+1 查询 | v2 | ✅ FindMapByProductCode+批量 |
-| CreateProduct 非事务 | v2 | ✅ TransactCtx |
-| GetUserPerms 冗余查询 | v2 | ✅ isSuperAdmin参数 |
-| 错误信息泄露 | v2 | ✅ 统一"服务器内部错误"+logx |
-| VerifyToken 断言无保护 | v2 | ✅ ok检查 |
-| addMember 不校验产品/用户存在性 | v3 | ✅ FindOneByCode+FindOne预检 |
-| bindRoles/bindPerms/setPerms 不校验目标存在性 | v3 | ✅ FindOne预检 |
-| changePassword 新旧相同/截断 | v3 | ✅ len>72+相同密码校验 |
-| CreateDept 非事务 | v3 | ✅ TransactCtx |
-| gRPC errors.New 非规范 | v3 | ✅ status.Error(codes.Xxx) |
-| SyncPerms 不返回 disabled | v3 | ✅ DisableNotInCodes返回int64 |
-| RemoveMember 三步非事务 | v3 | ✅ TransactCtx+*ForProductTx |
-| 全部路由改POST | v3 | ✅ perm.api统一POST |
-| updateUser 无法清空字段 | v4 | ✅ *string 指针类型 |
-| pageSize 无上限 | v4 | ✅ NormalizePage cap 100 |
-| createUser TOCTOU 错误不友好 | v4 | ✅ 捕获 Duplicate entry→ErrConflict |
-| email/phone 无格式校验 | v4 | ✅ util.IsValidEmail/IsValidPhone |
-| changePassword 新密码允许空字符串 | v4 | ✅ len<6 最小长度校验 |
-| createRole 重复角色名返回500 | v5 | ✅ 捕获 Duplicate entry→ErrConflict |
-| memberList N+1 查用户 | v5 | ✅ FindByIds批量查询+map |
-| userList N+1 查成员类型 | v5 | ✅ FindMapByProductCodeUserIds批量 |
-| updateUser DeptId无法清零 | v5 | ✅ *int64 指针类型 |
-| createDeptLogic 事务内 FindOne 隔离 bug | v6 | ✅ FindOne→FindOneWithTx(session) |
-| Model _gen.go 新增 FindOneWithTx / FindOneBy...WithTx | v6 | ✅ 16 个新事务查询方法 |
-| DeptType 字段新增 | v6 | ✅ 创建/更新/树逻辑+consts |
-| GetUserPerms DEV 部门全权限分支 | v6 | ✅ deptId参数+DeptTypeDev检查 |
-| BUG-001: Handler 参数校验返回 500 | v7 | ✅ ErrBadRequest |
-| BUG-002: JWT token 类型无区分 | v7 | ✅ TokenType 字段 |
-| FindByPathPrefix LIKE 注入 | v7 | ✅ NewReplacer 转义 |
-| BUG-003: FindMinPermsLevel sql.NullInt64 不兼容 | v8 | ✅ IFNULL+int64 |
-| BUG-004: cleanByPattern Lua SCAN cursor 类型不匹配 | v9 | ✅ tonumber(nextCursor) |
-| refreshToken 无限续期安全隐患 | v10 | ✅ 不再重新生成 refreshToken,原样返回 |
-| refreshToken 从 body 改为 header 获取 | v10 | ✅ `header:"Authorization"` |
-| ExpiresIn → Expires 字段重命名+含义变更 | v11 | ✅ 秒数→unix时间戳 |
+| :--- | :--- | :--- | :--- |
+| 1 | BindRoles/BindPerms/SetPerms 非事务 | v2 | ✅ TransactCtx + \*WithTx |
+| 2 | DeleteRole 无级联删除 | v2 | ✅ 事务内三步级联 |
+| 3 | RemoveMember 无级联清理 | v2→v3 | ✅ v2增加ForProduct; v3升级事务 |
+| 4 | UpdateUserStatus 无校验 | v2 | ✅ status∈{1,2}+自我保护+超管保护 |
+| 5 | SyncPerms N+1 查询 | v2 | ✅ FindMapByProductCode+批量 |
+| 6 | CreateProduct 非事务 | v2 | ✅ TransactCtx |
+| 7 | GetUserPerms 冗余查询 | v2 | ✅ isSuperAdmin参数 |
+| 8 | 错误信息泄露 | v2 | ✅ 统一"服务器内部错误"+logx |
+| 9 | VerifyToken 断言无保护 | v2 | ✅ ok检查 |
+| 10 | addMember 不校验产品/用户存在性 | v3 | ✅ FindOneByCode+FindOne预检 |
+| 11 | bindRoles/bindPerms/setPerms 不校验目标存在性 | v3 | ✅ FindOne预检 |
+| 12 | changePassword 新旧相同/截断 | v3 | ✅ len>72+相同密码校验 |
+| 13 | CreateDept 非事务 | v3 | ✅ TransactCtx |
+| 14 | gRPC errors.New 非规范 | v3 | ✅ status.Error(codes.Xxx) |
+| 15 | SyncPerms 不返回 disabled | v3 | ✅ DisableNotInCodes返回int64 |
+| 16 | RemoveMember 三步非事务 | v3 | ✅ TransactCtx+\*ForProductTx |
+| 17 | 全部路由改POST | v3 | ✅ perm.api统一POST |
+| 18 | updateUser 无法清空字段 | v4 | ✅ \*string 指针类型 |
+| 19 | pageSize 无上限 | v4 | ✅ NormalizePage cap 100 |
+| 20 | createUser TOCTOU 错误不友好 | v4 | ✅ 捕获 Duplicate entry→ErrConflict |
+| 21 | email/phone 无格式校验 | v4 | ✅ util.IsValidEmail/IsValidPhone |
+| 22 | changePassword 新密码允许空字符串 | v4 | ✅ len<6 最小长度校验 |
+| 23 | createRole 重复角色名返回500 | v5 | ✅ 捕获 Duplicate entry→ErrConflict |
+| 24 | memberList N+1 查用户 | v5 | ✅ FindByIds批量查询+map |
+| 25 | userList N+1 查成员类型 | v5 | ✅ FindMapByProductCodeUserIds批量 |
+| 26 | updateUser DeptId无法清零 | v5 | ✅ \*int64 指针类型 |
+| 27 | createDeptLogic 事务内 FindOne 隔离 bug | v6 | ✅ FindOne→FindOneWithTx(session) |
+| 28 | Model \_gen.go 新增 FindOneWithTx / FindOneBy...WithTx | v6 | ✅ 16 个新事务查询方法 |
+| 29 | DeptType 字段新增 | v6 | ✅ 创建/更新/树逻辑+consts |
+| 30 | GetUserPerms DEV 部门全权限分支 | v6 | ✅ deptId参数+DeptTypeDev检查 |
+| 31 | BUG-001: Handler 参数校验返回 500 | v7 | ✅ ErrBadRequest |
+| 32 | BUG-002: JWT token 类型无区分 | v7 | ✅ TokenType 字段 |
+| 33 | FindByPathPrefix LIKE 注入 | v7 | ✅ NewReplacer 转义 |
+| 34 | BUG-003: FindMinPermsLevel sql.NullInt64 不兼容 | v8 | ✅ IFNULL+int64 |
+| 35 | BUG-004: cleanByPattern Lua SCAN cursor 类型不匹配 | v9 | ✅ tonumber(nextCursor) |
+| 36 | refreshToken 无限续期安全隐患 | v10 | ✅ 不再重新生成 refreshToken,原样返回 |
+| 37 | refreshToken 从 body 改为 header 获取 | v10 | ✅ `header:"Authorization"` |
+| 38 | ExpiresIn → Expires 字段重命名+含义变更 | v11 | ✅ 秒数→unix时间戳 |
 
 ### 3.2 v8 新增模块审计(access 控制 + UserDetailsLoader)
 
 #### 新增文件
 
 | 文件 | 说明 | 审计结论 |
-|:---|:---|:---|
+| :--- | :--- | :--- |
 | `internal/loaders/userDetailsLoader.go` | 用户详情加载器 + Redis 缓存 | 见下方详细分析 |
 | `internal/logic/auth/access.go` | 集中访问控制函数 | 逻辑正确 |
 
 #### access.go 审计结论
 
 | 函数 | 审计结论 |
-|:---|:---|
+| :--- | :--- |
 | `RequireSuperAdmin` | ✅ 正确:nil 检查 → IsSuperAdmin 检查 |
 | `RequireProductAdmin` | ✅ 正确:nil → SuperAdmin → Admin → 拒绝 |
 | `CheckMemberTypeAssignment` | ✅ 正确:SuperAdmin 豁免,优先级比较用 `>=` 阻止同级分配 |
@@ -687,7 +688,7 @@
 #### UserDetailsLoader 审计发现
 
 | # | 发现 | 风险等级 | 说明 |
-|:---|:---|:---|:---|
+| :--- | :--- | :--- | :--- |
 | ~~BUG-003~~ | ~~`FindMinPermsLevelByUserIdAndProductCode` 使用 `sql.NullInt64` 与 go-zero 不兼容~~ | ~~🔴 高~~ | ✅ **已修复 (v9)**:改为 `IFNULL(MIN(...), -1)` + `int64`,level < 0 时返回 `ErrNotFound`。TC-0492、TC-0493 恢复通过。 |
 | ~~WARN-001~~ | ~~`cleanByPattern` 使用 Redis `KEYS` 命令~~ | ~~⚠️ 中~~ | ✅ **已修改 (v9)**:改用 Lua 脚本 + `SCAN` 替代 `KEYS`。但引入 BUG-004(见下)。 |
 | ~~BUG-004~~ | ~~`cleanByPattern` Lua SCAN cursor 类型断言失败~~ | ~~🟡 中~~ | ✅ **已修复 (v10)**:Lua 脚本中 `return nextCursor` 改为 `return tonumber(nextCursor)`,Go 侧 `val.(int64)` 断言恢复正常。TC-0463、TC-0464 通过。 |
@@ -697,7 +698,7 @@
 #### 中间件变更审计
 
 | 变更 | 审计结论 |
-|:---|:---|
+| :--- | :--- |
 | 新增 `loader.Load()` 调用 | ✅ 正确:每次请求加载最新用户详情 |
 | 冻结账号 403 拦截 | ✅ 正确:`ud.Status != consts.StatusEnabled` |
 | UserDetails 注入 context | ✅ 正确:替代旧的多个 context key |
@@ -706,7 +707,7 @@
 #### Logic 层 access 控制接入审计
 
 | Logic | access 检查 | 缓存失效 | 审计结论 |
-|:---|:---|:---|:---|
+| :--- | :--- | :--- | :--- |
 | createDept | RequireSuperAdmin | — | ✅ |
 | updateDept | RequireSuperAdmin | Clean(dept 下所有用户) | ✅ |
 | deleteDept | RequireSuperAdmin | — | ✅ |
@@ -762,7 +763,7 @@
 ### 测试覆盖统计
 
 | 指标 | 数值 |
-|:---|:---|
+| :--- | :--- |
 | TC 总数 | 499 |
 | 已实现 | 498 (99.8%) |
 | 跳过 | 1 (TC-0189,`!ok` 防御性不可达分支) |