# 权限管理系统 (perms-system-server) — 全路径覆盖测试设计 > 测试范围: API (go-zero REST, 全 POST) + gRPC (status codes) + Model 层 (_gen.go 模板生成 + 自定义方法) + Logic 单元测试 + util 层 + 访问控制 + UserDetailsLoader > 测试报告与代码审计详见 [test-report.md](./test-report.md) --- ## 一、系统架构与逻辑链路 ### 1.1 整体调用链路 ```text HTTP Client gRPC Client │ │ ▼ ▼ rest.Server (go-zero) zrpc.Server (go-zero) │ (全部 POST 路由) │ ▼ ▼ Handler 层 (JSON Body 解析) PermServer (permserver.go) │ (status.Error + codes.Xxx) ▼ │ JwtAuth Middleware (鉴权/上下文注入) │ │ │ ▼ ▼ Logic 层 (业务逻辑) ◄────── 共享 ────► authHelper (jwt.go / perms.go) │ │ ▼ ▼ util 层 (NormalizePage / IsValidEmail / IsValidPhone) │ ▼ Model 层 (go-zero sqlc + cache + TransactCtx + 批量查询) │ ├── _gen.go (自定义模板生成: CRUD/Batch/WithTx/缓存管理) │ └── 自定义方法 (分页/按条件查询/级联删除等) ▼ MySQL (InnoDB) + Redis Cache ``` ### 1.2 Model 层接口全景 共 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 组 | 自定义模板 | | _gen.go 内部辅助 | TransactCtx, TableName, findListByPrimaryKeys, getPrimaryKeyValue, buildBatchUpdateQuery, formatPrimary, queryPrimary | 7 | 自定义模板 | | 自定义方法 | 分页查询/按条件查询/级联删除/批量ID查询等 | 3~7 | 手写 | ### 1.3 权限计算逻辑链路 ```text 输入: userId + deptId + productCode + isSuperAdmin(bool) │ ├─ isSuperAdmin=true → 产品全部启用权限 + "SUPER_ADMIN" ├─ 非产品成员 → nil + "" ├─ DEVELOPER/ADMIN → 产品全部启用权限 + memberType ├─ MEMBER + deptId>0 + dept.DeptType="DEV" → 产品全部启用权限 + "MEMBER" ├─ MEMBER + deptId>0 + dept查询失败/DeptType≠"DEV" → 继续走角色权限流程 └─ MEMBER → (角色权限 ∪ ALLOW) - DENY → 过滤 status=1 ``` --- ## 二、REST API 测试用例 > **注意**: 所有路由统一为 POST 方法,请求参数均通过 JSON Body 传递。 ### 2.1 产品端登录 `POST /api/auth/login` | TC编号 | 接口/方法 | 测试场景 | 输入参数 (JSON) | 预期结果 | 测试类型 | 优先级 | 覆盖说明 | | :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- | | TC-0001 | POST /api/auth/login | 正常登录(普通用户+productCode) | `{"username":"user1","password":"123456","productCode":"test"}` | code=0, accessToken/refreshToken/userInfo | 正常路径 | P0 | loginLogic全路径 | | TC-0002 | POST /api/auth/login | 正常登录-带productCode+ADMIN成员 | `{"username":"user1","password":"123456","productCode":"test"}` | code=0, perms含用户可用权限, memberType="ADMIN" | 正常路径 | P0 | GetUserPerms(false) MEMBER分支 | | TC-0003 | POST /api/auth/login | 超管通过产品端登录被拒绝 | `{"username":"super","password":"x","productCode":"p1"}` | code=403, "超级管理员不允许通过产品端登录,请使用管理后台" | 安全 | P0 | IsSuperAdmin==1 → ErrForbidden | | TC-0004 | POST /api/auth/login | 超管无productCode被拒绝 | `{"username":"super","password":"x"}` | code=403, "超级管理员不允许通过产品端登录,请使用管理后台" | 安全 | P0 | IsSuperAdmin==1 → ErrForbidden | | TC-0005 | POST /api/auth/login | 用户不存在 | `{"username":"notexist","password":"x"}` | code=401, "用户名或密码错误" | 异常路径 | P0 | ErrNotFound分支 | | TC-0006 | POST /api/auth/login | DB异常(非ErrNotFound) | FindOneByUsername连接失败 | code=500, "服务器内部错误" | 异常路径 | P1 | 透传err→Setup兜底 | | TC-0007 | POST /api/auth/login | 密码错误 | `{"username":"admin","password":"wrong"}` | code=401 | 异常路径 | P0 | bcrypt比对失败 | | TC-0008 | POST /api/auth/login | 账号冻结 | status=2用户 | code=403, "账号已被冻结" | 分支覆盖 | P0 | u.Status!=1 | | TC-0009 | POST /api/auth/login | 非产品成员 | productCode指向用户不属于的产品 | code=403, "您不是该产品的成员" | 安全 | P0 | 非成员禁止登录 | | TC-0010 | POST /api/auth/login | DEVELOPER成员 | DEVELOPER类型成员 | perms全量, memberType="DEVELOPER" | 分支覆盖 | P1 | perms.go DEVELOPER分支 | | TC-0011 | POST /api/auth/login | SQL注入 | `{"username":"' OR 1=1 --","password":"x"}` | code=401 | 安全 | P0 | 参数化查询 | | TC-0012 | POST /api/auth/login | 缺少必填字段 | `{}` | HTTP 400 | 边界 | P1 | httpx.Parse校验(productCode现为必填) | | TC-0013 | POST /api/auth/login | 产品成员被禁用时拒绝登录 | member.status=Disabled | 403 "您的产品成员资格已被禁用" | 安全 | P0 | H-3: loginService | | TC-0014 | POST /api/auth/login | 产品被禁用时拒绝登录 | product.status=Disabled | 403 "该产品已被禁用" | 安全 | P0 | H-3: loginService | ### 2.1b 管理后台登录 `POST /api/auth/adminLogin` | TC编号 | 接口/方法 | 测试场景 | 输入参数 (JSON) | 预期结果 | 测试类型 | 优先级 | 覆盖说明 | | :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- | | TC-0015 | POST /api/auth/adminLogin | 超管正常登录 | `{"username":"super","password":"x","managementKey":"valid"}` | code=0, accessToken/refreshToken/userInfo, isSuperAdmin=1, memberType="SUPER_ADMIN", perms为空 | 正常路径 | P0 | adminLoginLogic全路径 | | TC-0016 | POST /api/auth/adminLogin | 普通用户被拒绝 | `{"username":"user1","password":"x","managementKey":"valid"}` | code=401, "用户名或密码错误" | 安全 | P0 | 审计M-7修复: 统一错误消息防用户枚举 | | TC-0017 | POST /api/auth/adminLogin | managementKey无效 | `{"username":"user1","password":"x","managementKey":"wrong"}` | code=401, "managementKey无效" | 安全 | P0 | 第一个校验点 | | TC-0018 | POST /api/auth/adminLogin | managementKey为空 | `{"username":"user1","password":"x","managementKey":""}` | code=401, "managementKey无效" | 安全 | P0 | 空字符串≠config值 | | TC-0019 | POST /api/auth/adminLogin | 用户不存在 | `{"username":"notexist","password":"x","managementKey":"valid"}` | code=401, "用户名或密码错误" | 异常路径 | P0 | ErrNotFound分支 | | TC-0020 | POST /api/auth/adminLogin | 密码错误 | `{"username":"user1","password":"wrong","managementKey":"valid"}` | code=401, "用户名或密码错误" | 异常路径 | P0 | bcrypt比对失败 | | TC-0021 | POST /api/auth/adminLogin | 账号冻结 | status=2用户 | code=401, "用户名或密码错误" | 安全 | P0 | 审计M-7修复: 冻结/非超管统一返回同一错误防枚举 | | TC-0022 | POST /api/auth/adminLogin | 不带productCode时perms为空 | 管理后台登录超管 | userInfo.perms为空, memberType="SUPER_ADMIN"(超管标记由Loader自动填充) | 功能验证 | P0 | Load(ctx, uid, "") | | TC-0023 | POST /api/auth/adminLogin | 缺少必填字段 | `{}` | HTTP 400 | 边界 | P1 | httpx.Parse校验 | | TC-0024 | POST /api/auth/adminLogin | SQL注入username | `{"username":"' OR 1=1 --","password":"x","managementKey":"valid"}` | code=401 | 安全 | P0 | 参数化查询 | | TC-0025 | POST /api/auth/adminLogin | adminLogin 用户名限流 | 对同一用户名连续多次失败登录 | 触发后返回 429 "请求过于频繁" | 安全 | P0 | H-2: 防用户名枚举爆破 | ### 2.2 刷新Token `POST /api/auth/refreshToken` | TC编号 | 接口/方法 | 测试场景 | 输入参数 (JSON) | 预期结果 | 测试类型 | 优先级 | 覆盖说明 | | :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- | | TC-0026 | POST /api/auth/refreshToken | 正常刷新 | Header `Authorization: Bearer ` | code=0, 新accessToken, 新refreshToken(token轮转,保留原始过期时间) | 正常路径 | P0 | refreshTokenLogic全路径 | | TC-0027 | POST /api/auth/refreshToken | 不带productCode(回退) | Header Authorization, 无productCode | 使用claims.ProductCode | 分支覆盖 | P1 | productCode=""回退 | | TC-0028 | POST /api/auth/refreshToken | token无效 | Header `Authorization: Bearer invalid` | code=401 | 异常路径 | P0 | ParseRefreshToken失败 | | TC-0029 | POST /api/auth/refreshToken | 用户已删除 | token中userId不存在 | code=403, "账号已被冻结" | 异常路径 | P1 | UserDetailsLoader返回Status=0 | | TC-0030 | POST /api/auth/refreshToken | 账号冻结 | 冻结用户 | code=403 | 分支覆盖 | P0 | Status!=1 | | TC-0031 | POST /api/auth/refreshToken | 超管+productCode(token中已含相同pc) | isSuperAdmin=1, token中productCode=pc, req.ProductCode=pc | refreshToken原样返回, SUPER_ADMIN权限 | 分支覆盖 | P1 | isSuperAdmin分支+productCode不变 | | TC-0032 | POST /api/auth/refreshToken | 尝试切换产品被拒绝 | token中productCode="p1", req.ProductCode="p2" | code=400, "刷新令牌不允许切换产品" | 安全 | P0 | H-02修复: 禁止跨产品切换 | | TC-0033 | POST /api/auth/refreshToken | TokenVersion不匹配时拒绝刷新 | refreshToken含tokenVersion=999, DB中tokenVersion=0 | 401 "登录状态已失效,请重新登录" | 安全 | P0 | claims.TokenVersion != ud.TokenVersion | | TC-0034 | POST /api/auth/refreshToken | 使用accessToken作为refreshToken被拒绝 | 用accessSecret签发的accessToken作为refreshToken传入 | 401 "refreshToken无效或已过期" | 安全 | P0 | ParseRefreshToken校验TokenType!=refresh | | TC-0035 | POST /api/auth/refreshToken | 产品成员已移除时拒绝刷新 | refreshToken含productCode, 但用户已从该产品移除 | 403 "您已不是该产品的成员" | 安全 | P0 | ud.MemberType=="" && !ud.IsSuperAdmin | ### 2.3 同步权限 `POST /api/perm/sync` | TC编号 | 接口/方法 | 测试场景 | 输入参数 (JSON) | 预期结果 | 测试类型 | 优先级 | 覆盖说明 | | :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- | | TC-0036 | 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-0037 | POST /api/perm/sync | 更新已有(名称变更) | 已存在code但name不同 | updated=1 | 正常路径 | P0 | toUpdate→BatchUpdate | | TC-0038 | POST /api/perm/sync | 无变化 | 已存在且name/remark/status均相同 | added=0, updated=0 | 分支覆盖 | P1 | 跳过更新 | | TC-0039 | POST /api/perm/sync | 禁用权限重启 | 已status=2的权限在列表中 | updated=1, status恢复1 | 分支覆盖 | P1 | Status!=1条件 | | TC-0040 | POST /api/perm/sync | 移除不在列表的权限 | DB有多余权限 | disabled>0 | 正常路径 | P0 | DisableNotInCodes | | TC-0041 | POST /api/perm/sync | 空perms数组被拒绝 | `{"...","perms":[]}` | code=400, "权限列表不能为空" | 输入校验 | P0 | 空列表校验,防止意外批量禁用 | | TC-0042 | POST /api/perm/sync | 验证disabled返回值 | 已知DB有5条,perms仅含2条 | disabled=3 | 功能验证 | P0 | RowsAffected() | | TC-0043 | POST /api/perm/sync | appKey无效 | `{"appKey":"invalid"}` | code=401 | 异常路径 | P0 | FindOneByAppKey失败 | | TC-0044 | POST /api/perm/sync | appSecret错误 | secret不匹配 | code=401 | 异常路径 | P0 | AppSecret比对 | | TC-0045 | POST /api/perm/sync | 产品已禁用 | product.Status!=1 | code=403 | 分支覆盖 | P0 | Status!=1 | | TC-0046 | POST /api/perm/sync | 大批量(1000条) | 1000条perms | added=1000 | 性能 | P2 | BatchInsert性能 | | TC-0047 | POST /api/perm/sync | 重复code去重 | perms中包含两个相同code | 仅处理一次, added=1(而非2) | 分支覆盖 | P0 | M-09修复: seen去重 | | TC-0048 | POST /api/perm/sync | 事务保护-中途失败回滚 | 模拟BatchUpdate失败 | 全部操作回滚, 返回SyncPermsError(500,"同步权限事务失败"), 不透传DB错误 | 事务验证 | P0 | H-05/H-03 修复:LockByCodeTx→FindMapByProductCodeWithTx→BatchInsert→BatchUpdate 任一失败统一 500 | ### 2.4 获取用户信息 `POST /api/auth/userInfo` | TC编号 | 接口/方法 | 测试场景 | 输入参数 (JSON) | 预期结果 | 测试类型 | 优先级 | 覆盖说明 | | :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- | | TC-0049 | POST /api/auth/userInfo | 正常获取-含productCode | Bearer token (含productCode) | code=0, 完整UserInfo+实时perms | 正常路径 | P0 | userInfoLogic全路径 | | TC-0050 | POST /api/auth/userInfo | 不含productCode | Bearer token (无productCode) | perms为空 | 分支覆盖 | P1 | productCode="" | | TC-0051 | POST /api/auth/userInfo | 未登录 | 无Authorization头 | code=401, "未登录" | 异常路径 | P0 | middleware拦截 | | TC-0052 | POST /api/auth/userInfo | token过期 | 过期token | code=401 | 异常路径 | P0 | middleware | | TC-0053 | POST /api/auth/userInfo | userId=0 | 伪造claims | code=401, "未登录" | 分支覆盖 | P1 | userId==0 | ### 2.5 修改密码 `POST /api/auth/changePassword` | TC编号 | 接口/方法 | 测试场景 | 输入参数 (JSON) | 预期结果 | 测试类型 | 优先级 | 覆盖说明 | | :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- | | TC-0054 | POST /api/auth/changePassword | 正常修改 | `{"oldPassword":"123456","newPassword":"654321"}` | code=0 | 正常路径 | P0 | changePasswordLogic全路径 | | TC-0055 | POST /api/auth/changePassword | mustChangePassword重置 | 正常修改后 | DB中mustChangePassword=2 | 功能验证 | P0 | user.MustChangePassword=2 | | TC-0056 | POST /api/auth/changePassword | 原密码错误 | `{"oldPassword":"wrong","newPassword":"newpwd"}` | code=400, "原密码错误" | 异常路径 | P0 | bcrypt失败 | | TC-0057 | POST /api/auth/changePassword | 新密码少于8字符 | `{"oldPassword":"old","newPassword":"Pas1234"}` | code=400, "密码长度不能少于8个字符" | 输入校验 | P0 | len<8 | | TC-0058 | POST /api/auth/changePassword | 新密码恰好8字符(含大小写+数字) | `{"oldPassword":"old","newPassword":"Abcdef1x"}` | code=0 | 边界 | P1 | len==8,含大小写+数字 | | TC-0059 | POST /api/auth/changePassword | 新密码空字符串 | `{"oldPassword":"old","newPassword":""}` | code=400 | 边界 | P0 | len("")=0<8 | | TC-0060 | POST /api/auth/changePassword | 新密码超过72字符 | `{"oldPassword":"old","newPassword":"a*73"}` | code=400, "密码长度不能超过72个字符" | 输入校验 | P0 | len>72 | | TC-0061 | POST /api/auth/changePassword | 新密码恰好72字符 | `{"oldPassword":"old","newPassword":"a*72"}` | code=0 | 边界 | P1 | len==72 | | TC-0062 | POST /api/auth/changePassword | 新旧密码相同 | `{"oldPassword":"123456","newPassword":"123456"}` | code=400, "新密码不能与原密码相同" | 输入校验 | P0 | OldPassword==NewPassword | | TC-0063 | POST /api/auth/changePassword | 用户不存在 | token中userId已删除 | code=404 | 异常路径 | P1 | FindOne失败 | ### 2.6 创建产品 `POST /api/product/create` | TC编号 | 接口/方法 | 测试场景 | 输入参数 (JSON) | 预期结果 | 测试类型 | 优先级 | 覆盖说明 | | :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- | | TC-0064 | POST /api/product/create | 正常创建 | `{"code":"new","name":"新产品"}` | code=0, id/appKey/appSecret/adminUser/adminPassword | 正常路径 | P0 | TransactCtx全路径 | | TC-0065 | POST /api/product/create | 事务回滚-用户创建失败 | 模拟InsertWithTx User失败 | 返回错误, DB无新产品 | 事务验证 | P0 | TransactCtx回滚 | | TC-0066 | POST /api/product/create | 事务回滚-成员创建失败 | 模拟InsertWithTx Member失败 | 产品和用户均回滚 | 事务验证 | P0 | TransactCtx回滚 | | TC-0067 | POST /api/product/create | 编码已存在 | `{"code":"existing","name":"x"}` | code=409 | 异常路径 | P0 | FindOneByCode成功 | | TC-0068 | POST /api/product/create | 并发创建同编码 | 两请求同时 | 一成功一冲突 | 并发 | P1 | uk_code | | TC-0069 | POST /api/product/create | createProduct 含空格被拒绝 | code="abc def" | 400 "产品编码格式不合法" | 输入校验 | P0 | productCodeRegexp | | TC-0070 | POST /api/product/create | createProduct 含特殊字符被拒绝 | code="abc@def" | 400 | 输入校验 | P0 | productCodeRegexp | | TC-0071 | POST /api/product/create | createProduct 全中文被拒绝 | code="产品一" | 400 | 输入校验 | P0 | productCodeRegexp | | TC-0072 | POST /api/product/create | createProduct 纯数字开头被拒绝 | code="1abc" | 400 | 输入校验 | P0 | productCodeRegexp 首字符限定 | | TC-0073 | POST /api/product/create | createProduct 空字符串被拒绝 | code="" | 400 | 边界 | P0 | | | TC-0074 | POST /api/product/create | createProduct 长度>64 被拒绝 | code="a"*65 | 400 "产品编码长度不能超过64个字符" | 边界 | P0 | len>64 | | TC-0075 | POST /api/product/create | createProduct 合法编码(含下划线/中划线/数字) | code="pc_01-test" | 创建成功 | 正常路径 | P0 | Regexp 正向匹配 | ### 2.7 产品更新/列表/详情 | TC编号 | 接口/方法 | 测试场景 | 输入参数 (JSON) | 预期结果 | 测试类型 | 优先级 | 覆盖说明 | | :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- | | TC-0076 | POST /api/product/update | 正常更新 | `{"id":1,"name":"新名","status":1}` | code=0 | 正常路径 | P0 | updateProductLogic | | TC-0077 | POST /api/product/update | 不存在 | `{"id":9999,"name":"x"}` | code=404 | 异常路径 | P0 | FindOne失败 | | TC-0078 | POST /api/product/update | 不传status | `{"id":1,"name":"x"}` | status不变 | 分支覆盖 | P1 | Status>0 | | ~~TC-0079~0083~~ | ~~POST /api/product/list 分页边界~~ | ~~正常/默认/超限/0/负值~~ | — | — | — | — | **已删除**:分页边界由 `util.TestNormalizePage` 单元测试覆盖;列表语义被 M-2 拆分为"超管走 FindList / 非超管只看自己"两条独立契约(TC-0850、TC-0871) | | ~~TC-0084~0085~~ | ~~POST /api/product/detail 正常/不存在~~ | — | — | — | — | — | **已删除**:被 M-2 契约合并改写为 TC-0852(他人产品 → 404)、TC-0853(自己产品 AppKey 脱敏)、TC-0872(FindOne 错误 → 404 无差别响应) | | ~~TC-0086 / TC-0088~~ | ~~非超管 AppKey 隐藏~~ | — | — | — | — | — | **已删除**:由 TC-0850(list)+ TC-0853(detail)覆盖 | | ~~TC-0087 / TC-0089~~ | ~~超管可见 AppKey~~ | — | — | — | — | — | **已删除**:由 TC-0854(detail)+ TC-0871(list)覆盖 | | TC-0090 | POST /api/product/update | updateProduct 非法状态值被拒绝 | status=99 | 400 "产品状态值无效" | 输入校验 | P0 | H-4: 仅允许 1/2 | ### 2.8 创建部门 `POST /api/dept/create` | TC编号 | 接口/方法 | 测试场景 | 输入参数 (JSON) | 预期结果 | 测试类型 | 优先级 | 覆盖说明 | | :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- | | TC-0091 | POST /api/dept/create | 创建顶级部门 | `{"parentId":0,"name":"总部"}` | code=0, path="/{id}/" | 正常路径 | P0 | TransactCtx, parentPath="/" | | TC-0092 | POST /api/dept/create | 创建子部门 | `{"parentId":1,"name":"技术部"}` | code=0, path=parent.path+id+"/" | 正常路径 | P0 | parentId>0分支 | | TC-0093 | POST /api/dept/create | 父部门不存在 | `{"parentId":9999,"name":"x"}` | code=404, "父部门不存在" | 异常路径 | P0 | FindOneWithTx失败 | | TC-0094 | POST /api/dept/create | 不传DeptType默认NORMAL | `{"parentId":0,"name":"x"}` | DB deptType="NORMAL" | 分支覆盖 | P0 | deptType=""→DeptTypeNormal | | TC-0095 | POST /api/dept/create | 传DeptType=DEV | `{"parentId":0,"name":"x","deptType":"DEV"}` | DB deptType="DEV" | 正常路径 | P0 | req.DeptType赋值 | | TC-0096 | POST /api/dept/create | 事务内FindOneWithTx可见性 | TransactCtx内InsertWithTx后FindOneWithTx | 事务内可读到未提交数据 | 事务验证 | P0 | FindOneWithTx(session) | | TC-0097 | POST /api/dept/create | 事务回滚-Insert失败 | 模拟InsertWithTx失败 | DB无新记录 | 事务验证 | P0 | TransactCtx回滚 | | TC-0098 | POST /api/dept/create | 事务回滚-UpdateWithTx失败 | 模拟UpdateWithTx失败 | Insert也回滚 | 事务验证 | P1 | TransactCtx回滚 | | TC-0099 | POST /api/dept/create | 多层嵌套(5层) | 递归创建5层 | path正确拼接 | 深度测试 | P2 | path逻辑 | | TC-0100 | POST /api/dept/create | 通过Logic创建+验证Path | CreateDeptLogic.CreateDept→FindOne | path包含/{id}/ | 集成验证 | P0 | FindOneWithTx修复后端到端 | ### 2.9 部门更新/删除/树 | TC编号 | 接口/方法 | 测试场景 | 输入参数 (JSON) | 预期结果 | 测试类型 | 优先级 | 覆盖说明 | | :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- | | TC-0101 | POST /api/dept/update | 正常更新 | `{"id":1,"name":"新名","sort":5}` | code=0 | 正常路径 | P0 | updateDeptLogic | | TC-0102 | POST /api/dept/update | 不存在 | `{"id":9999,"name":"x"}` | code=404 | 异常路径 | P0 | FindOne失败 | | TC-0103 | POST /api/dept/update | DeptType NORMAL→DEV | `{"id":1,"deptType":"DEV"}` | DB deptType="DEV" | 正常路径 | P0 | DeptType合法值更新 | | TC-0104 | POST /api/dept/update | DeptType无效值返回错误 | `{"id":1,"deptType":"INVALID"}` | code=400, "部门类型无效", DB deptType不变 | 输入校验 | P0 | DeptType校验,仅NORMAL/DEV | | TC-0105 | POST /api/dept/update | DeptType变更时级联清除子部门用户缓存 | 部门从NORMAL改为DEV,有子部门含用户 | code=0, 子部门下用户缓存被清除 | 缓存验证 | P0 | M-10修复: 级联缓存失效 | | TC-0106 | POST /api/dept/delete | 正常删除(无子部门) | `{"id":5}` | code=0 | 正常路径 | P0 | deleteDeptLogic | | TC-0107 | POST /api/dept/delete | 有子部门 | `{"id":1}` | code=400, "存在子部门" | 业务约束 | P0 | len(children)>0 | | TC-0108 | POST /api/dept/delete | 不存在的部门 | `{"id":9999}` | code=0(Delete对不存在行不报错) | 边界 | P1 | FindByParentId空+Delete | | TC-0109 | POST /api/dept/delete | 部门下有关联用户 | 部门id指向含用户的部门 | code=400, "该部门下仍有关联用户,无法删除" | 业务约束 | P0 | H-07修复: 检查关联用户 | | ~~TC-0110~0112~~ | ~~POST /api/dept/tree 正常/空/孤儿~~ | — | — | — | — | — | **已删除**:M-2 后 DeptTree 按 caller 身份剪枝,旧测试假定任何身份可拿全树已不成立;新契约由 TC-0855/0856/0857(`deptTreeAccessControl_audit_test.go`)覆盖,"孤儿→根"行为隐含在 fullAccess 路径中 | ### 2.10 权限列表 `POST /api/perm/list` | TC编号 | 接口/方法 | 测试场景 | 输入参数 (JSON) | 预期结果 | 测试类型 | 优先级 | 覆盖说明 | | :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- | | TC-0113 | POST /api/perm/list | 正常查询 | `{"productCode":"p1","page":1,"pageSize":10}` | code=0, total/list | 正常路径 | P0 | permListLogic | | TC-0114 | POST /api/perm/list | 默认分页 | `{"productCode":"p1"}` | page=1, pageSize=20 | 分支覆盖 | P1 | NormalizePage | | TC-0115 | POST /api/perm/list | pageSize超过上限 | `{"productCode":"p1","pageSize":200}` | 实际pageSize=100 | 边界 | P0 | NormalizePage cap | | TC-0116 | POST /api/perm/list | 不存在的productCode | `{"productCode":"notexist"}` | total=0, list=[] | 边界 | P1 | 空结果 | ### 2.11 角色管理 | TC编号 | 接口/方法 | 测试场景 | 输入参数 (JSON) | 预期结果 | 测试类型 | 优先级 | 覆盖说明 | | :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- | | TC-0117 | POST /api/role/create | 正常创建 | `{"productCode":"p1","name":"管理员","permsLevel":1}` | code=0, id>0 | 正常路径 | P0 | createRoleLogic | | TC-0118 | POST /api/role/create | 重复角色名 | 同产品同名 | code=409, "该产品下角色名已存在" | 业务约束 | P0 | Duplicate entry→ErrConflict | | TC-0119 | POST /api/role/create | 并发同名创建 | 两请求同时 | 一成功一冲突409 | 并发 | P1 | 唯一索引+1062捕获 | | TC-0120 | POST /api/role/update | 正常更新 | `{"id":1,"name":"新名","permsLevel":2}` | code=0 | 正常路径 | P0 | updateRoleLogic | | TC-0121 | POST /api/role/update | 不存在 | `{"id":9999,...}` | code=404 | 异常路径 | P0 | FindOne失败 | | TC-0122 | POST /api/role/list | 正常查询 | `{"productCode":"p1","page":1,"pageSize":10}` | code=0 | 正常路径 | P0 | roleListLogic | | TC-0123 | POST /api/role/list | pageSize超过上限 | `{"productCode":"p1","pageSize":200}` | 实际pageSize=100 | 边界 | P0 | NormalizePage cap | | TC-0124 | POST /api/role/detail | 正常查询 | `{"id":1}` | code=0, 含permIds | 正常路径 | P0 | roleDetailLogic | | TC-0125 | POST /api/role/detail | 不存在 | `{"id":9999}` | code=404 | 异常路径 | P0 | FindOne失败 | ### 2.12 删除角色 `POST /api/role/delete` | TC编号 | 接口/方法 | 测试场景 | 输入参数 (JSON) | 预期结果 | 测试类型 | 优先级 | 覆盖说明 | | :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- | | TC-0126 | POST /api/role/delete | 正常删除+级联 | `{"id":5}` (含权限/用户绑定) | code=0, role_perm/user_role同步清理 | 正常+事务 | P0 | TransactCtx全路径 | | TC-0127 | POST /api/role/delete | 事务回滚 | 模拟DeleteWithTx失败 | 级联删除回滚 | 事务验证 | P0 | TransactCtx | | TC-0128 | POST /api/role/delete | 无关联数据 | 新角色无绑定 | code=0 | 分支覆盖 | P1 | 删0条 | ### 2.13 绑定角色权限 `POST /api/role/bindPerms` | TC编号 | 接口/方法 | 测试场景 | 输入参数 (JSON) | 预期结果 | 测试类型 | 优先级 | 覆盖说明 | | :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- | | TC-0129 | POST /api/role/bindPerms | 正常绑定 | `{"roleId":1,"permIds":[1,2,3]}` | code=0 | 正常路径 | P0 | TransactCtx | | TC-0130 | POST /api/role/bindPerms | 角色不存在 | `{"roleId":9999,"permIds":[1]}` | code=404, "角色不存在" | 存在性校验 | P0 | FindOne预检 | | TC-0131 | POST /api/role/bindPerms | 清空权限 | `{"roleId":1,"permIds":[]}` | code=0, 全清空 | 分支覆盖 | P1 | len==0→return | | TC-0132 | POST /api/role/bindPerms | 重复permId | `{"roleId":1,"permIds":[1,1]}` | DB唯一索引→事务回滚 | 边界 | P1 | uk_role_perm | | TC-0133 | POST /api/role/bindPerms | 事务回滚 | 模拟BatchInsertWithTx失败 | 旧数据回滚还原 | 事务验证 | P0 | TransactCtx | ### 2.14 创建用户 `POST /api/user/create` | TC编号 | 接口/方法 | 测试场景 | 输入参数 (JSON) | 预期结果 | 测试类型 | 优先级 | 覆盖说明 | | :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- | | TC-0134 | POST /api/user/create | 正常创建 | `{"username":"new","password":"123456"}` | code=0, id>0 | 正常路径 | P0 | createUserLogic | | TC-0135 | POST /api/user/create | 用户名已存在(预检) | `{"username":"existing","password":"x"}` | code=409, "用户名已存在" | 异常路径 | P0 | FindOneByUsername成功 | | TC-0136 | POST /api/user/create | 带完整可选字段 | 含nickname/email/phone/remark/deptId | code=0 | 正常路径 | P1 | 各字段赋值 | | TC-0137 | POST /api/user/create | 非法email格式 | `{"...","email":"not-an-email"}` | code=400, "邮箱格式不正确" | 输入校验 | P0 | util.IsValidEmail | | TC-0138 | POST /api/user/create | 合法email | `{"...","email":"user@example.com"}` | code=0 | 正常路径 | P1 | IsValidEmail通过 | | TC-0139 | POST /api/user/create | email为空(可选) | `{"...","email":""}` | code=0, 跳过校验 | 分支覆盖 | P1 | email!=""判断 | | TC-0140 | POST /api/user/create | 非法phone格式 | `{"...","phone":"abc"}` | code=400, "手机号格式不正确" | 输入校验 | P0 | util.IsValidPhone | | TC-0141 | POST /api/user/create | 合法phone(国际) | `{"...","phone":"+8613800138000"}` | code=0 | 正常路径 | P1 | IsValidPhone通过 | | TC-0142 | POST /api/user/create | phone为空(可选) | `{"...","phone":""}` | code=0, 跳过校验 | 分支覆盖 | P1 | phone!=""判断 | | TC-0143 | POST /api/user/create | 并发同username(TOCTOU) | 两请求同时 | 一成功一冲突(1062) | 并发 | P0 | Duplicate entry→ErrConflict | | TC-0144 | POST /api/user/create | 唯一索引冲突消息 | 预检通过后DB冲突 | code=409, "用户名已存在" | 异常路径 | P0 | strings.Contains "1062" | | TC-0145 | POST /api/user/create | 密码少于8字符 | `{"username":"x","password":"Pas1234"}` | code=400, "密码长度不能少于8个字符" | 输入校验 | P0 | H-10修复: 密码强度校验(8+字符,含大小写+数字) | | TC-0146 | POST /api/user/create | 密码缺少大写字母 | `{"username":"x","password":"pass123456"}` | code=400, "密码必须包含大写字母、小写字母和数字" | 输入校验 | P0 | 密码复杂性: 无大写 | | TC-0147 | POST /api/user/create | 密码缺少小写字母 | `{"username":"x","password":"PASS123456"}` | code=400, "密码必须包含大写字母、小写字母和数字" | 输入校验 | P0 | 密码复杂性: 无小写 | | TC-0148 | POST /api/user/create | 密码缺少数字 | `{"username":"x","password":"Passpasspass"}` | code=400, "密码必须包含大写字母、小写字母和数字" | 输入校验 | P0 | 密码复杂性: 无数字 | | TC-0149 | POST /api/user/create | 密码超过72字符 | `{"username":"x","password":"a*73"}` | code=400, "密码长度不能超过72个字符" | 输入校验 | P0 | H-10修复: 密码强度校验 | | TC-0150 | POST /api/user/create | 用户名含特殊字符被拒绝 | `{"username":"user@name!","password":"pass123456"}` | 400 "用户名只能包含字母、数字和下划线,长度2-64个字符" | 输入校验 | P0 | usernameRegexp不匹配 | | TC-0151 | POST /api/user/create | 用户名太短(1字符)被拒绝 | `{"username":"a","password":"pass123456"}` | 400 "用户名只能包含字母、数字和下划线,长度2-64个字符" | 边界值 | P0 | 最小长度2 | | TC-0152 | POST /api/user/create | 用户名太长(65字符)被拒绝 | `{"username":"a*65","password":"pass123456"}` | 400 "用户名只能包含字母、数字和下划线,长度2-64个字符" | 边界值 | P0 | 最大长度64 | | TC-0153 | POST /api/user/create | 部门不存在被拒绝 | `{"username":"x","password":"pass123456","deptId":999999999}` | 400 "部门不存在" | 异常路径 | P1 | DeptId>0时校验FindOne | | TC-0154 | POST /api/user/create | 昵称超过64字符被拒绝 | `{"username":"x","password":"pass123456","nickname":"n*65"}` | 400 "昵称长度不能超过64个字符" | 边界值 | P1 | len(Nickname)>64 | | TC-0155 | POST /api/user/create | 备注超过255字符被拒绝 | `{"username":"x","password":"pass123456","remark":"r*256"}` | 400 "备注长度不能超过255个字符" | 边界值 | P1 | len(Remark)>255 | ### 2.15 用户更新 `POST /api/user/update` (指针类型+DeptId可清零) | TC编号 | 接口/方法 | 测试场景 | 输入参数 (JSON) | 预期结果 | 测试类型 | 优先级 | 覆盖说明 | | :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- | | TC-0156 | POST /api/user/update | 正常更新 | `{"id":1,"nickname":"n","email":"a@b.com"}` | code=0 | 正常路径 | P0 | updateUserLogic | | TC-0157 | POST /api/user/update | 不存在 | `{"id":9999}` | code=404 | 异常路径 | P0 | FindOne失败 | | TC-0158 | POST /api/user/update | 仅传id | `{"id":1}` | 仅updateTime变 | 分支覆盖 | P1 | 所有指针nil | | TC-0159 | POST /api/user/update | 清空nickname | `{"id":1,"nickname":""}` | DB nickname→空字符串 | 功能 | P0 | *string非nil+空值 | | TC-0160 | POST /api/user/update | 清空email | `{"id":1,"email":""}` | DB email→空字符串(跳过校验) | 功能 | P0 | *email!=""判断 | | TC-0161 | POST /api/user/update | 清空remark | `{"id":1,"remark":""}` | DB remark清空 | 功能 | P1 | *string→"" | | TC-0162 | POST /api/user/update | 非法email格式 | `{"id":1,"email":"bad-email"}` | code=400, "邮箱格式不正确" | 输入校验 | P0 | util.IsValidEmail | | TC-0163 | POST /api/user/update | 非法phone格式 | `{"id":1,"phone":"12345"}` | code=400, "手机号格式不正确" | 输入校验 | P0 | util.IsValidPhone | | TC-0164 | POST /api/user/update | 合法phone | `{"id":1,"phone":"+8613800138000"}` | code=0 | 正常路径 | P1 | IsValidPhone通过 | | TC-0165 | POST /api/user/update | 不传email(nil) | `{"id":1,"nickname":"x"}` | email不变 | 分支覆盖 | P1 | req.Email==nil | | TC-0166 | POST /api/user/update | DeptId设为0(取消部门) | `{"id":1,"deptId":0}` | DB deptId→0 | 功能 | P0 | \*int64, \*req.DeptId=0 | | TC-0167 | POST /api/user/update | DeptId设为正值 | `{"id":1,"deptId":5}` | DB deptId→5 | 正常路径 | P0 | *int64指针 | | TC-0168 | POST /api/user/update | DeptId不传(nil) | `{"id":1,"nickname":"x"}` | deptId不变 | 分支覆盖 | P1 | req.DeptId==nil | | TC-0169 | POST /api/user/update | 超管不能冻结另一超管 | caller=超管A, target=超管B, status=2 | 403 "不能通过此接口修改超级管理员的状态" | 安全 | P0 | H-2: IsSuperAdmin==Yes 保护 | | TC-0170 | POST /api/user/update | updateUser-产品管理员可管理范围内用户 | ctx=ADMIN, target在管理范围内 | 更新成功 | 正常路径 | P0 | Audit#4修复: CheckManageAccess允许产品管理员 | | TC-0171 | POST /api/user/update | updateUser-昵称超长拒绝 | nickname=65字符 | 400 "昵称长度不能超过64个字符" | 边界 | P1 | 输入校验 | | TC-0172 | POST /api/user/update | updateUser-部门不存在 | deptId=999999 | 400 "部门不存在" | 异常路径 | P1 | 关联对象不存在校验 | | TC-0173 | POST /api/user/update | updateUser 修改状态时递增 tokenVersion | req.Status=Disabled, 原Status=Enabled | 更新成功, tokenVersion+1 | 正常路径 | P0 | H-1: 状态变更强制下线 | | TC-0174 | POST /api/user/update | updateUser 仅改 profile 不递增 tokenVersion | req.Nickname+Email | 更新成功, tokenVersion不变 | 正常路径 | P0 | H-1: 非状态字段不影响会话 | | TC-0175 | POST /api/user/update | updateUser 乐观锁冲突 -> 409 | 基于过期 updateTime 更新 | 返回 CodeError(409, "数据已被其他操作修改...") | 并发/异常 | P0 | H-1: ErrUpdateConflict 透传 | ### 2.16 用户列表/详情/状态 及其他用户操作 | TC编号 | 接口/方法 | 测试场景 | 输入参数 (JSON) | 预期结果 | 测试类型 | 优先级 | 覆盖说明 | | :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- | | TC-0176 | POST /api/user/list | 含productCode | `{"productCode":"p1","page":1,"pageSize":10}` | 每用户含memberType(批量查) | 正常路径 | P0 | FindMapByProductCodeUserIds | | TC-0177 | POST /api/user/list | 不含productCode | `{"page":1}` | memberType全空,不调批量查 | 分支覆盖 | P1 | productCode="" | | TC-0178 | POST /api/user/list | pageSize超过上限 | `{"pageSize":500}` | 实际pageSize=100 | 边界 | P0 | NormalizePage cap | | TC-0179 | POST /api/user/list | 用户不在产品中 | productCode指定,部分用户不是成员 | memberType为空 | 分支覆盖 | P1 | memberMap无对应key | | TC-0180 | POST /api/user/list | 批量查询DB异常 | FindMapByProductCodeUserIds失败 | code=500 | 异常路径 | P1 | err→透传 | | TC-0181 | POST /api/user/detail | 正常查询 | `{"id":1}` | 含roleIds | 正常路径 | P0 | userDetailLogic | | TC-0182 | POST /api/user/detail | 正常查询-含Avatar | 有Avatar用户 | avatar字段非空 | 分支覆盖 | P1 | Avatar.Valid=true | | TC-0183 | POST /api/user/detail | 不存在 | `{"id":9999}` | code=404 | 异常路径 | P0 | FindOne失败 | | TC-0184 | POST /api/user/bindRoles | 正常绑定 | `{"userId":1,"roleIds":[1,2]}` | code=0 | 正常路径 | P0 | TransactCtx | | TC-0185 | POST /api/user/bindRoles | 用户不存在 | `{"userId":9999,"roleIds":[1]}` | code=404, "用户不存在" | 存在性校验 | P0 | FindOne预检 | | TC-0186 | POST /api/user/bindRoles | 清空角色 | `{"userId":1,"roleIds":[]}` | code=0 | 分支覆盖 | P1 | len==0 | | TC-0187 | POST /api/user/bindRoles | 事务回滚 | 模拟失败 | 旧数据还原 | 事务验证 | P0 | TransactCtx | | TC-0188 | POST /api/user/bindRoles | 角色不属于当前产品 | roleId属于其他产品 | code=400, "角色不属于当前产品" | 安全 | P0 | H-03修复: 校验角色归属 | | TC-0189 | POST /api/user/bindRoles | 角色已禁用 | roleId状态为禁用 | code=400, "角色已禁用" | 安全 | P0 | H-03修复: 校验角色状态 | | TC-0190 | POST /api/user/bindRoles | 角色不存在 | roleId不存在 | code=400, "角色不存在" | 安全 | P0 | H-03修复: 校验角色存在 | | TC-0191 | POST /api/user/bindRoles | 非产品成员绑定角色被拒绝 | 目标用户非当前产品成员 | 400 "不是当前产品的成员" | 安全 | P0 | L-4: BindRoles | | TC-0192 | POST /api/user/setPerms | 正常ALLOW | `{"userId":1,"perms":[{"permId":1,"effect":"ALLOW"}]}` | code=0 | 正常路径 | P0 | TransactCtx | | TC-0193 | POST /api/user/setPerms | 用户不存在 | `{"userId":9999,"perms":[...]}` | code=404, "用户不存在" | 存在性校验 | P0 | FindOne预检 | | TC-0194 | POST /api/user/setPerms | DENY权限 | effect="DENY" | code=0 | 正常路径 | P0 | effect="DENY" | | TC-0195 | POST /api/user/setPerms | 清空权限 | `{"userId":1,"perms":[]}` | code=0 | 分支覆盖 | P1 | len==0 | | TC-0196 | POST /api/user/setPerms | 无效Effect值 | effect="INVALID" | code=400, "无效的权限效果" | 安全 | P0 | H-04修复: Effect白名单 | | TC-0197 | POST /api/user/setPerms | PermId不存在 | permId=99999 | code=400, "权限不存在" | 安全 | P0 | H-04修复: 校验PermId | | TC-0198 | POST /api/user/setPerms | 权限不属于当前产品 | permId属于其他产品 | code=400, "权限不属于当前产品" | 安全 | P0 | H-04修复: 校验权限归属 | | TC-0199 | POST /api/user/setPerms | 非产品成员设置权限被拒绝 | 目标用户非当前产品成员 | 400 "不是当前产品的成员" | 安全 | P0 | L-5: SetUserPerms | | TC-0200 | POST /api/user/updateStatus | 正常冻结 | `{"id":普通用户,"status":2}` | code=0 | 正常路径 | P0 | updateUserStatusLogic | | TC-0201 | POST /api/user/updateStatus | 正常解冻 | `{"id":普通用户,"status":1}` | code=0 | 正常路径 | P0 | status=1 | | TC-0202 | POST /api/user/updateStatus | 非法status(0) | `{"id":1,"status":0}` | code=400, "状态值无效" | 输入校验 | P0 | status!=1&&!=2 | | TC-0203 | POST /api/user/updateStatus | 冻结自己 | id=当前登录userId | code=400, "不能修改自己的状态" | 自我保护 | P0 | callerId==req.Id | | TC-0204 | POST /api/user/updateStatus | 冻结超管 | id=超管 | code=403, "不能修改超级管理员的状态" | 超管保护 | P0 | IsSuperAdmin==1 | | TC-0205 | POST /api/user/list | userList-非超管仅可见产品成员 | ctx=ADMIN(非超管), productCode指定 | 仅返回该产品成员, 不返回非成员 | 安全 | P0 | Audit#1修复: FindListByProductMembers数据隔离 | | TC-0206 | POST /api/user/list | userList-非超管未指定productCode被拒绝 | ctx=ADMIN(非超管), productCode="" | 403 "非超管用户必须指定产品编码" | 安全 | P0 | Audit#1修复: 强制productCode | | TC-0207 | POST /api/user/list | userList-非超管使用错误productCode被拒绝 | ctx=ADMIN, productCode!=ctx.ProductCode | 403 | 安全 | P0 | Audit#1修复: productCode一致性校验 | | TC-0208 | POST /api/user/bindRoles | bindRoles-permsLevel越权拒绝 | ctx=ADMIN(MinPermsLevel=50), role.permsLevel=1 | 403 "不能分配权限级别高于自身的角色" | 安全 | P0 | Audit#2修复: 角色权限级别越权防护 | | TC-0209 | POST /api/user/bindRoles | bindRoles-超管可分配任意级别角色 | ctx=SuperAdmin, role.permsLevel=1 | 绑定成功 | 正常路径 | P0 | Audit#2修复: 超管无permsLevel限制 | | TC-0210 | POST /api/user/setPerms | 同一权限ID冲突Effect被拒绝 | perms含[{permId:1,effect:"ALLOW"},{permId:1,effect:"DENY"}] | 400 "同一权限ID不能同时为 ALLOW 和 DENY" | 业务约束 | P0 | seen[permId]冲突检测 | | TC-0211 | POST /api/user/setPerms | 重复权限ID相同Effect去重 | perms含[{permId:1,effect:"ALLOW"},{permId:1,effect:"ALLOW"}] | 成功, DB仅1条记录 | 数据鲁棒性 | P1 | seen去重,uniquePerms | | TC-0212 | POST /api/user/setPerms | 已禁用权限不能被设置 | perm.Status=2(Disabled) | 400 "权限 xxx 已被禁用,无法设置" | 业务约束 | P0 | p.Status != StatusEnabled | ### 2.17 成员管理 | TC编号 | 接口/方法 | 测试场景 | 输入参数 (JSON) | 预期结果 | 测试类型 | 优先级 | 覆盖说明 | | :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- | | TC-0213 | POST /api/member/add | 正常添加 | `{"productCode":"p1","userId":1,"memberType":"MEMBER"}` | code=0, id>0 | 正常路径 | P0 | addMemberLogic | | TC-0214 | POST /api/member/add | 产品不存在 | `{"productCode":"notexist",...}` | code=404, "产品不存在" | 存在性校验 | P0 | FindOneByCode预检 | | TC-0215 | POST /api/member/add | 用户不存在 | `{"userId":9999,...}` | code=404, "用户不存在" | 存在性校验 | P0 | FindOne预检 | | TC-0216 | POST /api/member/add | 已是成员 | 重复添加 | code=409, "已是成员" | 异常路径 | P0 | FindOneByProductCodeUserId成功 | | TC-0217 | POST /api/member/add | 并发添加 | 两请求同时 | 一成功一冲突 | 并发 | P1 | uk_product_user | | TC-0218 | POST /api/member/add | 无效MemberType | `{"memberType":"INVALID"}` | code=400, "无效的成员类型" | 输入校验 | P0 | M-06修复: MemberType白名单 | | TC-0219 | POST /api/member/update | 正常更新 | `{"id":1,"memberType":"ADMIN"}` | code=0 | 正常路径 | P0 | updateMemberLogic | | TC-0220 | POST /api/member/update | 不存在 | `{"id":9999,...}` | code=404 | 异常路径 | P0 | FindOne失败 | | TC-0221 | POST /api/member/update | 无效MemberType | `{"id":1,"memberType":"INVALID"}` | code=400, "无效的成员类型" | 输入校验 | P0 | M-06修复: MemberType白名单 | | TC-0222 | POST /api/member/list | 正常查询(批量查用户) | `{"productCode":"p1","page":1,"pageSize":10}` | 含username/nickname | 正常路径 | P0 | FindByIds批量 | | TC-0223 | POST /api/member/list | 成员用户已删除 | userId不存在于FindByIds结果 | username/nickname为空 | 分支覆盖 | P1 | userMap无对应key | | TC-0224 | POST /api/member/list | pageSize超过上限 | `{"productCode":"p1","pageSize":200}` | 实际pageSize=100 | 边界 | P0 | NormalizePage cap | | TC-0225 | POST /api/member/list | 空成员列表 | productCode下无成员 | total=0, list=[], 不调FindByIds | 分支覆盖 | P1 | userIds空 | | TC-0226 | POST /api/member/remove | 正常移除+级联(事务内) | `{"id":1}` (含角色/权限) | code=0, user_role+user_perm同步清理 | 正常+事务 | P0 | TransactCtx全路径 | | TC-0227 | POST /api/member/remove | 跨产品隔离 | 用户在多产品有角色 | 仅清理该产品的 | 深度业务 | P0 | ForProductTx子查询 | | TC-0228 | POST /api/member/remove | 成员不存在 | `{"id":9999}` | code=404, "成员不存在" | 异常路径 | P0 | FindOne失败 | | TC-0229 | POST /api/member/remove | 事务回滚 | 模拟DeleteWithTx失败 | 级联删除全部回滚 | 事务验证 | P0 | TransactCtx | --- ## 三、gRPC 接口测试用例 ### 3.1 gRPC SyncPermissions | TC编号 | 接口/方法 | 测试场景 | 输入 | 预期结果 | 测试类型 | 优先级 | 覆盖说明 | | :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- | | TC-0230 | SyncPermissions | 正常同步 | valid req | added/updated/disabled计数正确 | 正常路径 | P0 | permserver.go SyncPermissions | | TC-0231 | SyncPermissions | appKey无效 | invalid appKey | codes.Unauthenticated | 异常路径 | P0 | status.Error | | TC-0232 | SyncPermissions | appSecret错误 | wrong secret | codes.Unauthenticated | 异常路径 | P0 | status.Error | | TC-0233 | SyncPermissions | 产品已禁用 | disabled product | codes.PermissionDenied | 分支覆盖 | P0 | status.Error | | TC-0234 | SyncPermissions | 验证disabled计数 | DB有5条,perms含2条 | disabled=3 | 功能验证 | P0 | RowsAffected | ### 3.2 gRPC Login / RefreshToken / VerifyToken / GetUserPerms | TC编号 | 接口/方法 | 测试场景 | 输入 | 预期结果 | 测试类型 | 优先级 | 覆盖说明 | | :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- | | TC-0235 | Login | 正常登录(普通用户+productCode) | valid credentials + productCode | token对+userInfo(含nickname) | 正常路径 | P0 | permserver.go Login; resp.Nickname应返回用户昵称 | | TC-0236 | Login | 用户不存在 | wrong username | codes.Unauthenticated | 异常路径 | P0 | status.Error | | TC-0237 | Login | 密码错误 | wrong password | codes.Unauthenticated | 异常路径 | P0 | status.Error | | TC-0238 | Login | 账号冻结 | frozen user | codes.PermissionDenied | 分支覆盖 | P0 | status.Error | | TC-0239 | Login | 超管被拒绝 | isSuperAdmin=1+productCode | codes.PermissionDenied, "超级管理员不允许通过产品端登录" | 安全 | P0 | IsSuperAdmin==1 → 拒绝 | | TC-0240 | Login | 普通用户+productCode | 普通MEMBER+productCode | perms含角色权限, memberType="MEMBER" | 分支覆盖 | P0 | !isSuperAdmin && productCode!="" | | TC-0241 | Login | 产品成员被禁用时拒绝登录 | member.status=Disabled | PermissionDenied | 安全 | P0 | H-3: permserver Login | | TC-0242 | Login | productCode为空 | productCode="" | codes.InvalidArgument, "productCode不能为空" | 输入校验 | P0 | 第一个校验点 | | TC-0243 | RefreshToken | 正常刷新 | valid token | 新token对 | 正常路径 | P0 | RefreshToken | | TC-0244 | RefreshToken | token无效 | invalid token | codes.Unauthenticated | 异常路径 | P0 | status.Error | | TC-0245 | RefreshToken | 账号冻结 | frozen | codes.PermissionDenied | 分支覆盖 | P0 | status.Error | | TC-0246 | RefreshToken | productCode回退到claims | req.ProductCode="", claims含productCode | 使用claims.ProductCode | 分支覆盖 | P0 | productCode==""回退 | | TC-0247 | RefreshToken | 超管+productCode | isSuperAdmin=1+productCode | memberType="SUPER_ADMIN", perms全量 | 分支覆盖 | P0 | isSuperAdmin && productCode!="" | | TC-0248 | RefreshToken | 普通用户+productCode | 普通MEMBER+productCode | perms含角色权限 | 分支覆盖 | P0 | !isSuperAdmin && productCode!="" | | TC-0249 | VerifyToken | 有效token | valid | valid=true, userId/perms/productCode正确 | 正常路径 | P0 | VerifyToken; resp.ProductCode应返回产品编码 | | TC-0250 | VerifyToken | 无效token | invalid | valid=false | 异常路径 | P0 | err或!Valid | | TC-0251 | VerifyToken | 缺少userId | 伪造claims | valid=false | 安全 | P0 | !ok断言保护 | | TC-0252 | VerifyToken | 冻结用户token返回Invalid | user.status=Disabled | Valid=false | 安全 | P0 | H-4: 实时查DB | | TC-0253 | VerifyToken | 非成员token返回Invalid | user非产品成员 | Valid=false | 安全 | P0 | H-4: 实时查成员状态 | | TC-0254 | VerifyToken | 返回实时MemberType和Perms | DB中ADMIN+自定义权限 | 返回实时数据而非token中旧数据 | 安全 | P0 | H-4: 实时数据 | | TC-0255 | GetUserPerms | 用户不存在(需先通过AppKey/Secret认证) | userId=9999, 合法AppKey/AppSecret | codes.NotFound | 异常路径 | P0 | status.Error; 先认证再查用户 | | TC-0256 | GetUserPerms | 超管(需先通过AppKey/Secret认证) | isSuperAdmin, 合法AppKey/AppSecret | perms全量, "SUPER_ADMIN" | 正常路径 | P0 | GetUserPerms(true); AppKey认证前置 | | TC-0257 | GetUserPerms | MEMBER-DENY覆盖(需先通过AppKey/Secret认证) | 角色有permA, DENY permA, 合法AppKey/AppSecret | perms不含permA | 深度业务 | P0 | denySet过滤; AppKey认证前置 | --- ## 四、JWT中间件 / 统一响应测试用例 | TC编号 | 测试场景 | 输入 | 预期结果 | 类型 | 优先级 | 覆盖说明 | | :--- | :--- | :--- | :--- | :--- | :--- | :--- | | TC-0258 | 正常Bearer token | `Authorization: Bearer {valid}` | 通过, ctx注入5个值 | 正常路径 | P0 | middleware全路径 | | TC-0259 | 无Authorization头 | 无Header | code=401, "未登录" | 异常 | P0 | authHeader=="" | | TC-0260 | 无Bearer前缀 | `Authorization: xxx` | code=401, "token格式错误" | 异常 | P0 | TrimPrefix相等 | | TC-0261 | token签名错误 | 错误secret | code=401, "token无效或已过期" | 异常 | P0 | !Valid | | TC-0262 | token过期 | expired | code=401 | 异常 | P0 | jwt过期 | | TC-0263 | claims类型断言失败 | 非标准claims | code=401, "token无效或类型错误" | 异常 | P1 | !ok 防御性分支,jwt.ParseWithClaims(&Claims{}) 下不可达;TokenType 检查由 TC-0264 覆盖 | | TC-0264 | refresh token被拒绝 | 用refresh token访问API | code=401, "token无效或类型错误" | 安全 | P0 | TokenType="refresh"时拒绝 | | TC-0265 | 业务错误(CodeError) | 触发404等 | `{code:业务码, msg:业务消息}` | 正常 | P0 | errors.As成功 | | TC-0266 | 内部错误 | DB异常 | `{code:500, msg:"服务器内部错误"}` | 安全 | P0 | logx.Errorf+兜底 | | TC-0267 | 成功(有data) | 正常请求 | `{code:0, msg:"ok", data:{...}}` | 正常 | P0 | v!=nil | | TC-0268 | 成功(无data) | 返回nil | `{code:0, msg:"ok"}` | 正常 | P0 | v==nil | --- ## 五、util 层测试用例 ### 5.1 NormalizePage | TC编号 | 测试场景 | 输入 | 预期结果 | 类型 | 优先级 | 覆盖说明 | | :--- | :--- | :--- | :--- | :--- | :--- | :--- | | TC-0269 | 正常值 | page=2, pageSize=10 | (2, 10) | 正常路径 | P0 | 无修正 | | TC-0270 | page<=0 | page=0, pageSize=10 | (1, 10) | 边界 | P0 | page<=0→1 | | TC-0271 | page=-1 | page=-1, pageSize=10 | (1, 10) | 边界 | P0 | page<=0→1 | | TC-0272 | pageSize<=0 | page=1, pageSize=0 | (1, 20) | 边界 | P0 | pageSize<=0→20 | | TC-0273 | pageSize>100 | page=1, pageSize=500 | (1, 100) | 边界-上限 | P0 | pageSize>100→100 | | TC-0274 | pageSize=100 | page=1, pageSize=100 | (1, 100) | 边界 | P1 | 恰好不触发 | | TC-0275 | pageSize=101 | page=1, pageSize=101 | (1, 100) | 边界 | P1 | 恰好触发 | | TC-0276 | 双零 | page=0, pageSize=0 | (1, 20) | 边界 | P1 | 两条件同时 | ### 5.2 IsValidEmail | TC编号 | 测试场景 | 输入 | 预期结果 | 类型 | 优先级 | 覆盖说明 | | :--- | :--- | :--- | :--- | :--- | :--- | :--- | | TC-0277 | 正常邮箱 | `user@example.com` | true | 正常路径 | P0 | 标准格式 | | TC-0278 | 含点号 | `user.name@example.com` | true | 正常路径 | P1 | 允许点号 | | TC-0279 | 含加号 | `user+tag@example.com` | true | 正常路径 | P1 | 允许加号 | | TC-0280 | 缺少@ | `userexample.com` | false | 异常路径 | P0 | 无@ | | TC-0281 | 缺少域名 | `user@` | false | 异常路径 | P0 | 无域名 | | TC-0282 | 缺少TLD | `user@example` | false | 异常路径 | P0 | TLD<2字符 | | TC-0283 | 空字符串 | `""` | false | 边界 | P1 | 空 | ### 5.3 IsValidPhone | TC编号 | 测试场景 | 输入 | 预期结果 | 类型 | 优先级 | 覆盖说明 | | :--- | :--- | :--- | :--- | :--- | :--- | :--- | | TC-0284 | 国内手机号 | `13800138000` | true | 正常路径 | P0 | 11位数字 | | TC-0285 | 带+国际码 | `+8613800138000` | true | 正常路径 | P0 | +前缀 | | TC-0286 | 太短(6位) | `123456` | false | 边界 | P0 | <7位 | | TC-0287 | 恰好7位 | `1234567` | true | 边界 | P1 | 最小长度 | | TC-0288 | 最长15位 | `+123456789012345` | true | 边界 | P1 | 最大长度 | | TC-0289 | 超长16位 | `1234567890123456` | false | 边界 | P1 | 超限 | | TC-0290 | 包含字母 | `1380013abc` | false | 异常路径 | P0 | 非数字 | | TC-0291 | 空字符串 | `""` | false | 边界 | P1 | 空 | --- ## 六、Logic 层单元测试用例 > 以下针对 Logic 层中的核心共享函数,使用 mock Model 接口进行纯单元测试。 ### 6.1 auth/jwt.go — GenerateAccessToken | TC编号 | 测试场景 | 输入 | 预期结果 | 类型 | 优先级 | 覆盖说明 | | :--- | :--- | :--- | :--- | :--- | :--- | :--- | | TC-0292 | 正常生成 | secret="s", expire=3600, userId=1, username="u", productCode="p", memberType="M", perms=["a"] | 返回非空token, err=nil | 正常路径 | P0 | jwt.NewWithClaims(HS256) | | TC-0293 | 解析token验证claims | 上述token | ParseWithClaims可解析出正确userId/username/productCode/memberType/perms | 功能验证 | P0 | claims完整性 | | TC-0294 | 空secret | secret="" | 仍能生成token(空key签名) | 边界 | P2 | HS256 允许空key | | TC-0295 | 空perms | perms=nil | token生成成功, 解析后perms=nil | 边界 | P1 | nil slice | | TC-0296 | 过期时间验证 | expireSeconds=1, sleep 2s | ParseWithClaims返回过期错误 | 功能验证 | P0 | ExpiresAt | ### 6.2 auth/jwt.go — GenerateRefreshToken | TC编号 | 测试场景 | 输入 | 预期结果 | 类型 | 优先级 | 覆盖说明 | | :--- | :--- | :--- | :--- | :--- | :--- | :--- | | TC-0297 | 正常生成 | secret="s", expire=86400, userId=1, productCode="p" | 返回非空token | 正常路径 | P0 | RefreshClaims | | TC-0298 | 解析验证 | 上述token | ParseRefreshToken解析出userId=1, productCode="p" | 功能验证 | P0 | 往返一致 | | TC-0299 | productCode为空 | productCode="" | 生成成功, 解析后productCode="" | 边界 | P1 | 空字符串 | ### 6.3 auth/jwt.go — ParseRefreshToken | TC编号 | 测试场景 | 输入 | 预期结果 | 类型 | 优先级 | 覆盖说明 | | :--- | :--- | :--- | :--- | :--- | :--- | :--- | | TC-0300 | 正常解析 | 有效token+正确secret | 返回RefreshClaims, err=nil | 正常路径 | P0 | token.Valid | | TC-0301 | 错误secret | 有效token+错误secret | err!=nil | 异常路径 | P0 | 签名验证失败 | | TC-0302 | 无效token字符串 | "invalid-token" | err!=nil | 异常路径 | P0 | 解析失败 | | TC-0303 | 空token | "" | err!=nil | 边界 | P1 | 空字符串 | | TC-0304 | 过期token | 已过期的token | err!=nil (token expired) | 异常路径 | P0 | ExpiresAt已过 | | TC-0305 | AccessToken误用 | 用AccessToken当RefreshToken解析 | err!=nil (TokenType="access"≠"refresh") | 安全 | P0 | TokenType字段校验 | ### 6.4 middleware — 辅助函数单元测试 > **M-5/M-6重构**:`GetUserPerms` (auth/perms.go) 以及 `GetUsername` / `GetMemberType` / `IsSuperAdmin` 等 context helper 作为死代码被移除;统一使用 `GetUserDetails` 读取完整 `UserDetails` 后访问字段。 | TC编号 | 测试场景 | 输入 | 预期结果 | 类型 | 优先级 | 覆盖说明 | | :--- | :--- | :--- | :--- | :--- | :--- | :--- | | TC-0306 | GetUserId-正常 | ctx含userId=100 | 100 | 正常路径 | P0 | 类型断言成功 | | TC-0307 | GetUserId-空ctx | 空ctx | 0 | 边界 | P0 | 断言失败→零值 | | TC-0308 | GetProductCode-正常 | ctx含productCode="p1" | "p1" | 正常路径 | P0 | 类型断言 | | TC-0309 | GetUserDetails 返回完整字段 | ctx含UserDetails{UserId,Username,ProductCode,MemberType,IsSuperAdmin} | 读出字段全部一致, 空ctx返回nil | 正常路径 | P0 | 替代已移除的 GetUsername/GetMemberType/IsSuperAdmin | --- ## 七、Model 层 _gen.go 模板生成方法测试用例 > 所有 9 个 Model 的 `_gen.go` 均由自定义模板 (`cli/goctl/model/`) 生成,包含非标准方法(批量操作、事务变体、`buildBatchUpdateQuery` 等)。 > 以下以 **通用测试模式** 列出,适用于全部 9 个 Model(注明差异部分)。 ### 7.1 通用 CRUD 方法 (每个 Model 均需测试) > 适用: SysUser, SysProduct, SysPerm, SysDept, SysRole, SysRolePerm, SysUserPerm, SysUserRole, SysProductMember | TC编号 | 方法 | 测试场景 | 预期结果 | 类型 | 优先级 | 覆盖说明 | | :--- | :--- | :--- | :--- | :--- | :--- | :--- | | TC-0310 | **Insert** | 正常插入 | 返回Result+nil, DB有新记录 | 正常路径 | P0 | ExecCtx+缓存key清理 | | TC-0311 | **Insert** | 正常插入含TokenVersion | err=nil, DB中tokenVersion=0(默认) | 正常路径 | P0 | 验证Insert包含tokenVersion | | TC-0312 | **Insert** | 唯一索引冲突 | 返回DB错误(1062) | 异常路径 | P0 | MySQL uk | | TC-0313 | **Insert** | 缓存key生成正确 | 验证清理的缓存key包含主键和唯一索引 | 功能验证 | P0 | cacheSys*Prefix | | TC-0314 | **InsertWithTx** | 事务内插入 | 使用session执行, 返回Result | 正常路径 | P0 | session.ExecCtx | | TC-0315 | **InsertWithTx** | 事务内插入含TokenVersion | err=nil, 事务内可读到正确tokenVersion | 正常路径 | P0 | 验证InsertWithTx包含tokenVersion | | TC-0316 | **InsertWithTx** | 事务回滚后无数据 | 事务内Insert+外部回滚→DB无记录 | 事务验证 | P0 | TransactCtx | | TC-0317 | **FindOne** | 正常查询(缓存未命中) | 返回记录, 缓存已写入 | 正常路径 | P0 | QueryRowCtx→DB | | TC-0318 | **FindOne** | 正常查询(缓存命中) | 不触发DB查询, 返回缓存数据 | 正常路径 | P0 | QueryRowCtx→cache | | TC-0319 | **FindOne** | 记录不存在 | 返回ErrNotFound | 异常路径 | P0 | sqlc.ErrNotFound→ErrNotFound | | TC-0320 | **FindOne** | DB异常(非ErrNotFound) | 返回原始error | 异常路径 | P1 | default分支 | | TC-0321 | **FindOneWithTx** | 事务内正常查询 | 使用session.QueryRowCtx, 返回记录 | 正常路径 | P0 | session直查无缓存 | | TC-0322 | **FindOneWithTx** | 事务内记录不存在 | 返回ErrNotFound | 异常路径 | P0 | sqlx.ErrNotFound | | TC-0323 | **FindOneWithTx** | 事务内可见性 | InsertWithTx后FindOneWithTx可读到 | 事务验证 | P0 | 同session内可见 | | TC-0324 | **Update** | 正常更新 | 旧缓存key+新缓存key均被清理 | 正常路径 | P0 | FindOne→ExecCtx | | TC-0325 | **Update** | 正常更新含TokenVersion | err=nil, DB中tokenVersion正确更新 | 正常路径 | P0 | 验证Update包含tokenVersion | | TC-0326 | **Update** | 记录不存在 | FindOne失败→返回ErrNotFound | 异常路径 | P0 | FindOne err | | TC-0327 | **UpdateWithTx** | 事务内更新 | 使用session, 缓存被清理 | 正常路径 | P0 | session.ExecCtx | | TC-0328 | **Delete** | 正常删除 | 记录被删, 缓存key被清理 | 正常路径 | P0 | FindOne→ExecCtx DELETE | | TC-0329 | **Delete** | 记录不存在 | FindOne失败→返回ErrNotFound | 异常路径 | P0 | FindOne err | | TC-0330 | **DeleteWithTx** | 事务内删除 | 使用session, 缓存被清理 | 正常路径 | P0 | session.ExecCtx | | TC-0331 | **TransactCtx** | 正常事务 | fn执行成功→提交 | 正常路径 | P0 | conn.TransactCtx | | TC-0332 | **TransactCtx** | fn返回错误 | 自动回滚 | 异常路径 | P0 | 回滚 | | TC-0333 | **TableName** | 获取表名 | 返回正确表名(如 `` `sys_user` ``) | 正常路径 | P0 | m.table | ### 7.2 批量插入方法 | TC编号 | 方法 | 测试场景 | 预期结果 | 类型 | 优先级 | 覆盖说明 | | :--- | :--- | :--- | :--- | :--- | :--- | :--- | | TC-0334 | **BatchInsert** | 空列表 | 直接返回nil, 不执行SQL | 边界 | P0 | len==0 early return | | TC-0335 | **BatchInsert** | 单条记录 | 生成1组VALUES, 执行成功 | 正常路径 | P0 | 单条 | | TC-0336 | **BatchInsert** | 多条记录(3条) | 生成3组VALUES, SQL正确, 缓存key全清理 | 正常路径 | P0 | 多条+缓存 | | TC-0337 | **BatchInsert** | 批量插入含TokenVersion | err=nil, 所有记录tokenVersion正确 | 正常路径 | P0 | 验证BatchInsert包含tokenVersion | | TC-0338 | **BatchInsert** | 唯一索引冲突 | 全部失败, 返回DB错误 | 异常路径 | P0 | MySQL uk | | TC-0339 | **BatchInsert** | 大批量(1000条) | SQL长度合理, 执行成功 | 性能 | P2 | 拼接性能 | | TC-0340 | **BatchInsertWithTx** | 空列表 | 直接返回nil | 边界 | P0 | len==0 | | TC-0341 | **BatchInsertWithTx** | 正常多条 | 使用session执行 | 正常路径 | P0 | session.ExecCtx | | TC-0342 | **BatchInsertWithTx** | 事务回滚 | 外部回滚→无新记录 | 事务验证 | P0 | TransactCtx | ### 7.3 批量更新方法 | TC编号 | 方法 | 测试场景 | 预期结果 | 类型 | 优先级 | 覆盖说明 | | :--- | :--- | :--- | :--- | :--- | :--- | :--- | | TC-0343 | **BatchUpdate** | 空列表 | 直接返回nil | 边界 | P0 | len==0 early return | | TC-0344 | **BatchUpdate** | 单条记录 | CASE-WHEN SQL正确, 更新成功 | 正常路径 | P0 | buildBatchUpdateQuery 单条 | | TC-0345 | **BatchUpdate** | 多条记录(3条) | CASE-WHEN生成3个WHEN子句, 旧缓存key全清理 | 正常路径 | P0 | buildBatchUpdateQuery 多条 | | TC-0346 | **BatchUpdate** | 批量更新不污染数据 | err=nil, tokenVersion/createTime/updateTime均正确 | 正常路径 | P0 | 验证buildBatchUpdateQuery值对齐 | | TC-0347 | **BatchUpdate** | 部分id不存在 | findListByPrimaryKeys返回部分→仅清理存在的缓存 | 边界 | P1 | oldList可能少于dataList | | TC-0348 | **BatchUpdateWithTx** | 空列表 | 直接返回nil | 边界 | P0 | len==0 | | TC-0349 | **BatchUpdateWithTx** | 正常多条 | 使用session执行 | 正常路径 | P0 | session.ExecCtx | | TC-0350 | **buildBatchUpdateQuery** | 单条 | SQL: `UPDATE SET field=CASE WHEN id=? THEN ? ELSE field END WHERE id IN (?)` | 功能验证 | P0 | SQL结构 | | TC-0351 | **buildBatchUpdateQuery** | 多条 | 每个字段均有多个WHEN子句, WHERE IN含全部id | 功能验证 | P0 | SQL正确性 | | TC-0352 | **buildBatchUpdateQuery** | vals数量正确 | vals = N*(fields*2) + N (WHERE IN) | 功能验证 | P0 | 参数计数 | ### 7.4 批量删除方法 | TC编号 | 方法 | 测试场景 | 预期结果 | 类型 | 优先级 | 覆盖说明 | | :--- | :--- | :--- | :--- | :--- | :--- | :--- | | TC-0353 | **BatchDelete** | 空ids | 直接返回nil | 边界 | P0 | len==0 early return | | TC-0354 | **BatchDelete** | 单个id | DELETE WHERE id IN (?), 缓存清理 | 正常路径 | P0 | 单条 | | TC-0355 | **BatchDelete** | 多个id(3个) | 3个占位符, 旧数据查询→缓存key全清理 | 正常路径 | P0 | findListByPrimaryKeys | | TC-0356 | **BatchDelete** | 包含不存在id | findListByPrimaryKeys返回部分, 不报错 | 边界 | P1 | 部分存在 | | TC-0357 | **BatchDeleteWithTx** | 空ids | 直接返回nil | 边界 | P0 | len==0 | | TC-0358 | **BatchDeleteWithTx** | 正常多条 | 使用session执行 | 正常路径 | P0 | session.ExecCtx | ### 7.5 唯一索引查询方法 (按 Model 差异) | TC编号 | Model | 方法 | 测试场景 | 预期结果 | 优先级 | 覆盖说明 | | :--- | :--- | :--- | :--- | :--- | :--- | :--- | | TC-0359 | SysUser | FindOneByUsername | 正常查询 | 返回用户, 缓存写入 (索引缓存→主键缓存双层) | P0 | QueryRowIndexCtx | | TC-0360 | SysUser | FindOneByUsername | 不存在 | 返回ErrNotFound | P0 | sqlc.ErrNotFound | | TC-0361 | SysUser | FindOneByUsernameWithTx | 事务内正常查询 | 返回用户, 使用session直查 | P0 | session.QueryRowCtx | | TC-0362 | SysUser | FindOneByUsernameWithTx | 事务内不存在 | 返回ErrNotFound | P0 | sqlx.ErrNotFound | | TC-0363 | SysProduct | FindOneByAppKey | 正常查询 | 返回产品 | P0 | appKey唯一索引 | | TC-0364 | SysProduct | FindOneByAppKey | 不存在 | 返回ErrNotFound | P0 | | | TC-0365 | SysProduct | FindOneByAppKeyWithTx | 事务内正常查询 | 返回产品 | P0 | session直查 | | TC-0366 | SysProduct | FindOneByAppKeyWithTx | 事务内不存在 | 返回ErrNotFound | P0 | | | TC-0367 | SysProduct | FindOneByCode | 正常查询 | 返回产品 | P0 | code唯一索引 | | TC-0368 | SysProduct | FindOneByCode | 不存在 | 返回ErrNotFound | P0 | | | TC-0369 | SysProduct | FindOneByCodeWithTx | 事务内正常查询 | 返回产品 | P0 | session直查 | | TC-0370 | SysProduct | FindOneByCodeWithTx | 事务内不存在 | 返回ErrNotFound | P0 | | | TC-0371 | SysPerm | FindOneByProductCodeCode | 正常查询 | 返回权限(复合唯一索引) | P0 | productCode+code | | TC-0372 | SysPerm | FindOneByProductCodeCode | 不存在 | 返回ErrNotFound | P0 | | | TC-0373 | SysPerm | FindOneByProductCodeCodeWithTx | 事务内正常查询 | 返回权限 | P0 | session直查 | | TC-0374 | SysPerm | FindOneByProductCodeCodeWithTx | 事务内不存在 | 返回ErrNotFound | P0 | | | TC-0375 | SysRole | FindOneByProductCodeName | 正常查询 | 返回角色(复合唯一索引) | P0 | productCode+name | | TC-0376 | SysRole | FindOneByProductCodeName | 不存在 | 返回ErrNotFound | P0 | | | TC-0377 | SysRole | FindOneByProductCodeNameWithTx | 事务内正常查询 | 返回角色 | P0 | session直查 | | TC-0378 | SysRole | FindOneByProductCodeNameWithTx | 事务内不存在 | 返回ErrNotFound | P0 | | | TC-0379 | SysRolePerm | FindOneByRoleIdPermId | 正常查询 | 返回关联记录 | P0 | roleId+permId | | TC-0380 | SysRolePerm | FindOneByRoleIdPermId | 不存在 | 返回ErrNotFound | P0 | | | TC-0381 | SysRolePerm | FindOneByRoleIdPermIdWithTx | 事务内正常查询 | 返回关联记录 | P0 | session直查 | | TC-0382 | SysRolePerm | FindOneByRoleIdPermIdWithTx | 事务内不存在 | 返回ErrNotFound | P0 | | | TC-0383 | SysUserPerm | FindOneByUserIdPermId | 正常查询 | 返回关联记录 | P0 | userId+permId | | TC-0384 | SysUserPerm | FindOneByUserIdPermId | 不存在 | 返回ErrNotFound | P0 | | | TC-0385 | SysUserPerm | FindOneByUserIdPermIdWithTx | 事务内正常查询 | 返回关联记录 | P0 | session直查 | | TC-0386 | SysUserPerm | FindOneByUserIdPermIdWithTx | 事务内不存在 | 返回ErrNotFound | P0 | | | TC-0387 | SysUserRole | FindOneByUserIdRoleId | 正常查询 | 返回关联记录 | P0 | userId+roleId | | TC-0388 | SysUserRole | FindOneByUserIdRoleId | 不存在 | 返回ErrNotFound | P0 | | | TC-0389 | SysUserRole | FindOneByUserIdRoleIdWithTx | 事务内正常查询 | 返回关联记录 | P0 | session直查 | | TC-0390 | SysUserRole | FindOneByUserIdRoleIdWithTx | 事务内不存在 | 返回ErrNotFound | P0 | | | TC-0391 | SysProductMember | FindOneByProductCodeUserId | 正常查询 | 返回成员记录 | P0 | productCode+userId | | TC-0392 | SysProductMember | FindOneByProductCodeUserId | 不存在 | 返回ErrNotFound | P0 | | | TC-0393 | SysProductMember | FindOneByProductCodeUserIdWithTx | 事务内正常查询 | 返回成员记录 | P0 | session直查 | | TC-0394 | SysProductMember | FindOneByProductCodeUserIdWithTx | 事务内不存在 | 返回ErrNotFound | P0 | | ### 7.6 内部辅助方法 | TC编号 | 方法 | 测试场景 | 预期结果 | 类型 | 优先级 | 覆盖说明 | | :--- | :--- | :--- | :--- | :--- | :--- | :--- | | TC-0395 | **findListByPrimaryKeys** | 空ids | 返回空slice, 不执行SQL | 边界 | P0 | len==0 | | TC-0396 | **findListByPrimaryKeys** | 正常ids | 返回匹配记录(无缓存) | 正常路径 | P0 | QueryRowsNoCacheCtx | | TC-0397 | **findListByPrimaryKeys** | 部分不存在 | 仅返回存在的记录 | 边界 | P1 | IN查询 | | TC-0398 | **findListByPrimaryKeys** | DB异常 | 返回nil, err | 异常路径 | P1 | err透传 | | TC-0399 | **getPrimaryKeyValue** | 正常 | 返回data.Id | 功能验证 | P0 | interface{} | | TC-0400 | **formatPrimary** | 正常 | 返回 "cache:sysXxx:id:{id}" | 功能验证 | P0 | 缓存key格式 | | TC-0401 | **queryPrimary** | 正常 | 执行 SELECT WHERE id=? | 功能验证 | P0 | SQL | ### 7.7 缓存key与前缀初始化 | TC编号 | 测试场景 | 输入 | 预期结果 | 类型 | 优先级 | 覆盖说明 | | :--- | :--- | :--- | :--- | :--- | :--- | :--- | | TC-0402 | cachePrefix为空 | cachePrefix="" | 使用默认前缀 (如 "cache:sysUser:id:") | 分支覆盖 | P0 | if cachePrefix!="" 未进入 | | TC-0403 | cachePrefix非空 | cachePrefix="test" | 前缀变为 "test:cache:sysUser:id:" | 分支覆盖 | P0 | if cachePrefix!="" 进入 | | TC-0404 | 多唯一索引前缀(SysProduct) | cachePrefix="test" | 3个缓存前缀均更新: id/appKey/code | 功能验证 | P0 | 3个变量均修改 | --- ## 八、Model 层自定义方法测试用例 ### 8.1 SysUserModel | TC编号 | 方法 | 测试场景 | 输入 | 预期结果 | 类型 | 优先级 | 覆盖说明 | | :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- | | TC-0405 | FindListByPage | 正常分页 | page=1, pageSize=10, DB有20条 | 返回10条+total=20 | 正常路径 | P0 | count+limit offset | | TC-0406 | FindListByPage | 第二页 | page=2, pageSize=10 | offset=10, 返回后10条 | 正常路径 | P0 | (page-1)*pageSize | | TC-0407 | FindListByPage | 空表 | 无数据 | total=0, list为空 | 边界 | P0 | count=0 | | TC-0408 | FindListByPage | count查询失败 | DB异常 | 返回0,0,err | 异常路径 | P1 | 第一个err | | TC-0409 | FindListByPage | list查询失败 | DB异常 | 返回0,total,err | 异常路径 | P1 | 第二个err | | TC-0410 | FindListByProductMembers | 正常查询 | productCode="p1", page=1, pageSize=10 | 返回该产品所有成员用户, total正确 | 正常路径 | P0 | 替代FindListByDeptIds: INNER JOIN sys_product_member | | TC-0411 | FindListByProductMembers | productCode不存在 | productCode="no_such_pc" | total=0, list空 | 边界 | P1 | JOIN 无匹配 | | TC-0412 | FindByIds | 正常批量查询 | ids=[1,2,3] | 返回3条 | 正常路径 | P0 | IN查询 | | TC-0413 | FindByIds | 空ids | ids=[] | 返回nil,nil | 边界 | P0 | len==0 | | TC-0414 | FindByIds | 部分id不存在 | ids=[1,9999] | 仅返回存在的 | 边界 | P1 | IN不报错 | | TC-0415 | FindByIds | DB异常 | 连接失败 | 返回nil,err | 异常路径 | P1 | err透传 | | TC-0416 | FindIdsByDeptId | 有用户的部门 | deptId=1(有用户) | 返回id列表 | 正常路径 | P0 | WHERE deptId=? | | TC-0417 | FindIdsByDeptId | 无用户部门 | deptId=999 | 空slice | 边界 | P1 | | | TC-0418 | UpdateProfile | 状态未变-不递增tokenVersion | statusChanged=false | 成功, tokenVersion不变 | 正常路径 | P0 | H-1修复: 非状态字段更新不影响会话 | | TC-0419 | UpdateProfile | 状态变更-tokenVersion+1 | statusChanged=true | 成功, tokenVersion+1 | 正常路径 | P0 | H-1修复: 状态变更使会话失效 | | TC-0420 | UpdateProfile | 乐观锁冲突 | expectedUpdateTime 与DB不符 | 返回ErrUpdateConflict | 异常路径 | P0 | H-1修复: WHERE updateTime=? | | TC-0421 | UpdateProfile | 并发场景 | 两个 goroutine 基于同一 updateTime 并发更新 | 仅一方成功, 另一方得到 ErrUpdateConflict | 并发 | P0 | H-1修复: 乐观锁仅允许一个成功 | | TC-0422 | UpdateProfile | userId不存在 | id=9999999 | 返回 ErrUpdateConflict (affected=0) | 异常路径 | P1 | WHERE 不匹配 | ### 8.2 SysProductModel | TC编号 | 方法 | 测试场景 | 输入 | 预期结果 | 类型 | 优先级 | 覆盖说明 | | :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- | | TC-0423 | FindList | 正常分页 | page=1, pageSize=10 | 返回list+total | 正常路径 | P0 | count+limit | | TC-0424 | FindList | 空表 | 无数据 | total=0, list空 | 边界 | P0 | | | TC-0425 | FindList | count失败 | DB异常 | 返回err | 异常路径 | P1 | 第一个err | ### 8.3 SysPermModel | TC编号 | 方法 | 测试场景 | 输入 | 预期结果 | 类型 | 优先级 | 覆盖说明 | | :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- | | TC-0426 | FindListByProductCode | 正常分页 | productCode="p1", page=1, pageSize=10 | list+total | 正常路径 | P0 | WHERE productCode=? | | TC-0427 | FindListByProductCode | 不存在的productCode | "notexist" | total=0, list空 | 边界 | P1 | | | TC-0428 | FindAllCodesByProductCode | 正常查询 | DB有3条启用权限 | 返回3个code | 正常路径 | P0 | SELECT code WHERE status=1 | | TC-0429 | FindAllCodesByProductCode | 空结果 | 无匹配 | 空slice | 边界 | P1 | | | TC-0430 | FindByIds | 正常 | ids=[1,2] | 返回2条 | 正常路径 | P0 | IN查询 | | TC-0431 | FindByIds | 空ids | [] | 返回nil,nil | 边界 | P0 | len==0 | | TC-0432 | FindMapByProductCode | 正常查询 | productCode="p1" | map[code]*SysPerm, key为code | 正常路径 | P0 | result[p.Code]=p | | TC-0433 | FindMapByProductCode | 空结果 | 无匹配 | 空map | 边界 | P1 | | | TC-0434 | FindMapByProductCode | key唯一性 | 同productCode有3条 | map长度=3 | 功能验证 | P0 | code唯一 | | TC-0435 | DisableNotInCodesWithTx | codes非空-正常 | session+productCode="p1", codes=["a","b"], DB有a/b/c | 禁用c, 返回affected=1 | 正常路径 | P0 | 事务内 NOT IN | | TC-0436 | DisableNotInCodesWithTx | codes为空-全部禁用 | codes=[] | 全部已启用的被禁用 | 分支覆盖 | P0 | len==0分支 | | TC-0437 | DisableNotInCodesWithTx | 无需禁用 | codes包含所有已启用 | affected=0 | 边界 | P1 | 0行更新 | | TC-0438 | DisableNotInCodesWithTx | DB异常 | session.Exec报错 | 返回0,err | 异常路径 | P1 | err | | TC-0439 | FindAllCodesByProductCode | 有权限产品 | productCode="p1" | 返回code列表(仅status=1) | 正常路径 | P0 | WHERE status=1 | | TC-0440 | FindAllCodesByProductCode | 无权限产品 | productCode="notexist" | 空slice | 边界 | P1 | | | TC-0441 | FindAllCodesByProductCode | 全部已禁用 | 所有perm.status=2 | 空slice | 边界 | P1 | WHERE status=1 过滤, 全禁用时返回空 | ### 8.4 SysDeptModel | TC编号 | 方法 | 测试场景 | 输入 | 预期结果 | 类型 | 优先级 | 覆盖说明 | | :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- | | TC-0442 | FindAll | 正常查询 | DB有5条 | 返回5条, 按sort asc排序 | 正常路径 | P0 | ORDER BY sort, id | | TC-0443 | FindAll | 空表 | 无数据 | 空slice | 边界 | P0 | | | TC-0444 | FindByParentId | 正常查询 | parentId=1 | 返回子部门列表 | 正常路径 | P0 | WHERE parentId=? | | TC-0445 | FindByParentId | 无子部门 | parentId=999 | 空slice | 边界 | P1 | | | TC-0446 | FindByPathPrefix | 正常查询 | pathPrefix="/1/" | 返回路径以/1/开头的部门 | 正常路径 | P0 | LIKE pathPrefix% | | TC-0447 | FindByPathPrefix | LIKE注入已阻止 | pathPrefix含% | 空slice(%和_已转义,不作为通配符) | 安全 | P1 | NewReplacer转义 | | TC-0448 | FindByPathPrefix | 无匹配 | "/999/" | 空slice | 边界 | P1 | | ### 8.5 SysRoleModel | TC编号 | 方法 | 测试场景 | 输入 | 预期结果 | 类型 | 优先级 | 覆盖说明 | | :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- | | TC-0449 | FindListByProductCode | 正常分页 | productCode="p1" | 按permsLevel asc排序 | 正常路径 | P0 | ORDER BY permsLevel, id | | TC-0450 | FindListByProductCode | 空结果 | 无匹配 | total=0 | 边界 | P1 | | | TC-0451 | FindByIds | 正常 | ids=[1,2] | 返回2条 | 正常路径 | P0 | IN查询 | | TC-0452 | FindByIds | 空ids | [] | 返回nil,nil | 边界 | P0 | len==0 | | TC-0453 | FindMinPermsLevelByUserIdAndProductCode | 有角色用户 | userId=1, productCode="p1" | 返回最小permsLevel | 正常路径 | P0 | MIN聚合 | | TC-0454 | FindMinPermsLevelByUserIdAndProductCode | 无角色用户 | userId=无角色用户 | error(ErrNotFound) | 边界 | P0 | IFNULL返回-1→level<0→ErrNotFound | ### 8.6 SysRolePermModel | TC编号 | 方法 | 测试场景 | 输入 | 预期结果 | 类型 | 优先级 | 覆盖说明 | | :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- | | TC-0455 | FindPermIdsByRoleId | 正常查询 | roleId=1, DB有3条 | 返回3个permId | 正常路径 | P0 | SELECT permId WHERE roleId=? | | TC-0456 | FindPermIdsByRoleId | 无绑定 | roleId=999 | 空slice | 边界 | P1 | | | TC-0457 | FindPermIdsByRoleIds | 正常查询 | roleIds=[1,2] | 返回去重后的permId | 正常路径 | P0 | DISTINCT + IN | | TC-0458 | FindPermIdsByRoleIds | 空roleIds | [] | 返回nil,nil | 边界 | P0 | len==0 | | TC-0459 | FindPermIdsByRoleIds | 去重验证 | 两角色有相同permId | 结果中permId不重复 | 功能验证 | P0 | DISTINCT | | TC-0460 | DeleteByRoleIdTx | 正常事务内删除 | session+roleId | 使用session执行 | 正常路径 | P0 | session.ExecCtx | | TC-0461 | DeleteByRoleIdTx | 无绑定 | session+roleId=999 | 删0行, 不报错 | 边界 | P1 | session.ExecCtx, affected=0 不报错 | ### 8.7 SysUserPermModel | TC编号 | 方法 | 测试场景 | 输入 | 预期结果 | 类型 | 优先级 | 覆盖说明 | | :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- | | TC-0462 | FindPermIdsByUserIdAndEffectForProduct | ALLOW-指定产品 | userId=1, effect="ALLOW", productCode="p1" | 返回该用户在该产品下ALLOW的permIds | 正常路径 | P0 | H-1修复: 跨产品权限隔离 | | TC-0463 | FindPermIdsByUserIdAndEffectForProduct | DENY-指定产品 | userId=1, effect="DENY", productCode="p1" | 返回DENY的permIds | 正常路径 | P0 | H-1修复: 跨产品权限隔离 | | TC-0464 | FindPermIdsByUserIdAndEffectForProduct | 无记录/其他产品 | productCode不匹配 | 空slice | 边界 | P1 | WHERE 子句过滤 productCode 后无匹配 | | TC-0465 | DeleteByUserIdForProductTx | 事务内跨产品删除 | session+userId+productCode | 使用session | 正常路径 | P0 | session.ExecCtx, 仅删该产品下配置 | | TC-0466 | DeleteByUserIdForProductTx | 跨产品隔离 | 用户在多产品有配置 | 仅删目标产品的 | 深度业务 | P0 | WHERE userId=? AND productCode=? 隔离其他产品 | ### 8.8 SysUserRoleModel | TC编号 | 方法 | 测试场景 | 输入 | 预期结果 | 类型 | 优先级 | 覆盖说明 | | :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- | | TC-0467 | FindRoleIdsByUserId | 正常查询 | userId=1, DB有3条 | 返回3个roleId | 正常路径 | P0 | SELECT roleId | | TC-0468 | FindRoleIdsByUserId | 无绑定 | userId=999 | 空slice | 边界 | P1 | | | TC-0469 | DeleteByRoleIdTx | 正常删除 | session+roleId | 删除该角色的所有用户绑定 | 正常路径 | P0 | session.ExecCtx | | TC-0470 | DeleteByUserIdForProductTx | 事务内跨产品删除 | session+userId+productCode | 使用session | 正常路径 | P0 | session.ExecCtx | | TC-0471 | DeleteByUserIdForProductTx | 跨产品隔离 | 用户在多产品有角色 | 仅删目标产品的 | 深度业务 | P0 | WHERE userId=? AND productCode=? 子查询过滤 | | TC-0472 | FindUserIdsByRoleId | 有绑定的角色 | roleId=1 | 返回userId列表 | 正常路径 | P0 | WHERE roleId=? | | TC-0473 | FindUserIdsByRoleId | 无绑定角色 | roleId=999 | 空slice | 边界 | P1 | | | TC-0474 | FindRoleIdsByUserIdForProduct | 跨产品过滤 | userId=1, productCode="p1" | 仅返回该产品下绑定的roleId | 深度业务 | P0 | 差量更新依赖此接口 | ### 8.9 SysProductMemberModel | TC编号 | 方法 | 测试场景 | 输入 | 预期结果 | 类型 | 优先级 | 覆盖说明 | | :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- | | TC-0475 | FindListByProductCode | 正常分页 | productCode="p1" | list+total | 正常路径 | P0 | WHERE productCode=? | | TC-0476 | FindListByProductCode | 空结果 | 无匹配 | total=0 | 边界 | P1 | | | TC-0477 | FindMapByProductCodeUserIds | 正常批量 | productCode="p1", userIds=[1,2] | map key=userId | 正常路径 | P0 | IN+productCode | | TC-0478 | FindMapByProductCodeUserIds | 空userIds | [] | 返回空map | 边界 | P0 | len==0 | | TC-0479 | FindMapByProductCodeUserIds | 部分不是成员 | userIds含非成员 | map仅含成员 | 边界 | P1 | | | TC-0480 | FindMapByProductCodeUserIds | map key正确 | 查询结果 | key=userId, val=*SysProductMember | 功能验证 | P0 | result[pm.UserId] | ## 九、访问控制 (auth/access.go) ### 9.1 RequireSuperAdmin | TC编号 | 测试场景 | 输入 | 预期结果 | 类型 | 优先级 | 覆盖说明 | | :--- | :--- | :--- | :--- | :--- | :--- | :--- | | TC-0481 | 超管通过 | ctx含SuperAdmin UserDetails | nil (允许) | 正常路径 | P0 | caller.IsSuperAdmin | | TC-0482 | 非超管拒绝 | ctx含ADMIN UserDetails | 403 "仅超级管理员" | 异常路径 | P0 | !IsSuperAdmin | | TC-0483 | MEMBER拒绝 | ctx含MEMBER UserDetails | 403 "仅超级管理员" | 异常路径 | P0 | | | TC-0484 | 未登录 | ctx无UserDetails | 401 "未登录" | 边界 | P0 | caller==nil | ### 9.2 RequireProductAdminFor(ctx, targetProductCode) | TC编号 | 测试场景 | 输入 | 预期结果 | 类型 | 优先级 | 覆盖说明 | | :--- | :--- | :--- | :--- | :--- | :--- | :--- | | TC-0485 | 超管通过 | ctx含SuperAdmin, productCode="p1" | nil | 正常路径 | P0 | IsSuperAdmin | | TC-0486 | ADMIN通过(同产品) | ctx含ADMIN(p1), productCode="p1" | nil | 正常路径 | P0 | MemberType==ADMIN且productCode匹配 | | TC-0487 | DEVELOPER拒绝 | ctx含DEVELOPER(p1), productCode="p1" | 403 | 异常路径 | P0 | 非Admin | | TC-0488 | MEMBER拒绝 | ctx含MEMBER(p1), productCode="p1" | 403 | 异常路径 | P0 | 非Admin | | TC-0489 | 未登录 | ctx无UserDetails, productCode="p1" | 401 | 边界 | P0 | caller==nil | | TC-0490 | ADMIN跨产品拒绝 | ctx含ADMIN(p1), productCode="other" | 403 | 安全 | P0 | productCode不匹配→拒绝 | ### 9.3 CheckMemberTypeAssignment | TC编号 | 测试场景 | 输入 | 预期结果 | 类型 | 优先级 | 覆盖说明 | | :--- | :--- | :--- | :--- | :--- | :--- | :--- | | TC-0491 | 超管可分配任何类型 | caller=SuperAdmin, assigned=ADMIN | nil | 正常路径 | P0 | IsSuperAdmin豁免 | | TC-0492 | ADMIN分配DEVELOPER | caller=ADMIN, assigned=DEVELOPER | nil | 正常路径 | P0 | callerPri(1) < assignPri(2) | | TC-0493 | ADMIN分配ADMIN(同级拒绝) | caller=ADMIN, assigned=ADMIN | 403 | 深度业务 | P0 | callerPri >= assignPri | | TC-0494 | DEVELOPER分配ADMIN(越级拒绝) | caller=DEVELOPER, assigned=ADMIN | 403 | 深度业务 | P0 | callerPri > assignPri | | TC-0495 | MEMBER分配MEMBER(同级拒绝) | caller=MEMBER, assigned=MEMBER | 403 | 深度业务 | P0 | | | TC-0496 | 未登录 | ctx无UserDetails | 401 | 边界 | P0 | | ### 9.4 CheckManageAccess | TC编号 | 测试场景 | 输入 | 预期结果 | 类型 | 优先级 | 覆盖说明 | | :--- | :--- | :--- | :--- | :--- | :--- | :--- | | TC-0497 | 超管可管理任何人 | caller=SuperAdmin | nil | 正常路径 | P0 | IsSuperAdmin豁免 | | TC-0498 | 操作自己 | caller.UserId==targetUserId | nil | 正常路径 | P0 | self豁免 | | TC-0499 | ADMIN跳过部门检查 | caller=ADMIN | nil (直接比级别) | 深度业务 | P0 | checkDeptHierarchy ADMIN豁免 | | TC-0500 | 非ADMIN无部门拒绝 | caller.DeptId=0 | 403 "未归属部门" | 边界 | P0 | caller.DeptId==0 | | TC-0501 | 目标用户无部门 | target.DeptId=0 | 403 "目标用户未归属部门" | 边界 | P0 | target.DeptId==0 | | TC-0502 | 目标在不同部门 | 目标不在caller子部门 | 403 "无权管理其他部门" | 深度业务 | P0 | !HasPrefix | | TC-0503 | 未登录 | ctx无UserDetails | 401 | 边界 | P0 | | | TC-0504 | caller.DeptPath为空时拒绝 | caller有DeptId但DeptPath="" | 403 "无权管理" | 安全 | P0 | H-08修复: DeptPath空串保护 | ### 9.5 memberTypePriority | TC编号 | 测试场景 | 输入 | 预期结果 | 类型 | 优先级 | 覆盖说明 | | :--- | :--- | :--- | :--- | :--- | :--- | :--- | | TC-0505 | 各类型优先级正确 | 全部4种+未知 | SA=0,A=1,D=2,M=3,unknown=MaxInt32 | 白盒 | P0 | switch分支 | ## 十、UserDetailsLoader (loaders/userDetailsLoader.go) ### 10.1 Load / 缓存 | TC编号 | 测试场景 | 输入 | 预期结果 | 类型 | 优先级 | 覆盖说明 | | :--- | :--- | :--- | :--- | :--- | :--- | :--- | | TC-0506 | DB加载(缓存miss) | 有效userId+productCode | 返回完整UserDetails | 正常路径 | P0 | loadFromDB全链路 | | TC-0507 | 缓存命中 | 第二次Load同key | 从Redis返回,不查DB | 正常路径 | P0 | GetCtx hit | | TC-0508 | 用户不存在 | userId=999999 | 返回零值UserDetails(Status=0) | 边界 | P0 | loadUser失败 | | TC-0509 | productCode为空 | productCode="" | 跳过产品/成员/角色/权限加载 | 边界 | P1 | 各方法guard | ### 10.2 缓存失效 | TC编号 | 测试场景 | 输入 | 预期结果 | 类型 | 优先级 | 覆盖说明 | | :--- | :--- | :--- | :--- | :--- | :--- | :--- | | TC-0510 | Del删除指定缓存 | Del(uid, pc) | 缓存被删除,下次Load查DB | 正常路径 | P0 | DelCtx | | TC-0511 | Clean清除用户所有产品缓存 | Clean(uid) | 该用户所有key被删 | 正常路径 | P0 | KEYS pattern | | TC-0512 | CleanByProduct清除产品所有用户 | CleanByProduct(pc) | 该产品所有key被删 | 正常路径 | P0 | KEYS pattern | | TC-0513 | BatchDel批量删除 | BatchDel([uid1,uid2], pc) | 多个key被删 | 正常路径 | P0 | DelCtx多key | | TC-0514 | BatchDel空数组 | BatchDel([], pc) | 无操作 | 边界 | P1 | len==0 guard | ### 10.3 loadPerms权限计算 | TC编号 | 测试场景 | 输入 | 预期结果 | 类型 | 优先级 | 覆盖说明 | | :--- | :--- | :--- | :--- | :--- | :--- | :--- | | TC-0515 | 超管全量权限 | IsSuperAdmin=true | Perms=全部启用的权限码 | 正常路径 | P0 | 超管分支 | | TC-0516 | ADMIN全量权限 | MemberType=ADMIN | Perms=全量 | 正常路径 | P0 | ADMIN分支 | | TC-0517 | DEVELOPER全量权限 | MemberType=DEVELOPER | Perms=全量 | 正常路径 | P0 | DEVELOPER分支 | | TC-0518 | DEV部门全量权限 | DeptType=DEV | Perms=全量 | 正常路径 | P0 | DeptTypeDev分支 | | TC-0519 | MEMBER角色权限+ALLOW-DENY | 有角色+ALLOW+DENY | 正确计算 | 深度业务 | P0 | denySet过滤 | | TC-0520 | 用户ALLOW权限不跨产品泄漏 | 用户在产品A/B各有ALLOW权限 | 加载产品A时仅含A权限,不含B权限 | 安全 | P0 | H-1: FindPermIdsByUserIdAndEffectForProduct | | TC-0521 | 禁用DEV部门成员无全量权限 | dept.type=DEV, dept.status=Disabled | ud.Perms为空 | 安全 | P0 | M-3: DeptStatus检查 | ### 10.4 loadRoles + MinPermsLevel | TC编号 | 测试场景 | 输入 | 预期结果 | 类型 | 优先级 | 覆盖说明 | | :--- | :--- | :--- | :--- | :--- | :--- | :--- | | TC-0522 | 多角色取最小permsLevel | 用户有level=10和level=5的角色 | MinPermsLevel=5 | 正常路径 | P0 | min计算 | | TC-0523 | 无角色 | 用户无角色 | MinPermsLevel=MaxInt64 | 边界 | P0 | 默认值 | | TC-0524 | 角色跨产品过滤 | 角色在不同产品 | 仅加载当前产品角色 | 深度业务 | P0 | productCode过滤 | | TC-0525 | 禁用角色不计入 | 角色status=2 | 不在Roles列表中 | 深度业务 | P0 | Status==Enabled | ### 10.5 loadMembership | TC编号 | 测试场景 | 输入 | 预期结果 | 类型 | 优先级 | 覆盖说明 | | :--- | :--- | :--- | :--- | :--- | :--- | :--- | | TC-0526 | 超管自动设置SUPER_ADMIN | IsSuperAdmin=true | MemberType=SUPER_ADMIN, 不查DB | 正常路径 | P0 | 早期return | | TC-0527 | 非成员MemberType为空 | 用户非该产品成员 | MemberType="" | 边界 | P0 | ErrNotFound | | TC-0528 | 禁用成员MemberType为空 | member.status=Disabled | ud.MemberType="" | 安全 | P0 | H-3: loadMembership | ## 十一、中间件 — 冻结账号拦截 | TC编号 | 测试场景 | 输入 | 预期结果 | 类型 | 优先级 | 覆盖说明 | | :--- | :--- | :--- | :--- | :--- | :--- | :--- | | TC-0529 | 冻结用户被403 | 有效token但Status=2 | code=403 "账号已被冻结" | 安全 | P0 | ud.Status!=Enabled | | TC-0530 | 用户不存在(Status=0) | token中userId不存在 | code=403 "账号已被冻结" | 安全 | P0 | loadUser失败→Status=0 | | TC-0531 | UserDetails注入context | 正常请求 | GetUserDetails(ctx)非nil | 正常路径 | P0 | WithUserDetails | ## 十二、Logic层 — 访问控制负面测试 | TC编号 | 测试场景 | 输入 | 预期结果 | 类型 | 优先级 | 覆盖说明 | | :--- | :--- | :--- | :--- | :--- | :--- | :--- | | TC-0532 | createDept非超管拒绝 | ctx=ADMIN | 403 "仅超级管理员" | 安全 | P0 | RequireSuperAdmin | | TC-0533 | updateDept非超管拒绝 | ctx=ADMIN | 403 "仅超级管理员" | 安全 | P0 | RequireSuperAdmin | | TC-0534 | deleteDept非超管拒绝 | ctx=ADMIN | 403 "仅超级管理员" | 安全 | P0 | RequireSuperAdmin | | TC-0535 | createProduct非超管拒绝 | ctx=ADMIN | 403 "仅超级管理员" | 安全 | P0 | RequireSuperAdmin | | TC-0536 | updateProduct非超管拒绝 | ctx=ADMIN | 403 "仅超级管理员" | 安全 | P0 | RequireSuperAdmin | | TC-0537 | createUser非产品管理员拒绝 | ctx=MEMBER | 403 "仅超级管理员或产品管理员" | 安全 | P0 | RequireProductAdminFor(ctx, productCode) | | TC-0538 | createRole非产品管理员拒绝 | ctx=MEMBER | 403 | 安全 | P0 | RequireProductAdminFor(ctx, productCode) | | TC-0539 | updateRole非产品管理员拒绝 | ctx=MEMBER | 403 | 安全 | P0 | RequireProductAdminFor(ctx, productCode) | | TC-0540 | deleteRole非产品管理员拒绝 | ctx=MEMBER | 403 | 安全 | P0 | RequireProductAdminFor(ctx, productCode) | | TC-0541 | bindRolePerms非产品管理员拒绝 | ctx=MEMBER | 403 | 安全 | P0 | RequireProductAdminFor(ctx, productCode) | | TC-0542 | updateUser-MEMBER不能管理他人 | ctx=MEMBER, id!=self | 403 (CheckManageAccess拒绝) | 安全 | P0 | Audit#4修复: CheckManageAccess权限校验 | | TC-0543 | updateUser自己修改DeptId被拒绝 | ctx含userId=X, req.Id=X, req.DeptId!=nil | 403 "不允许修改自己的部门和状态" | 安全 | P0 | H-01修复: 自编辑限制DeptId | | TC-0544 | updateUser自己修改Status被拒绝 | ctx含userId=X, req.Id=X, req.Status!=0 | 403 "不允许修改自己的部门和状态" | 安全 | P0 | H-01修复: 自编辑限制Status | | TC-0545 | updateUser未登录被拒绝 | ctx无UserDetails | 401 "未登录" | 安全 | P0 | H-01修复: caller==nil | --- ## 十三、限流中间件 (middleware/ratelimitMiddleware.go) | TC编号 | 测试场景 | 输入 | 预期结果 | 类型 | 优先级 | 覆盖说明 | | :--- | :--- | :--- | :--- | :--- | :--- | :--- | | TC-0546 | 正常请求(未超限) | 首次请求 | 请求正常通过, next被调用 | 正常路径 | P0 | code!=OverQuota→next | | TC-0547 | 超限请求被拒绝 | 超出配额后的请求 | code=429, "请求过于频繁,请稍后再试" | 异常路径 | P0 | code==OverQuota→ErrTooManyRequests | | TC-0548 | behindProxy=false时XFF被忽略 | behindProxy=false, 不同XFF头+相同RemoteAddr | 仍被限流, nextCount保持为1 | 安全 | P0 | behindProxy=false: 仅用RemoteAddr | | TC-0549 | behindProxy=false时X-Real-IP被忽略 | behindProxy=false, 不同XRI头+相同RemoteAddr | 仍被限流, nextCount保持为1 | 安全 | P0 | behindProxy=false: 仅用RemoteAddr | | TC-0550 | IP从RemoteAddr解析 | 无代理头, RemoteAddr="ip:port" | 使用SplitHostPort解析host作为限流key | 分支覆盖 | P0 | SplitHostPort解析host | | TC-0551 | 不同IP独立限流 | 两个不同IP | 各自独立计数, 互不影响 | 功能验证 | P0 | key隔离 | | TC-0552 | behindProxy=true时信任X-Real-IP | behindProxy=true, 不同X-Real-IP头 | 按X-Real-IP独立限流 | 正常路径 | P0 | behindProxy=true: X-Real-IP优先 | | TC-0553 | behindProxy=true时无X-Real-IP回退RemoteAddr | behindProxy=true, 无X-Real-IP头 | 使用RemoteAddr作为限流key | 分支覆盖 | P0 | X-Real-IP为空→fallback RemoteAddr | | ~~TC-0554~~ | ~~behindProxy=true 时 XFF 仍被忽略~~ | — | — | — | — | **已删除**:M-6 显式反转契约 —— behindProxy=true 时 XFF 首段合法应优先;新契约由 TC-0862~0866(`ratelimitMiddlewareXff_audit_test.go`)覆盖 | | TC-0555 | RemoteAddr无端口格式 | RemoteAddr="1.2.3.4"(无端口) | 返回原始RemoteAddr "1.2.3.4" | 边界 | P1 | SplitHostPort失败→r.RemoteAddr | --- ## 十四、审计修复回归测试 (audit-report.md 2026-04 修复集) > 新增测试用于验证近期针对 `audit-report.md` 的高/中/低风险项所提交的修复,确保修复行为严格生效且不回归。 ### H-1 BindRoles permsLevel 仅对 MEMBER 生效 | TC编号 | 测试场景 | 输入 | 预期结果 | 类型 | 优先级 | 覆盖说明 | | :--- | :--- | :--- | :--- | :--- | :--- | :--- | | TC-0208 | MEMBER 调用者不能分配权限级别高于自身的角色 | caller=MEMBER, MinPermsLevel=50, 目标角色 permsLevel=100 | 403 "不能分配权限级别高于自身的角色" | 越权/安全 | P0 | BindRoles H-1 修复:permsLevel 校验仍作用于 MEMBER | | TC-0711 | ADMIN 调用者豁免 permsLevel 校验 | caller=ADMIN, MinPermsLevel=math.MaxInt64, 目标角色任意 permsLevel | 成功绑定 | 正常路径 | P0 | H-1 修复:ADMIN/DEVELOPER 不再受 permsLevel 约束 | | TC-0712 | DEVELOPER 调用者豁免 permsLevel 校验 | caller=DEVELOPER, 目标角色任意 permsLevel | 成功绑定 | 正常路径 | P0 | H-1 修复:DEVELOPER 豁免 | | TC-0713 | MinPermsLevel=MaxInt64 的 MEMBER 不被误阻断 | caller=MEMBER, MinPermsLevel=math.MaxInt64(未持角色) | 不触发 "不能分配权限级别高于自身" 错误 | 分支覆盖 | P0 | H-1 修复:sentinel 值语义 | ### H-2/H-3 gRPC GetUserPerms 状态+成员校验 | TC编号 | 测试场景 | 输入 | 预期结果 | 类型 | 优先级 | 覆盖说明 | | :--- | :--- | :--- | :--- | :--- | :--- | :--- | | TC-0700 | 冻结用户 (Status=Disabled) | GetUserPerms 请求冻结账号 | gRPC PermissionDenied,msg 含"冻结" | 安全 | P0 | H-2 修复:对齐 VerifyToken 的 StatusEnabled 判定 | | TC-0701 | 非产品成员 | 启用用户但非目标产品成员 | gRPC PermissionDenied,msg 含"成员" | 安全 | P0 | H-2 修复:MemberType=="" 拒绝 | | TC-0702 | DEV 部门但产品成员被禁用 | dept.DeptType=DEV & member.Status=Disabled | gRPC PermissionDenied | 安全 | P0 | H-2+H-3 修复:DEV 部门不再旁路已禁用成员校验 | | TC-0703 | 启用 ADMIN 成员(正向回归) | 正常启用成员,产品存在权限 | 成功,返回 MemberType=ADMIN 且 Perms 含已配置项 | 正常路径 | P0 | H-2 修复后正常路径未被误伤 | | TC-0704 | Loader 层:DEV 部门 + 产品成员禁用 | DEV 启用,member.Status=Disabled | UserDetails.MemberType="",Perms=[] | 安全 | P0 | H-3 修复:禁用成员走入 MemberType 清空分支后不再命中全量权限 | ### M-2 批量删除回归 | TC编号 | 测试场景 | 输入 | 预期结果 | 类型 | 优先级 | 覆盖说明 | | :--- | :--- | :--- | :--- | :--- | :--- | :--- | | TC-0707 | DeleteByUserIdAndRoleIdsTx 批量删除 | 插入 3 条 (user, roleX),批量删除其中 2 条 | 仅保留未被删除的 1 条 | 正常路径 | P0 | M-2:循环 DELETE → 批量 IN | | TC-0708 | 批量删除空列表为 no-op | roleIds=[] | 无任何删除,原记录保留 | 边界 | P0 | M-2:空集合保护 | | TC-0709 | 批量删除仅作用于指定 userId | 同 roleId 下两个 user,仅删 user1 | user2 的绑定不受影响 | 约束 | P0 | M-2:WHERE userId 严格约束 | ### M-3 SuperAdmin 在产品上下文只返回该产品角色 | TC编号 | 测试场景 | 输入 | 预期结果 | 类型 | 优先级 | 覆盖说明 | | :--- | :--- | :--- | :--- | :--- | :--- | :--- | | TC-0181 | 超管在产品上下文查 userDetail | 用户同时持有 test_product 与 other_product 角色 | resp.RoleIds 只含 test_product 的 roleIds | 越权/隔离 | P0 | M-3 修复:不再跨产品返回 | ### M-4 FindRoleIdsByUserIdForProduct 过滤 r.status=1 | TC编号 | 测试场景 | 输入 | 预期结果 | 类型 | 优先级 | 覆盖说明 | | :--- | :--- | :--- | :--- | :--- | :--- | :--- | | TC-0706 | 同一产品下同时存在启用/禁用角色 | user 绑定启用+禁用 2 个角色 | 仅返回启用角色的 id | 安全/过滤 | P0 | M-4 修复:SQL 加入 r.status=1 | ### M-5 UpdateDept 乐观锁 + 精准缓存清理 | TC编号 | 测试场景 | 输入 | 预期结果 | 类型 | 优先级 | 覆盖说明 | | :--- | :--- | :--- | :--- | :--- | :--- | :--- | | TC-0105 | DeptType/Status 变更仅清自己的成员缓存 | 变更部门类型 | UpdateWithOptLock 被调用,不再对子部门做 FindIdsByDeptId 级联清理 | 正常路径 | P1 | M-5 修复:不再级联 | | TC-0714 | DeptType/Status 未变更时不清缓存 | 只改 name | 无 Clean 调用 | 分支覆盖 | P1 | M-5:unchanged 分支 | | TC-0715 | 乐观锁冲突返回 ErrConflict | UpdateWithOptLock 返回 0 行 | 返回 409/Conflict | 并发 | P0 | M-5:版本号冲突 | ### M-6 JWT Claims.Perms 已移除 | TC编号 | 测试场景 | 输入 | 预期结果 | 类型 | 优先级 | 覆盖说明 | | :--- | :--- | :--- | :--- | :--- | :--- | :--- | | TC-0716 | access token payload 中不得含 perms | 生成 access token 后 base64 解码 payload | JSON 中不存在 "perms" key | 安全 | P0 | M-6:Dead field 清理 | ### M-11 DeleteDept TOCTOU 修复 | TC编号 | 测试场景 | 输入 | 预期结果 | 类型 | 优先级 | 覆盖说明 | | :--- | :--- | :--- | :--- | :--- | :--- | :--- | | TC-0108 | 删除不存在的部门 | 任意不存在的 deptId | 返回 404 "部门不存在" | 错误路径 | P0 | M-11:事务内 SELECT FOR UPDATE + 不存在显式报错 | ### L-2 产品/管后登录限流独立桶 | TC编号 | 测试场景 | 输入 | 预期结果 | 类型 | 优先级 | 覆盖说明 | | :--- | :--- | :--- | :--- | :--- | :--- | :--- | | TC-0710 | 同 IP,产品登录用尽配额后管后登录仍放行 | productLoginRL 配额=1 打满,再打 adminLoginRL | adminLoginRL 正常放行 1 次 | 安全 | P0 | L-2 修复:keyPrefix 区分 product/admin | ### L-5 Loader 不缓存不存在用户 | TC编号 | 测试场景 | 输入 | 预期结果 | 类型 | 优先级 | 覆盖说明 | | :--- | :--- | :--- | :--- | :--- | :--- | :--- | | TC-0705 | Load 不存在用户 | userId=999999999 | 返回空 Username;第二次 Load 行为与首次一致(无缓存污染) | 边界/缓存 | P1 | L-5 修复:!ok 分支不缓存零值 | ## 十一、 本轮新增对抗性用例(审计修复回归) > 针对 `audit-report.md` 中 H-4 / M-1 / M-14 / L-3 / L-5 五个关键修复点补充的"攻击性"测试,覆盖: > > * 最后 ADMIN 保护(移除 & 降级、活跃/禁用 ADMIN 计数差异) > * Logout 接口 tokenVersion 递增 + loader 缓存清理 > * setUserPerms 对产品禁用的拦截 > * updateRole 非超管降低 `PermsLevel` 的禁止 + 超管例外 > * addMember 对已禁用产品的拦截 ### M-1 Logout 接口:tokenVersion 递增 + 缓存失效 | TC编号 | 测试场景 | 输入 | 预期结果 | 类型 | 优先级 | 覆盖说明 | | :--- | :--- | :--- | :--- | :--- | :--- | :--- | | TC-0720 | 正常登出 | 已登录用户 ctx + WithUserDetails | 用户 tokenVersion=0 → 1;重新 Load 时 TokenVersion 已递增 | 正常路径 | P0 | M-1:IncrementTokenVersion + UserDetailsLoader.Del | | TC-0721 | 未登录调用 /auth/logout | ctx 中无 userDetails | 返回 401 "未登录" | 错误路径 | P0 | M-1:未登录兜底 | | TC-0722 | 同一用户连续两次 logout | 登出两次 | tokenVersion 累加至 2 | 幂等/累加 | P1 | M-1:每次自增,不被覆盖 | ### H-4 产品最后一个 ADMIN 保护 | TC编号 | 测试场景 | 输入 | 预期结果 | 类型 | 优先级 | 覆盖说明 | | :--- | :--- | :--- | :--- | :--- | :--- | :--- | | TC-0723 | 移除产品唯一 ADMIN | 1 个启用 ADMIN | 400 "不能移除该产品的最后一个管理员",ADMIN 仍存在 | 安全 | P0 | H-4:CountActiveAdmins<=1 拒绝 | | TC-0724 | 有 2 个 ADMIN 时移除其一 | 2 个 ADMIN | 成功删除 1 个,另一个保留 | 正常路径 | P0 | H-4:非 last-admin 场景放行 | | TC-0725 | 降级产品唯一 ADMIN 为 MEMBER | 1 个启用 ADMIN | 400 "不能降级该产品的最后一个管理员",MemberType 不变 | 安全 | P0 | H-4:updateMember 同逻辑 | | TC-0726 | 有 2 个 ADMIN 时降级其一 | 2 个 ADMIN | 成功降级为 MEMBER | 正常路径 | P0 | H-4:非 last-admin 允许 | | TC-0727 | 2 个 ADMIN 但只有 1 个启用,降级该启用 ADMIN | ADMIN(status=1)+ADMIN(status=2) | 400 "不能降级该产品的最后一个管理员" | 安全/边界 | P0 | H-4:CountActiveAdmins 只计 status=1 | | TC-0728 | 移除非 ADMIN(MEMBER) | 1 个 MEMBER | 成功删除,不受 last-admin 保护 | 正常路径 | P1 | H-4:仅 ADMIN 触发校验 | | TC-0729 | 对禁用产品 addMember | product.status=2 | 400 "产品已被禁用,无法添加成员" | 安全 | P0 | L-5:addMemberLogic 新增 product.Status 校验 | ### L-3 非超管不得降低 PermsLevel | TC编号 | 测试场景 | 输入 | 预期结果 | 类型 | 优先级 | 覆盖说明 | | :--- | :--- | :--- | :--- | :--- | :--- | :--- | | TC-0730 | 产品 admin 把 roleA.PermsLevel 从 100 降到 10 | AdminCtx,PermsLevel 100→10 | 403 "非超管不能降低角色的权限级别",DB 保持 100 | 安全 | P0 | L-3:caller.IsSuperAdmin=false && new=old 放行 | | TC-0732 | 超管降低 PermsLevel | SuperAdminCtx,500→10 | 成功 | 正常路径 | P0 | L-3:IsSuperAdmin 绕开 | | TC-0733 | PermsLevel 越界(0/-1/1000/10000) | 任意非法 PermsLevel | 400 "权限级别必须在 1-999 之间" | 边界 | P0 | L-3 前置校验 | ### M-14 setUserPerms 产品禁用拦截 | TC编号 | 测试场景 | 输入 | 预期结果 | 类型 | 优先级 | 覆盖说明 | | :--- | :--- | :--- | :--- | :--- | :--- | :--- | | TC-0734 | 产品已禁用 | product.status=2 | 400 "产品已被禁用,无法设置权限" | 安全 | P0 | M-14:新增 product.Status 校验 | | TC-0735 | 产品不存在 | 虚构 productCode | 404 "产品不存在" | 错误路径 | P0 | M-14:FindOneByCode ErrNotFound | ## 十二、 本轮新增对抗性用例(审计修复回归 · 第二批) > 针对 `audit-report.md` 中 H-A / H-B / M-B / M-C / L-B / L-C / L-F 七个关键修复点补充的"攻击性"测试,覆盖: > > * `SetUserPerms` 自我提权前置拦截(RequireProductAdminFor) > * `IncrementTokenVersion` 模型层原子递增 + 返回值 = DB 持久化值 + 缓存强制失效 + 并发唯一性 > * `/auth/refreshToken`、`/auth/logout` 接入 `TokenOpLimiter`,限流后不得再递增 tokenVersion,且按 userId 隔离 > * `jwtauthMiddleware` 校验顺序:TokenVersion 失效优先于 ProductStatus / MemberType > * 产品端登录用户名枚举防护(dummy bcrypt + 统一错误文案 + `ip:username` 限流 key) > * `UpdateUser` 跨部门转移 DeptPath 前缀校验(DEVELOPER 不得把目标挪出自身子树) ### H-A `SetUserPerms` 调用者必须是同产品 ADMIN(或超管) | TC编号 | 测试场景 | 输入 | 预期结果 | 类型 | 优先级 | 覆盖说明 | | :--- | :--- | :--- | :--- | :--- | :--- | :--- | | TC-0743 | 普通 MEMBER 给自己授权 | MemberCtx, targetUserId=self | 403 "需要产品管理员权限",DB 中 userperm 无任何写入 | 安全 | P0 | H-A:`RequireProductAdminFor` 前置拦截 self-escalation | | TC-0744 | DEVELOPER 调用者(非 ADMIN)操作他人 | DeveloperCtx, targetUserId=other | 403 "需要产品管理员权限",DB 无写入 | 安全 | P0 | H-A:DEVELOPER 也必须被拦截(非仅 self 场景) | | TC-0745 | 同产品 ADMIN 操作合法 MEMBER 目标 | AdminCtx, targetUserId=member | 200 OK,userperm 插入成功 | 正常路径 | P0 | H-A:修复后 admin 正向通路仍通畅 | ### H-B `IncrementTokenVersion` 原子递增 + 缓存失效 + 并发唯一 | TC编号 | 测试场景 | 输入 | 预期结果 | 类型 | 优先级 | 覆盖说明 | | :--- | :--- | :--- | :--- | :--- | :--- | :--- | | TC-0736 | 单次 Increment 返回值 == DB 持久值 | 初始 tokenVersion=v0 | 返回值 = v0+1 == DB 读取值 | 正常路径 | P0 | H-B:`LAST_INSERT_ID(tokenVersion+1)` 原子自增,不依赖缓存旧值 | | TC-0737 | Increment 后缓存必须被主动清理 | 先 Load 预热缓存,再 Increment | 再次 Load 读到的 TokenVersion 为自增后值(非 stale) | 缓存一致性 | P0 | H-B:事务成功后 `DelCacheCtx` | | TC-0738 | 10 goroutine 并发自增同一用户 | 起始 v0,并发 Increment×10 | 10 次返回值互不重复,最终 DB = v0+10 | 并发/竞态 | P0 | H-B:原子 UPDATE,不会丢失更新 | ### L-C `/auth/logout` 接入 `TokenOpLimiter` | TC编号 | 测试场景 | 输入 | 预期结果 | 类型 | 优先级 | 覆盖说明 | | :--- | :--- | :--- | :--- | :--- | :--- | :--- | | TC-0739 | quota=2 的 limiter,第 3 次 logout | 同一 userId 连续 logout 3 次 | 前 2 次成功 + tokenVersion+2;第 3 次 429 "请求过于频繁",**tokenVersion 不再递增** | 安全/限流 | P0 | L-C:超限请求不得进入业务层,否则攻击者可反复"自增搅乱缓存" | | TC-0740 | 限流按 userId 隔离 | userA 打满配额后 userB 登出 | userB 正常 logout,不受 userA 限流影响 | 安全/隔离 | P0 | L-C:限流 key 必须按 userId 分桶 | ### M-B `/auth/refreshToken` 接入 `TokenOpLimiter` | TC编号 | 测试场景 | 输入 | 预期结果 | 类型 | 优先级 | 覆盖说明 | | :--- | :--- | :--- | :--- | :--- | :--- | :--- | | TC-0741 | quota=1 limiter,burst 第 2 次 | 同一 userId 刷新 2 次 | 第 1 次 200;第 2 次 429,**tokenVersion 不得递增**(避免攻击者持续废除 refresh token) | 安全/限流 | P0 | M-B:refresh 同样走 TokenOpLimiter | | TC-0742 | 限流按 userId 隔离(productCode 无关) | userA(p1) 满额 → userB(p1) 刷新 | userB 正常 200 | 安全/隔离 | P0 | M-B:key 不含 productCode,不会跨用户误伤 | ### L-B `jwtauthMiddleware` 校验顺序:TokenVersion 优先 | TC编号 | 测试场景 | 输入 | 预期结果 | 类型 | 优先级 | 覆盖说明 | | :--- | :--- | :--- | :--- | :--- | :--- | :--- | | TC-0749 | token.tokenVersion != DB & 产品被禁用 | 同时具备两个失败条件 | 返回 401 "令牌已失效"(而非 403 "产品已禁用") | 安全/顺序 | P0 | L-B:TokenVersion 比 ProductStatus 先判,避免客户端看到"用户被踢出"后误以为是产品被禁用 | | TC-0750 | token.tokenVersion == DB,产品被禁用 | TokenVersion 对齐,product.status=2 | 403 "该产品已被禁用" | 安全 | P0 | L-B:TokenVersion 通过后才看 ProductStatus | ### M-C 产品端登录用户名枚举防护 | TC编号 | 测试场景 | 输入 | 预期结果 | 类型 | 优先级 | 覆盖说明 | | :--- | :--- | :--- | :--- | :--- | :--- | :--- | | TC-0751 | 用户名不存在 + 任意密码 | username="\_\_no\_such\_user\_\_" | 返回与"存在用户但密码错"**完全一致**的错误文案("用户名或密码错误"),仍会走 dummy bcrypt 耗时 | 安全/枚举 | P0 | M-C:用 dummy hash 比对,防止通过响应差异枚举用户名 | | TC-0752 | 用户名存在但密码错 | 存在用户 + 错误密码 | 同 TC-0751 相同 code、相同文案 | 安全/对照 | P0 | M-C:与 TC-0751 联动,断言两条分支对外表现一致 | | TC-0753 | 登录限流 key 必须基于 `ip:username` | 同 IP 攻击 userA 耗尽配额 | userA 被限流 429,但同 IP 登录 userB 仍放行 | 安全/限流 | P0 | M-C:`UsernameLoginLimit` key = `ip:username`,避免单 IP 单用户暴力破解拖垮同一 IP 其他用户登录 | ### L-F `UpdateUser` 跨部门转移的 DeptPath 前缀校验 | TC编号 | 测试场景 | 输入 | 预期结果 | 类型 | 优先级 | 覆盖说明 | | :--- | :--- | :--- | :--- | :--- | :--- | :--- | | TC-0746 | DEVELOPER 把成员挪出自己子树 | Caller.DeptPath=/100/,targetNewDept.DeptPath=/999/ | 403 "无权将用户转移到该部门",DB 保持原 deptId | 安全 | P0 | L-F:`strings.HasPrefix(newDept.DeptPath, caller.DeptPath)` 前缀校验 | | TC-0747 | DEVELOPER 在自己子树内移动成员 | Caller=/100/,newDept=/100/200/ | 200 OK,DB 更新 deptId | 正常路径 | P0 | L-F:子树内放行 | | TC-0748 | 产品 ADMIN 不受子树限制 | AdminCtx 跨任意部门 | 200 OK | 正常路径 | P1 | L-F:ADMIN 只过 `CheckManageAccess`,不走子树约束 | ## 十三、审计修复回归 — 第四轮 (audit-report.md 2026-04-19) > 覆盖第四轮审计报告修复项:H-1 ~ H-5(P0),M-1/M-2/M-5/M-6/M-7/M-10/M-11/M-13/M-14/M-15(P1/P2),L-1/L-2(P3)。 ### H-1 `UpdateMember` 禁用最后 ADMIN 绕过修复 | TC编号 | 测试场景 | 输入 | 预期结果 | 类型 | 优先级 | 覆盖说明 | | :--- | :--- | :--- | :--- | :--- | :--- | :--- | | TC-0760 | 保持 memberType=ADMIN 但 status 改为 Disabled | 产品唯一 ADMIN,`{"memberType":"ADMIN","status":2}` | 400 "不能降级或禁用该产品的最后一个管理员" | 安全 | P0 | H-1:`wasActiveAdmin && !willBeActiveAdmin` 覆盖 status 变化 | | TC-0761 | 同时降级 + 禁用唯一 ADMIN | `{"memberType":"MEMBER","status":2}` | 400 同上 | 安全 | P0 | H-1:memberType + status 同时变化 | | TC-0762 | 有 2 个 ADMIN 时禁用其一 | 2 个启用 ADMIN | 成功,目标 status=2 | 正常路径 | P0 | H-1:非 last-admin 场景放行 | ### H-2 `RemoveMember` 事务内视图脱钩修复 | TC编号 | 测试场景 | 输入 | 预期结果 | 类型 | 优先级 | 覆盖说明 | | :--- | :--- | :--- | :--- | :--- | :--- | :--- | | TC-0763 | 移除活跃 ADMIN(事务内用 locked 数据判断) | 唯一 ADMIN | 400 "不能移除该产品的最后一个管理员" | 安全 | P0 | H-2:`locked.MemberType` 替代事务外 `member.MemberType` | | TC-0764 | 移除非 ADMIN 不触发 last-admin 校验 | MEMBER 身份 | 成功移除 | 正常路径 | P0 | H-2:`locked.MemberType != ADMIN` 跳过检查 | ### H-3 `CountActiveAdminsTx` FOR UPDATE 锁定修复 | TC编号 | 测试场景 | 输入 | 预期结果 | 类型 | 优先级 | 覆盖说明 | | :--- | :--- | :--- | :--- | :--- | :--- | :--- | | TC-0765 | CountActiveAdminsTx 返回正确计数 | 产品下 2 个启用 ADMIN + 1 个禁用 ADMIN | 返回 2 | 功能验证 | P0 | H-3:`SELECT id ... FOR UPDATE` 仅计活跃行 | ### H-4 `DeleteDept` 子部门/用户 TOCTOU 修复 | TC编号 | 测试场景 | 输入 | 预期结果 | 类型 | 优先级 | 覆盖说明 | | :--- | :--- | :--- | :--- | :--- | :--- | :--- | | TC-0766 | 删除有子部门的部门(FOR UPDATE 锁定读) | parentId 指向目标部门的子部门存在 | 400 "该部门下存在子部门,无法删除" | 业务约束 | P0 | H-4:`SELECT id ... FOR UPDATE` 子部门存在性锁定读 | | TC-0767 | 删除有关联用户的部门(FOR UPDATE 锁定读) | deptId 指向目标部门的用户存在 | 400 "该部门下仍有关联用户,无法删除" | 业务约束 | P0 | H-4:`SELECT id ... FOR UPDATE` 用户存在性锁定读 | | TC-0768 | CreateDept 父部门 FOR SHARE 锁生效 | parentId > 0 | 事务内对父部门 `SELECT FOR SHARE`;父不存在则 404 | 安全 | P0 | H-4:`CreateDept` 防并发删除父部门 | ### H-5 + M-15 `ChangePassword` 限流 + 冻结检查 | TC编号 | 测试场景 | 输入 | 预期结果 | 类型 | 优先级 | 覆盖说明 | | :--- | :--- | :--- | :--- | :--- | :--- | :--- | | TC-0769 | ChangePassword 超过 TokenOpLimiter 配额 | 同一 userId 连续调用超限 | 429 "操作过于频繁,请稍后再试" | 安全/限流 | P0 | H-5:`TokenOpLimiter.Take("chpwd:%d")` | | TC-0770 | 冻结用户调用 ChangePassword | user.Status=Disabled | 403 "账号已被冻结" | 安全 | P0 | M-15:bcrypt 前检查 `user.Status` | | TC-0771 | 原密码错误时记录日志 | 错误密码 | 400 "原密码错误" + 日志含 `change-password old-password mismatch` | 可观测 | P1 | H-5:失败日志可审计 | ### M-1 `UpdateUserStatus` 无变化不递增 tokenVersion | TC编号 | 测试场景 | 输入 | 预期结果 | 类型 | 优先级 | 覆盖说明 | | :--- | :--- | :--- | :--- | :--- | :--- | :--- | | TC-0772 | 状态无实际变化(已启用→再启用) | user.Status=1, req.Status=1 | 返回成功,tokenVersion 不变,用户不被踢下线 | 功能 | P0 | M-1:`user.Status == req.Status` 时跳过写操作 | | TC-0773 | 状态实际变化(启用→冻结) | user.Status=1, req.Status=2 | 成功,tokenVersion+1,用户被踢下线 | 正常路径 | P0 | M-1:真实变更时正常递增 | ### M-2 `generateRandomHex` 熵修复 | TC编号 | 测试场景 | 输入 | 预期结果 | 类型 | 优先级 | 覆盖说明 | | :--- | :--- | :--- | :--- | :--- | :--- | :--- | | TC-0774 | appKey 长度=32 hex字符 (16字节) | CreateProduct | appKey 长度 = 32 | 功能 | P0 | M-2:`generateRandomHex(16)` → 32 hex chars | | TC-0775 | appSecret 长度=64 hex字符 (32字节) | CreateProduct | appSecret 长度 = 64 | 功能 | P0 | M-2:`generateRandomHex(32)` → 64 hex chars | | TC-0776 | 初始管理员密码长度=24 hex字符 (12字节) | CreateProduct | adminPassword 长度 = 24 | 功能 | P0 | M-2:`generateRandomHex(12)` → 24 hex chars,96 bit 熵 | ### M-5 `UpdateRole`/`BindRolePerms` 错误处理 | TC编号 | 测试场景 | 输入 | 预期结果 | 类型 | 优先级 | 覆盖说明 | | :--- | :--- | :--- | :--- | :--- | :--- | :--- | | TC-0777 | FindUserIdsByRoleId 失败时返回 500 | mock DB 错误 | 500 "角色已更新但缓存刷新失败" | 容错 | P0 | M-5:不再忽略错误 | ### M-6 乐观锁 — UpdateRole / UpdateProduct / UpdateMember | TC编号 | 测试场景 | 输入 | 预期结果 | 类型 | 优先级 | 覆盖说明 | | :--- | :--- | :--- | :--- | :--- | :--- | :--- | | TC-0778 | UpdateRole 乐观锁冲突 | 并发修改同一角色 | 409 "数据已被其他操作修改,请刷新后重试" | 并发安全 | P0 | M-6:`UpdateWithOptLock` WHERE updateTime=? | | TC-0779 | UpdateProduct 乐观锁冲突 | 并发修改同一产品 | 409 同上 | 并发安全 | P0 | M-6:`UpdateWithOptLock` WHERE updateTime=? | | TC-0780 | UpdateMember 基于事务内 locked 数据更新 | 正常更新 | 成功,使用 `locked` 行数据组装 UPDATE | 数据一致 | P0 | M-6:事务内 `FindOneForUpdateTx` 结果作为更新基础 | ### M-7 `AdminLogin` 防用户枚举 | TC编号 | 测试场景 | 输入 | 预期结果 | 类型 | 优先级 | 覆盖说明 | | :--- | :--- | :--- | :--- | :--- | :--- | :--- | | TC-0016 | 普通用户(非超管)登录管理后台 | 正确密码 | code=401, "用户名或密码错误" | 安全 | P0 | M-7:统一错误消息,不暴露账号状态 | | TC-0021 | 冻结用户登录管理后台 | 正确密码 | code=401, "用户名或密码错误" | 安全 | P0 | M-7:冻结状态不暴露 | | TC-0781 | 不存在用户登录管理后台响应时间恒定 | 不存在的用户名 | code=401, "用户名或密码错误",执行 dummy bcrypt | 安全 | P0 | M-7:dummy bcrypt 恒时对齐 | ### M-10 `VerifyToken` 失败日志 | TC编号 | 测试场景 | 输入 | 预期结果 | 类型 | 优先级 | 覆盖说明 | | :--- | :--- | :--- | :--- | :--- | :--- | :--- | | TC-0782 | VerifyToken 各失败分支记录日志 | invalid token / disabled user / version mismatch 等 | 日志含 `verifyToken fail reason=` | 可观测 | P1 | M-10:每个失败分支有 logx.Infof | ### M-11 gRPC Login 缺 peer 时不跳过限流 | TC编号 | 测试场景 | 输入 | 预期结果 | 类型 | 优先级 | 覆盖说明 | | :--- | :--- | :--- | :--- | :--- | :--- | :--- | | TC-0783 | peer.FromContext 失败时仍限流 | 无 peer 的 ctx | 限流 key 使用 `"unknown"`,超限仍返回 ResourceExhausted | 安全 | P0 | M-11:fail-closed 不跳过限流 | ### M-14 `IsDuplicateEntryErr` 类型断言 | TC编号 | 测试场景 | 输入 | 预期结果 | 类型 | 优先级 | 覆盖说明 | | :--- | :--- | :--- | :--- | :--- | :--- | :--- | | TC-0784 | MySQL 1062 错误正确识别 | `mysql.MySQLError{Number: 1062}` | `IsDuplicateEntryErr` 返回 true | 功能 | P0 | M-14:类型断言替代字符串匹配 | | TC-0785 | 非 1062 错误不误判 | 其他 MySQL 错误或普通 error | 返回 false | 功能 | P0 | M-14:只匹配 1062 | ### L-1 去重不修改 req 入参 | TC编号 | 测试场景 | 输入 | 预期结果 | 类型 | 优先级 | 覆盖说明 | | :--- | :--- | :--- | :--- | :--- | :--- | :--- | | TC-0786 | SetUserPerms 调用后 req.Perms 不变 | 带重复 permId 的请求 | 调用后 req.Perms 长度与调用前一致 | 代码质量 | P1 | L-1:使用局部变量去重 | | TC-0787 | BindRoles 调用后 req.RoleIds 不变 | 带重复 roleId 的请求 | 调用后 req.RoleIds 长度与调用前一致 | 代码质量 | P1 | L-1:使用局部变量去重 | | TC-0788 | BindRolePerms 调用后 req.PermIds 不变 | 带重复 permId 的请求 | 调用后 req.PermIds 长度与调用前一致 | 代码质量 | P1 | L-1:使用局部变量去重 | ### L-2 空 memberType 显式拒绝 | TC编号 | 测试场景 | 输入 | 预期结果 | 类型 | 优先级 | 覆盖说明 | | :--- | :--- | :--- | :--- | :--- | :--- | :--- | | TC-0789 | caller.MemberType="" 调用 CheckMemberTypeAssignment | 空 memberType 的 caller | 403 "缺少产品成员上下文" | 安全 | P0 | L-2:显式分支替代 sentinel 值 | ## 十三、 本轮新增对抗性用例(QA 主动补齐 · 第三批) > 响应"后续测试建议不应由用户手写"的要求, QA 自主完成以下 7 类共 18 个测试: > > * **JWT 鉴权优先级完整矩阵**(L-B 延伸, 覆盖 UserDeleted / Frozen / NonMember / SuperAdmin bypass 四维顺序) > * **UpdateDept 真实并发乐观锁**(M-5 延伸, 用 goroutine 直打 MySQL, 断言恰好 1 胜 9 冲突) > * **TokenOpLimiter 滚动窗口恢复 + Redis fail-open**(L-C 延伸, 冻结时间窗语义与宕机容错契约) > * **Loader singleflight 合并并发 + 缓存命中**(L-5 延伸, 用计数包装拦截 `FindOne`) > * **gRPC VerifyToken / GetUserPerms 契约层 fuzz**(新增, 覆盖 13+6 个攻击性 payload, 断言 never-panic + 错误码 taxonomy 稳定) > * **handler 薄层 HTTP 契约**(新增, logout / refreshToken / changePassword, 冻结参数解析 + 透传协议) ### JWT 鉴权优先级完整矩阵(`jwtauth_checkorder_audit_test.go`) | TC编号 | 测试场景 | 输入 | 预期结果 | 类型 | 优先级 | 覆盖说明 | | :--- | :--- | :--- | :--- | :--- | :--- | :--- | | TC-0754 | 用户已被删除 + TokenVersion 失配 | Username="" + tokenVersion mismatch | 401 "用户不存在或已被删除" | 安全/顺序 | P0 | L-B 矩阵:Username empty 必须先于 TokenVersion 裁决, 否则软删除语义泄漏成"登录已失效" | | TC-0755 | 账号冻结 + TokenVersion 失配 + 产品禁用 | 三重 failing 同时命中 | 403 "账号已被冻结"(而非 401/ProductDisabled) | 安全/顺序 | P0 | L-B 矩阵:账号级 > 会话级 > 产品级 的优先级契约 | | TC-0756 | TokenVersion OK + 产品启用 + 非超管 + MemberType="" | 曾是成员后被移除 | 403 "您已不是该产品的有效成员" | 安全 | P0 | L-B 矩阵:成员移除后 old token 必须被识别 | | TC-0757 | SuperAdmin + ProductCode + MemberType="" | 超管 claim 携带 productCode | 200 放行到下游 handler | 正常路径 | P0 | L-B 矩阵:超管 bypass 成员校验不可被移除 | | TC-0758 | Frozen 用户 + TokenVersion 失配(无 ProductCode) | 冻结账号 + stale token | 403 "账号已被冻结" | 安全 | P0 | L-B 矩阵:不走产品分支时 Status 仍先于 TokenVersion | ### UpdateDept 真实并发乐观锁(`internal/model/dept/updateWithOptLock_concurrent_audit_test.go`) | TC编号 | 测试场景 | 输入 | 预期结果 | 类型 | 优先级 | 覆盖说明 | | :--- | :--- | :--- | :--- | :--- | :--- | :--- | | TC-0759 | 10 goroutine 同时 UpdateWithOptLock 同一行 | 共享 expectedUpdateTime=t0 | 恰好 1 成功 + 9 `ErrUpdateConflict`; DB 里 UpdateTime 被推进, Remark 非初值 | 并发/竞态 | P0 | M-5:`WHERE updateTime=?` + RowsAffected 判定, 挡"无声覆盖"退化 | ### TokenOpLimiter 滚动窗口恢复 + Redis fail-open(`internal/logic/auth/logoutRateLimit_audit_test.go`) | TC编号 | 测试场景 | 输入 | 预期结果 | 类型 | 优先级 | 覆盖说明 | | :--- | :--- | :--- | :--- | :--- | :--- | :--- | | TC-0790 | period=1s quota=1, 打满后 sleep 1.2s 再调 | 同一 userId 连续 logout | 第 1 次 200; 第 2 次 429; sleep 后第 3 次 200 且 tokenVersion=2 | 安全/限流 | P0 | L-C:限流必须是滚动窗口, 不能退化成永久 deny | | TC-0791 | Redis 不可达(`127.0.0.1:1` + NonBlock) | limit.Take 返回 err | logout 仍成功(fail-OPEN), tokenVersion=1 | 容错契约 | P0 | L-C:`code, _ :=` 的工程取舍被冻结, 未来改 fail-close 需 code review | ### Loader singleflight 合并并发(`internal/loaders/userDetailsLoader_singleflight_audit_test.go`) | TC编号 | 测试场景 | 输入 | 预期结果 | 类型 | 优先级 | 覆盖说明 | | :--- | :--- | :--- | :--- | :--- | :--- | :--- | | TC-0792 | 50 goroutine barrier 同时 Load 同 userId | 缓存已清空 | 每个 goroutine 都拿到完整数据; `FindOne` 调用次数 ≤ workers/5, >0 | 并发/缓存 | P0 | L-5:`singleflight.Group.Do` 合并冷启动击穿 | | TC-0793 | 首次 Load 后再 20 次 Load | 缓存已预热 | 首次 DB 命中 1 次, 后续 0 次(全部走 Redis cache) | 缓存命中 | P0 | L-5:写 Redis 成功后应走 fast-path 不再打 DB | ### gRPC 契约层 Fuzz(`internal/server/permserver_fuzz_audit_test.go`) | TC编号 | 测试场景 | 输入 | 预期结果 | 类型 | 优先级 | 覆盖说明 | | :--- | :--- | :--- | :--- | :--- | :--- | :--- | | TC-0794 | VerifyToken 对任意畸形 AccessToken | 13 条种子(空串/alg=none/长串/Unicode/控制符)+ 运行期可扩 `-fuzz=...` | never-panic, `(resp,nil)`, `resp.Valid=false` | 协议健壮性 | P0 | gRPC 契约:畸形令牌不得使服务端返回 `err != nil` 或崩溃 | | TC-0795 | GetUserPerms 任意 (appKey,appSecret,productCode,userId) | 6 条种子(空/不存在/SQL 注入样本/Unicode) | 错误码只能在 Unauthenticated/PermissionDenied/InvalidArgument/NotFound/Internal 中 | 协议健壮性 | P0 | 错误码 taxonomy 冻结, 产品侧权限网关依赖此集合 | ### Handler 薄层契约(`internal/handler/auth/*`、`internal/handler/pub/*`) | TC编号 | 测试场景 | 输入 | 预期结果 | 类型 | 优先级 | 覆盖说明 | | :--- | :--- | :--- | :--- | :--- | :--- | :--- | | TC-0796 | LogoutHandler 无 userDetails ctx | 空 ctx | 401 "未登录" | 契约 | P0 | handler 正确透传 logic 错误, 不吞成 200/5xx | | TC-0797 | LogoutHandler 携带合法 ctx | 注入 UserDetails | 200 + DB 中 tokenVersion=1 | 契约 | P0 | handler 必须真正触达 logic, 不能 stub 式伪装成功 | | TC-0798 | ChangePasswordHandler 非法 JSON body | `{not-json` | 400, 文案不含业务词"原密码" | 契约 | P0 | httpx.Parse 错误 → 400 而非 500; 不泄露业务语义 | | TC-0799 | ChangePasswordHandler 缺必填字段 | `{}` | 400, 文案点名 `oldPassword`/`newPassword` | 契约 | P0 | goctl required/optional 标注防退化 | | TC-0800 | RefreshTokenHandler 缺 Authorization | 无 header | 401 或 400, 文案不含 `sql`/`redis` | 契约 | P0 | handler 错误文案不得泄露实现细节 | | TC-0801 | RefreshTokenHandler 非法 bearer | `Bearer garbage.token.value` | 401(绝不 500/200/panic) | 契约 | P0 | refresh token 畸形时等价于未登录 | ## 十四、 本轮新增对抗性用例(QA 主动补齐 · 第四批 / 审计报告修复回归) > 本批与最新的 `audit-report.md` 逐项对齐。每条修复的高/中/低风险点都挂一条或一组独立 TC, > 断言 **修复后的预期行为**(不是源码当前的观测),一旦未来有人把修复改回旧路径,本批用例立刻红。 > > 覆盖域: > > * **H-1** `IncrementTokenVersionIfMatch` 原子 CAS + logic 层并发刷新胜负 + 缓存一致性 > * **H-2 / M-7** gRPC Refresh / Verify IP 级限流 + `extractClientIP` 端口剥离契约 > * **H-3** `BindRoles` 等级 `>=` 护栏(等级平行时亦拒绝赋权) > * **H-4** `UpdateUser` 将 `deptId` 设为 0 需 ADMIN / 超管权限 > * **L-1** `CreateUser` 未显式指定时 `mustChangePassword` 默认为 1(强制首次改密) > * **L-4** `CheckManageAccess` / `checkPermLevel` 面对 DB 瞬时错误必须 fail-close → 500,而非 "没有角色 → 403" > * **M-3** `UserDetailsLoader` 负缓存 sentinel,保证大量携带"已删除用户 token"的请求不再反复击穿 DB > * **M-5** `CreateProduct` 并发冲突(1062 唯一键)必须映射为 409,而非 500 + 脆弱字符串匹配 > * **M-6** `SyncPerms` 事务内锁产品 + 事务内读 perm map + 入参去重 + 1062 → 409 > * **M-B** HTTP 路由 `/api/auth/refreshToken` 必须挂载 `RefreshTokenRateLimit`(路由静态 wiring + 行为双重验证) ### H-1 CAS 会话劫持防线(`internal/model/user/incrementTokenVersionIfMatch_audit_test.go` + `internal/logic/pub/refreshTokenCas_audit_test.go`) | TC编号 | 测试场景 | 输入 | 预期结果 | 类型 | 优先级 | 覆盖说明 | | :--- | :--- | :--- | :--- | :--- | :--- | :--- | | TC-0802 | `expected == DB.tokenVersion` | expected=5, DB=5 | 返回 6;DB 落盘 6 | 正常路径 | P0 | CAS 成功分支 | | TC-0803 | `expected != DB.tokenVersion` | expected=9, DB=10 | `ErrTokenVersionMismatch`;DB 零副作用 | 安全/并发 | P0 | 会话劫持窗口拦截 | | ~~TC-0804~~ | ~~用户不存在~~ | — | — | — | — | **已删除**:M-8 取消模型内 FindOne 预检,"CAS 未命中 = ErrTokenVersionMismatch"为最新契约;用户状态分支改由上游 `UserDetailsLoader.Load` 的 status 字段分流 | | TC-0805 | 8 goroutine 同时 CAS 同 expected | N=8 | 恰好 1 成功 + 7 `ErrTokenVersionMismatch`;DB `tokenVersion` 只递增 1 | 并发/竞态 | P0 | 原子性外部可观察证据 | | TC-0806 | 成功后 id-key / username-key 缓存一致性 | CAS→1 | 再读两路都看到 1(非 stale 0) | 缓存 | P0 | 防 middleware 读 stale tokenVersion 放行旧 token | | TC-0812 | logic 层 6 goroutine 并发 RefreshToken 同一旧 rt | N=6 | 1 成功 + 5 × 401 "登录状态已失效";DB 递增 1 | 并发/协议 | P0 | H-1 纵深,覆盖 logic 层分支到 CodeError | ### H-2 + M-7 gRPC 限流 + client IP 剥端口(`internal/server/grpc_rate_limit_audit_test.go`) | TC编号 | 测试场景 | 输入 | 预期结果 | 类型 | 优先级 | 覆盖说明 | | :--- | :--- | :--- | :--- | :--- | :--- | :--- | | TC-0828 | 同 IP 两次 gRPC RefreshToken,quota=1 | peer `10.1.2.3:11111` → `10.1.2.3:22222` | 第 1 次 Unauthenticated(业务放行);第 2 次 ResourceExhausted + "过于频繁" | 安全/限流 | P0 | H-2:端口变化不得绕过限流 | | TC-0829 | 同 IP 两次 gRPC VerifyToken,quota=1 | peer `10.9.8.7:30001` → `10.9.8.7:30002` | 第 1 次 `Valid=false` + nil err;第 2 次 ResourceExhausted | 安全/限流 | P0 | VerifyToken 作为 token oracle 必须受限流保护 | | TC-0830 | `extractClientIP` 对 "host:port" 剥离 | `192.168.0.1:54321` | 返回 `192.168.0.1`;无 peer 时 error | 契约 | P0 | M-7:剥端口契约不得回退 | | TC-0831 | gRPC refresh 成功后重放旧 rt(换端口) | quota 宽松 | 第 2 次 Unauthenticated + "登录状态已失效" | 安全/并发 | P0 | H-1 + M-7 纵深交叉 | ### H-3 BindRoles 等级 `>=` 护栏(`internal/logic/user/bindRolesEqualLevel_audit_test.go`) | TC编号 | 测试场景 | 输入 | 预期结果 | 类型 | 优先级 | 覆盖说明 | | :--- | :--- | :--- | :--- | :--- | :--- | :--- | | TC-0813 | MEMBER 调用者给他人赋予 "与自己 permsLevel 相同" 的角色 | caller level=50, role level=50 | 403(等级不允许);DB 的 sys_user_role 关系无变化 | 安全/越权 | P0 | `GuardRoleLevelAssignable` 的 `>=` 防自等升权 | ### H-4 UpdateUser deptId=0 门禁(`internal/logic/user/updateUserDeptZero_audit_test.go`) | TC编号 | 测试场景 | 输入 | 预期结果 | 类型 | 优先级 | 覆盖说明 | | :--- | :--- | :--- | :--- | :--- | :--- | :--- | | TC-0814 | DEVELOPER 将他人 deptId 置 0 | caller=DEVELOPER | 403;目标 deptId 不变 | 安全/越权 | P0 | 防"把用户挪出部门树以逃出管理视野" | | TC-0815 | MEMBER 将他人 deptId 置 0 | caller=MEMBER | 403;目标 deptId 不变 | 安全/越权 | P0 | 同上 | | TC-0816 | 产品 ADMIN 将他人 deptId 置 0 | caller=ADMIN | 200;目标 deptId=0 | 正常路径 | P1 | 合法操作不被误伤 | | TC-0817 | SuperAdmin 将他人 deptId 置 0 | caller=SuperAdmin | 200;目标 deptId=0 | 正常路径 | P1 | 顶级权限链路通畅 | ### L-1 CreateUser 默认强制首次改密(`internal/logic/user/createUserMustChangePwd_audit_test.go`) | TC编号 | 测试场景 | 输入 | 预期结果 | 类型 | 优先级 | 覆盖说明 | | :--- | :--- | :--- | :--- | :--- | :--- | :--- | | TC-0818 | SuperAdmin 创建用户未显式指定 `mustChangePassword` | req 缺字段 | DB 落盘 `MustChangePassword=1` | 默认值/安全 | P1 | 默认 Yes 才能保证账号发出后立刻被改密 | ### L-4 checkPermLevel fail-close(`internal/logic/auth/checkPermLevelFailClose_audit_test.go`) | TC编号 | 测试场景 | 输入 | 预期结果 | 类型 | 优先级 | 覆盖说明 | | :--- | :--- | :--- | :--- | :--- | :--- | :--- | | TC-0819 | DB 层瞬时错误(mock `errors.New("boom")`) | mock 抛通用 err | `CodeError.Code == 500`,不是 403 | 鲁棒性/安全 | P0 | 禁止把 DB 故障曲解为"没角色 → 403" | | TC-0820 | DB 返回 `sqlx.ErrNotFound` | mock `sqlx.ErrNotFound` | 403(保留原业务语义) | 分支区分 | P0 | 只有真正"无角色"才 403,保证可审计 | ### M-3 UserDetailsLoader 负缓存(`internal/loaders/userDetailsLoader_negativeCache_audit_test.go`) | TC编号 | 测试场景 | 输入 | 预期结果 | 类型 | 优先级 | 覆盖说明 | | :--- | :--- | :--- | :--- | :--- | :--- | :--- | | TC-0821 | 同一"不存在 userId" 第 2 次 Load | 首次写入 sentinel 后再 Load | 第 2 次 0 次 DB 调用;sentinel 持有 `negativeCacheTTL` | 防 DoS/缓存 | P0 | 携带已删除用户 token 的请求不再反复击穿 DB | | TC-0822 | 负缓存不登记到 `userIndex`/`productIndex` | 查不存在用户后观察集合 | sentinel 不进 Clean 索引,避免误伤 | 缓存一致性 | P0 | 防 Clean 误删合法 key 或污染统计 | | TC-0823 | 50 并发 Load 同一不存在 userId | singleflight + 负缓存协同 | 最终 Redis key = 负缓存 sentinel;无 panic | 并发/缓存 | P0 | 防并发惊群 + 负缓存收敛 | ### M-5 CreateProduct 并发唯一键冲突(`internal/logic/product/createProductConflict_audit_test.go`) | TC编号 | 测试场景 | 输入 | 预期结果 | 类型 | 优先级 | 覆盖说明 | | :--- | :--- | :--- | :--- | :--- | :--- | :--- | | TC-0827 | mock `InsertWithTx` 返回 mysql error 1062,message 不含 "uk_code" | `&mysql.MySQLError{Number:1062,Message:"generic"}` | 返回 `response.ErrConflict`(409) | 错误映射 | P0 | 去掉脆弱 `strings.Contains` 依赖,靠 `mysql.MySQLError.Number` 判定 | ### M-6 SyncPerms 事务一致性 + 409(`internal/logic/pub/syncPermsConflict_audit_test.go` + 基础设施 `internal/model/perm/findMapByProductCodeWithTx_audit_test.go` + `internal/model/product/lockByCodeTx_audit_test.go`) | TC编号 | 测试场景 | 输入 | 预期结果 | 类型 | 优先级 | 覆盖说明 | | :--- | :--- | :--- | :--- | :--- | :--- | :--- | | TC-0807 | `FindMapByProductCodeWithTx` 与非事务版返回等价 | Tx + 产品有启/禁用权限 | 两次调用数据一致;仅 `status=1` 被返回 | 一致性 | P0 | 基础设施:事务内读 perm map | | TC-0808 | 产品无权限时返回 | 空集 | 非 nil 空 map(防 upstream NPE) | 健壮性 | P1 | 空产品语义固定 | | TC-0809 | `LockByCodeTx` 对存在 code 返回完整行 | Tx 内 `SELECT ... FOR UPDATE` | 行数据完整 | 正常路径 | P0 | 基础设施:事务内锁产品行 | | TC-0810 | 对不存在 code | Tx | `sqlx.ErrNotFound` | 分支 | P0 | 让 logic 层分辨"产品不存在" vs "DB 错误" | | TC-0811 | 两个事务同时锁同一行 | 并发 `LockByCodeTx` | 后者被阻塞,前者 commit 后才继续 | 并发/锁 | P0 | 实证 FOR UPDATE 的行级锁语义 | | ~~TC-0824 / TC-0825~~ | ~~1062 映射成 409 / logic 映射 409→HTTP 409~~ | — | — | — | — | **已删除**:H-3 的 `LockByCodeTx` FOR UPDATE 已经串行化并发同步,1062 在新架构下不可达;tx 内任何未命名错误统一为 `SyncPermsError{500}`,409 重试契约随之取消 | | TC-0826 | 同一 perm code 在 req 中重复 | `perms = [A, A]` | 落盘仅 1 条(入参内部去重) | 防自伤 | P0 | 入参级去重,避免 tx 内自撞 UNIQUE | ### M-B HTTP /refreshToken 中间件挂载(`internal/handler/refreshTokenRouteWiring_audit_test.go`) | TC编号 | 测试场景 | 输入 | 预期结果 | 类型 | 优先级 | 覆盖说明 | | :--- | :--- | :--- | :--- | :--- | :--- | :--- | | TC-0832 | 静态 wiring 检查:routes.go 中 `/auth/refreshToken` 所在 `rest.WithMiddlewares(...)` 块必须包含 `serverCtx.RefreshTokenRateLimit` | 正则匹配 | 命中 | 架构/wiring | P0 | 防有人把中间件从路由剥离而忘了通知 QA | | TC-0833 | 行为验证:构造等价中间件链(quota=1),同 IP 连打 2 次;再换 IP 打 1 次 | RemoteAddr 三个样本 | 首次放行(业务层 401);同 IP 第 2 次 `Code=429` "过于频繁";不同 IP 不受影响 | 安全/限流 | P0 | 与 wiring 正交交叉验证,限流真实生效且按 IP 隔离 | ## 十五、 本轮新增对抗性用例(QA 主动补齐 · 第五批 / 审计报告第 6 轮修复回归) > 本批与 2026-04-19 第 6 轮 `audit-report.md` 逐项对齐。每条修复均挂一条或一组独立 TC, > 断言 **修复后的预期行为**(不是源码当前的观测)。任何人只要把修复改回旧路径,相应用例立刻 FAIL。 > > 覆盖域: > > * **H-1** `AdminLogin` 限流 key 使用 `admin::` 双维,换 IP 不得被同一用户名上一次的配额连带锁死 > * **H-2** `ValidateProductLogin` 无条件 bcrypt + 冻结/超管状态只在密码正确后才披露(消除账号存在性 & 冻结状态 oracle) > * **H-3** `SyncPermsService` 事务内 `LockByCodeTx` + `FindMapByProductCodeWithTx` 串行化同 product 并发同步 > * **M-1** `UpdateDeptLogic` 改用 `CleanByUserIds` 批量清缓存;`FindIdsByDeptId` 失败仅 Errorf 不再吞错且不返回 500 > * **M-2** `ProductList` / `ProductDetail` / `DeptTree` 三个接口对非超管实施行/资源级访问控制 > * **M-4** `BindRolePerms` / `UpdateRole` post-commit 缓存获取失败不再映射 500(degraded 成功) > * **M-5** `CheckManageAccess` 支持 `WithPrefetchedTarget` option,避免单次请求内重复 FindOne > * **M-6** `ExtractClientIP` 解析 `X-Forwarded-For` 首段 + `net.ParseIP` 合法性校验 + `behindProxy=false` 忽略请求头 > * **M-8** `IncrementTokenVersionIfMatch` 新签名 `(ctx, id, username, expected)`,调用方透传 username 省掉多一次 FindOne > * **L-5** `CountOtherActiveAdminsTx` 新方法:排除目标自己后计数,正/反向用例均匹配 > * **L-4 (复盘第 5 轮)** `checkPermLevel` 对 DB 瞬时错误 fail-close 500(已验证) > * **新增负面契约** H-1/H-2 回归:frozen + wrong password 不得返回 403、远端换 IP 不得被上一次 per-username 计数锁死 ### H-1 AdminLogin 限流按 IP+username 双维(`internal/logic/pub/adminLoginIpLimit_audit_test.go`) | TC编号 | 测试场景 | 输入 | 预期结果 | 类型 | 优先级 | 覆盖说明 | | :--- | :--- | :--- | :--- | :--- | :--- | :--- | | TC-0834 | 相同 IP + 相同 username 连续打满 quota | clientIP=`1.2.3.4`, username=superA,连打 >quota 次错误密码 | 超限后返回 429 "登录尝试过于频繁,请5分钟后再试",同 IP 下同 username 不再放行 | 安全/限流 | P0 | H-1:修复后 key=`admin::`;按 quota 阈值打入,需命中 429 | | TC-0835 | 同 username 但换远端 IP | clientIP=`1.2.3.4` 打满后,换 clientIP=`5.6.7.8` 继续 | 不同 IP 分别计数,换 IP 仍应触达 bcrypt → 进入下游业务断言(此处为密码错误 401),而非继承上一桶 429 | 安全/限流 | P0 | H-1:确认 key 含 IP 维度,远端"任何 IP"都能永久锁死的攻击路径被阻断 | | TC-0836 | clientIP 缺失(未挂 RateLimit 中间件) | clientIP 未注入 ctx | key 退化为 `admin:unknown:`,仍能正常限流到共享桶,不得直接 panic 或跳过限流 | 安全/鲁棒 | P0 | H-1:fail-closed 兜底;未来删除中间件只会退化 key 不会绕过 | | TC-0837 | managementKey 无效路径不得消耗 username 配额 | 任意密码,managementKey 错误 | 401 "managementKey无效";同 username 立刻换 managementKey 正确再来仍有完整 quota | 安全 | P1 | H-1:managementKey 校验在限流 `Take` 之前,防匿名攻击者只靠错 key 就把配额打满 | ### H-2 ValidateProductLogin 恒时 bcrypt & 延迟披露状态(`internal/logic/pub/loginServiceConstantTime_audit_test.go`) | TC编号 | 测试场景 | 输入 | 预期结果 | 类型 | 优先级 | 覆盖说明 | | :--- | :--- | :--- | :--- | :--- | :--- | :--- | | TC-0838 | 用户冻结 + **错误密码** | status=2 的用户,密码错误 | 401 "用户名或密码错误"(**不能**是 403 "账号已被冻结") | 安全/侧信道 | P0 | H-2:账号存在性 + 冻结状态必须在密码正确之前完全不可观察 | | TC-0839 | 用户冻结 + **正确密码** | status=2 的用户,密码正确 | 403 "账号已被冻结"(奖励性披露) | 正常 | P0 | H-2:只有拿到密码后才披露状态,仍保留业务可见性 | | TC-0840 | 超管走产品端登录 + 错误密码 | IsSuperAdmin=1,密码错误 | 401 "用户名或密码错误"(不得提前暴露"超管"身份) | 安全/侧信道 | P0 | H-2:超管状态同样延迟披露 | | TC-0841 | 超管走产品端登录 + 正确密码 | IsSuperAdmin=1,密码正确 | 403 "超级管理员不允许通过产品端登录,请使用管理后台" | 正常 | P0 | H-2:披露顺序反转后的正面路径仍保持原有业务语义 | | TC-0842 | 用户名不存在 | 不存在用户名 + 任意密码 | 401 "用户名或密码错误",走 dummy bcrypt 恒时对齐 | 安全/枚举 | P0 | H-2:沿用 dummy hash 路径,不被 H-2 新顺序破坏 | ### H-3 SyncPermsService 事务内锁产品 & 事务内读 perm map(`internal/logic/pub/syncPermsTxLock_audit_test.go`) | TC编号 | 测试场景 | 输入 | 预期结果 | 类型 | 优先级 | 覆盖说明 | | :--- | :--- | :--- | :--- | :--- | :--- | :--- | | TC-0843 | mock 断言事务内必调用 `LockByCodeTx` | 1 次正常 sync | `LockByCodeTx` 在 tx 内被调用过且先于 `FindMapByProductCodeWithTx` | 架构 | P0 | H-3:锁必须落在 tx 内,顺序固定 | | TC-0844 | mock `LockByCodeTx` 返回 `sqlx.ErrNotFound` | 在 tx 内返回 NotFound | `SyncPermsError.Code == 404`,文案 "产品不存在" | 分支 | P0 | H-3:tx 内识别产品被删 → 404 | | TC-0845 | mock `LockByCodeTx` 返回通用 error | boom | SyncPermsError/500 包裹;不泄露原始 driver 错误 | 容错 | P1 | H-3:非 NotFound 错误必须回滚为 500 | ### M-1 UpdateDept 批量 Clean + 错误不再吞掉(`internal/logic/dept/updateDeptCleanBatch_audit_test.go` + `internal/loaders/userDetailsLoaderCleanByUserIds_audit_test.go`) | TC编号 | 测试场景 | 输入 | 预期结果 | 类型 | 优先级 | 覆盖说明 | | :--- | :--- | :--- | :--- | :--- | :--- | :--- | | TC-0846 | `CleanByUserIds` 批量清理多用户缓存 | 预埋 3 用户各 2 产品缓存 | Redis 中 6 条 `ud:userId:productCode` + 3 条 `ud:idx:u:*` 均被删除 | 正确性 | P0 | M-1 基础设施:SUNION + 批 DEL 必须覆盖所有索引 | | TC-0847 | `CleanByUserIds` 空 ids 切片 | `[]` | 立即返回,不 panic,不调用 Redis | 边界 | P1 | M-1:防未来调用方传空列表打空 RTT | | TC-0848 | `UpdateDept` 改 deptType 时调 CleanByUserIds | mock: FindIdsByDeptId → [100,101],断言 CleanByUserIds 路径(通过 mock 的 FindIdsByDeptId 期望 +真实 loader 执行) | 无错误返回;FindIdsByDeptId 被调用恰好 1 次 | 行为 | P0 | M-1:UpdateDept 在变更时才触达用户列表 | | TC-0849 | `UpdateDept` 的 `FindIdsByDeptId` 失败 | mock 返回 err | 返回 `nil`(不是 500);旧权限缓存 TTL 兜底 | 容错 | P0 | M-1:修复后的 degraded 成功语义 | ### M-2 ProductList / ProductDetail / DeptTree 访问控制(`internal/logic/product/productListAccessControl_audit_test.go`、`internal/logic/product/productDetailAccessControl_audit_test.go`、`internal/logic/dept/deptTreeAccessControl_audit_test.go`) | TC编号 | 测试场景 | 输入 | 预期结果 | 类型 | 优先级 | 覆盖说明 | | :--- | :--- | :--- | :--- | :--- | :--- | :--- | | TC-0850 | MEMBER 调 ProductList | `caller.ProductCode=pA` | 仅返回 pA 一条(即使 DB 内有 pB、pC) | 安全/访问控制 | P0 | M-2:非超管只见自己产品 | | TC-0851 | MEMBER 调 ProductList 且 `ProductCode==""` | 游离 MEMBER | 返回空列表 `Total=0, List=[]` | 边界 | P0 | M-2:无 productCode 时降级为 0 条 | | TC-0852 | MEMBER 调 ProductDetail 查他产品 | 目标 id 属于 pB | 404 "产品不存在"(不暴露存在性) | 安全/枚举 | P0 | M-2:区分开"存在但无权"会被当 oracle | | TC-0853 | MEMBER 调 ProductDetail 查自己产品 | 目标 id 属于 pA | 200 OK,`AppKey` 字段为空(保持原 AppKey-hidden 语义) | 正常路径 | P0 | M-2:字段级脱敏不被取消 | | TC-0854 | 超管调 ProductDetail | 任意 id | 200 OK + AppKey 可见 | 正常路径 | P1 | M-2:超管路径不受访问控制影响 | | TC-0855 | MEMBER 调 DeptTree | `DeptPath="/1/2/"` | 返回树中 Path 前缀匹配的子树;父部门/兄弟部门不可见 | 安全 | P0 | M-2:按 DeptPath 剪枝 | | TC-0856 | MEMBER 调 DeptTree 且 DeptPath="" | 游离成员 | 返回空切片 `[]` | 边界 | P0 | M-2:无 DeptPath 降级空树 | | TC-0857 | ADMIN 调 DeptTree | 产品 ADMIN | 返回完整树(ADMIN fullAccess) | 正常路径 | P1 | M-2:ADMIN 保留组织视图 | ### M-4 BindRolePerms/UpdateRole 缓存获取失败降级(`internal/logic/role/postCommitCacheDegrade_audit_test.go`) | TC编号 | 测试场景 | 输入 | 预期结果 | 类型 | 优先级 | 覆盖说明 | | :--- | :--- | :--- | :--- | :--- | :--- | :--- | | TC-0858 | `BindRolePerms`:事务 OK,`FindUserIdsByRoleId` 返回 err | mock | 返回 nil(200)——不再映射 500 | 错误映射 | P0 | M-4:degraded 成功;客户端不应重试 | | TC-0859 | `UpdateRole`:事务 OK,`FindUserIdsByRoleId` 返回 err | mock | 返回 nil(200) | 错误映射 | P0 | M-4:同上 | ### M-5 CheckManageAccess Prefetched(`internal/logic/auth/checkManageAccessPrefetched_audit_test.go`) | TC编号 | 测试场景 | 输入 | 预期结果 | 类型 | 优先级 | 覆盖说明 | | :--- | :--- | :--- | :--- | :--- | :--- | :--- | | TC-0860 | 传入 prefetched target,不再查 FindOne | caller=MEMBER,prefetched.DeptId 合法 | `SysUserModel.FindOne` 次数 = 0;业务结果同无 option 版本 | 性能/契约 | P1 | M-5:避免重复 FindOne | | TC-0861 | prefetched target.Id 与参数 targetUserId 不一致 | 被 defensive 忽略 | 依然触发一次 FindOne 真实查询(option 失效) | 安全 | P1 | M-5:prefetched 自洽校验,不让调用方传错 id 绕过访问控制 | ### M-6 ExtractClientIP XFF/合法性/behindProxy(`internal/middleware/ratelimitMiddlewareXff_audit_test.go`) | TC编号 | 测试场景 | 输入 | 预期结果 | 类型 | 优先级 | 覆盖说明 | | :--- | :--- | :--- | :--- | :--- | :--- | :--- | | TC-0862 | behindProxy=true + `X-Forwarded-For: 1.1.1.1, 2.2.2.2` | 首段合法 | 返回 `1.1.1.1` | 契约 | P0 | M-6:XFF 首段优先 | | TC-0863 | behindProxy=true + `X-Forwarded-For` 全非法 + `X-Real-IP: 10.0.0.1` | XFF=`garbage, abc`,XRI 合法 | fallthrough 到 `10.0.0.1` | 契约 | P0 | M-6:非法段跳过后走 XRI | | TC-0864 | behindProxy=true + 两头均空 | 无 XFF / 无 XRI | 回落到 `RemoteAddr` 剥端口后的 host | 容错 | P1 | M-6:降级路径 | | TC-0865 | behindProxy=true + `X-Forwarded-For: " 3.3.3.3 "` (空白) | 首段带空白 | 返回 `3.3.3.3`(trim 后合法) | 边界 | P1 | M-6:trim 后再解析 | | TC-0866 | behindProxy=false + XFF=`1.1.1.1` | RemoteAddr=`5.5.5.5:8080` | 忽略 XFF,返回 `5.5.5.5` | 安全/伪造 | P0 | M-6:不信任客户端注入的头部 | ### M-8 IncrementTokenVersionIfMatch 签名(已落地·契约冻结) | TC编号 | 测试场景 | 输入 | 预期结果 | 类型 | 优先级 | 覆盖说明 | | :--- | :--- | :--- | :--- | :--- | :--- | :--- | | TC-0867 | 透传 username 与 DB 一致 | CAS 正常 | 成功且 username-key 缓存也失效(沿用 TC-0806 验证) | 契约 | P0 | M-8:签名扩展不破坏既有 CAS 语义 | ### L-5 CountOtherActiveAdminsTx(`internal/model/productmember/countOtherActiveAdminsTx_audit_test.go`) | TC编号 | 测试场景 | 输入 | 预期结果 | 类型 | 优先级 | 覆盖说明 | | :--- | :--- | :--- | :--- | :--- | :--- | :--- | | TC-0868 | 产品内 3 个 active admin,排除其中 1 | excludeId=第二个 admin | 返回 2 | 计数 | P0 | L-5:排除目标后正确计数 | | TC-0869 | 唯一 active admin,排除他自己 | excludeId=唯一 admin | 返回 0 → 上层识别为"最后一个" | 语义 | P0 | L-5:removeMember/updateMember 据此防"降级/移除最后一个 admin" | | TC-0870 | 存在 1 个 active + 1 个 disabled admin | excludeId=active | 返回 0(disabled 不计入) | 语义 | P0 | L-5:仍需状态=enabled 才算 | --- ### 4.14 第 6 轮过时用例按新契约重写(2026-04-19 · QA 清理补充) > 以下 TC 是在清理阶段按 **新契约** 重写,用于替代被删除的旧用例(见 §4.12、§4.13 各行"已删除"备注)。 | TC编号 | 测试场景 | 输入 | 预期结果 | 类型 | 优先级 | 覆盖说明 | | :--- | :--- | :--- | :--- | :--- | :--- | :--- | | TC-0871 | 超管调 ProductList 走分页路径 | ctx=SuperAdmin, page/pageSize=1/20 | 调用 `FindList(1,20)`,返回列表且每条 `AppKey` 原样可见 | 正常路径 | P0 | M-2:取代旧 TC-0079/0087,钉死"超管路径 != 非超管路径"分叉 | | TC-0872 | ProductDetail FindOne 失败 | DB 返回 err | 统一返回 `CodeError{404,"产品不存在"}`,文案与"他人产品 → 404"完全一致 | 异常路径 | P0 | M-2:取代旧 TC-0085,钉死"无差别 404"避免被用作存在性 oracle | --- ## 五、第 7 轮审计驱动测试(2026-04-19 · QA 新增) > 本轮围绕 `audit-report.md` 中 H-1 / H-3 / H-4 / M-1 / M-2 / M-3 / M-4 / L-3 / L-5 / L-6 的修复逐条建立回归锚点。 > 所有新用例均在"真实 DB + Redis + 原子语义"的一等场景下验证,严禁仅靠 mock 对实现做同义复写。 > 对既有用例的兼容性调整(两处 bindRoles 测试 + RefreshToken_UserDeleted)见 §5.9。 ### 5.1 M-4 FetchInitialCredentials 一次性凭据取回(`internal/logic/product/fetchInitialCredentialsLogic_audit_test.go`) | TC编号 | 测试场景 | 输入 | 预期结果 | 类型 | 优先级 | 覆盖说明 | | :--- | :--- | :--- | :--- | :--- | :--- | :--- | | TC-0901 | happy path:超管用有效 ticket 取回初始凭据 | superAdmin ctx + 合法 ticket | 返回 AppKey/AppSecret/AdminUser/AdminPassword;AppSecret 与 DB 中 bcrypt 匹配 | 正常路径 | P0 | M-4:凭据落地 Redis 后端,响应体不再明文外泄 | | TC-0902 | 相同 ticket 二次消费 | 同上 ticket 第二次取 | 第二次 404 "凭据票据不存在或已被消费" | 一次性 | P0 | M-4:`Redis.GetDelCtx` 原子 GET+DEL | | TC-0903 | ticket 为空 | ticket="" | 400 "ticket 不能为空" | 异常路径 | P0 | M-4:入参校验 | | TC-0904 | ticket 未知 | 随机 64 字符 ticket | 404 "凭据票据不存在或已被消费" | 异常路径 | P0 | M-4:无存在性差异化,避免枚举 oracle | | TC-0905 | 非超管调用 | ADMIN ctx | 403 | 权限 | P0 | M-4:RequireSuperAdmin 生效 | | TC-0906 | 未登录调用 | context.Background() | 401/403 | 权限 | P0 | M-4:无 UserDetails 时拒绝 | | TC-0907 | Redis 中 payload 被人为破坏 | 手工写入非 JSON 字符串 | 500 "凭据数据异常" 并删除该 key | 健壮性 | P1 | M-4:防腐败数据长留 | | TC-0908 | Redis key 结构正确 | 观察实际 key | `pm:initcred:{ticket}`;TTL ≤ 300s | 契约 | P1 | M-4:运维可定位 | | TC-0909 | TTL 与响应 expiresAt 一致 | 观察返回的 credentialsExpiresAt | Redis TTL == Unix(expiresAt)-now (±5s) | 契约 | P1 | M-4:客户端过期提示与后端一致 | | TC-0910 | 并发消费同一 ticket | 32 goroutine 同时 Fetch | 仅 1 个成功,其余 31 个返回 404 | 并发 | P0 | M-4:GetDelCtx 原子性抗竞态 | | TC-0911 | CreateProductResp JSON 不含 appSecret/adminPassword | marshal resp → json | 字段 `appSecret`/`adminPassword` 不出现 | 契约 | P0 | M-4:响应体永不明文 | | TC-0912 | CreateProductResp 必含 credentialsTicket + credentialsExpiresAt | marshal resp | 两字段均非空/正数 | 契约 | P0 | M-4:新的获取链路必备字段 | ### 5.2 M-1 / H-1 / L-3 / L-6 Loader 契约(`internal/loaders/userDetailsLoader_contract_audit_test.go`) | TC编号 | 测试场景 | 输入 | 预期结果 | 类型 | 优先级 | 覆盖说明 | | :--- | :--- | :--- | :--- | :--- | :--- | :--- | | TC-0913 | 用户不存在 → `(ud, nil)` 且 `ud.Username == ""` | userId=999999999 | 无 err,Username 空,caller 自行映射 401 | 契约 | P0 | M-1:401 vs 503 区分 | | TC-0914 | 新建用户刚落地 `CreateUser` 后立刻 `Load` | 新 username | 读到真实 user,不命中负缓存哨兵 | 契约 | P0 | L-6:CreateUser 反向清除负缓存 | | TC-0915 | partial load 失败(幽灵 deptId)| 人为把 user.deptId 改成不存在 | 返回 ud(含错误子加载),但不写 5 分钟正缓存 | 契约 | P0 | M-1/L-3:局部失败不污染主缓存 | | TC-0916 | 全绿 MEMBER 正路径 | 正常 user + product + 无角色 | `loadOk=true`,命中 5 分钟正缓存 | 契约 | P0 | H-1:保证好路径仍能缓存 | ### 5.3 M-2 UpdatePassword/UpdateStatus RowsAffected(`internal/model/user/updatePasswordStatus_rowsaffected_audit_test.go`) | TC编号 | 测试场景 | 输入 | 预期结果 | 类型 | 优先级 | 覆盖说明 | | :--- | :--- | :--- | :--- | :--- | :--- | :--- | | TC-0924 | UpdatePassword:FindOne 填缓存后行被并发删除 | 直接 SQL 删行绕过缓存失效 | `ErrUpdateConflict`,不得静默成功 | 并发/TOCTOU | P0 | M-2:RowsAffected=0 必须升格 | | TC-0925 | UpdatePassword:正常写入 | 存在且未并发 | 持久化 + tokenVersion 递增 | 正常路径 | P0 | M-2:hot path 不回归 | | TC-0926 | UpdatePassword:user 不存在 | id=非法 | `ErrNotFound`(FindOne 先挂) | 异常路径 | P0 | M-2:直接失败 | | TC-0927 | UpdateStatus:行被并发删除 | 同 TC-0924 手法 | `ErrUpdateConflict` | 并发/TOCTOU | P0 | M-2:对称覆盖 | | TC-0928 | UpdateStatus:正常禁用 | status=2 | 持久化 + tokenVersion 递增(用户被踢) | 正常路径 | P0 | M-2:禁用副作用 | | TC-0929 | UpdateStatus:user 不存在 | id=非法 | `ErrNotFound` | 异常路径 | P0 | M-2 | ### 5.4 M-3 GuardRoleLevelAssignable Fresh Read(`internal/logic/auth/guardRoleLevelAssignable_freshRead_audit_test.go`) | TC编号 | 测试场景 | 输入 | 预期结果 | 类型 | 优先级 | 覆盖说明 | | :--- | :--- | :--- | :--- | :--- | :--- | :--- | | TC-0930 | caller 的 MinPermsLevel 缓存过期(偏高) | mock 返回 DB=100,caller 缓存=5,role=50 | 403(以 DB=100 判,50 ≤ 100 越级) | TOCTOU | P0 | M-3:不得信任缓存高权值 | | TC-0931 | 同级分配 | DB=50,role=50 | 403 "不能分配权限级别高于自身的角色(含同级)" | 契约 | P0 | M-3:含同级 | | TC-0932 | 严格低一级分配 | DB=50,role=51 | 放行 | 正常路径 | P0 | M-3:正值域 | | TC-0933 | SuperAdmin 绕过 DB | superAdmin caller | 不查 DB,直接放行 | 正常路径 | P0 | M-3:短路 | | TC-0934 | Product ADMIN 绕过 DB | memberType=ADMIN | 不查 DB,直接放行 | 正常路径 | P0 | M-3:短路 | | TC-0935 | DEVELOPER 绕过 DB | memberType=DEVELOPER | 不查 DB,直接放行 | 正常路径 | P0 | M-3:短路 | | TC-0936 | caller 在 DB 没有任何角色 | FindMin... 返回 ErrNotFound | 403 "您没有可分配的角色等级" | 异常路径 | P0 | M-3:fail-close | | TC-0937 | caller 查询 DB 遇到一般错误 | 非 ErrNotFound | 500(fail-close,不透传原文) | 容错 | P0 | M-3:不泄细节 | | TC-0938 | caller 为 nil(无 UserDetails) | ctx 未带 | 401(未授权) | 异常路径 | P0 | M-3:nil 保护 | ### 5.5 H-3 CheckAddMemberAccess 部门链 + 超管防御(`internal/logic/auth/checkAddMemberAccess_audit_test.go` + `internal/logic/member/auditFixes_test.go`) | TC编号 | 测试场景 | 输入 | 预期结果 | 类型 | 优先级 | 覆盖说明 | | :--- | :--- | :--- | :--- | :--- | :--- | :--- | | TC-0940 | Product ADMIN 拉跨部门树的人 | caller path=/100/,target path=/200/201/ | 403,错误含 "其他部门" | 权限 | P0 | H-3:ADMIN 不再绕过部门树 | | TC-0941 | Product ADMIN 在自己部门树内 | target 在 /100/101/ | 放行 | 正常路径 | P0 | H-3:正值域 | | TC-0942 | SuperAdmin 跨一切部门 | caller superAdmin | 放行,不查 dept | 正常路径 | P0 | H-3:短路 | | TC-0943 | 自己加自己 | target.Id == caller.UserId | 放行 | 正常路径 | P0 | H-3:self-bypass | | TC-0944 | caller 没有部门(DeptId=0 或 DeptPath="") | 非超管 | 403 | 异常路径 | P0 | H-3:无部门 caller 必须拒绝 | | TC-0945 | target 无部门(DeptId=0) | 非自己 | 403 | 异常路径 | P0 | H-3:目标无部门视为不可纳管 | | TC-0946 | ctx 无 caller | context.Background() | 401 | 权限 | P0 | H-3 | | TC-0947 | dept.FindOne 报错 | mock 返回 err | 403(fail-close,文案不泄细节) | 容错 | P0 | H-3 | | TC-0948 | target 为 nil | pass nil | 400 BadRequest | 契约 | P0 | H-3 | | TC-0949 | AddMember 集成:跨部门被拒 | 真实 DB,Product ADMIN 拉树外 | 403 + 不写 sys_product_member | 集成 | P0 | H-3 端到端 | | TC-0950 | AddMember 集成:target=SuperAdmin | superAdmin 被作为 MEMBER 加入 | 403 "超级管理员" + 不写 sys_product_member | 安全 | P0 | H-3:超管防混入 | ### 5.6 H-4 JWT 签名算法断言(`internal/logic/auth/parseWithHMAC_audit_test.go`) | TC编号 | 测试场景 | 输入 | 预期结果 | 类型 | 优先级 | 覆盖说明 | | :--- | :--- | :--- | :--- | :--- | :--- | :--- | | TC-0951 | 合法 HS256 + 正确 secret | 正常 token | 解析成功,claims 可读 | 正常路径 | P0 | H-4:good path | | TC-0952 | alg=none 伪造 | 手工拼无签名段 | 拒绝,错误含 "unexpected signing method" | 安全 | P0 | H-4:alg=none 零信任 | | TC-0953 | alg=RS256 但用 HS secret 签名 | RSA→HMAC 混淆攻击 | 拒绝 | 安全 | P0 | H-4:算法混淆防御 | | TC-0954 | alg=ES256 header | 非 HMAC 族 | 拒绝 | 安全 | P0 | H-4:非 HMAC 一律拒 | | TC-0955 | HS256 但 secret 错 | 同算法错密钥 | 拒绝(签名校验失败) | 安全 | P0 | H-4:基础用例 | | TC-0956 | ParseRefreshToken 复用 ParseWithHMAC | 伪造 alg=none | 拒绝 | 安全 | P0 | H-4:上层也防 | | TC-0957 | 乱码 token | "abc.def" | 拒绝(malformed) | 容错 | P1 | H-4 | | TC-0958 | ParseRefreshToken 的 tokenType 校验 | AccessToken 类型 | 拒绝 TokenTypeMismatch | 契约 | P0 | H-4:不被 H-4 修复破坏 | | TC-0959 | 合法 HS256 + 非标 `typ` header | typ=JWT-X | 解析成功(只严格校验 alg) | 兼容 | P1 | H-4:不过度收紧 | | TC-0960 | 回放过期 token | exp 已过 | 拒绝(解析器底层校验) | 安全 | P0 | H-4 | ### 5.7 L-5 清理:删除僵尸方法 | 删除对象 | 受影响测试 | 处理 | | :--- | :--- | :--- | | `SysPermModel.FindMapByProductCode`(非事务) | `sysPermModel_test.go` | 改用 `FindMapByProductCodeWithTx`(在事务上下文中调用);`findMapByProductCodeWithTx_audit_test.go` 独立验证事务版契约 | | `SysProductMemberModel.FindMapByProductCodeUserIds` | `sysProductMemberModel_test.go` | 删除 3 个关联测试(TC-xxx 已标记为已删除) | | `SysProductMemberModel.CountActiveAdmins` | — | 无外部调用者,直接删除 | ### 5.8 M-1 RefreshToken 用户不存在 vs 账号冻结契约分离 | TC编号 | 测试场景 | 输入 | 预期结果 | 类型 | 优先级 | 覆盖说明 | | :--- | :--- | :--- | :--- | :--- | :--- | :--- | | TC-0029(重写) | refreshToken 合法但 userId 不存在 | nonExistentUserId | **401 "用户不存在或已被删除"** | 契约 | P0 | M-1:取代旧 "403 账号已被冻结",前端可正确走"清本地会话+回登录页" | ### 5.9 M-4 Handler 薄层 + 路由 Wiring(`internal/handler/product/fetchInitialCredentialsHandler_audit_test.go` + `internal/handler/fetchInitialCredentialsRouteWiring_audit_test.go`) > 填补 §10.4 中"handler/wiring 未测场景"空白:此前 TC-0901 ~ TC-0912 只在 logic 层覆盖 M-4,但 handler 的解析契约、鉴权中间件挂载、前缀归属都未被钉死。 > 本组 TC 双通道验证:一端走真实 `http.HandlerFunc` + Redis 发 HTTP 请求,一端 static-scan `routes.go` 源码断言 middleware 绑定,互为保险。 | TC编号 | 测试场景 | 输入 | 预期结果 | 类型 | 优先级 | 覆盖说明 | | :--- | :--- | :--- | :--- | :--- | :--- | :--- | | TC-0961 | body 非法 JSON | `{not-valid-json` + superAdmin ctx | 400,且文案不含 sql/redis/ticket 关键字 | 契约/健壮性 | P0 | M-4:`httpx.Parse` 错误透传;不泄字段与实现细节 | | TC-0962 | 无登录上下文 | 不注入 UserDetails | 401 "未登录" | 权限 | P0 | M-4:handler 自身也必须 fail-close,不依赖 JwtAuth 中间件 | | TC-0963 | 非超管 ctx | MemberType=ADMIN | 403 "仅超级管理员可执行此操作",文案不含 "ticket" | 权限/信息泄漏 | P0 | M-4:RequireSuperAdmin 透传;防 ticket 存在性 oracle | | TC-0964 | 超管 + 空 ticket | `{"ticket":""}` | 400,文案含 "ticket" | 契约 | P0 | M-4:入参必填校验 | | TC-0965 | 超管 + 未知 ticket | 随机字符串 | 400 "凭证票据无效或已过期",与"过期"共用文案 | 安全 | P0 | M-4:防枚举 oracle,与 logic TC-0904 同契约 | | TC-0966 | 超管 + 已落地 ticket | 手工 SETEX 合法 JSON payload | HTTP 200 + 4 字段完整映射;Redis key 被 `GetDel` 消费 | 正常路径/一致性 | P0 | M-4:字段映射正确 + 一次性消费 | | TC-0967 | 静态 wiring:JwtAuth 绑定 | 读取 `routes.go` 源码 | `/fetchInitialCredentials` 所在 `rest.WithMiddlewares` 列表含 `serverCtx.JwtAuth`;prefix=`/api/product` | 回归/静态 | P0 | M-4:防未来 goctl 覆写丢失中间件 | | TC-0968 | 静态反证:不得挂到限流组 | 读取 `routes.go` 源码 | `/fetchInitialCredentials` 绝不出现在 `AdminLoginRateLimit` / `ProductLoginRateLimit` / `RefreshTokenRateLimit` / `SyncRateLimit` 的中间件块内 | 回归/静态 | P0 | M-4:防被错迁到无鉴权组 | ### 5.10 既有用例兼容性调整(M-3 fresh DB 读) | 用例 | 文件 | 调整说明 | | :--- | :--- | :--- | | TC-0813 | `bindRolesEqualLevel_audit_test.go` | 新增 `seedCallerWithRoleLevel` 辅助,在 DB 落地真实 caller user + role + user_role;不再仅依赖 `UserDetails.MinPermsLevel` 直出 | | TC-0208 | `bindRolesLogic_test.go` | 同上,seed caller permsLevel=50 后触发"越级分配 permsLevel=1"断言 | > 上述调整的根因:M-3 修复后 `GuardRoleLevelAssignable` 强一致从 DB 读取 caller 的 MinPermsLevel,原测试用假 UserId 会命中 `ErrNotFound` → 403 "您没有可分配的角色等级",与真正要验的"含同级"/"越级"语义错位。 --- ## 六、第 8 轮审计驱动测试设计(2026-04-19) > 本轮对照 `audit-report.md` 第 8 版逐项建立回归锚点:H-2 + M-1 + M-2 + M-3 + L-1 + L-4 五项已落地修复的对抗性回归, > 以及 H-1 + L-3 两项**未落地**缺陷的"契约先行"骨架测试(默认 `t.Skip` 并带 `AUDIT_PENDING` 标签,fix 落地后直接打开 `AUDIT_RUN_PENDING=1` 即可强制跑红)。 > > 本轮既有测试兼容性调整(Round 7 → Round 8 代码变更触发):4 个已有 test 的 mock 需补 caller 侧 `FindMinPermsLevelByUserIdAndProductCode` 期望、1 个 productmember 测试移除对已删除 `CountActiveAdminsTx` 的引用,详见 §6.10。 ### 6.1 H-2 `checkPermLevel` fresh DB 读(`internal/logic/auth/checkPermLevelFreshRead_audit_test.go`) > 修复目标:`checkPermLevel` 不再信任 `caller.MinPermsLevel` 缓存,改走 `loadFreshMinPermsLevel` 强一致读 DB, > 与 `GuardRoleLevelAssignable`(Round 7 已修)口径对齐,关闭降级 admin 的 5min 缓存 TOCTOU 窗口。 | TC编号 | 测试场景 | 输入 | 预期结果 | 类型 | 优先级 | 覆盖说明 | | :--- | :--- | :--- | :--- | :--- | :--- | :--- | | TC-0969 | caller 缓存级别高(10)但 DB 已降级(20),target 级别 15 | 管理高级目标 | 403 "无权管理权限级别高于或等于您的用户" | 对抗/安全 | P0 | H-2:钉死缓存 TOCTOU | | TC-0970 | caller 在 DB 中无任何角色(`ErrNotFound`) | 非超管/非 self 管理路径 | 403(按"无角色 = 最低等级"处理) | 安全 | P0 | H-2:ErrNotFound 等同最低 | | TC-0971 | caller DB 级别(5)严格高于 target(15) | 正常管理 | 放行 | 正向 | P0 | H-2 正向不回归 | | TC-0972 | caller 侧 `FindMinPermsLevelByUserIdAndProductCode` 通用 DB 错 | — | 500 "校验权限级别失败"(fail-close) | 容错 | P0 | H-2:一般错误非 ErrNotFound → 500 | | TC-0973 | caller 是 SuperAdmin | CheckManageAccess 链路 | 短路放行,不发生 caller-side FindMin 查询 | 正向/优化 | P1 | H-2:SuperAdmin 必短路 | | TC-0974 | caller.UserId == targetUserId(自操作) | 同上 | 短路放行,不发生 caller-side FindMin 查询 | 正向/优化 | P1 | H-2:self 必短路 | | TC-0975 | 共享 helper `loadFreshMinPermsLevel` 的契约对齐 | 通用 err / ErrNotFound | 分别返回 `(0, false, err)` 与 `(0, true, nil)` | 契约 | P0 | H-2:helper 契约与 `GuardRoleLevelAssignable` 同步 | ### 6.2 M-1 `CreateProduct` Redis/ticket 失败补偿(`internal/logic/product/createProductCompensation_audit_test.go`) > 修复目标:DB 事务 commit 之后任何一步(RandomHex / Marshal / Redis SetexCtx)失败,必须回滚补偿掉 `sys_product` + `sys_user` + `sys_product_member` 三张新建行,防止"产品 + 管理员孤儿化 → 只能 DBA 手动 SQL 清"。 | TC编号 | 测试场景 | 输入 | 预期结果 | 类型 | 优先级 | 覆盖说明 | | :--- | :--- | :--- | :--- | :--- | :--- | :--- | | TC-0976 | Redis 整个不可达(`SetexCtx` 永久失败) | 正常 CreateProduct 请求 + broken Redis | 返回 5xx 错误;`sys_product`/`sys_user`/`sys_product_member` 行数各 0 | 对抗/一致性 | P0 | M-1:失败链路必须把三张新建行全部补偿掉 | | TC-0977 | Redis 失败 + 补偿成功后以同 Code 重建 | 正常 Redis + 相同 productCode | 第二次创建成功,不被 UNIQUE 约束阻塞 | 正向/幂等 | P0 | M-1:补偿把位点清空,同 Code 不卡住 | | TC-0978 | 补偿顺序显式校验(child → parent) | 观察三表最终行数 | `sys_product_member`、`sys_user`、`sys_product` 均为 0 | 契约 | P0 | M-1:删除顺序与外键契约一致 | ### 6.3 M-2 `SyncPerms` 404 映射(REST + gRPC) | 文件 | 覆盖通道 | TC 编号 | | :--- | :--- | :--- | | `internal/logic/pub/syncPerms404_audit_test.go` | REST(`SyncPermsLogic`) | TC-0979 / TC-0980 | | `internal/server/syncPermissions404_audit_test.go` | gRPC(`PermServer.SyncPermissions`) | TC-0981 / TC-0982 | | TC编号 | 测试场景 | 输入 | 预期结果 | 类型 | 优先级 | 覆盖说明 | | :--- | :--- | :--- | :--- | :--- | :--- | :--- | | TC-0979 | REST:tx 内 `LockByCodeTx → ErrNotFound` | AppKey/AppSecret 有效但产品被并发删 | 返回 `response.CodeError{Code:404, "产品不存在"}` | 契约 | P0 | M-2:REST 侧必映射 404 | | TC-0980 | REST 反例:未映射 `se.Code=500` | tx 内非业务 err | 继续走 default 分支原样透传 `SyncPermsError` | 契约/反向 | P1 | M-2:防一刀切把 500 误归 404 | | TC-0981 | gRPC:同 LockByCodeTx ErrNotFound | 同 TC-0979 输入 | `status.Code() == codes.NotFound`,文案 "产品不存在" | 契约 | P0 | M-2:gRPC 对外契约 | | TC-0982 | gRPC 反例:未映射 code | 同 TC-0980 | `codes.Internal`,不得被误分类为 NotFound | 契约/反向 | P1 | M-2:防 SDK 误触发重试 | ### 6.4 M-3 `RefreshToken` 先签后 CAS(`internal/logic/pub/refreshTokenSignBeforeCas_audit_test.go`) > 修复目标:调换 `签名 ↔ CAS ↔ Clean` 顺序,任何签名失败不得推进 `tokenVersion`;配合 M-3 顺序收敛"签 token 副作用"与"DB 状态变更"。 > > HMAC 本身不会失败,因此本组通过可观测的"预测版本 ↔ DB 新版本 ↔ 新 token.TokenVersion 三者必须严格相等"来把契约钉死。 | TC编号 | 测试场景 | 输入 | 预期结果 | 类型 | 优先级 | 覆盖说明 | | :--- | :--- | :--- | :--- | :--- | :--- | :--- | | TC-0983 | 成功路径:DB tokenVersion +1 且新 access/refresh claims.TokenVersion 严格等于 DB 新值 | 合法旧 refresh | 三者相等 | 正向/契约 | P0 | M-3:预签版本 ↔ CAS 版本一致 | | TC-0984 | CAS 不命中("其他并发赢家已把 DB 推到 version=1"后再来一次 claims=0) | 抢先 IncrementTokenVersionIfMatch | 返回 401 "登录状态已失效",DB tokenVersion 不得再 +1 | 对抗/一致性 | P0 | M-3:失败分支绝不推进 DB | | TC-0985 | 多轮链式刷新 + 旧 token 重放 | 连刷两次后重放第 1 次的新 refreshToken | 第三次 401,DB tokenVersion 不得再 +1 | 对抗 | P0 | M-3:新 token 版本号必须匹配 DB,重放必拦 | ### 6.5 L-1 `sysPermModel` 占位符契约(`internal/model/perm/l1StatusPlaceholder_audit_test.go`) > 修复目标:`FindAllCodesByProductCode` / `DisableNotInCodesWithTx` 的 `status = ?` 必须走 prepared statement 参数占位符,与 L-4 修复风格对齐;SQL 指纹稳定 + 未来新增枚举值(如 "审核中")不被 Sprintf 版误收。 | TC编号 | 测试场景 | 输入 | 预期结果 | 类型 | 优先级 | 覆盖说明 | | :--- | :--- | :--- | :--- | :--- | :--- | :--- | | TC-0986 | 产品含 status ∈ {1, 2, 99} 三类行 | FindAllCodesByProductCode | 只返回 status=1 的 code | 契约 | P1 | L-1:严格 `=` 不模糊 | | TC-0987 | DisableNotInCodesWithTx 只禁用 status=1 且不在白名单的行 | 同上数据 + 白名单含 status=1 的一条 | 仅 1 行被 1→2,99 和既有 2 均不动 | 契约 | P1 | L-1:WHERE 严格等值,未来枚举值不被误伤 | ### 6.6 L-4 `SetUserPerms` 事务末 COUNT 复核(`internal/logic/user/setUserPermsCountRecheck_audit_test.go`) > 修复目标:关闭 `FindByIds` 通过 → tx 内 BatchInsertWithTx 落盘之间的 TOCTOU 窗口,防止"脏 user_perm 行与 sys_perm 的 disabled 状态不一致"。 | TC编号 | 测试场景 | 输入 | 预期结果 | 类型 | 优先级 | 覆盖说明 | | :--- | :--- | :--- | :--- | :--- | :--- | :--- | | TC-0988 | FindByIds 前置校验通过(装饰器撒谎 status=1)但 DB 实际 status=2 | 正常请求 | 409 "部分权限在提交时已被禁用",`sys_user_perm` 必须 0 行脏数据 | 对抗/一致性 | P0 | L-4:COUNT 复核失效 → 立即可见 | | TC-0989 | 全部真实 Enabled 的正向基线 | 两条 perm + ALLOW/DENY 各一 | 2 行落盘 | 正向 | P0 | L-4:防止误杀 | ### 6.7 H-1 PII 暴露 —— 契约先行骨架(`internal/logic/user/userDetailPIIMask_audit_test.go`) > **本组为"对抗性未修复回归"**:默认 `t.Skipf("AUDIT_PENDING H-1 …")`,CI 通过 `AUDIT_RUN_PENDING=1` 可强制跑红。修复方案参考 `audit-report.md` H-1 条目(`CanViewContact` + `filterPIIForCaller` + `MaskEmail` / `MaskPhone`)。 | TC编号 | 测试场景 | 输入 | 预期结果(fix 后) | 类型 | 优先级 | 覆盖说明 | | :--- | :--- | :--- | :--- | :--- | :--- | :--- | | TC-0990 | 同产品 MEMBER 互看 | caller 与 target 同产品同级 | Email 脱敏、Phone 脱敏、Remark 清空 | 安全/PII | P0 | H-1:最核心攻击面 | | TC-0991 | 自看 | caller.UserId == target.Id | 原样返回 Email/Phone/Remark | 正向 | P0 | H-1:fix 后不得误伤 self-view | | TC-0992 | SuperAdmin 看任何人 | caller.IsSuperAdmin | 原样返回 Email/Phone/Remark | 正向 | P0 | H-1:防回归(超管被误脱敏) | ### 6.8 L-3 DeptId=0 legacy 账号契约先行骨架(`internal/logic/auth/checkManageAccessDeptZero_audit_test.go`) > 同样默认 `t.Skipf("AUDIT_PENDING L-3 …")`。 | TC编号 | 测试场景 | 输入 | 预期结果(fix 后) | 类型 | 优先级 | 覆盖说明 | | :--- | :--- | :--- | :--- | :--- | :--- | :--- | | TC-0993 | legacy DEVELOPER(DeptId=0、DeptPath="")去管理他人 | 合法 target + productCode | `response.CodeError{Code:403}`,文案含 "未归属"(供运维触发数据迁移工单) | 契约 | P2 | L-3:文案与错误结构一致化 | ### 6.9 第 8 轮新增 TC 汇总 | 审计条目 | 文件 | TC 编号区间 | 数量 | 状态 | | :--- | :--- | :--- | ---: | :---: | | H-2 | `checkPermLevelFreshRead_audit_test.go` | TC-0969 ~ TC-0975 | 7 | ✅ | | M-1 | `createProductCompensation_audit_test.go` | TC-0976 ~ TC-0978 | 3 | ✅ | | M-2 (REST) | `syncPerms404_audit_test.go` | TC-0979 / TC-0980 | 2 | ✅ | | M-2 (gRPC) | `syncPermissions404_audit_test.go` | TC-0981 / TC-0982 | 2 | ✅ | | M-3 | `refreshTokenSignBeforeCas_audit_test.go` | TC-0983 ~ TC-0985 | 3 | ✅ | | L-1 | `l1StatusPlaceholder_audit_test.go` | TC-0986 / TC-0987 | 2 | ✅ | | L-4 | `setUserPermsCountRecheck_audit_test.go` | TC-0988 / TC-0989 | 2 | ✅ | | H-1(未修) | `userDetailPIIMask_audit_test.go` | TC-0990 ~ TC-0992 | 3 | ⏸ Skip | | L-3(未修) | `checkManageAccessDeptZero_audit_test.go` | TC-0993 | 1 | ⏸ Skip | | **合计** | — | **TC-0969 ~ TC-0993** | **25** | 20 ✅ + 5 ⏸ | ### 6.10 既有测试兼容性调整(Round 8 代码变更触发) | 用例 | 文件 | 调整说明 | | :--- | :--- | :--- | | TC-0818 / TC-0820 / TC-0821 | `internal/logic/auth/checkManageAccessPrefetch_audit_test.go` | H-2 修复后 `checkPermLevel` 会对 caller.UserId 发一次 `FindMinPermsLevelByUserIdAndProductCode`,原 mock 只覆盖 target 侧,补上 caller 侧 EXPECT(返回 50 或 sqlx.ErrNotFound)。 | | TC-0823(`TestCheckManageAccess_ErrNotFound_StillTreatedAsNoRole`) | `internal/logic/auth/checkPermLevelFailClose_audit_test.go` | 同上。 | | TC-0869(`CountOtherActiveAdminsTx` ↔ `CountActiveAdminsTx` 一致性) | `internal/model/productmember/countOtherActiveAdmins_audit_test.go` | L-2 修复删除了 `CountActiveAdminsTx`,原断言改为直接断言 `int64(2)` 对齐已知种子数据。 | > 调整根因:H-2 的 fresh read 把原本只需要 target 侧的 mock 扩展到 **target + caller 两路**;L-2 的 API 面收敛要求任何依赖 `CountActiveAdminsTx` 的代码必须切到 `CountOtherActiveAdminsTx` 或直接种子数据。 ## 7. Round 9 审计驱动测试(M-N1 / M-N2 / M-N3 / M-N4 / L-N1 / L-N2 / L-N3 / L-N4) 本轮 7 项修复(`audit-report.md` 本轮结论节)共增 TC-0994 ~ TC-1014 计 21 条新用例,并对既有测试按新契约作适配。 ### 7.1 M-N4 CreateUser 部门链防护(`internal/logic/user/createUserDeptChain_audit_test.go`) > 修复目标:`CreateUser` 必须对非超管 caller 做 `caller.DeptPath` → `newDept.Path` 前缀校验,并拒绝非超管 `DeptId=0`;目标部门 `status!=Enabled` 一律拒绝(与 L-N2 对齐)。 | TC编号 | 测试场景 | 输入 | 预期结果 | 类型 | 优先级 | 覆盖说明 | | :--- | :--- | :--- | :--- | :--- | :--- | :--- | | TC-0994 | 产品 ADMIN 为非自己管辖部门创建用户 | caller `DeptPath=/100/` 目标部门 `/999/` | 403 "无权在非自己管辖的部门下创建用户" | 对抗/越权 | P0 | M-N4:防产品 ADMIN 预埋 admin_*/ops_* 等关键用户名合谋 AddMember | | TC-0995 | 产品 ADMIN 在自己子树下创建用户 | caller `/200/` → 目标 `/200/1/` | 正常创建,DB 落盘 | 正向 | P0 | M-N4:正向回归,不得误伤合法路径 | | TC-0996 | SuperAdmin 在任意部门 / DeptId=0 均可创建 | 超管两条路径 | 两条均成功,支撑跨组织系统账号语义 | 正向 | P0 | M-N4:超管豁免分层 | | TC-0997 | caller.DeptPath=="" 的 legacy 产品 ADMIN | DeptId 指向真实部门 | 403 "您未归属任何部门,无权创建用户" | 对抗 | P1 | M-N4:legacy 账号 fail-close | | TC-0998 | 非超管 caller 传 DeptId=0 | 任意合法用户名 | 400 "必须指定部门" | 契约 | P1 | M-N4:阻断非超管在部门树外开口 | | TC-0999 | 目标部门 status=Disabled | 超管 → 已禁用部门 | 400 "目标部门已停用" | 契约 | P1 | L-N2:与 UpdateDept 闭环 | ### 7.2 M-N3 RoleDetail 枚举 Oracle 关闭(`internal/logic/role/roleDetailOracle_audit_test.go`) > 修复目标:非超管跨产品访问 `role` 与 "id 不存在" 必须返回**完全一致**的 404 响应,关闭"遍历 id 画出跨产品 roleId 分布图"的侧信道。 | TC编号 | 测试场景 | 输入 | 预期结果 | 类型 | 优先级 | 覆盖说明 | | :--- | :--- | :--- | :--- | :--- | :--- | :--- | | TC-1000 | 非超管访问别的产品的 role | Admin in `test_product`;目标 role 在 `mn3_other_xxx` | 404 "角色不存在" | 安全/Oracle | P0 | M-N3:跨产品必须 404 而非 403 | | TC-1001 | "id 不存在" vs "跨产品" 响应对比 | 两条路径对照 | code + body 完全一致 | 安全/Oracle | P0 | M-N3:彻底消除枚举 oracle | | TC-1002 | 超管跨产品访问 | 超管 → 跨产品 role + permIds | 正常返回完整 RoleItem | 正向 | P0 | M-N3:审计/运维路径不得被误伤 | ### 7.3 M-N2 UserDetailsLoader.BatchDel Pipelined 索引清理(`internal/loaders/userDetailsLoader_batchdel_mn2_audit_test.go`) > 修复目标:`BatchDel` 把 N 个用户的 `userIndex`/`productIndex` SREM 合进一次 Pipelined RTT,替换历史 2N 次串行调用,关闭"角色绑千人 → UpdateRole/BindRoles 尾延迟秒级"风险。 | TC编号 | 测试场景 | 输入 | 预期结果 | 类型 | 优先级 | 覆盖说明 | | :--- | :--- | :--- | :--- | :--- | :--- | :--- | | TC-1013 | N=2 的真实缓存场景 | 2 用户各 Load 预热后 BatchDel | 主 key DEL、userIndex/productIndex 中 2×3 元素全部 SREM | 契约/缓存 | P1 | M-N2:同步清理不能被回退 | | TC-1014 | `productCode=""` 分支 | 无效 uid + 空 productCode | 不 panic / 不报错 | 契约/防御 | P2 | M-N2:pipeline 分支 fail-safe | ### 7.4 M-N1 UserDetailsLoader.Load 返回 `ErrLoaderDegraded`(`internal/loaders/userDetailsLoader_contract_audit_test.go`) > 修复目标:`partial load failure` 必须向上冒 `ErrLoaderDegraded`(而非半成品 `(ud, nil)`),由调用方映射为 503 / codes.Unavailable;TC-0917 新增 sentinel 稳定性契约。 | TC编号 | 测试场景 | 输入 | 预期结果 | 类型 | 优先级 | 覆盖说明 | | :--- | :--- | :--- | :--- | :--- | :--- | :--- | | TC-0915(重写) | productCode 不存在触发 `loadProduct` 失败 | `Load(userId, "unknown_code")` | `ud=nil, err=ErrLoaderDegraded`;5 分钟正缓存不写入 | 契约/错误 | P0 | M-N1:fail-close 不生成半成品 | | TC-0914(补真实产品) | 负缓存投毒防御 (L-6 主题) 不得被 M-N1 误伤 | 真实 productCode + 新用户首次 Load | 不写 `_NOT_FOUND_` 哨兵 | 正向 | P0 | M-N1:L-6 路径不得退化 | | TC-0917(新增) | `ErrLoaderDegraded` 作为 sentinel 可 `errors.Is` 断言 | 直接断言导出符号 | `errors.Is(ErrLoaderDegraded, ErrLoaderDegraded) == true` | 契约 | P2 | M-N1:调用方可稳定识别 | ### 7.5 L-N1 ParseWithHMAC 中央化(`internal/middleware/parseWithHMAC_centralized_audit_test.go`) > 修复目标:`ParseWithHMAC` 上移到 `middleware` 层作为 HTTP 中间件、gRPC VerifyToken、RefreshToken 等所有 JWT 解析点的唯一入口;`logic/auth` 的同名函数仅是 alias,避免"算法混淆防御"被任何调用点 drift。 | TC编号 | 测试场景 | 输入 | 预期结果 | 类型 | 优先级 | 覆盖说明 | | :--- | :--- | :--- | :--- | :--- | :--- | :--- | | TC-1003 | 合法 HS256 token | 正常 `Claims` | 解析通过,`token.Valid == true` | 正向 | P0 | L-N1:中央入口正向路径 | | TC-1004 | alg=RS256(公钥→HMAC 混淆)伪造 | 攻击者用 secret 当 HS256 密钥签署 | `unexpected signing method` 错误 | 安全 | P0 | L-N1:CVE-2016-10555 同类防御 | | TC-1005 | alg=none 伪造 | 空签名段 | 错误返回 | 安全 | P0 | L-N1:深度防御 | | TC-1006 | HS256 但错误 secret | 合法结构 + 猜测 secret | 签名校验失败 | 安全 | P0 | L-N1:签名错误必拦 | | TC-1007 | 任意 `jwt.Claims` 结构体 | 自定义 `customClaims` | 解析通过,字段可转型回取 | 契约 | P1 | L-N1:与具体 claims 类型解耦,所有调用方可复用 | ### 7.6 L-N3 AdminLogin 三分支时序等齐(`internal/logic/pub/adminLoginTiming_audit_test.go`) > 修复目标:`IsSuperAdmin` 判断前置到真 bcrypt 之前,"存在但非超管"分支也必须跑一次 `dummyBcryptHash`,关闭"基于耗时差筛出存在的超管账号"的时序 oracle。 | TC编号 | 测试场景 | 输入 | 预期结果 | 类型 | 优先级 | 覆盖说明 | | :--- | :--- | :--- | :--- | :--- | :--- | :--- | | TC-1008 | 非超管+错密码 vs 用户不存在 | 错密码 / 不存在用户名 | code + body 完全一致 | 安全/Oracle | P0 | L-N3:响应不得区分两条分支 | | TC-1009 | 非超管+正确密码 | 真实普通用户正确密码 | 仍 401 "用户名或密码错误" | 安全 | P0 | L-N3:不得以 200 暴露账号存在性 | | TC-1010 | 两条 dummy bcrypt 分支时序 | 连续 3 次平均耗时 | `非超管+错密` 与 `不存在` 耗时比 <3× | 时序/性能 | P0 | L-N3:若比例 >3× 说明 L-N3 被回退(非超管分支跳过了 dummy bcrypt) | ### 7.7 L-N4 UpdateUserStatus 乐观锁(`internal/logic/user/updateUserStatusOptLock_audit_test.go` + `internal/model/user/updatePasswordStatus_rowsaffected_audit_test.go`) > 修复目标:`SysUserModel.UpdateStatus(ctx, id, status, expectedUpdateTime)` 以 `updateTime` 作为 CAS 锚点;并发冻结/解冻 last-write-wins 被关闭,Logic 把 `ErrUpdateConflict` 映射为 409。 | TC编号 | 测试场景 | 输入 | 预期结果 | 类型 | 优先级 | 覆盖说明 | | :--- | :--- | :--- | :--- | :--- | :--- | :--- | | TC-1011 | 他人先冻结后本轮解冻 | 先跑 Update → UpdateTime 推进,本轮仍持旧 updateTime 直冲 model | model 层 `ErrUpdateConflict`;Logic happy path 解冻成功且 `updateTime` 推进 | 并发/CAS | P0 | L-N4:CAS 失败路径 + 正向回归 | | TC-1012 | Logic 层错误映射 | model 层强制 `ErrUpdateConflict` | 映射为 `response.ErrConflict(409, "数据已被其他操作修改,请刷新后重试")` | 契约 | P1 | L-N4:文案与 code 对齐 | | 既有 TC(适配) | `TestSysUserModel_UpdateStatus_*` 三条 | 传入 `expectedUpdateTime` | 签名变更后仍通过;happy path 新增 `time.Sleep` 保证秒级 updateTime 推进 | 适配 | P0 | L-N4:mock/测试对齐新签名 | ### 7.8 L-N2 UpdateUserLogic 目标部门状态校验 > 与 `CreateUser` 一致(见 TC-0999),`UpdateUser` 侧已在 `updateUserLogic.go` 加入 `newDept.Status != Enabled` 分支;目标覆盖由现有 `updateUserDeptScope_audit_test.go` 家族承担,本轮无需新增(改动语义与 TC-0999 对称)。 ### 7.9 既有测试兼容性调整(Round 9 代码变更触发) | 用例 | 文件 | 调整说明 | | :--- | :--- | :--- | | `TestSysUserModel_UpdateStatus_*` 三条 | `internal/model/user/updatePasswordStatus_rowsaffected_audit_test.go` | L-N4:`UpdateStatus` 新签名 `(ctx, id, status, expectedUpdateTime)`。冲突用例改为先 `FindOne` 拿 `UpdateTime` 再传;happy path 追加 1.1s `time.Sleep` 确保秒级 `updateTime` 严格推进;NotFound 用例传 `0`。 | | `MockSysUserModel.UpdateStatus` | `internal/testutil/mocks/mock_user_model.go` | 对齐 `sysUserModel` 的新签名,新增 `expectedUpdateTime any` 参数。 | | TC-0915 (M-N1 重写) | `internal/loaders/userDetailsLoader_contract_audit_test.go` | 改断言 `ErrLoaderDegraded` + `ud==nil`;放弃旧"半成品 + 未写正缓存"双重契约。 | | TC-0914 (L-6 去相干) | 同上 | 补插真实 `sys_product` 行,避开被 M-N1 升级为 `ErrLoaderDegraded` 的干扰路径。 | | `refreshTokenLogic.go` / `jwtauthMiddleware.go` | `internal/logic/pub/refreshTokenLogic.go` 与 `internal/middleware/jwtauthMiddleware.go` | Load 返回 `err != nil` 时一律映射 503 "服务暂时不可用,请稍后重试",不得与"用户不存在"同化为 401。本调整在 Round 9 已有对应回归路径,未改测试用例名。 | ### 7.10 Round 9 新增 TC 汇总 | 审计条目 | 文件 | TC 编号区间 | 数量 | 状态 | | :--- | :--- | :--- | ---: | :---: | | M-N1 | `userDetailsLoader_contract_audit_test.go` | TC-0915(重写)/ TC-0917(新增) | 2 | ✅ | | M-N2 | `userDetailsLoader_batchdel_mn2_audit_test.go` | TC-1013 / TC-1014 | 2 | ✅ | | M-N3 | `roleDetailOracle_audit_test.go` | TC-1000 ~ TC-1002 | 3 | ✅ | | M-N4 | `createUserDeptChain_audit_test.go` | TC-0994 ~ TC-0998 | 5 | ✅ | | L-N1 | `parseWithHMAC_centralized_audit_test.go` | TC-1003 ~ TC-1007 | 5 | ✅ | | L-N2 | `createUserDeptChain_audit_test.go` | TC-0999 | 1 | ✅ | | L-N3 | `adminLoginTiming_audit_test.go` | TC-1008 ~ TC-1010 | 3 | ✅ | | L-N4 | `updateUserStatusOptLock_audit_test.go` | TC-1011 / TC-1012 | 2 | ✅ | | **合计** | — | **TC-0994 ~ TC-1014(含 TC-0915 重写 / TC-0917 新增)** | **23** | 23 ✅ |