فهرست منبع

feat: 静态代码审计,修复逻辑bug和安全漏洞

BaiLuoYan 4 هفته پیش
والد
کامیت
535b685218
94فایلهای تغییر یافته به همراه2472 افزوده شده و 2244 حذف شده
  1. 230 382
      audit-report.md
  2. 2 2
      gen-model.sh
  3. 1 1
      internal/handler/pub/adminLoginHandler_test.go
  4. 1 1
      internal/handler/routes.go
  5. 60 20
      internal/loaders/userDetailsLoader.go
  6. 23 23
      internal/loaders/userDetailsLoader_test.go
  7. 24 24
      internal/logic/auth/access_test.go
  8. 10 10
      internal/logic/auth/changePasswordLogic_test.go
  9. 17 14
      internal/logic/auth/jwt.go
  10. 13 13
      internal/logic/auth/jwt_test.go
  11. 1 1
      internal/logic/auth/perms_mock_test.go
  12. 16 16
      internal/logic/auth/perms_test.go
  13. 4 4
      internal/logic/auth/userInfoLogic_test.go
  14. 7 0
      internal/logic/dept/createDeptLogic.go
  15. 4 4
      internal/logic/dept/createDeptLogic_mock_test.go
  16. 9 9
      internal/logic/dept/createDeptLogic_test.go
  17. 5 5
      internal/logic/dept/deleteDeptLogic_test.go
  18. 3 3
      internal/logic/dept/deptTreeLogic_test.go
  19. 7 0
      internal/logic/dept/updateDeptLogic.go
  20. 1 1
      internal/logic/dept/updateDeptLogic_mock_test.go
  21. 6 6
      internal/logic/dept/updateDeptLogic_test.go
  22. 6 6
      internal/logic/member/addMemberLogic_test.go
  23. 10 0
      internal/logic/member/memberListLogic.go
  24. 9 9
      internal/logic/member/memberListLogic_test.go
  25. 1 1
      internal/logic/member/removeMemberLogic_mock_test.go
  26. 3 3
      internal/logic/member/removeMemberLogic_test.go
  27. 3 3
      internal/logic/member/updateMemberLogic_test.go
  28. 10 0
      internal/logic/perm/permListLogic.go
  29. 9 9
      internal/logic/perm/permListLogic_test.go
  30. 10 0
      internal/logic/product/createProductLogic.go
  31. 2 2
      internal/logic/product/createProductLogic_mock_test.go
  32. 5 5
      internal/logic/product/createProductLogic_test.go
  33. 4 4
      internal/logic/product/productDetailLogic_test.go
  34. 7 7
      internal/logic/product/productListLogic_test.go
  35. 7 0
      internal/logic/product/updateProductLogic.go
  36. 4 4
      internal/logic/product/updateProductLogic_test.go
  37. 8 3
      internal/logic/pub/adminLoginLogic.go
  38. 17 18
      internal/logic/pub/adminLoginLogic_test.go
  39. 2 2
      internal/logic/pub/loginLogic_test.go
  40. 4 3
      internal/logic/pub/loginService.go
  41. 5 1
      internal/logic/pub/refreshTokenLogic.go
  42. 13 13
      internal/logic/pub/refreshTokenLogic_test.go
  43. 1 1
      internal/logic/pub/syncPermsLogic_mock_test.go
  44. 12 12
      internal/logic/pub/syncPermsLogic_test.go
  45. 12 0
      internal/logic/role/bindRolePermsLogic.go
  46. 1 1
      internal/logic/role/bindRolePermsLogic_mock_test.go
  47. 11 7
      internal/logic/role/bindRolePermsLogic_test.go
  48. 7 0
      internal/logic/role/createRoleLogic.go
  49. 4 4
      internal/logic/role/createRoleLogic_test.go
  50. 1 1
      internal/logic/role/deleteRoleLogic_mock_test.go
  51. 3 3
      internal/logic/role/deleteRoleLogic_test.go
  52. 9 0
      internal/logic/role/roleDetailLogic.go
  53. 5 5
      internal/logic/role/roleDetailLogic_test.go
  54. 10 0
      internal/logic/role/roleListLogic.go
  55. 7 7
      internal/logic/role/roleListLogic_test.go
  56. 7 0
      internal/logic/role/updateRoleLogic.go
  57. 3 3
      internal/logic/role/updateRoleLogic_test.go
  58. 1 1
      internal/logic/user/bindRolesLogic_mock_test.go
  59. 8 8
      internal/logic/user/bindRolesLogic_test.go
  60. 9 0
      internal/logic/user/createUserLogic.go
  61. 1 1
      internal/logic/user/createUserLogic_mock_test.go
  62. 16 16
      internal/logic/user/createUserLogic_test.go
  63. 8 8
      internal/logic/user/setUserPermsLogic_test.go
  64. 17 2
      internal/logic/user/updateUserLogic.go
  65. 19 19
      internal/logic/user/updateUserLogic_test.go
  66. 6 6
      internal/logic/user/updateUserStatusLogic_test.go
  67. 11 0
      internal/logic/user/userDetailLogic.go
  68. 7 7
      internal/logic/user/userDetailLogic_test.go
  69. 15 0
      internal/logic/user/userListLogic.go
  70. 3 3
      internal/logic/user/userListLogic_mock_test.go
  71. 9 9
      internal/logic/user/userListLogic_test.go
  72. 11 6
      internal/middleware/jwtauthMiddleware.go
  73. 8 8
      internal/middleware/jwtauthMiddleware_test.go
  74. 10 2
      internal/middleware/ratelimitMiddleware.go
  75. 6 6
      internal/middleware/ratelimitMiddleware_test.go
  76. 32 32
      internal/model/dept/sysDeptModel_test.go
  77. 48 48
      internal/model/perm/sysPermModel_test.go
  78. 28 28
      internal/model/product/sysProductModel_test.go
  79. 40 40
      internal/model/productmember/sysProductMemberModel_test.go
  80. 50 50
      internal/model/role/sysRoleModel_test.go
  81. 40 40
      internal/model/roleperm/sysRolePermModel_test.go
  82. 2 2
      internal/model/user/sysUserModel.go
  83. 12 11
      internal/model/user/sysUserModel_gen.go
  84. 172 43
      internal/model/user/sysUserModel_test.go
  85. 41 41
      internal/model/userperm/sysUserPermModel_test.go
  86. 43 43
      internal/model/userrole/sysUserRoleModel_test.go
  87. 8 8
      internal/response/response_test.go
  88. 13 2
      internal/server/permserver.go
  89. 37 37
      internal/server/permserver_test.go
  90. 3 0
      internal/svc/servicecontext.go
  91. 3 3
      internal/util/validate_test.go
  92. 24 20
      perm.sql
  93. 449 427
      test-design.md
  94. 586 591
      test-report.md

+ 230 - 382
audit-report.md

@@ -1,8 +1,8 @@
-# 权限管理系统 (perms-system-server) 深度代码审计报告
+# 权限管理系统 - 深度代码审计报告
 
-> 审计时间:2026-04-17
-> 审计范围:全部非测试业务源代码(logic / model / middleware / loaders / server / handler)
-> 审计维度:逻辑一致性、并发与竞态、资源管理、数据完整性、安全漏洞、边界崩溃
+> 审计范围:`internal/` 下所有非测试源代码,包括 logic、model、middleware、handler、config、loader、server 层  
+> 审计时间:2026-04-17  
+> 排除范围:`*_test.go`、`*_mock_test.go`、`testutil/`、`cli/`、`pb/`(生成代码)
 
 ---
 
@@ -10,228 +10,170 @@
 
 ---
 
-### H-1:UserDetailsLoader.loadPerms 跨产品权限泄漏
-
-- **描述**:`internal/loaders/userDetailsLoader.go` 第 329-330 行,在为普通成员加载权限时,`FindPermIdsByUserIdAndEffect` 查询的 SQL 是 `SELECT permId FROM sys_user_perm WHERE userId = ? AND effect = ?`,该查询**没有按 productCode 过滤**。如果一个用户是多个产品的成员,且在不同产品下都被设置了用户级权限(ALLOW/DENY),则加载产品 A 的上下文时,会把产品 B 的用户级权限也混入计算。
-
-  具体污染路径:
-  1. 用户在产品 B 下有 ALLOW 权限 `permId=100`(code 为 `order:delete`)
-  2. 当加载产品 A 的上下文时,`allowIds` 包含 `permId=100`
-  3. `permId=100` 被加入 `permIdSet`,最终 `FindByIds` 返回时没有过滤 `productCode`
-  4. 产品 B 的权限 code `order:delete` 泄漏到产品 A 的 `Perms` 列表和 JWT Token 中
-
-  更严重的是:产品 B 中的 DENY 规则也会泄漏,可能**错误阻止**产品 A 中本应拥有的权限。
+### H1. AdminLogin 未校验用户是否为超级管理员 —— 越权访问风险
 
+- **文件**:`internal/logic/pub/adminLoginLogic.go:33-91`
+- **描述**:`AdminLogin` 接口仅校验了 `ManagementKey` 和用户名密码,但**没有校验用户的 `isSuperAdmin` 字段**。任何普通用户只要知道 `ManagementKey`,就能通过管理后台登录接口获取一个 `productCode=""` 的 Token。
 - **影响**:
-  - 跨产品权限授予:用户在未授权的产品中获得额外权限
-  - 跨产品权限拒绝:产品 B 的 DENY 规则错误地屏蔽产品 A 的合法权限
-  - JWT Token 中包含其他产品的权限信息(信息泄漏)
-
+  - 拿到此 Token 后,用户通过 JWT 中间件校验,可以调用所有 `JwtAuth` 保护的接口。
+  - 虽然 `RequireSuperAdmin()` 会在创建产品、管理部门等操作中拦截,但 `UserDetail`、`UserList`、`RoleList`、`ProductList`、`DeptTree` 等查询接口**没有额外权限校验**,非超管用户可以通过此途径浏览所有系统数据。
+  - `ManagementKey` 一旦泄露(如被抓包、配置文件泄露),整个系统对该用户门户大开。
 - **修复方案**:
 
-  方案一(推荐):在 `loadPerms` 中查询时增加产品过滤
-
-  ```go
-  // internal/loaders/userDetailsLoader.go - loadPerms 方法中
-  // 修改为通过子查询限定 productCode
-  allowIds, _ := l.models.SysUserPermModel.FindPermIdsByUserIdAndEffectForProduct(
-      ctx, ud.UserId, consts.PermEffectAllow, ud.ProductCode)
-  denyIds, _ := l.models.SysUserPermModel.FindPermIdsByUserIdAndEffectForProduct(
-      ctx, ud.UserId, consts.PermEffectDeny, ud.ProductCode)
-  ```
-
-  对应新增 Model 方法:
-
-  ```go
-  func (m *customSysUserPermModel) FindPermIdsByUserIdAndEffectForProduct(
-      ctx context.Context, userId int64, effect string, productCode string,
-  ) ([]int64, error) {
-      var ids []int64
-      query := fmt.Sprintf(
-          "SELECT up.`permId` FROM %s up "+
-          "INNER JOIN `sys_perm` p ON up.`permId` = p.`id` "+
-          "WHERE up.`userId` = ? AND up.`effect` = ? AND p.`productCode` = ?",
-          m.table)
-      if err := m.QueryRowsNoCacheCtx(ctx, &ids, query, userId, effect, productCode); err != nil {
-          return nil, err
-      }
-      return ids, nil
-  }
-  ```
+```go
+// adminLoginLogic.go — 在密码验证通过后增加超管校验
+if u.IsSuperAdmin != consts.IsSuperAdminYes {
+    return nil, response.ErrForbidden("仅超级管理员可通过管理后台登录")
+}
+```
 
 ---
 
-### H-2:UpdateUser 接口可绕过超管状态保护
+### H2. 限流中间件 IP 提取逻辑有两个严重缺陷
 
-- **描述**:`internal/logic/user/updateUserStatusLogic.go` 明确禁止修改超级管理员的状态:
-
-  ```go
-  if user.IsSuperAdmin == consts.IsSuperAdminYes {
-      return response.ErrForbidden("不能修改超级管理员的状态")
-  }
-  ```
-
-  但 `internal/logic/user/updateUserLogic.go` 中也接受 `Status` 字段,对于非本人操作只要求调用者是超管,**没有检查目标用户是否也是超管**:
+- **文件**:`internal/middleware/ratelimitMiddleware.go:24-28`
+- **描述**:
+  1. **`r.RemoteAddr` 包含端口号**:Go 的 `http.Request.RemoteAddr` 格式为 `IP:Port`(如 `192.168.1.1:54321`)。由于每个 TCP 连接的临时端口不同,限流 Key 变成了**每个连接独立计数**,同一个客户端 IP 几乎不可能触发限流。
+  2. **未处理反向代理场景**:生产环境通常有 Nginx/Envoy 做反向代理,此时 `RemoteAddr` 是代理服务器的 IP,所有客户端共享同一个限流桶,导致少量正常请求就会触发全局限流。
+- **影响**:登录接口(`/auth/login`、`/auth/adminLogin`)的暴力破解防护**形同虚设**。攻击者可以不受限制地进行密码爆破。
+- **修复方案**:
 
-  ```go
-  } else {
-      if !caller.IsSuperAdmin {
-          return response.ErrForbidden("仅允许修改自己的信息或超管操作")
-      }
-  }
-  // ... 后续直接设置 status,无超管保护
-  if req.Status == consts.StatusEnabled || req.Status == consts.StatusDisabled {
-      user.Status = req.Status
-  }
-  ```
+```go
+func (m *RateLimitMiddleware) Handle(next http.HandlerFunc) http.HandlerFunc {
+    return func(w http.ResponseWriter, r *http.Request) {
+        ip := extractClientIP(r)
+        key := fmt.Sprintf("ip:%s", ip)
+        code, _ := m.limiter.Take(key)
+        if code == limit.OverQuota {
+            httpx.ErrorCtx(r.Context(), w, response.ErrTooManyRequests("请求过于频繁,请稍后再试"))
+            return
+        }
+        next(w, r)
+    }
+}
 
-  攻击路径:超管 A 通过 `POST /api/user/update` 传入 `{"id": <超管B的ID>, "status": 2}`,即可冻结超管 B,绕过 `updateUserStatus` 的保护逻辑。
+func extractClientIP(r *http.Request) string {
+    // 优先从反代标准头提取
+    if ip := r.Header.Get("X-Real-IP"); ip != "" {
+        return ip
+    }
+    if forwarded := r.Header.Get("X-Forwarded-For"); forwarded != "" {
+        // 取第一个 IP(最靠近客户端的)
+        if idx := strings.Index(forwarded, ","); idx != -1 {
+            return strings.TrimSpace(forwarded[:idx])
+        }
+        return strings.TrimSpace(forwarded)
+    }
+    // 兜底:去掉端口
+    host, _, err := net.SplitHostPort(r.RemoteAddr)
+    if err != nil {
+        return r.RemoteAddr
+    }
+    return host
+}
+```
 
-- **影响**:超级管理员之间可互相冻结账号,破坏系统管理根基。在最坏场景下,攻击者获取任一超管账号后可瘫痪所有其他超管。
+---
 
-- **修复方案**:在 `updateUserLogic.go` 中增加超管保护检查:
+### H3. 多个查询接口存在水平越权 —— 无跨产品/无权限校验
+
+- **文件**:
+  - `internal/logic/user/userDetailLogic.go` — 任意用户可查看任意用户详情
+  - `internal/logic/user/userListLogic.go` — 任意用户可列出全系统所有用户
+  - `internal/logic/role/roleListLogic.go` — 可传入任意 `productCode` 查看其他产品角色
+  - `internal/logic/role/roleDetailLogic.go` — 可查看任意角色详情(含所绑定的权限 ID 列表)
+  - `internal/logic/perm/permListLogic.go` — 可传入任意 `productCode` 查看其他产品权限
+  - `internal/logic/member/memberListLogic.go` — 可传入任意 `productCode` 查看其他产品成员
+- **描述**:上述接口只经过 `JwtAuth` 中间件校验了登录态,但**没有校验调用者是否属于目标产品、是否有权访问该数据**。一个产品 A 的普通成员(MEMBER),可以通过构造请求查看产品 B 的角色、权限、成员信息。
+- **影响**:**信息泄露**。权限系统本身的数据(角色名称、权限列表、用户列表、成员关系)被无差别暴露给所有已登录用户,违反了多产品之间的数据隔离原则。
+- **修复方案**:对含 `productCode` 参数的查询接口,增加产品归属校验:
+
+```go
+// 在 roleListLogic.go 等接口中增加
+caller := middleware.GetUserDetails(l.ctx)
+if caller == nil {
+    return nil, response.ErrUnauthorized("未登录")
+}
+if !caller.IsSuperAdmin {
+    if caller.ProductCode != req.ProductCode {
+        return nil, response.ErrForbidden("无权访问该产品的数据")
+    }
+}
+```
 
-  ```go
-  if caller.UserId != req.Id {
-      if !caller.IsSuperAdmin {
-          return response.ErrForbidden("仅允许修改自己的信息或超管操作")
-      }
-      // 新增:禁止通过此接口修改其他超管的状态
-      if req.Status != 0 && user.IsSuperAdmin == consts.IsSuperAdminYes {
-          return response.ErrForbidden("不能通过此接口修改超级管理员的状态")
-      }
-  }
-  ```
+对 `UserDetail` 和 `UserList`,应限制非超管用户只能查看自己所在产品的成员。
 
 ---
 
-### H-3:产品成员被禁用后仍可正常登录
+### H4. UpdateUser 权限校验与同类接口不一致 —— 超管可被越权修改
 
-- **描述**:`sys_product_member` 表有 `status` 字段(1=启用,2=禁用),但登录流程和 UserDetailsLoader 均未校验此字段。
+- **文件**:`internal/logic/user/updateUserLogic.go:31-45`
+- **描述**:`UpdateUser` 的权限逻辑为「只能改自己,或者超管改别人」。但对比 `UpdateUserStatus`(使用了 `CheckManageAccess` 检查部门层级和权限等级),`UpdateUser` 缺少以下校验:
+  1. **超管 A 可以修改超管 B 的信息**(包括部门、状态),没有类似 `UpdateUserStatus` 中 "不能修改超级管理员的状态" 的保护。
+  2. **没有 `CheckManageAccess`**:不校验部门层级关系和 `permsLevel`,超管可以直接修改任何用户的部门归属(`DeptId`),这可能绕过部门层级隔离的安全模型。
+- **影响**:如果系统中有多个超级管理员,超管 A 可以将超管 B 的状态改为 `StatusDisabled`(冻结),或将其部门改为下级部门从而降低其管理范围。
+- **修复方案**:
 
-  - `loginLogic.go` 第 67 行仅检查成员是否存在,不检查 status:
-    ```go
-    if _, memberErr := l.svcCtx.SysProductMemberModel.FindOneByProductCodeUserId(
-        l.ctx, req.ProductCode, u.Id); memberErr != nil {
-        return nil, response.ErrForbidden("您不是该产品的成员")
+```go
+// 对非自身操作增加更严格的校验
+if caller.UserId != req.Id {
+    // 仅超管可操作
+    if !caller.IsSuperAdmin {
+        return response.ErrForbidden("仅超管可修改其他用户信息")
     }
-    ```
-  - `permserver.go` 第 140 行 gRPC Login 同样如此
-  - `userDetailsLoader.go` 的 `loadMembership` 方法也不检查 `member.Status`
-
-- **影响**:管理员将某个成员禁用后,该成员仍然可以正常登录、获取 Token、使用该产品的所有权限,禁用操作形同虚设。
-
-- **修复方案**:
+    // 不允许通过此接口修改其他超管
+    if user.IsSuperAdmin == consts.IsSuperAdminYes {
+        if req.Status != 0 || req.DeptId != nil {
+            return response.ErrForbidden("不能修改其他超级管理员的状态和部门")
+        }
+    }
+}
+```
 
-  在 `loginLogic.go` 和 `permserver.go` 的 Login 中增加成员状态检查:
+---
 
-  ```go
-  member, memberErr := l.svcCtx.SysProductMemberModel.FindOneByProductCodeUserId(
-      l.ctx, req.ProductCode, u.Id)
-  if memberErr != nil {
-      return nil, response.ErrForbidden("您不是该产品的成员")
-  }
-  if member.Status != consts.StatusEnabled {
-      return nil, response.ErrForbidden("您在该产品下的成员资格已被禁用")
-  }
-  ```
+### H5. BindRolePerms 未对 PermIds 去重 —— 重复 ID 导致数据库约束错误
 
-  在 `userDetailsLoader.go` 的 `loadMembership` 中增加状态检查:
+- **文件**:`internal/logic/role/bindRolePermsLogic.go:41-54`
+- **描述**:`BindRoles` 接口对 `RoleIds` 做了去重处理(第 47-57 行),但 `BindRolePerms` 没有对 `PermIds` 做同样的去重。当客户端传入重复的 `PermIds`(如 `[1, 1, 2]`)时:
+  - `FindByIds` 返回去重后的 2 条记录
+  - `len(perms) != len(req.PermIds)` → `2 != 3` → 返回「包含无效的权限ID」
+  - 错误信息具有误导性,实际上权限 ID 都是有效的,只是有重复
+  - 如果绕过该检查(例如未来修改了校验逻辑),`BatchInsertWithTx` 会因 `UNIQUE KEY uk_role_perm (roleId, permId)` 约束而报错
+- **影响**:前端传入重复数据时,用户收到令人困惑的错误提示,体验差且难以排查。
+- **修复方案**:
 
-  ```go
-  if member.Status != consts.StatusEnabled {
-      return // 禁用的成员视为无成员身份
-  }
-  ud.MemberType = member.MemberType
-  ```
+```go
+// 在 BindRolePerms 方法开头增加去重逻辑(同 BindRoles 的处理方式)
+if len(req.PermIds) > 0 {
+    seen := make(map[int64]bool, len(req.PermIds))
+    uniqueIds := make([]int64, 0, len(req.PermIds))
+    for _, id := range req.PermIds {
+        if !seen[id] {
+            seen[id] = true
+            uniqueIds = append(uniqueIds, id)
+        }
+    }
+    req.PermIds = uniqueIds
+}
+```
 
 ---
 
-### H-4:gRPC VerifyToken 不检查用户实时状态
-
-- **描述**:`internal/server/permserver.go` 的 `VerifyToken` 方法仅验证 JWT 签名和过期时间,不检查用户当前是否仍然处于启用状态:
-
-  ```go
-  func (s *PermServer) VerifyToken(ctx context.Context, req *pb.VerifyTokenReq) (*pb.VerifyTokenResp, error) {
-      token, err := jwt.ParseWithClaims(req.AccessToken, &middleware.Claims{}, ...)
-      if err != nil || !token.Valid {
-          return &pb.VerifyTokenResp{Valid: false}, nil
-      }
-      claims, ok := token.Claims.(*middleware.Claims)
-      if !ok || claims.TokenType != consts.TokenTypeAccess {
-          return &pb.VerifyTokenResp{Valid: false}, nil
-      }
-      // 直接返回 valid=true,未检查用户实时状态
-      return &pb.VerifyTokenResp{Valid: true, ...}, nil
-  }
-  ```
-
-  而 HTTP 端的 JWT 中间件(`jwtauthMiddleware.go`)会通过 `UserDetailsLoader.Load` 实时检查用户状态。
-
-- **影响**:当用户被冻结或从产品中移除后,如果其他微服务通过 gRPC `VerifyToken` 接口验证 Token,仍会得到 `Valid: true`,导致已冻结用户继续使用系统直到 Token 过期(默认 2 小时)。
-
-- **修复方案**:在 `VerifyToken` 中增加实时状态检查:
-
-  ```go
-  func (s *PermServer) VerifyToken(ctx context.Context, req *pb.VerifyTokenReq) (*pb.VerifyTokenResp, error) {
-      // ... JWT 签名验证保持不变 ...
-
-      // 新增:加载用户实时状态
-      ud := s.svcCtx.UserDetailsLoader.Load(ctx, claims.UserId, claims.ProductCode)
-      if ud.Status != consts.StatusEnabled {
-          return &pb.VerifyTokenResp{Valid: false}, nil
-      }
-      if claims.ProductCode != "" && !ud.IsSuperAdmin && ud.MemberType == "" {
-          return &pb.VerifyTokenResp{Valid: false}, nil
-      }
-
-      return &pb.VerifyTokenResp{
-          Valid:      true,
-          UserId:     claims.UserId,
-          Username:   claims.Username,
-          MemberType: ud.MemberType,  // 使用实时数据而非 Token 中的缓存数据
-          Perms:      ud.Perms,
-      }, nil
-  }
-  ```
-
----
+### H6. SyncPerms 接口缺乏鉴权强度 —— 仅靠 LoginRateLimit 保护
 
-### H-5:gRPC Login 端点无速率限制
-
-- **描述**:HTTP 登录端口通过 `LoginRateLimit` 中间件进行速率限制(60 秒内最多 20 次),但 gRPC 端口的 `Login` 方法(`permserver.go` 第 112 行)**没有任何速率限制**。
-
-  ```go
-  // routes.go - HTTP Login 有速率限制
-  rest.WithMiddlewares(
-      []rest.Middleware{serverCtx.LoginRateLimit},
-      []rest.Route{
-          {Method: http.MethodPost, Path: "/auth/login", Handler: pub.LoginHandler(serverCtx)},
-      }...,
-  )
-
-  // permserver.go - gRPC Login 无速率限制
-  func (s *PermServer) Login(ctx context.Context, req *pb.LoginReq) (*pb.LoginResp, error) {
-      // 直接进入业务逻辑,无任何限流
-  }
-  ```
-
-- **影响**:攻击者可通过 gRPC 端口对登录接口进行高频暴力破解,绕过 HTTP 的速率限制防护。由于使用 bcrypt 校验密码,高并发攻击还可能导致 CPU 资源耗尽。
-
-- **修复方案**:为 gRPC 服务添加 UnaryInterceptor 形式的速率限制,或在 `Login` 方法入口处增加限流逻辑:
-
-  ```go
-  func (s *PermServer) Login(ctx context.Context, req *pb.LoginReq) (*pb.LoginResp, error) {
-      // 使用 peer 获取客户端 IP 进行限流
-      p, _ := peer.FromContext(ctx)
-      ip := p.Addr.String()
-      code, _ := s.limiter.Take(fmt.Sprintf("grpc:login:%s", ip))
-      if code == limit.OverQuota {
-          return nil, status.Error(codes.ResourceExhausted, "请求过于频繁")
-      }
-      // ... 原有逻辑 ...
-  }
-  ```
+- **文件**:`internal/handler/routes.go:176-188` + `internal/logic/pub/syncPermsLogic.go`
+- **描述**:`/api/perm/sync` 接口被分配到 `LoginRateLimit` 中间件组(而非 `JwtAuth`),且由于 H2 中的限流失效问题,该接口实际上**几乎没有任何访问频率限制**。虽然接口内部使用 `appKey + appSecret` 做认证,但:
+  - `appKey` 和 `appSecret` 是长期有效的静态凭证
+  - 没有 IP 白名单、签名时间戳、Nonce 等额外防重放机制
+  - 攻击者获取凭证后可无限次调用,覆盖或禁用产品的所有权限
+- **影响**:一旦 `appKey/appSecret` 泄露,攻击者可以:
+  - 传入空的 `Perms` 列表,将目标产品**所有权限禁用**
+  - 注入恶意权限 Code,污染权限数据
+- **修复方案**:
+  1. 为 SyncPerms 接口增加独立的限流策略(区别于登录限流)
+  2. 考虑增加请求签名(`timestamp + nonce + HMAC(appSecret, body)`)防重放
+  3. 在运维层面增加 IP 白名单
 
 ---
 
@@ -239,238 +181,144 @@
 
 ---
 
-### M-1:Rate Limiter IP 提取可被伪造
-
-- **描述**:`ratelimitMiddleware.go` 按 `X-Forwarded-For → X-Real-IP → RemoteAddr` 顺序提取客户端 IP:
-
-  ```go
-  ip := r.Header.Get("X-Forwarded-For")
-  if ip == "" {
-      ip = r.Header.Get("X-Real-IP")
-  }
-  ```
-
-  `X-Forwarded-For` 头可以被客户端直接伪造。如果服务不在可信反向代理后面,攻击者可以在每次请求中设置不同的 `X-Forwarded-For` 值来绕过速率限制。
-
-- **影响**:登录接口的速率限制可被绕过,使暴力破解攻击成为可能。
-
-- **修复方案**:如果部署在反向代理后,应提取 `X-Forwarded-For` 中的**第一个非信任 IP**;如果直接对外暴露,应优先使用 `RemoteAddr`。建议根据部署拓扑配置信任代理列表:
+### M1. 配置文件明文存储敏感信息
 
-  ```go
-  ip := extractRealIP(r, trustedProxies)
-  ```
+- **文件**:`etc/perm-api-dev.yaml`(及其他环境配置)
+- **描述**:MySQL 密码、Redis 密码、JWT Secret、ManagementKey 均以明文存储在 YAML 文件中。如果这些文件被提交到 Git 仓库,所有有仓库访问权限的人都能获取生产环境密钥。
+- **建议**:
+  - 生产环境使用环境变量注入或密钥管理服务(如 Vault、AWS Secrets Manager)
+  - 开发环境配置加入 `.gitignore`,仅保留 `perm-api-example.yaml` 模板
 
 ---
 
-### M-2:RefreshToken 端点无速率限制
+### M2. CreateUser 不会自动关联产品成员 —— 业务流程断裂
 
-- **描述**:`/api/auth/refreshToken` 在 `routes.go` 中注册时既无 JWT 认证中间件,也无速率限制中间件。
-
-  ```go
-  server.AddRoutes(
-      []rest.Route{
-          {Method: http.MethodPost, Path: "/auth/refreshToken", Handler: pub.RefreshTokenHandler(serverCtx)},
-          {Method: http.MethodPost, Path: "/perm/sync", Handler: pub.SyncPermsHandler(serverCtx)},
-      },
-      rest.WithPrefix("/api"),
-  )
-  ```
-
-- **影响**:如果 RefreshToken 泄漏,攻击者可以无限次调用此端点,持续生成新的 AccessToken,且无法通过限流缓解。
-
-- **修复方案**:为 RefreshToken 端点增加独立的速率限制,基于 token 中的 userId 进行限流。
+- **文件**:`internal/logic/user/createUserLogic.go`
+- **描述**:`CreateUser` 接口要求 `RequireProductAdminFor(productCode)` 校验调用者是产品管理员,但创建用户后**并不自动将新用户加入该产品**。调用方需要额外调用 `AddMember` 接口,形成两步操作。
+- **影响**:
+  - 如果 `CreateUser` 成功但 `AddMember` 失败(网络中断、前端 Bug),系统中会出现"孤儿用户"——用户存在但不属于任何产品,无法登录任何产品
+  - 增加了前端集成的复杂度和出错概率
+- **建议**:在 `CreateUser` 事务中同时插入 `sys_product_member` 记录,或者至少返回一个明确的提示告知前端需要调用 AddMember。
 
 ---
 
-### M-3:部门禁用后不影响其下用户的登录和权限
-
-- **描述**:`sys_dept` 表有 `status` 字段,但整个系统中没有任何地方在加载用户信息时检查其所属部门是否处于启用状态。
-
-  - `userDetailsLoader.go` 的 `loadDept` 不检查部门状态
-  - `loadPerms` 中依赖 `ud.DeptType == consts.DeptTypeDev` 判断是否授予全量权限,也不检查部门是否被禁用
-  - 一个被禁用的 DEV 类型部门下的用户仍然会获得该产品的全部权限
-
-- **影响**:管理员禁用部门后,预期该部门下用户的权限应受到影响(至少 DEV 部门的自动全权限应该失效),但实际上没有任何效果。
+### M3. Model 初始化修改包级变量 —— 非线程安全
 
-- **修复方案**:在 `loadPerms` 中判断 `DeptType` 时,增加部门状态检查:
-
-  ```go
-  if ud.IsSuperAdmin ||
-      ud.MemberType == consts.MemberTypeAdmin ||
-      ud.MemberType == consts.MemberTypeDeveloper ||
-      (ud.DeptType == consts.DeptTypeDev && ud.DeptStatus == consts.StatusEnabled) {
-      // 全量权限
-  }
-  ```
+- **文件**:`internal/model/user/sysUserModel_gen.go:75-80`(所有 model 的 `_gen.go` 均有此问题)
+- **描述**:`newSysUserModel()` 函数在初始化时会修改包级变量 `cacheSysUserIdPrefix` 和 `cacheSysUserUsernamePrefix`。这些变量在包加载时已有初始值,被函数调用覆写。
+  - 虽然当前代码只在 `NewModels()` 中调用一次,不会出现并发问题
+  - 但作为生成代码模板,如果未来存在多实例或单测并行场景,会产生数据竞争
+- **建议**:将 cache prefix 存储在 struct 实例中,而非修改包级变量。由于这是 goctl 生成代码,建议修改 `cli/goctl/model/model-new.tpl` 模板。
 
 ---
 
-### M-4:Redis SCAN 在 Cluster 模式下不兼容
-
-- **描述**:`userDetailsLoader.go` 的 `cleanByPattern` 使用 `SCAN` 命令按通配符模式清除缓存。代码注释已标注此问题,但仍是一个部署风险。
-
-  ```go
-  // NOTE: SCAN only works on single-node Redis. For Redis Cluster, consider using hash tags
-  func (l *UserDetailsLoader) cleanByPattern(ctx context.Context, pattern string) {
-      var cursor uint64
-      for {
-          keys, cur, err := l.rds.ScanCtx(ctx, cursor, pattern, 100)
-          // ...
-      }
-  }
-  ```
-
-  此方法被 `Clean`(清除某用户所有产品缓存)和 `CleanByProduct`(清除某产品所有用户缓存)调用。
+### M4. gRPC Login 的限流器可能为 nil
 
-- **影响**:如果未来 Redis 切换为 Cluster 模式,`SCAN` 只能在单个节点上执行,无法跨节点匹配 key,导致缓存无法正确失效,引发权限数据过期不清除问题。
-
-- **修复方案**:使用 Redis Hash Tag `{tag}` 将相关 key 路由到同一 slot,或改用主动记录关联 key 的方式(如维护一个 Set 记录用户关联的所有缓存 key),清除时通过 Set 成员精确删除
+- **文件**:`internal/server/permserver.go:116-125`
+- **描述**:`Login` 方法中有 `if s.svcCtx.GrpcLoginLimiter != nil` 的判断,说明设计上允许 limiter 为 nil。但在 `servicecontext.go:30` 中 limiter 总是被创建。如果未来配置变更导致 Redis 不可用,limiter 创建会 panic(`redis.MustNewRedis`),而非优雅降级。
+- **建议**:与当前实现保持一致,确保 `GrpcLoginLimiter` 始终非 nil,或在 `NewServiceContext` 中做容错处理。
 
 ---
 
-### M-5:HTTP 与 gRPC 登录逻辑高度重复
-
-- **描述**:`loginLogic.go` 和 `permserver.go Login` 中的登录逻辑几乎完全相同(校验用户名密码、检查状态、检查产品、检查成员关系、生成 Token),但分别独立实现。
-
-- **影响**:当修复上述 H-3(成员状态检查)等问题时,需要同时修改两处代码,容易遗漏。未来任何登录逻辑的变更都需要双重维护。
+### M5. UserDetailsLoader.loadPerms 中研发部门判定可能不符合预期
 
-- **修复方案**:将核心登录逻辑抽取为共享的 service 方法,HTTP handler 和 gRPC server 都调用同一个方法。
+- **文件**:`internal/loaders/userDetailsLoader.go:312-316`
+- **描述**:
 
----
+```go
+if ud.IsSuperAdmin ||
+    ud.MemberType == consts.MemberTypeAdmin ||
+    ud.MemberType == consts.MemberTypeDeveloper ||
+    (ud.DeptType == consts.DeptTypeDev && ud.DeptStatus == consts.StatusEnabled) {
+```
 
-## ⚠️ 健壮性与性能建议 (Low)
+  研发部门(`DeptType == "DEV"`)的判定**不与 productCode 关联**——只要用户所在部门类型是 `DEV` 且部门启用,该用户在**所有产品**下都自动拥有全量权限。这意味着一个被拉进产品 A 的 MEMBER 类型成员,如果碰巧在研发部门,他在产品 A 下拥有的权限和 ADMIN 一样。
+- **影响**:研发部门成员的权限范围可能超出业务预期,与成员类型(MEMBER)赋予的权限不匹配。
+- **建议**:确认此行为是否为设计意图。如果研发部门全量权限仅应作用于特定产品,需增加产品关联判断。
 
 ---
 
-### L-1:SyncPerms 接口无速率限制
-
-- **描述**:`/api/perm/sync` 端点无任何中间件保护(无 JWT、无 RateLimit),仅靠 `appKey + appSecret` 认证。虽然密钥泄漏的概率较低,但一旦泄漏,攻击者可以高频调用此接口触发大量数据库写操作和缓存清除。
+### M6. RefreshToken 不会续签 refreshToken 本身
 
-- **修复方案**:为 SyncPerms 增加基于 appKey 的速率限制。
+- **文件**:`internal/logic/pub/refreshTokenLogic.go:67-68`
+- **描述**:`RefreshToken` 接口返回新的 `accessToken`,但**原样返回旧的 `refreshToken`**。随着时间推移,refreshToken 会过期(7 天),用户被迫重新登录。
+- **影响**:对于需要长期保持登录状态的场景(如桌面客户端、后台管理系统),用户体验不佳——每 7 天必须重新输入密码。
+- **建议**:根据业务需求决定是否在每次刷新时签发新的 refreshToken(滑动过期策略)。如果不续签,应在 API 文档中明确说明 refreshToken 有效期为固定 7 天。
 
 ---
 
-### L-2:CreateProduct 响应暴露 AppSecret 明文
-
-- **描述**:`createProductLogic.go` 创建产品后将 `AppSecret` 明文返回给前端,且 AppSecret 在数据库中也以明文存储。
-
-  ```go
-  return &types.CreateProductResp{
-      AppSecret:     appSecret,       // 明文返回
-      AdminPassword: adminPassword,   // 管理员初始密码明文返回
-  }
-  ```
+### M7. UserDetailsLoader 缓存清理使用 SCAN —— 性能与兼容性风险
 
-- **影响**:AppSecret 相当于产品的 API 密钥,一旦通过网络被截获或日志记录,将可被用于调用 SyncPerms 等无需登录的接口。AdminPassword 同理。
-
-- **修复方案**:这是一次性展示场景(创建后只展示一次),风险可控。建议:
-  1. 确保传输层使用 HTTPS
-  2. 确认日志中不会记录响应体(当前 `response.go` 的错误处理不会记录成功响应体,但需确认 go-zero 框架层面的 access log 配置)
-  3. AppSecret 数据库存储可考虑改为哈希存储,验证时对比哈希值
+- **文件**:`internal/loaders/userDetailsLoader.go:162-180`
+- **描述**:`cleanByPattern` 使用 `SCAN` 命令按 pattern 匹配并删除缓存 key。代码注释中已标注此方法不兼容 Redis Cluster。此外:
+  - `CleanByProduct` 使用 pattern `*:ud:*:{productCode}`,在产品成员较多时可能扫描大量 key
+  - `UpdateDept` 中对每个子部门的每个用户逐个调用 `Clean`,如果部门内有几十个用户,会产生多次 SCAN 操作
+- **建议**:
+  - 如果确定使用单节点 Redis,当前实现可接受
+  - 若考虑未来迁移到 Redis Cluster,建议使用 Hash Tag(如 `{productCode}:ud:userId`)或维护一个 Set 记录某个产品下的所有缓存 key,以支持批量删除
 
 ---
 
-### L-3:UserDetailsLoader.Load 中 singleflight 的 panic 风险
-
-- **描述**:`userDetailsLoader.go` 第 106-118 行:
-
-  ```go
-  v, _, _ := l.sf.Do(key, func() (interface{}, error) {
-      ud, ok := l.loadFromDB(ctx, userId, productCode)
-      if ok {
-          // ... 缓存
-      }
-      return ud, nil
-  })
-  return v.(*UserDetails) // 如果 loadFromDB 的 ud 为 nil,此处会 panic
-  ```
+### M8. 部分接口缺少输入长度校验
 
-  `loadFromDB` 在 `loadUser` 失败时返回 `(ud, false)`,此时 `ud` 不是 nil(是一个初始化过的 `&UserDetails{}`),所以实际上不会 panic。但如果未来重构时 `loadFromDB` 的返回值逻辑变化,此处的类型断言缺乏安全检查。
+- **文件**:各 `createXxxLogic.go`、`updateXxxLogic.go`
+- **描述**:以下字段没有长度校验,但数据库有 `varchar` 长度限制:
+  - `username`(最大 64)、`nickname`(最大 64)、`email`(最大 64)
+  - `productCode`(最大 64)、`productName`(最大 64)
+  - `roleName`(最大 64)、`remark`(最大 255)
+  - `dept.name`(最大 64)、`dept.path`(最大 512)
+  
+  当前未做前端/后端长度校验,超长输入会直接触发 MySQL 的 `Data too long` 错误(1406),返回不友好的 500 错误。
+- **建议**:在 Logic 层统一增加关键字段的长度校验,返回可读的 400 错误信息。
 
-- **修复方案**:使用安全类型断言:
+---
 
-  ```go
-  ud, ok := v.(*UserDetails)
-  if !ok || ud == nil {
-      return &UserDetails{UserId: userId, ProductCode: productCode}
-  }
-  return ud
-  ```
+## 💡 低风险优化建议 (Low)
 
 ---
 
-### L-4:BindRolesLogic 绑定角色未检查目标用户是否为产品成员
-
-- **描述**:`bindRolesLogic.go` 在为用户绑定角色时,只检查用户是否存在和角色是否属于当前产品,但**没有检查目标用户是否是当前产品的成员**。
+### L1. JWT Claims 中存储完整权限列表可能导致 Token 膨胀
 
-  ```go
-  if _, err := l.svcCtx.SysUserModel.FindOne(l.ctx, req.UserId); err != nil {
-      return response.ErrNotFound("用户不存在")
-  }
-  // 缺少:检查用户是否为当前产品成员
-  ```
+- **文件**:`internal/logic/auth/jwt.go:22-38`
+- **描述**:`Claims.Perms` 是 `[]string`,权限 Code 字符串数组被完整编码进 JWT。如果某个产品配置了数百个权限,Token 可能达到数 KB 甚至超过 HTTP Header 限制(通常 8KB)。
+- **建议**:考虑只在 JWT 中存储必要标识(userId、productCode、memberType),权限列表由服务端通过 `UserDetailsLoader` 实时获取(当前中间件已经在这样做)。
 
-- **影响**:可以为非产品成员的用户绑定角色,这些角色在 `loadRoles` 时会被加载但因用户不是成员所以 `loadMembership` 不会设置 MemberType,权限不会实际生效。但数据库中会存在孤立的无效关联数据。
+### L2. DeptTree 返回所有部门包含已禁用的
 
-- **修复方案**:在绑定前校验目标用户是否为当前产品成员。
+- **文件**:`internal/logic/dept/deptTreeLogic.go:27`
+- **描述**:`FindAll` 查询所有部门不区分状态,禁用的部门也会出现在树中。
+- **建议**:根据业务需求,可增加参数控制是否过滤禁用部门,或在返回中标注状态供前端处理。
 
----
+### L3. CreateProduct 返回明文 AdminPassword
 
-### L-5:SetUserPerms 同理未校验产品成员关系
+- **文件**:`internal/logic/product/createProductLogic.go:116-124`
+- **描述**:创建产品时自动生成的管理员密码在 HTTP 响应中明文返回。若响应被日志系统记录(如 access log、网关日志),密码可能泄露。
+- **建议**:确保 API 网关/日志系统不记录响应体,或改为邮件/消息通知的方式下发初始密码。
 
-- **描述**:与 L-4 类似,`setUserPermsLogic.go` 在设置用户权限时没有检查目标用户是否为当前产品的成员,可以为非成员用户设置权限(虽然实际不会生效)。
+### L4. 错误处理中 `errors.As` 与 `==` 混用
 
-- **修复方案**:在设置前校验目标用户是否为当前产品成员。
+- **文件**:`internal/logic/pub/loginService.go:33` 使用 `== user.ErrNotFound`,而 `response.go:47` 使用 `errors.As`
+- **描述**:`ErrNotFound` 比较使用 `==`(值比较),如果未来 ErrNotFound 被 `fmt.Errorf("%w", ...)` 包装,`==` 会失效。
+- **建议**:统一使用 `errors.Is(err, user.ErrNotFound)` 进行哨兵错误判断。
 
----
+### L5. ChangePassword 成功后不会使旧 Token 失效
 
-### L-6:删除部门时忽略了 FindIdsByDeptId 的错误
+- **文件**:`internal/logic/auth/changePasswordLogic.go`
+- **描述**:修改密码后清理了 UserDetails 缓存,但已签发的 Access Token 和 Refresh Token 仍然有效(最长可达 7 天)。如果用户因密码泄露而修改密码,攻击者持有的旧 Token 仍可正常使用。
+- **建议**:引入 Token 版本号(存储在用户记录中),修改密码时递增版本号,中间件校验时比对版本号。
 
-- **描述**:`deleteDeptLogic.go` 第 39 行使用 `_` 忽略了查询错误:
-
-  ```go
-  userIds, _ := l.svcCtx.SysUserModel.FindIdsByDeptId(l.ctx, req.Id)
-  if len(userIds) > 0 {
-      return response.ErrBadRequest("该部门下仍有关联用户,无法删除")
-  }
-  ```
-
-  如果数据库查询出错(比如连接异常),`userIds` 为 nil,`len(userIds)` 为 0,会误判为"无关联用户",导致在仍有用户关联时删除部门。
+---
 
-- **影响**:在数据库异常时可能导致误删有用户关联的部门,造成这些用户的 `deptId` 指向一个已不存在的部门。
+## 📊 审计总结
 
-- **修复方案**:处理错误并在查询失败时阻止删除:
+| 级别 | 数量 | 关键词 |
+|------|------|--------|
+| 🚩 High | 6 | 越权登录、限流失效、水平越权、权限校验不一致、数据校验缺失、接口防护不足 |
+| ⚠️ Medium | 8 | 明文密钥、流程断裂、线程安全、缓存一致性、权限范围、输入校验 |
+| 💡 Low | 5 | Token 膨胀、状态过滤、明文密码返回、错误处理、Token 吊销 |
 
-  ```go
-  userIds, err := l.svcCtx.SysUserModel.FindIdsByDeptId(l.ctx, req.Id)
-  if err != nil {
-      return err
-  }
-  ```
+**优先修复建议**:H1(管理后台越权)→ H2(限流失效)→ H3(水平越权)→ H4(权限不一致)→ M1(密钥管理)→ H5/H6
 
 ---
 
-## 📋 审计总结
-
-| 级别 | 编号 | 问题摘要 | 影响面 |
-|------|------|----------|--------|
-| 🔴 High | H-1 | loadPerms 跨产品权限泄漏 | 数据安全 |
-| 🔴 High | H-2 | UpdateUser 可绕过超管状态保护 | 权限控制 |
-| 🔴 High | H-3 | 禁用成员仍可登录 | 访问控制 |
-| 🔴 High | H-4 | gRPC VerifyToken 不检查实时状态 | 访问控制 |
-| 🔴 High | H-5 | gRPC Login 无速率限制 | 安全防护 |
-| 🟡 Medium | M-1 | Rate Limiter IP 可伪造 | 安全防护 |
-| 🟡 Medium | M-2 | RefreshToken 端点无速率限制 | 安全防护 |
-| 🟡 Medium | M-3 | 部门禁用不影响下属用户权限 | 逻辑一致性 |
-| 🟡 Medium | M-4 | Redis SCAN 不兼容 Cluster | 可部署性 |
-| 🟡 Medium | M-5 | HTTP/gRPC 登录逻辑重复 | 可维护性 |
-| 🟢 Low | L-1 | SyncPerms 无速率限制 | 安全防护 |
-| 🟢 Low | L-2 | CreateProduct 暴露 AppSecret | 信息泄漏 |
-| 🟢 Low | L-3 | singleflight 类型断言无安全检查 | 健壮性 |
-| 🟢 Low | L-4 | BindRoles 未校验产品成员关系 | 数据完整性 |
-| 🟢 Low | L-5 | SetUserPerms 未校验产品成员关系 | 数据完整性 |
-| 🟢 Low | L-6 | 删除部门时忽略查询错误 | 数据完整性 |
-
-**建议优先级**:H-1 > H-3 > H-2 > H-4 > H-5 > M-3 > M-1 > 其余
-
-H-1(跨产品权限泄漏)是最高优先级,因为它会导致静默的权限污染且难以被人工发现。H-3(禁用成员仍可登录)次之,因为管理员执行禁用操作后会产生"已生效"的错觉。
+*本报告基于静态代码审计,未涉及运行时测试和渗透测试。建议在修复后进行集成测试验证。*

+ 2 - 2
gen-model.sh

@@ -47,14 +47,14 @@ split_ddl() {
     local printing=0
 
     while IFS= read -r line; do
-        if echo "$line" | grep -q "DROP TABLE IF EXISTS"; then
+        if echo "$line" | grep -qE "(DROP TABLE IF EXISTS|CREATE TABLE IF NOT EXISTS|CREATE TABLE)"; then
             current_table=$(echo "$line" | sed "s/.*\`\([^\`]*\)\`.*/\1/")
             outfile="$tmp_dir/${current_table}.sql"
             printing=1
             echo "$line" > "$outfile"
         elif [[ $printing -eq 1 ]]; then
             echo "$line" >> "$outfile"
-            if echo "$line" | grep -qE "^\).*ENGINE=.*;" ; then
+            if echo "$line" | grep -qE "^\).*ENGINE" ; then
                 printing=0
             fi
         fi

+ 1 - 1
internal/handler/pub/adminLoginHandler_test.go

@@ -15,7 +15,7 @@ import (
 	"github.com/stretchr/testify/require"
 )
 
-// TC-0508: 缺少必填字段(adminLogin)
+// TC-0023: 缺少必填字段(adminLogin)
 func TestAdminLoginHandler_MissingFields(t *testing.T) {
 	svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
 	handler := AdminLoginHandler(svcCtx)

+ 1 - 1
internal/handler/routes.go

@@ -175,7 +175,7 @@ func RegisterHandlers(server *rest.Server, serverCtx *svc.ServiceContext) {
 
 	server.AddRoutes(
 		rest.WithMiddlewares(
-			[]rest.Middleware{serverCtx.LoginRateLimit},
+			[]rest.Middleware{serverCtx.SyncRateLimit},
 			[]rest.Route{
 				{
 					Method:  http.MethodPost,

+ 60 - 20
internal/loaders/userDetailsLoader.go

@@ -35,6 +35,7 @@ type UserDetails struct {
 	MustChangePassword bool  `json:"mustChangePassword"`
 	MustChangePwdRaw   int64 `json:"mustChangePwdRaw"`
 	Status             int64 `json:"status"`
+	TokenVersion       int64 `json:"tokenVersion"`
 
 	// 部门信息 (sys_dept)
 	DeptId   int64  `json:"deptId"`
@@ -93,6 +94,14 @@ func (l *UserDetailsLoader) cacheKey(userId int64, productCode string) string {
 	return fmt.Sprintf("%s:ud:%d:%s", l.keyPrefix, userId, productCode)
 }
 
+func (l *UserDetailsLoader) userIndexKey(userId int64) string {
+	return fmt.Sprintf("%s:ud:idx:u:%d", l.keyPrefix, userId)
+}
+
+func (l *UserDetailsLoader) productIndexKey(productCode string) string {
+	return fmt.Sprintf("%s:ud:idx:p:%s", l.keyPrefix, productCode)
+}
+
 // Load 根据 userId 和 productCode 加载完整的 UserDetails。
 func (l *UserDetailsLoader) Load(ctx context.Context, userId int64, productCode string) *UserDetails {
 	key := l.cacheKey(userId, productCode)
@@ -111,6 +120,7 @@ func (l *UserDetailsLoader) Load(ctx context.Context, userId int64, productCode
 				if err := l.rds.SetexCtx(ctx, key, string(val), l.ttl); err != nil {
 					logx.WithContext(ctx).Errorf("set user details cache failed: %v", err)
 				}
+				l.registerCacheKey(ctx, key, userId, productCode)
 			}
 		}
 		return ud, nil
@@ -129,18 +139,19 @@ func (l *UserDetailsLoader) Del(ctx context.Context, userId int64, productCode s
 	if _, err := l.rds.DelCtx(ctx, key); err != nil {
 		logx.WithContext(ctx).Errorf("del user details cache [%s] failed: %v", key, err)
 	}
+	l.unregisterCacheKey(ctx, key, userId, productCode)
 }
 
 // Clean 清除指定用户所有产品下的缓存。
 func (l *UserDetailsLoader) Clean(ctx context.Context, userId int64) {
-	pattern := fmt.Sprintf("%s:ud:%d:*", l.keyPrefix, userId)
-	l.cleanByPattern(ctx, pattern)
+	idxKey := l.userIndexKey(userId)
+	l.cleanByIndex(ctx, idxKey)
 }
 
 // CleanByProduct 清除指定产品下所有用户的缓存。
 func (l *UserDetailsLoader) CleanByProduct(ctx context.Context, productCode string) {
-	pattern := fmt.Sprintf("%s:ud:*:%s", l.keyPrefix, productCode)
-	l.cleanByPattern(ctx, pattern)
+	idxKey := l.productIndexKey(productCode)
+	l.cleanByIndex(ctx, idxKey)
 }
 
 // BatchDel 批量删除多个用户在指定产品下的缓存。
@@ -155,27 +166,55 @@ func (l *UserDetailsLoader) BatchDel(ctx context.Context, userIds []int64, produ
 	if _, err := l.rds.DelCtx(ctx, keys...); err != nil {
 		logx.WithContext(ctx).Errorf("batch del user details cache failed: %v", err)
 	}
+	for i, uid := range userIds {
+		l.unregisterCacheKey(ctx, keys[i], uid, productCode)
+	}
 }
 
-// NOTE: SCAN only works on single-node Redis. For Redis Cluster, consider using hash tags
-// in key design or switching to a different cache invalidation strategy.
-func (l *UserDetailsLoader) cleanByPattern(ctx context.Context, pattern string) {
-	var cursor uint64
-	for {
-		keys, cur, err := l.rds.ScanCtx(ctx, cursor, pattern, 100)
-		if err != nil {
-			logx.WithContext(ctx).Errorf("scan keys [%s] failed: %v", pattern, err)
-			return
+func (l *UserDetailsLoader) cleanByIndex(ctx context.Context, indexKey string) {
+	keys, err := l.rds.SmembersCtx(ctx, indexKey)
+	if err != nil {
+		logx.WithContext(ctx).Errorf("smembers [%s] failed: %v", indexKey, err)
+		return
+	}
+	if len(keys) > 0 {
+		if _, err := l.rds.DelCtx(ctx, keys...); err != nil {
+			logx.WithContext(ctx).Errorf("del cached keys failed: %v", err)
 		}
-		if len(keys) > 0 {
-			if _, err := l.rds.DelCtx(ctx, keys...); err != nil {
-				logx.WithContext(ctx).Errorf("del keys failed: %v", err)
-			}
+	}
+	if _, err := l.rds.DelCtx(ctx, indexKey); err != nil {
+		logx.WithContext(ctx).Errorf("del index key [%s] failed: %v", indexKey, err)
+	}
+}
+
+func (l *UserDetailsLoader) registerCacheKey(ctx context.Context, cacheKey string, userId int64, productCode string) {
+	uIdxKey := l.userIndexKey(userId)
+	if _, err := l.rds.SaddCtx(ctx, uIdxKey, cacheKey); err != nil {
+		logx.WithContext(ctx).Errorf("sadd user index failed: %v", err)
+	}
+	if err := l.rds.ExpireCtx(ctx, uIdxKey, l.ttl+60); err != nil {
+		logx.WithContext(ctx).Errorf("expire user index failed: %v", err)
+	}
+
+	if productCode != "" {
+		pIdxKey := l.productIndexKey(productCode)
+		if _, err := l.rds.SaddCtx(ctx, pIdxKey, cacheKey); err != nil {
+			logx.WithContext(ctx).Errorf("sadd product index failed: %v", err)
 		}
-		if cur == 0 {
-			return
+		if err := l.rds.ExpireCtx(ctx, pIdxKey, l.ttl+60); err != nil {
+			logx.WithContext(ctx).Errorf("expire product index failed: %v", err)
+		}
+	}
+}
+
+func (l *UserDetailsLoader) unregisterCacheKey(ctx context.Context, cacheKey string, userId int64, productCode string) {
+	if _, err := l.rds.SremCtx(ctx, l.userIndexKey(userId), cacheKey); err != nil {
+		logx.WithContext(ctx).Errorf("srem user index failed: %v", err)
+	}
+	if productCode != "" {
+		if _, err := l.rds.SremCtx(ctx, l.productIndexKey(productCode), cacheKey); err != nil {
+			logx.WithContext(ctx).Errorf("srem product index failed: %v", err)
 		}
-		cursor = cur
 	}
 }
 
@@ -218,6 +257,7 @@ func (l *UserDetailsLoader) loadUser(ctx context.Context, ud *UserDetails) bool
 	ud.MustChangePwdRaw = u.MustChangePassword
 	ud.MustChangePassword = u.MustChangePassword == consts.MustChangePasswordYes
 	ud.Status = u.Status
+	ud.TokenVersion = u.TokenVersion
 	return true
 }
 

+ 23 - 23
internal/loaders/userDetailsLoader_test.go

@@ -145,7 +145,7 @@ func insertUserPerm(ctx context.Context, t *testing.T, m *model.Models, up *user
 	return id
 }
 
-// --------------- TC-0458: Load-DB加载(缓存miss) ---------------
+// --------------- TC-0485: Load-DB加载(缓存miss) ---------------
 
 func TestLoad_DBMiss(t *testing.T) {
 	ctx := context.Background()
@@ -232,7 +232,7 @@ func TestLoad_DBMiss(t *testing.T) {
 	assert.Contains(t, ud.Perms, "perm:"+uid)
 }
 
-// --------------- TC-0459: Load-缓存命中 ---------------
+// --------------- TC-0486: Load-缓存命中 ---------------
 
 func TestLoad_CacheHit(t *testing.T) {
 	ctx := context.Background()
@@ -275,7 +275,7 @@ func TestLoad_CacheHit(t *testing.T) {
 	assert.Equal(t, ud1.ProductName, ud2.ProductName)
 }
 
-// --------------- TC-0460: Load-用户不存在 ---------------
+// --------------- TC-0487: Load-用户不存在 ---------------
 
 func TestLoad_UserNotExist(t *testing.T) {
 	ctx := context.Background()
@@ -294,7 +294,7 @@ func TestLoad_UserNotExist(t *testing.T) {
 	loader.Del(ctx, nonExistId, "nonexist_product")
 }
 
-// --------------- TC-0461: Load-productCode为空 ---------------
+// --------------- TC-0488: Load-productCode为空 ---------------
 
 func TestLoad_EmptyProductCode(t *testing.T) {
 	ctx := context.Background()
@@ -330,7 +330,7 @@ func TestLoad_EmptyProductCode(t *testing.T) {
 	assert.Empty(t, ud.Perms)
 }
 
-// --------------- TC-0462: Del删除指定缓存 ---------------
+// --------------- TC-0489: Del删除指定缓存 ---------------
 
 func TestDel(t *testing.T) {
 	ctx := context.Background()
@@ -373,7 +373,7 @@ func TestDel(t *testing.T) {
 	assert.Equal(t, uid, ud2.Username)
 }
 
-// --------------- TC-0463: Clean清除用户所有产品缓存 ---------------
+// --------------- TC-0490: Clean清除用户所有产品缓存 ---------------
 
 func TestClean(t *testing.T) {
 	ctx := context.Background()
@@ -434,7 +434,7 @@ func TestClean(t *testing.T) {
 	assert.Empty(t, v2After)
 }
 
-// --------------- TC-0464: CleanByProduct清除产品所有用户 ---------------
+// --------------- TC-0491: CleanByProduct清除产品所有用户 ---------------
 
 func TestCleanByProduct(t *testing.T) {
 	ctx := context.Background()
@@ -495,7 +495,7 @@ func TestCleanByProduct(t *testing.T) {
 	assert.Empty(t, v2After)
 }
 
-// --------------- TC-0465: BatchDel批量删除 ---------------
+// --------------- TC-0492: BatchDel批量删除 ---------------
 
 func TestBatchDel(t *testing.T) {
 	ctx := context.Background()
@@ -555,7 +555,7 @@ func TestBatchDel(t *testing.T) {
 	assert.Empty(t, v2After)
 }
 
-// --------------- TC-0466: BatchDel空数组 ---------------
+// --------------- TC-0493: BatchDel空数组 ---------------
 
 func TestBatchDel_EmptySlice(t *testing.T) {
 	ctx := context.Background()
@@ -563,7 +563,7 @@ func TestBatchDel_EmptySlice(t *testing.T) {
 	loader.BatchDel(ctx, []int64{}, "some_code")
 }
 
-// --------------- TC-0467: loadPerms-超管拥有全部权限 ---------------
+// --------------- TC-0494: loadPerms-超管拥有全部权限 ---------------
 
 func TestLoadPerms_SuperAdmin(t *testing.T) {
 	ctx := context.Background()
@@ -617,7 +617,7 @@ func TestLoadPerms_SuperAdmin(t *testing.T) {
 	assert.Equal(t, expected, ud.Perms)
 }
 
-// --------------- TC-0468: loadPerms-ADMIN成员拥有全部权限 ---------------
+// --------------- TC-0495: loadPerms-ADMIN成员拥有全部权限 ---------------
 
 func TestLoadPerms_AdminMember(t *testing.T) {
 	ctx := context.Background()
@@ -668,7 +668,7 @@ func TestLoadPerms_AdminMember(t *testing.T) {
 	assert.Contains(t, ud.Perms, permCode)
 }
 
-// --------------- TC-0469: loadPerms-DEVELOPER成员拥有全部权限 ---------------
+// --------------- TC-0496: loadPerms-DEVELOPER成员拥有全部权限 ---------------
 
 func TestLoadPerms_DeveloperMember(t *testing.T) {
 	ctx := context.Background()
@@ -719,7 +719,7 @@ func TestLoadPerms_DeveloperMember(t *testing.T) {
 	assert.Contains(t, ud.Perms, permCode)
 }
 
-// --------------- TC-0470: loadPerms-DEV部门成员拥有全部权限 ---------------
+// --------------- TC-0497: loadPerms-DEV部门成员拥有全部权限 ---------------
 
 func TestLoadPerms_DevDept(t *testing.T) {
 	ctx := context.Background()
@@ -777,7 +777,7 @@ func TestLoadPerms_DevDept(t *testing.T) {
 	assert.Contains(t, ud.Perms, permCode)
 }
 
-// --------------- TC-0471: MEMBER角色权限+ALLOW-DENY ---------------
+// --------------- TC-0498: MEMBER角色权限+ALLOW-DENY ---------------
 
 func TestLoadPerms_MemberRolePermWithAllowDeny(t *testing.T) {
 	ctx := context.Background()
@@ -870,7 +870,7 @@ func TestLoadPerms_MemberRolePermWithAllowDeny(t *testing.T) {
 	assert.NotContains(t, ud.Perms, "permB:"+uid)
 }
 
-// --------------- TC-0472: loadRoles-多角色取最小permsLevel ---------------
+// --------------- TC-0501: loadRoles-多角色取最小permsLevel ---------------
 
 func TestLoadRoles_MinPermsLevel(t *testing.T) {
 	ctx := context.Background()
@@ -932,7 +932,7 @@ func TestLoadRoles_MinPermsLevel(t *testing.T) {
 	assert.Equal(t, int64(5), ud.MinPermsLevel)
 }
 
-// --------------- TC-0473: loadRoles-无角色 ---------------
+// --------------- TC-0502: loadRoles-无角色 ---------------
 
 func TestLoadRoles_NoRoles(t *testing.T) {
 	ctx := context.Background()
@@ -969,7 +969,7 @@ func TestLoadRoles_NoRoles(t *testing.T) {
 	assert.Equal(t, int64(math.MaxInt64), ud.MinPermsLevel)
 }
 
-// --------------- TC-0474: loadRoles-角色跨产品过滤 ---------------
+// --------------- TC-0503: loadRoles-角色跨产品过滤 ---------------
 
 func TestLoadRoles_CrossProductFilter(t *testing.T) {
 	ctx := context.Background()
@@ -1038,7 +1038,7 @@ func TestLoadRoles_CrossProductFilter(t *testing.T) {
 	assert.Equal(t, int64(10), ud.MinPermsLevel)
 }
 
-// --------------- TC-0475: loadRoles-禁用角色不计入 ---------------
+// --------------- TC-0504: loadRoles-禁用角色不计入 ---------------
 
 func TestLoadRoles_DisabledRoleExcluded(t *testing.T) {
 	ctx := context.Background()
@@ -1101,7 +1101,7 @@ func TestLoadRoles_DisabledRoleExcluded(t *testing.T) {
 	assert.Equal(t, int64(5), ud.MinPermsLevel)
 }
 
-// --------------- TC-0476: loadMembership-超管自动SUPER_ADMIN ---------------
+// --------------- TC-0505: loadMembership-超管自动SUPER_ADMIN ---------------
 
 func TestLoadMembership_SuperAdminAuto(t *testing.T) {
 	ctx := context.Background()
@@ -1139,7 +1139,7 @@ func TestLoadMembership_SuperAdminAuto(t *testing.T) {
 	assert.Equal(t, consts.MemberTypeSuperAdmin, ud.MemberType)
 }
 
-// --------------- TC-0477: loadMembership-非成员MemberType为空 ---------------
+// --------------- TC-0506: loadMembership-非成员MemberType为空 ---------------
 
 func TestLoadMembership_NonMemberEmpty(t *testing.T) {
 	ctx := context.Background()
@@ -1177,7 +1177,7 @@ func TestLoadMembership_NonMemberEmpty(t *testing.T) {
 	assert.Empty(t, ud.MemberType)
 }
 
-// --------------- TC-0551: loadPerms-用户ALLOW权限不跨产品泄漏(H-1修复验证) ---------------
+// --------------- TC-0499: loadPerms-用户ALLOW权限不跨产品泄漏(H-1修复验证) ---------------
 
 func TestLoadPerms_CrossProductPermIsolation(t *testing.T) {
 	ctx := context.Background()
@@ -1256,7 +1256,7 @@ func TestLoadPerms_CrossProductPermIsolation(t *testing.T) {
 	assert.NotContains(t, udB.Perms, "permA:"+uid, "产品B不应包含产品A的权限(H-1)")
 }
 
-// --------------- TC-0552: loadMembership-禁用成员MemberType为空(H-3修复验证) ---------------
+// --------------- TC-0507: loadMembership-禁用成员MemberType为空(H-3修复验证) ---------------
 
 func TestLoadMembership_DisabledMemberEmpty(t *testing.T) {
 	ctx := context.Background()
@@ -1299,7 +1299,7 @@ func TestLoadMembership_DisabledMemberEmpty(t *testing.T) {
 	assert.Empty(t, ud.MemberType, "禁用成员的MemberType应为空(H-3)")
 }
 
-// --------------- TC-0553: loadPerms-DEV部门禁用后不再拥有全部权限(M-3修复验证) ---------------
+// --------------- TC-0500: loadPerms-DEV部门禁用后不再拥有全部权限(M-3修复验证) ---------------
 
 func TestLoadPerms_DisabledDevDeptNoFullPerms(t *testing.T) {
 	ctx := context.Background()

+ 24 - 24
internal/logic/auth/access_test.go

@@ -28,13 +28,13 @@ import (
 // RequireSuperAdmin
 // =====================================================================
 
-// TC-0435: 超管通过
+// TC-0461: 超管通过
 func TestRequireSuperAdmin_SuperAdmin(t *testing.T) {
 	err := RequireSuperAdmin(ctxhelper.SuperAdminCtx())
 	assert.NoError(t, err)
 }
 
-// TC-0436: ADMIN → 403 "仅超级管理员"
+// TC-0462: ADMIN → 403 "仅超级管理员"
 func TestRequireSuperAdmin_Admin(t *testing.T) {
 	err := RequireSuperAdmin(ctxhelper.AdminCtx("p1"))
 	require.Error(t, err)
@@ -44,7 +44,7 @@ func TestRequireSuperAdmin_Admin(t *testing.T) {
 	assert.Contains(t, ce.Error(), "仅超级管理员")
 }
 
-// TC-0437: MEMBER → 403
+// TC-0463: MEMBER → 403
 func TestRequireSuperAdmin_Member(t *testing.T) {
 	err := RequireSuperAdmin(ctxhelper.MemberCtx("p1"))
 	require.Error(t, err)
@@ -53,7 +53,7 @@ func TestRequireSuperAdmin_Member(t *testing.T) {
 	assert.Equal(t, 403, ce.Code())
 }
 
-// TC-0438: 无 UserDetails → 401 "未登录"
+// TC-0464: 无 UserDetails → 401 "未登录"
 func TestRequireSuperAdmin_NoUserDetails(t *testing.T) {
 	err := RequireSuperAdmin(context.Background())
 	require.Error(t, err)
@@ -67,19 +67,19 @@ func TestRequireSuperAdmin_NoUserDetails(t *testing.T) {
 // RequireProductAdmin
 // =====================================================================
 
-// TC-0439: SuperAdmin → nil
+// TC-0465: SuperAdmin → nil
 func TestRequireProductAdmin_SuperAdmin(t *testing.T) {
 	err := RequireProductAdmin(ctxhelper.SuperAdminCtx())
 	assert.NoError(t, err)
 }
 
-// TC-0440: ADMIN → nil
+// TC-0466: ADMIN → nil
 func TestRequireProductAdmin_Admin(t *testing.T) {
 	err := RequireProductAdmin(ctxhelper.AdminCtx("p1"))
 	assert.NoError(t, err)
 }
 
-// TC-0441: DEVELOPER → 403
+// TC-0467: DEVELOPER → 403
 func TestRequireProductAdmin_Developer(t *testing.T) {
 	err := RequireProductAdmin(ctxhelper.DeveloperCtx("p1"))
 	require.Error(t, err)
@@ -88,7 +88,7 @@ func TestRequireProductAdmin_Developer(t *testing.T) {
 	assert.Equal(t, 403, ce.Code())
 }
 
-// TC-0442: MEMBER → 403
+// TC-0468: MEMBER → 403
 func TestRequireProductAdmin_Member(t *testing.T) {
 	err := RequireProductAdmin(ctxhelper.MemberCtx("p1"))
 	require.Error(t, err)
@@ -97,7 +97,7 @@ func TestRequireProductAdmin_Member(t *testing.T) {
 	assert.Equal(t, 403, ce.Code())
 }
 
-// TC-0443: 无 UserDetails → 401
+// TC-0469: 无 UserDetails → 401
 func TestRequireProductAdmin_NoUserDetails(t *testing.T) {
 	err := RequireProductAdmin(context.Background())
 	require.Error(t, err)
@@ -111,19 +111,19 @@ func TestRequireProductAdmin_NoUserDetails(t *testing.T) {
 // CheckMemberTypeAssignment
 // =====================================================================
 
-// TC-0444: 超管分配 ADMIN → nil
+// TC-0470: 超管分配 ADMIN → nil
 func TestCheckMemberTypeAssignment_SuperAdminAssignsAdmin(t *testing.T) {
 	err := CheckMemberTypeAssignment(ctxhelper.SuperAdminCtx(), consts.MemberTypeAdmin)
 	assert.NoError(t, err)
 }
 
-// TC-0445: ADMIN 分配 DEVELOPER → nil
+// TC-0471: ADMIN 分配 DEVELOPER → nil
 func TestCheckMemberTypeAssignment_AdminAssignsDeveloper(t *testing.T) {
 	err := CheckMemberTypeAssignment(ctxhelper.AdminCtx("p1"), consts.MemberTypeDeveloper)
 	assert.NoError(t, err)
 }
 
-// TC-0446: ADMIN 分配 ADMIN(同级)→ 403
+// TC-0472: ADMIN 分配 ADMIN(同级)→ 403
 func TestCheckMemberTypeAssignment_AdminAssignsAdmin(t *testing.T) {
 	err := CheckMemberTypeAssignment(ctxhelper.AdminCtx("p1"), consts.MemberTypeAdmin)
 	require.Error(t, err)
@@ -132,7 +132,7 @@ func TestCheckMemberTypeAssignment_AdminAssignsAdmin(t *testing.T) {
 	assert.Equal(t, 403, ce.Code())
 }
 
-// TC-0447: DEVELOPER 分配 ADMIN(更高级)→ 403
+// TC-0473: DEVELOPER 分配 ADMIN(更高级)→ 403
 func TestCheckMemberTypeAssignment_DeveloperAssignsAdmin(t *testing.T) {
 	err := CheckMemberTypeAssignment(ctxhelper.DeveloperCtx("p1"), consts.MemberTypeAdmin)
 	require.Error(t, err)
@@ -141,7 +141,7 @@ func TestCheckMemberTypeAssignment_DeveloperAssignsAdmin(t *testing.T) {
 	assert.Equal(t, 403, ce.Code())
 }
 
-// TC-0448: MEMBER 分配 MEMBER(同级)→ 403
+// TC-0474: MEMBER 分配 MEMBER(同级)→ 403
 func TestCheckMemberTypeAssignment_MemberAssignsMember(t *testing.T) {
 	err := CheckMemberTypeAssignment(ctxhelper.MemberCtx("p1"), consts.MemberTypeMember)
 	require.Error(t, err)
@@ -150,7 +150,7 @@ func TestCheckMemberTypeAssignment_MemberAssignsMember(t *testing.T) {
 	assert.Equal(t, 403, ce.Code())
 }
 
-// TC-0449: 无 UserDetails → 401
+// TC-0475: 无 UserDetails → 401
 func TestCheckMemberTypeAssignment_NoUserDetails(t *testing.T) {
 	err := CheckMemberTypeAssignment(context.Background(), consts.MemberTypeMember)
 	require.Error(t, err)
@@ -164,7 +164,7 @@ func TestCheckMemberTypeAssignment_NoUserDetails(t *testing.T) {
 // memberTypePriority (未导出,同包可测)
 // =====================================================================
 
-// TC-0457: 所有成员类型返回正确优先级
+// TC-0484: 所有成员类型返回正确优先级
 func TestMemberTypePriority(t *testing.T) {
 	tests := []struct {
 		name       string
@@ -221,7 +221,7 @@ func createTestUser(t *testing.T, ctx context.Context, svcCtx *svc.ServiceContex
 	return id
 }
 
-// TC-0450: SuperAdmin 跳过所有检查
+// TC-0476: SuperAdmin 跳过所有检查
 func TestCheckManageAccess_SuperAdminBypasses(t *testing.T) {
 	ctx := context.Background()
 	svcCtx := newIntegrationSvcCtx()
@@ -244,7 +244,7 @@ func TestCheckManageAccess_SuperAdminBypasses(t *testing.T) {
 	assert.NoError(t, err)
 }
 
-// TC-0451: 操作自己豁免
+// TC-0477: 操作自己豁免
 func TestCheckManageAccess_SelfManagement(t *testing.T) {
 	selfCtx := ctxhelper.CustomCtx(&loaders.UserDetails{
 		UserId:       100,
@@ -262,7 +262,7 @@ func TestCheckManageAccess_SelfManagement(t *testing.T) {
 	assert.NoError(t, err)
 }
 
-// TC-0452: ADMIN 跳过部门层级检查
+// TC-0478: ADMIN 跳过部门层级检查
 func TestCheckManageAccess_AdminSkipsDeptCheck(t *testing.T) {
 	ctx := context.Background()
 	svcCtx := newIntegrationSvcCtx()
@@ -308,7 +308,7 @@ func TestCheckManageAccess_AdminSkipsDeptCheck(t *testing.T) {
 	assert.NoError(t, err)
 }
 
-// TC-0453: 无 UserDetails → 401
+// TC-0479: 无 UserDetails → 401
 func TestCheckManageAccess_NoUserDetails(t *testing.T) {
 	svcCtx := newIntegrationSvcCtx()
 	err := CheckManageAccess(context.Background(), svcCtx, 1, "p1")
@@ -318,7 +318,7 @@ func TestCheckManageAccess_NoUserDetails(t *testing.T) {
 	assert.Equal(t, 401, ce.Code())
 }
 
-// TC-0454: DEVELOPER 无部门归属 → 403
+// TC-0480: DEVELOPER 无部门归属 → 403
 func TestCheckManageAccess_NoDept(t *testing.T) {
 	svcCtx := newIntegrationSvcCtx()
 
@@ -342,7 +342,7 @@ func TestCheckManageAccess_NoDept(t *testing.T) {
 	assert.Contains(t, ce.Error(), "未归属任何部门")
 }
 
-// TC-0455: DEVELOPER 操作不同部门的用户 → 403
+// TC-0481: DEVELOPER 操作不同部门的用户 → 403
 func TestCheckManageAccess_CrossDeptForbidden(t *testing.T) {
 	ctx := context.Background()
 	svcCtx := newIntegrationSvcCtx()
@@ -408,7 +408,7 @@ func TestCheckManageAccess_CrossDeptForbidden(t *testing.T) {
 	assert.Equal(t, 403, ce.Code())
 }
 
-// TC-0523: caller.DeptPath为空时拒绝
+// TC-0483: caller.DeptPath为空时拒绝
 func TestCheckManageAccess_EmptyDeptPath(t *testing.T) {
 	svcCtx := newIntegrationSvcCtx()
 
@@ -432,7 +432,7 @@ func TestCheckManageAccess_EmptyDeptPath(t *testing.T) {
 	assert.Contains(t, ce.Error(), "部门信息异常")
 }
 
-// TC-0456: DEVELOPER 操作同部门的 MEMBER 且权限级别更高 → nil
+// TC-0482: DEVELOPER 操作同部门的 MEMBER 且权限级别更高 → nil
 func TestCheckManageAccess_SameDeptLowerLevel(t *testing.T) {
 	ctx := context.Background()
 	svcCtx := newIntegrationSvcCtx()

+ 10 - 10
internal/logic/auth/changePasswordLogic_test.go

@@ -49,7 +49,7 @@ func insertTestUser(t *testing.T, ctx context.Context, username, password string
 	return id
 }
 
-// TC-0035: 正常修改
+// TC-0050: 正常修改
 func TestChangePassword_Success(t *testing.T) {
 	svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
 	conn := testutil.GetTestSqlConn()
@@ -74,7 +74,7 @@ func TestChangePassword_Success(t *testing.T) {
 	assert.NoError(t, bcrypt.CompareHashAndPassword([]byte(updated.Password), []byte(newPwd)))
 }
 
-// TC-0036: mustChangePassword重置
+// TC-0051: mustChangePassword重置
 func TestChangePassword_MustChangePasswordReset(t *testing.T) {
 	svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
 	conn := testutil.GetTestSqlConn()
@@ -99,7 +99,7 @@ func TestChangePassword_MustChangePasswordReset(t *testing.T) {
 	assert.Equal(t, int64(2), updated.MustChangePassword)
 }
 
-// TC-0037: 原密码错误
+// TC-0052: 原密码错误
 func TestChangePassword_WrongOldPassword(t *testing.T) {
 	svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
 	conn := testutil.GetTestSqlConn()
@@ -122,7 +122,7 @@ func TestChangePassword_WrongOldPassword(t *testing.T) {
 	assert.Equal(t, "原密码错误", codeErr.Error())
 }
 
-// TC-0038: 新密码少于6字符
+// TC-0053: 新密码少于6字符
 func TestChangePassword_NewPasswordTooShort(t *testing.T) {
 	svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
 
@@ -138,7 +138,7 @@ func TestChangePassword_NewPasswordTooShort(t *testing.T) {
 	assert.Equal(t, "密码长度不能少于6个字符", codeErr.Error())
 }
 
-// TC-0039: 新密码恰好6字符
+// TC-0054: 新密码恰好6字符
 func TestChangePassword_NewPasswordExactly6Chars(t *testing.T) {
 	svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
 	conn := testutil.GetTestSqlConn()
@@ -163,7 +163,7 @@ func TestChangePassword_NewPasswordExactly6Chars(t *testing.T) {
 	assert.NoError(t, bcrypt.CompareHashAndPassword([]byte(updated.Password), []byte(newPwd)))
 }
 
-// TC-0040: 新密码空字符串
+// TC-0055: 新密码空字符串
 func TestChangePassword_NewPasswordEmpty(t *testing.T) {
 	svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
 
@@ -179,7 +179,7 @@ func TestChangePassword_NewPasswordEmpty(t *testing.T) {
 	assert.Equal(t, "密码长度不能少于6个字符", codeErr.Error())
 }
 
-// TC-0041: 新密码超过72字符
+// TC-0056: 新密码超过72字符
 func TestChangePassword_NewPasswordTooLong(t *testing.T) {
 	svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
 
@@ -196,7 +196,7 @@ func TestChangePassword_NewPasswordTooLong(t *testing.T) {
 	assert.Equal(t, "密码长度不能超过72个字符", codeErr.Error())
 }
 
-// TC-0042: 新密码恰好72字符
+// TC-0057: 新密码恰好72字符
 func TestChangePassword_NewPasswordExactly72Chars(t *testing.T) {
 	svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
 	conn := testutil.GetTestSqlConn()
@@ -221,7 +221,7 @@ func TestChangePassword_NewPasswordExactly72Chars(t *testing.T) {
 	assert.NoError(t, bcrypt.CompareHashAndPassword([]byte(updated.Password), []byte(newPwd)))
 }
 
-// TC-0043: 新旧密码相同
+// TC-0058: 新旧密码相同
 func TestChangePassword_SameOldAndNew(t *testing.T) {
 	svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
 	conn := testutil.GetTestSqlConn()
@@ -245,7 +245,7 @@ func TestChangePassword_SameOldAndNew(t *testing.T) {
 	assert.Equal(t, "新密码不能与原密码相同", codeErr.Error())
 }
 
-// TC-0044: 用户不存在
+// TC-0059: 用户不存在
 func TestChangePassword_UserNotFound(t *testing.T) {
 	svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
 

+ 17 - 14
internal/logic/auth/jwt.go

@@ -13,21 +13,23 @@ import (
 var ErrTokenTypeMismatch = errors.New("token type mismatch")
 
 type RefreshClaims struct {
-	TokenType   string `json:"tokenType"`
-	UserId      int64  `json:"userId"`
-	ProductCode string `json:"productCode"`
+	TokenType    string `json:"tokenType"`
+	UserId       int64  `json:"userId"`
+	ProductCode  string `json:"productCode"`
+	TokenVersion int64  `json:"tokenVersion"`
 	jwt.RegisteredClaims
 }
 
-func GenerateAccessToken(secret string, expireSeconds int64, userId int64, username, productCode, memberType string, perms []string) (string, error) {
+func GenerateAccessToken(secret string, expireSeconds int64, userId int64, username, productCode, memberType string, perms []string, tokenVersion int64) (string, error) {
 	now := time.Now()
 	claims := middleware.Claims{
-		TokenType:   consts.TokenTypeAccess,
-		UserId:      userId,
-		Username:    username,
-		ProductCode: productCode,
-		MemberType:  memberType,
-		Perms:       perms,
+		TokenType:    consts.TokenTypeAccess,
+		UserId:       userId,
+		Username:     username,
+		ProductCode:  productCode,
+		MemberType:   memberType,
+		TokenVersion: tokenVersion,
+		Perms:        perms,
 		RegisteredClaims: jwt.RegisteredClaims{
 			ExpiresAt: jwt.NewNumericDate(now.Add(time.Duration(expireSeconds) * time.Second)),
 			IssuedAt:  jwt.NewNumericDate(now),
@@ -37,12 +39,13 @@ func GenerateAccessToken(secret string, expireSeconds int64, userId int64, usern
 	return token.SignedString([]byte(secret))
 }
 
-func GenerateRefreshToken(secret string, expireSeconds int64, userId int64, productCode string) (string, error) {
+func GenerateRefreshToken(secret string, expireSeconds int64, userId int64, productCode string, tokenVersion int64) (string, error) {
 	now := time.Now()
 	claims := RefreshClaims{
-		TokenType:   consts.TokenTypeRefresh,
-		UserId:      userId,
-		ProductCode: productCode,
+		TokenType:    consts.TokenTypeRefresh,
+		UserId:       userId,
+		ProductCode:  productCode,
+		TokenVersion: tokenVersion,
 		RegisteredClaims: jwt.RegisteredClaims{
 			ExpiresAt: jwt.NewNumericDate(now.Add(time.Duration(expireSeconds) * time.Second)),
 			IssuedAt:  jwt.NewNumericDate(now),

+ 13 - 13
internal/logic/auth/jwt_test.go

@@ -13,7 +13,7 @@ import (
 
 const testSecret = "test-jwt-secret-key"
 
-// TC-0217: secret="s", expire=3600, userId=1, username="u", productCode="p", memberType="M", perms=["a"]
+// TC-0257: secret="s", expire=3600, userId=1, username="u", productCode="p", memberType="M", perms=["a"]
 func TestGenerateAccessToken(t *testing.T) {
 	tests := []struct {
 		name        string
@@ -69,7 +69,7 @@ func TestGenerateAccessToken(t *testing.T) {
 
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
-			tokenStr, err := GenerateAccessToken(tt.secret, tt.expire, tt.userId, tt.username, tt.productCode, tt.memberType, tt.perms)
+			tokenStr, err := GenerateAccessToken(tt.secret, tt.expire, tt.userId, tt.username, tt.productCode, tt.memberType, tt.perms, 0)
 			require.NoError(t, err)
 			assert.NotEmpty(t, tokenStr)
 
@@ -94,9 +94,9 @@ func TestGenerateAccessToken(t *testing.T) {
 	}
 }
 
-// TC-0221: expireSeconds=1, sleep 2s
+// TC-0261: expireSeconds=1, sleep 2s
 func TestGenerateAccessToken_Expiry(t *testing.T) {
-	tokenStr, err := GenerateAccessToken(testSecret, 1, 1, "u", "", "", nil)
+	tokenStr, err := GenerateAccessToken(testSecret, 1, 1, "u", "", "", nil, 0)
 	require.NoError(t, err)
 
 	time.Sleep(2 * time.Second)
@@ -108,7 +108,7 @@ func TestGenerateAccessToken_Expiry(t *testing.T) {
 	assert.Contains(t, err.Error(), "token is expired")
 }
 
-// TC-0222: secret="s", expire=86400, userId=1, productCode="p"
+// TC-0262: secret="s", expire=86400, userId=1, productCode="p"
 func TestGenerateRefreshToken(t *testing.T) {
 	tests := []struct {
 		name        string
@@ -123,7 +123,7 @@ func TestGenerateRefreshToken(t *testing.T) {
 
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
-			tokenStr, err := GenerateRefreshToken(tt.secret, tt.expire, tt.userId, tt.productCode)
+			tokenStr, err := GenerateRefreshToken(tt.secret, tt.expire, tt.userId, tt.productCode, 0)
 			require.NoError(t, err)
 			assert.NotEmpty(t, tokenStr)
 
@@ -135,9 +135,9 @@ func TestGenerateRefreshToken(t *testing.T) {
 	}
 }
 
-// TC-0225: 有效token+正确secret
+// TC-0265: 有效token+正确secret
 func TestParseRefreshToken(t *testing.T) {
-	validToken, err := GenerateRefreshToken(testSecret, 3600, 42, "prod")
+	validToken, err := GenerateRefreshToken(testSecret, 3600, 42, "prod", 0)
 	require.NoError(t, err)
 
 	t.Run("valid token", func(t *testing.T) {
@@ -163,25 +163,25 @@ func TestParseRefreshToken(t *testing.T) {
 	})
 
 	t.Run("expired token", func(t *testing.T) {
-		expiredToken, err := GenerateRefreshToken(testSecret, 1, 1, "p")
+		expiredToken, err := GenerateRefreshToken(testSecret, 1, 1, "p", 0)
 		require.NoError(t, err)
 		time.Sleep(2 * time.Second)
 		_, err = ParseRefreshToken(expiredToken, testSecret)
 		assert.Error(t, err)
 	})
 
-	// TC-0230: AccessToken误用 — TokenType校验拒绝
+	// TC-0270: AccessToken误用 — TokenType校验拒绝
 	t.Run("access token used as refresh - should be rejected", func(t *testing.T) {
-		accessToken, err := GenerateAccessToken(testSecret, 3600, 1, "u", "p", "M", []string{"a"})
+		accessToken, err := GenerateAccessToken(testSecret, 3600, 1, "u", "p", "M", []string{"a"}, 0)
 		require.NoError(t, err)
 		_, err = ParseRefreshToken(accessToken, testSecret)
 		assert.Error(t, err, "BUG-002: access token 不应被 ParseRefreshToken 接受,应通过 TokenType 字段区分")
 	})
 }
 
-// TC-0219: secret=""
+// TC-0259: secret=""
 func TestGenerateAccessToken_EmptySecret(t *testing.T) {
-	tokenStr, err := GenerateAccessToken("", 3600, 1, "u", "p", "M", []string{"a"})
+	tokenStr, err := GenerateAccessToken("", 3600, 1, "u", "p", "M", []string{"a"}, 0)
 	require.NoError(t, err)
 	assert.NotEmpty(t, tokenStr)
 

+ 1 - 1
internal/logic/auth/perms_mock_test.go

@@ -15,7 +15,7 @@ import (
 	"github.com/stretchr/testify/require"
 )
 
-// TC-0534: GetUserPerms 委托到 UserDetailsLoader.Load()
+// TC-0271: GetUserPerms 委托到 UserDetailsLoader.Load()
 func TestGetUserPerms_DelegatesToLoader(t *testing.T) {
 	ctx := context.Background()
 	svcCtx := newTestSvcCtx()

+ 16 - 16
internal/logic/auth/perms_test.go

@@ -51,7 +51,7 @@ func createPermsTestUser(t *testing.T, ctx context.Context, svcCtx *svc.ServiceC
 	return id, func() { testutil.CleanTable(ctx, conn, "`sys_user`", id) }
 }
 
-// TC-0231: superAdmin gets all enabled perms
+// TC-0271: superAdmin gets all enabled perms
 func TestGetUserPerms_SuperAdmin(t *testing.T) {
 	ctx := context.Background()
 	svcCtx := newTestSvcCtx()
@@ -90,7 +90,7 @@ func TestGetUserPerms_SuperAdmin(t *testing.T) {
 	assert.ElementsMatch(t, []string{"sa_code1", "sa_code2"}, perms)
 }
 
-// TC-0232: superAdmin with empty product
+// TC-0271: superAdmin with empty product
 func TestGetUserPerms_SuperAdmin_EmptyProduct(t *testing.T) {
 	ctx := context.Background()
 	svcCtx := newTestSvcCtx()
@@ -104,7 +104,7 @@ func TestGetUserPerms_SuperAdmin_EmptyProduct(t *testing.T) {
 	assert.Empty(t, perms)
 }
 
-// TC-0233: non product member
+// TC-0271: non product member
 func TestGetUserPerms_NotProductMember(t *testing.T) {
 	ctx := context.Background()
 	svcCtx := newTestSvcCtx()
@@ -118,7 +118,7 @@ func TestGetUserPerms_NotProductMember(t *testing.T) {
 	assert.Nil(t, perms)
 }
 
-// TC-0235: DEVELOPER member
+// TC-0271: DEVELOPER member
 func TestGetUserPerms_Developer(t *testing.T) {
 	ctx := context.Background()
 	svcCtx := newTestSvcCtx()
@@ -152,7 +152,7 @@ func TestGetUserPerms_Developer(t *testing.T) {
 	assert.Contains(t, perms, "dev_c1")
 }
 
-// TC-0236: ADMIN member
+// TC-0271: ADMIN member
 func TestGetUserPerms_Admin(t *testing.T) {
 	ctx := context.Background()
 	svcCtx := newTestSvcCtx()
@@ -186,7 +186,7 @@ func TestGetUserPerms_Admin(t *testing.T) {
 	assert.Contains(t, perms, "adm_c1")
 }
 
-// TC-0243: MEMBER no roles no user perms
+// TC-0271: MEMBER no roles no user perms
 func TestGetUserPerms_Member_NoRolesNoUserPerms(t *testing.T) {
 	ctx := context.Background()
 	svcCtx := newTestSvcCtx()
@@ -213,7 +213,7 @@ func TestGetUserPerms_Member_NoRolesNoUserPerms(t *testing.T) {
 	assert.Empty(t, perms)
 }
 
-// TC-0244: MEMBER with roles
+// TC-0271: MEMBER with roles
 func TestGetUserPerms_Member_WithRoles(t *testing.T) {
 	ctx := context.Background()
 	svcCtx := newTestSvcCtx()
@@ -284,7 +284,7 @@ func TestGetUserPerms_Member_WithRoles(t *testing.T) {
 	assert.ElementsMatch(t, []string{p1.Code, p2.Code}, perms)
 }
 
-// TC-0248: DENY overrides role perm
+// TC-0271: DENY overrides role perm
 func TestGetUserPerms_Member_DENYOverridesRolePerm(t *testing.T) {
 	ctx := context.Background()
 	svcCtx := newTestSvcCtx()
@@ -360,7 +360,7 @@ func TestGetUserPerms_Member_DENYOverridesRolePerm(t *testing.T) {
 	assert.Equal(t, []string{permB.Code}, permsResult)
 }
 
-// TC-0247: ALLOW adds extra perm
+// TC-0271: ALLOW adds extra perm
 func TestGetUserPerms_Member_ALLOWAddsExtra(t *testing.T) {
 	ctx := context.Background()
 	svcCtx := newTestSvcCtx()
@@ -404,7 +404,7 @@ func TestGetUserPerms_Member_ALLOWAddsExtra(t *testing.T) {
 	assert.Contains(t, permsResult, permObj.Code)
 }
 
-// TC-0245: cross-product role filter
+// TC-0271: cross-product role filter
 func TestGetUserPerms_Member_CrossProductRoleFilter(t *testing.T) {
 	ctx := context.Background()
 	svcCtx := newTestSvcCtx()
@@ -478,7 +478,7 @@ func TestGetUserPerms_Member_CrossProductRoleFilter(t *testing.T) {
 	assert.Equal(t, []string{targetPerm.Code}, permsResult)
 }
 
-// TC-0246: disabled role filtered
+// TC-0271: disabled role filtered
 func TestGetUserPerms_Member_DisabledRoleFiltered(t *testing.T) {
 	ctx := context.Background()
 	svcCtx := newTestSvcCtx()
@@ -529,7 +529,7 @@ func TestGetUserPerms_Member_DisabledRoleFiltered(t *testing.T) {
 	assert.Empty(t, permsResult)
 }
 
-// TC-0251: disabled perm filtered
+// TC-0271: disabled perm filtered
 func TestGetUserPerms_Member_DisabledPermFiltered(t *testing.T) {
 	ctx := context.Background()
 	svcCtx := newTestSvcCtx()
@@ -570,7 +570,7 @@ func TestGetUserPerms_Member_DisabledPermFiltered(t *testing.T) {
 	assert.Empty(t, permsResult)
 }
 
-// TC-0249: DENY only excludes target perm
+// TC-0271: DENY only excludes target perm
 func TestGetUserPerms_Member_DENYOnlyExcludesTargetPerm(t *testing.T) {
 	ctx := context.Background()
 	svcCtx := newTestSvcCtx()
@@ -626,7 +626,7 @@ func TestGetUserPerms_Member_DENYOnlyExcludesTargetPerm(t *testing.T) {
 	assert.NotContains(t, permsResult, permB.Code, "DENY perm should be excluded even if it exists")
 }
 
-// TC-0250: ALLOW + role dedup
+// TC-0271: ALLOW + role dedup
 func TestGetUserPerms_Member_ALLOWAndRoleDedup(t *testing.T) {
 	ctx := context.Background()
 	svcCtx := newTestSvcCtx()
@@ -691,7 +691,7 @@ func TestGetUserPerms_Member_ALLOWAndRoleDedup(t *testing.T) {
 	assert.Equal(t, permObj.Code, permsResult[0])
 }
 
-// TC-0238: DEV dept member gets all perms
+// TC-0271: DEV dept member gets all perms
 func TestGetUserPerms_Member_DevDept_AllPerms(t *testing.T) {
 	ctx := context.Background()
 	svcCtx := newTestSvcCtx()
@@ -742,7 +742,7 @@ func TestGetUserPerms_Member_DevDept_AllPerms(t *testing.T) {
 	assert.ElementsMatch(t, []string{p1.Code, p2.Code}, permsResult)
 }
 
-// TC-0240: NORMAL dept member no auto perms
+// TC-0271: NORMAL dept member no auto perms
 func TestGetUserPerms_Member_NormalDept_NoAutoPerms(t *testing.T) {
 	ctx := context.Background()
 	svcCtx := newTestSvcCtx()

+ 4 - 4
internal/logic/auth/userInfoLogic_test.go

@@ -21,7 +21,7 @@ import (
 	"github.com/stretchr/testify/require"
 )
 
-// TC-0030: 正常获取-含productCode
+// TC-0045: 正常获取-含productCode
 func TestUserInfo_WithProductCode(t *testing.T) {
 	svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
 	conn := testutil.GetTestSqlConn()
@@ -87,7 +87,7 @@ func TestUserInfo_WithProductCode(t *testing.T) {
 	assert.Contains(t, info.Perms, permObj.Code)
 }
 
-// TC-0031: 不含productCode
+// TC-0046: 不含productCode
 func TestUserInfo_WithoutProductCode(t *testing.T) {
 	svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
 	conn := testutil.GetTestSqlConn()
@@ -130,7 +130,7 @@ func TestUserInfo_WithoutProductCode(t *testing.T) {
 	assert.Empty(t, info.MemberType)
 }
 
-// TC-0033: context中无UserDetails(未登录)
+// TC-0048: context中无UserDetails(未登录)
 func TestUserInfo_UserIdZero(t *testing.T) {
 	svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
 
@@ -144,7 +144,7 @@ func TestUserInfo_UserIdZero(t *testing.T) {
 	assert.Equal(t, "未登录", codeErr.Error())
 }
 
-// TC-0034: context中无UserDetails(中间件未注入)
+// TC-0049: context中无UserDetails(中间件未注入)
 func TestUserInfo_UserNotFound(t *testing.T) {
 	svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
 

+ 7 - 0
internal/logic/dept/createDeptLogic.go

@@ -35,6 +35,13 @@ func (l *CreateDeptLogic) CreateDept(req *types.CreateDeptReq) (resp *types.IdRe
 		return nil, err
 	}
 
+	if len(req.Name) > 64 {
+		return nil, response.ErrBadRequest("部门名称长度不能超过64个字符")
+	}
+	if len(req.Remark) > 255 {
+		return nil, response.ErrBadRequest("备注长度不能超过255个字符")
+	}
+
 	parentPath := "/"
 	if req.ParentId > 0 {
 		parent, err := l.svcCtx.SysDeptModel.FindOne(l.ctx, req.ParentId)

+ 4 - 4
internal/logic/dept/createDeptLogic_mock_test.go

@@ -20,7 +20,7 @@ type fakeResult struct{ id int64 }
 func (r fakeResult) LastInsertId() (int64, error) { return r.id, nil }
 func (r fakeResult) RowsAffected() (int64, error) { return 1, nil }
 
-// TC-0066: 事务回滚-Insert失败
+// TC-0085: 事务回滚-Insert失败
 func TestCreateDept_Mock_InsertWithTxFail(t *testing.T) {
 	ctrl := gomock.NewController(t)
 	defer ctrl.Finish()
@@ -50,7 +50,7 @@ func TestCreateDept_Mock_InsertWithTxFail(t *testing.T) {
 	assert.Nil(t, resp)
 }
 
-// TC-0063: 不传DeptType默认NORMAL
+// TC-0082: 不传DeptType默认NORMAL
 func TestCreateDept_Mock_DefaultDeptType(t *testing.T) {
 	ctrl := gomock.NewController(t)
 	defer ctrl.Finish()
@@ -80,7 +80,7 @@ func TestCreateDept_Mock_DefaultDeptType(t *testing.T) {
 	assert.NotNil(t, resp)
 }
 
-// TC-0064: 传DeptType=DEV
+// TC-0083: 传DeptType=DEV
 func TestCreateDept_Mock_DevDeptType(t *testing.T) {
 	ctrl := gomock.NewController(t)
 	defer ctrl.Finish()
@@ -111,7 +111,7 @@ func TestCreateDept_Mock_DevDeptType(t *testing.T) {
 	assert.NotNil(t, resp)
 }
 
-// TC-0067: 事务回滚-UpdateWithTx失败
+// TC-0086: 事务回滚-UpdateWithTx失败
 func TestCreateDept_Mock_UpdateWithTxFail(t *testing.T) {
 	ctrl := gomock.NewController(t)
 	defer ctrl.Finish()

+ 9 - 9
internal/logic/dept/createDeptLogic_test.go

@@ -43,7 +43,7 @@ func insertDeptRaw(ctx context.Context, svcCtx *svc.ServiceContext, parentId int
 	return id, svcCtx.SysDeptModel.Update(ctx, d)
 }
 
-// TC-0062: 父部门不存在
+// TC-0081: 父部门不存在
 func TestCreateDept_ParentNotFound(t *testing.T) {
 	ctx := ctxhelper.SuperAdminCtx()
 	svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
@@ -62,7 +62,7 @@ func TestCreateDept_ParentNotFound(t *testing.T) {
 	assert.Contains(t, ce.Error(), "父部门不存在")
 }
 
-// TC-0060: 创建顶级部门
+// TC-0079: 创建顶级部门
 func TestCreateDept_TopLevel_ViaRawInsert(t *testing.T) {
 	ctx := context.Background()
 	svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
@@ -82,7 +82,7 @@ func TestCreateDept_TopLevel_ViaRawInsert(t *testing.T) {
 	assert.Equal(t, int64(1), dept.Status)
 }
 
-// TC-0061: 创建子部门
+// TC-0080: 创建子部门
 func TestCreateDept_Child_ViaRawInsert(t *testing.T) {
 	ctx := context.Background()
 	svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
@@ -103,7 +103,7 @@ func TestCreateDept_Child_ViaRawInsert(t *testing.T) {
 	assert.Equal(t, fmt.Sprintf("/%d/%d/", parentId, childId), child.Path)
 }
 
-// TC-0068: 多层嵌套(5层)
+// TC-0087: 多层嵌套(5层)
 func TestCreateDept_MultiLevel(t *testing.T) {
 	ctx := context.Background()
 	svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
@@ -127,7 +127,7 @@ func TestCreateDept_MultiLevel(t *testing.T) {
 	assert.Equal(t, fmt.Sprintf("/%d/%d/%d/", l1Id, l2Id, l3Id), d3.Path)
 }
 
-// TC-0068: 多层嵌套(5层)
+// TC-0087: 多层嵌套(5层)
 func TestCreateDept_FiveLevelNesting(t *testing.T) {
 	ctx := context.Background()
 	svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
@@ -160,7 +160,7 @@ func TestCreateDept_FiveLevelNesting(t *testing.T) {
 	assert.Equal(t, expected, deepest.Path)
 }
 
-// TC-0063: 不传DeptType默认NORMAL
+// TC-0082: 不传DeptType默认NORMAL
 func TestCreateDept_DefaultDeptType(t *testing.T) {
 	ctx := ctxhelper.SuperAdminCtx()
 	svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
@@ -181,7 +181,7 @@ func TestCreateDept_DefaultDeptType(t *testing.T) {
 	assert.Contains(t, d.Path, fmt.Sprintf("/%d/", resp.Id))
 }
 
-// TC-0064: 传DeptType=DEV
+// TC-0083: 传DeptType=DEV
 func TestCreateDept_DevDeptType(t *testing.T) {
 	ctx := ctxhelper.SuperAdminCtx()
 	svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
@@ -203,7 +203,7 @@ func TestCreateDept_DevDeptType(t *testing.T) {
 	assert.Contains(t, d.Path, fmt.Sprintf("/%d/", resp.Id))
 }
 
-// TC-0069: 通过Logic创建+验证Path
+// TC-0088: 通过Logic创建+验证Path
 func TestCreateDept_ViaLogic_PathCorrect(t *testing.T) {
 	ctx := ctxhelper.SuperAdminCtx()
 	svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
@@ -235,7 +235,7 @@ func TestCreateDept_ViaLogic_PathCorrect(t *testing.T) {
 	assert.Equal(t, fmt.Sprintf("/%d/%d/", parentResp.Id, childResp.Id), child.Path)
 }
 
-// TC-0481: createDept非超管拒绝
+// TC-0511: createDept非超管拒绝
 func TestCreateDept_NonSuperAdminRejected(t *testing.T) {
 	ctx := ctxhelper.AdminCtx("test_product")
 	svcCtx := svc.NewServiceContext(testutil.GetTestConfig())

+ 5 - 5
internal/logic/dept/deleteDeptLogic_test.go

@@ -17,7 +17,7 @@ import (
 	"github.com/stretchr/testify/require"
 )
 
-// TC-0074: 正常删除(无子部门)
+// TC-0094: 正常删除(无子部门)
 func TestDeleteDept_NoChildren(t *testing.T) {
 	ctx := ctxhelper.SuperAdminCtx()
 	svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
@@ -35,7 +35,7 @@ func TestDeleteDept_NoChildren(t *testing.T) {
 	assert.Error(t, err)
 }
 
-// TC-0076: 不存在的部门
+// TC-0096: 不存在的部门
 func TestDeleteDept_NonExistentDept(t *testing.T) {
 	ctx := ctxhelper.SuperAdminCtx()
 	svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
@@ -45,7 +45,7 @@ func TestDeleteDept_NonExistentDept(t *testing.T) {
 	assert.NoError(t, err)
 }
 
-// TC-0075: 有子部门
+// TC-0095: 有子部门
 func TestDeleteDept_HasChildren(t *testing.T) {
 	ctx := ctxhelper.SuperAdminCtx()
 	svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
@@ -72,7 +72,7 @@ func TestDeleteDept_HasChildren(t *testing.T) {
 	assert.NoError(t, err)
 }
 
-// TC-0522: 部门下有关联用户
+// TC-0097: 部门下有关联用户
 func TestDeleteDept_HasAssociatedUsers(t *testing.T) {
 	ctx := ctxhelper.SuperAdminCtx()
 	svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
@@ -112,7 +112,7 @@ func TestDeleteDept_HasAssociatedUsers(t *testing.T) {
 	assert.NoError(t, err)
 }
 
-// TC-0483: deleteDept非超管拒绝
+// TC-0513: deleteDept非超管拒绝
 func TestDeleteDept_NonSuperAdminRejected(t *testing.T) {
 	ctx := ctxhelper.AdminCtx("test_product")
 	svcCtx := svc.NewServiceContext(testutil.GetTestConfig())

+ 3 - 3
internal/logic/dept/deptTreeLogic_test.go

@@ -12,7 +12,7 @@ import (
 	"github.com/stretchr/testify/require"
 )
 
-// TC-0077: 正常获取
+// TC-0098: 正常获取
 func TestDeptTree_Normal(t *testing.T) {
 	ctx := context.Background()
 	svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
@@ -50,7 +50,7 @@ func TestDeptTree_Normal(t *testing.T) {
 	assert.ElementsMatch(t, []int64{c1Id, c2Id}, childIds)
 }
 
-// TC-0078: 空数据
+// TC-0099: 空数据
 func TestDeptTree_Empty(t *testing.T) {
 	ctx := context.Background()
 	svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
@@ -63,7 +63,7 @@ func TestDeptTree_Empty(t *testing.T) {
 	// so we cannot assert len(tree)==0 here without isolating or truncating sys_dept.
 }
 
-// TC-0079: 孤儿节点
+// TC-0100: 孤儿节点
 func TestDeptTree_OrphanBecomesRoot(t *testing.T) {
 	ctx := context.Background()
 	svcCtx := svc.NewServiceContext(testutil.GetTestConfig())

+ 7 - 0
internal/logic/dept/updateDeptLogic.go

@@ -32,6 +32,13 @@ func (l *UpdateDeptLogic) UpdateDept(req *types.UpdateDeptReq) error {
 		return err
 	}
 
+	if len(req.Name) > 64 {
+		return response.ErrBadRequest("部门名称长度不能超过64个字符")
+	}
+	if len(req.Remark) > 255 {
+		return response.ErrBadRequest("备注长度不能超过255个字符")
+	}
+
 	dept, err := l.svcCtx.SysDeptModel.FindOne(l.ctx, req.Id)
 	if err != nil {
 		return response.ErrNotFound("部门不存在")

+ 1 - 1
internal/logic/dept/updateDeptLogic_mock_test.go

@@ -12,7 +12,7 @@ import (
 	"go.uber.org/mock/gomock"
 )
 
-// TC-0533: DeptType变更时级联清除子部门用户缓存
+// TC-0093: DeptType变更时级联清除子部门用户缓存
 func TestUpdateDept_Mock_CascadeCacheClean(t *testing.T) {
 	ctrl := gomock.NewController(t)
 	defer ctrl.Finish()

+ 6 - 6
internal/logic/dept/updateDeptLogic_test.go

@@ -14,7 +14,7 @@ import (
 	"github.com/stretchr/testify/require"
 )
 
-// TC-0070: 正常更新
+// TC-0089: 正常更新
 func TestUpdateDept_Normal(t *testing.T) {
 	ctx := ctxhelper.SuperAdminCtx()
 	svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
@@ -43,7 +43,7 @@ func TestUpdateDept_Normal(t *testing.T) {
 	assert.Equal(t, int64(2), d.Status)
 }
 
-// TC-0071: 不存在
+// TC-0090: 不存在
 func TestUpdateDept_NotFound(t *testing.T) {
 	ctx := ctxhelper.SuperAdminCtx()
 	svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
@@ -61,7 +61,7 @@ func TestUpdateDept_NotFound(t *testing.T) {
 	assert.Equal(t, "部门不存在", ce.Error())
 }
 
-// TC-0070: 正常更新
+// TC-0089: 正常更新
 func TestUpdateDept_StatusZeroUnchanged(t *testing.T) {
 	ctx := ctxhelper.SuperAdminCtx()
 	svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
@@ -88,7 +88,7 @@ func TestUpdateDept_StatusZeroUnchanged(t *testing.T) {
 	assert.Equal(t, int64(1), after.Status)
 }
 
-// TC-0072: DeptType NORMAL→DEV
+// TC-0091: DeptType NORMAL→DEV
 func TestUpdateDept_DeptType_NormalToDev(t *testing.T) {
 	ctx := ctxhelper.SuperAdminCtx()
 	svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
@@ -115,7 +115,7 @@ func TestUpdateDept_DeptType_NormalToDev(t *testing.T) {
 	assert.Equal(t, "DEV", after.DeptType)
 }
 
-// TC-0073: DeptType无效值忽略
+// TC-0092: DeptType无效值忽略
 func TestUpdateDept_DeptType_InvalidIgnored(t *testing.T) {
 	ctx := ctxhelper.SuperAdminCtx()
 	svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
@@ -138,7 +138,7 @@ func TestUpdateDept_DeptType_InvalidIgnored(t *testing.T) {
 	assert.Equal(t, "NORMAL", after.DeptType)
 }
 
-// TC-0482: updateDept非超管拒绝
+// TC-0512: updateDept非超管拒绝
 func TestUpdateDept_NonSuperAdminRejected(t *testing.T) {
 	ctx := ctxhelper.AdminCtx("test_product")
 	svcCtx := svc.NewServiceContext(testutil.GetTestConfig())

+ 6 - 6
internal/logic/member/addMemberLogic_test.go

@@ -19,7 +19,7 @@ import (
 	"github.com/stretchr/testify/require"
 )
 
-// TC-0146: 正常添加
+// TC-0178: 正常添加
 func TestAddMember_Normal(t *testing.T) {
 	ctx := ctxhelper.SuperAdminCtx()
 	svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
@@ -65,7 +65,7 @@ func TestAddMember_Normal(t *testing.T) {
 	assert.Equal(t, int64(1), member.Status)
 }
 
-// TC-0147: 产品不存在
+// TC-0179: 产品不存在
 func TestAddMember_ProductNotFound(t *testing.T) {
 	ctx := ctxhelper.SuperAdminCtx()
 	svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
@@ -83,7 +83,7 @@ func TestAddMember_ProductNotFound(t *testing.T) {
 	assert.Equal(t, "产品不存在", ce.Error())
 }
 
-// TC-0148: 用户不存在
+// TC-0180: 用户不存在
 func TestAddMember_UserNotFound(t *testing.T) {
 	ctx := ctxhelper.SuperAdminCtx()
 	svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
@@ -115,7 +115,7 @@ func TestAddMember_UserNotFound(t *testing.T) {
 	assert.Equal(t, "用户不存在", ce.Error())
 }
 
-// TC-0149: 已是成员
+// TC-0181: 已是成员
 func TestAddMember_AlreadyMember(t *testing.T) {
 	ctx := ctxhelper.SuperAdminCtx()
 	svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
@@ -164,7 +164,7 @@ func TestAddMember_AlreadyMember(t *testing.T) {
 	assert.Equal(t, "该用户已是该产品成员", ce.Error())
 }
 
-// TC-0530: 无效MemberType
+// TC-0183: 无效MemberType
 func TestAddMember_InvalidMemberType(t *testing.T) {
 	ctx := ctxhelper.SuperAdminCtx()
 	svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
@@ -205,7 +205,7 @@ func TestAddMember_InvalidMemberType(t *testing.T) {
 	assert.Equal(t, "无效的成员类型", ce.Error())
 }
 
-// TC-0150: 并发添加
+// TC-0182: 并发添加
 func TestAddMember_ConcurrentSameUserProduct(t *testing.T) {
 	ctx := ctxhelper.SuperAdminCtx()
 	svcCtx := svc.NewServiceContext(testutil.GetTestConfig())

+ 10 - 0
internal/logic/member/memberListLogic.go

@@ -3,6 +3,8 @@ package member
 import (
 	"context"
 
+	"perms-system-server/internal/middleware"
+	"perms-system-server/internal/response"
 	"perms-system-server/internal/svc"
 	"perms-system-server/internal/types"
 	"perms-system-server/internal/util"
@@ -27,6 +29,14 @@ func NewMemberListLogic(ctx context.Context, svcCtx *svc.ServiceContext) *Member
 func (l *MemberListLogic) MemberList(req *types.MemberListReq) (resp *types.PageResp, err error) {
 	page, pageSize := util.NormalizePage(req.Page, req.PageSize)
 
+	caller := middleware.GetUserDetails(l.ctx)
+	if caller == nil {
+		return nil, response.ErrUnauthorized("未登录")
+	}
+	if !caller.IsSuperAdmin && caller.ProductCode != req.ProductCode {
+		return nil, response.ErrForbidden("无权访问该产品的数据")
+	}
+
 	list, total, err := l.svcCtx.SysProductMemberModel.FindListByProductCode(l.ctx, req.ProductCode, page, pageSize)
 	if err != nil {
 		return nil, err

+ 9 - 9
internal/logic/member/memberListLogic_test.go

@@ -1,7 +1,6 @@
 package member
 
 import (
-	"context"
 	"database/sql"
 	"testing"
 	"time"
@@ -11,15 +10,16 @@ import (
 	userModel "perms-system-server/internal/model/user"
 	"perms-system-server/internal/svc"
 	"perms-system-server/internal/testutil"
+	"perms-system-server/internal/testutil/ctxhelper"
 	"perms-system-server/internal/types"
 
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
 )
 
-// TC-0153: 正常查询(批量查用户)
+// TC-0187: 正常查询(批量查用户)
 func TestMemberList_Normal(t *testing.T) {
-	ctx := context.Background()
+	ctx := ctxhelper.SuperAdminCtx()
 	svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
 	conn := testutil.GetTestSqlConn()
 	now := time.Now().Unix()
@@ -71,9 +71,9 @@ func TestMemberList_Normal(t *testing.T) {
 	assert.Equal(t, uId, items[0].UserId)
 }
 
-// TC-0156: 空成员列表
+// TC-0190: 空成员列表
 func TestMemberList_Empty(t *testing.T) {
-	ctx := context.Background()
+	ctx := ctxhelper.SuperAdminCtx()
 	svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
 
 	uid := testutil.UniqueId()
@@ -87,9 +87,9 @@ func TestMemberList_Empty(t *testing.T) {
 	assert.Equal(t, int64(0), resp.Total)
 }
 
-// TC-0155: pageSize超过上限
+// TC-0189: pageSize超过上限
 func TestMemberList_PageSizeLimit(t *testing.T) {
-	ctx := context.Background()
+	ctx := ctxhelper.SuperAdminCtx()
 	svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
 
 	uid := testutil.UniqueId()
@@ -103,9 +103,9 @@ func TestMemberList_PageSizeLimit(t *testing.T) {
 	assert.Equal(t, int64(0), resp.Total)
 }
 
-// TC-0154: 成员用户已删除
+// TC-0188: 成员用户已删除
 func TestMemberList_DeletedUserEmptyInfo(t *testing.T) {
-	ctx := context.Background()
+	ctx := ctxhelper.SuperAdminCtx()
 	svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
 	conn := testutil.GetTestSqlConn()
 	now := time.Now().Unix()

+ 1 - 1
internal/logic/member/removeMemberLogic_mock_test.go

@@ -15,7 +15,7 @@ import (
 	"go.uber.org/mock/gomock"
 )
 
-// TC-0160: 事务回滚
+// TC-0194: 事务回滚
 func TestRemoveMember_Mock_UserPermDeleteFail(t *testing.T) {
 	ctrl := gomock.NewController(t)
 	defer ctrl.Finish()

+ 3 - 3
internal/logic/member/removeMemberLogic_test.go

@@ -22,7 +22,7 @@ import (
 	"github.com/stretchr/testify/require"
 )
 
-// TC-0157: 正常移除+级联(事务内)
+// TC-0191: 正常移除+级联(事务内)
 func TestRemoveMember_WithCascade(t *testing.T) {
 	ctx := ctxhelper.SuperAdminCtx()
 	svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
@@ -105,7 +105,7 @@ func TestRemoveMember_WithCascade(t *testing.T) {
 	assert.Empty(t, perms)
 }
 
-// TC-0159: 成员不存在
+// TC-0193: 成员不存在
 func TestRemoveMember_NotFound(t *testing.T) {
 	ctx := ctxhelper.SuperAdminCtx()
 	svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
@@ -119,7 +119,7 @@ func TestRemoveMember_NotFound(t *testing.T) {
 	assert.Equal(t, "成员不存在", ce.Error())
 }
 
-// TC-0158: 跨产品隔离
+// TC-0192: 跨产品隔离
 func TestRemoveMember_CrossProductIsolation(t *testing.T) {
 	ctx := ctxhelper.SuperAdminCtx()
 	svcCtx := svc.NewServiceContext(testutil.GetTestConfig())

+ 3 - 3
internal/logic/member/updateMemberLogic_test.go

@@ -18,7 +18,7 @@ import (
 	"github.com/stretchr/testify/require"
 )
 
-// TC-0151: 正常更新
+// TC-0184: 正常更新
 func TestUpdateMember_Normal(t *testing.T) {
 	ctx := ctxhelper.SuperAdminCtx()
 	svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
@@ -68,7 +68,7 @@ func TestUpdateMember_Normal(t *testing.T) {
 	assert.Equal(t, int64(2), updated.Status)
 }
 
-// TC-0531: 无效MemberType
+// TC-0186: 无效MemberType
 func TestUpdateMember_InvalidMemberType(t *testing.T) {
 	ctx := ctxhelper.SuperAdminCtx()
 	svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
@@ -116,7 +116,7 @@ func TestUpdateMember_InvalidMemberType(t *testing.T) {
 	assert.Equal(t, "无效的成员类型", ce.Error())
 }
 
-// TC-0152: 不存在
+// TC-0185: 不存在
 func TestUpdateMember_NotFound(t *testing.T) {
 	ctx := ctxhelper.SuperAdminCtx()
 	svcCtx := svc.NewServiceContext(testutil.GetTestConfig())

+ 10 - 0
internal/logic/perm/permListLogic.go

@@ -3,6 +3,8 @@ package perm
 import (
 	"context"
 
+	"perms-system-server/internal/middleware"
+	"perms-system-server/internal/response"
 	"perms-system-server/internal/svc"
 	"perms-system-server/internal/types"
 	"perms-system-server/internal/util"
@@ -27,6 +29,14 @@ func NewPermListLogic(ctx context.Context, svcCtx *svc.ServiceContext) *PermList
 func (l *PermListLogic) PermList(req *types.PermListReq) (resp *types.PageResp, err error) {
 	page, pageSize := util.NormalizePage(req.Page, req.PageSize)
 
+	caller := middleware.GetUserDetails(l.ctx)
+	if caller == nil {
+		return nil, response.ErrUnauthorized("未登录")
+	}
+	if !caller.IsSuperAdmin && caller.ProductCode != req.ProductCode {
+		return nil, response.ErrForbidden("无权访问该产品的数据")
+	}
+
 	list, total, err := l.svcCtx.SysPermModel.FindListByProductCode(l.ctx, req.ProductCode, page, pageSize)
 	if err != nil {
 		return nil, err

+ 9 - 9
internal/logic/perm/permListLogic_test.go

@@ -1,22 +1,22 @@
 package perm
 
 import (
-	"context"
 	"testing"
 	"time"
 
 	permModel "perms-system-server/internal/model/perm"
 	"perms-system-server/internal/svc"
 	"perms-system-server/internal/testutil"
+	"perms-system-server/internal/testutil/ctxhelper"
 	"perms-system-server/internal/types"
 
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
 )
 
-// TC-0080: 正常查询
+// TC-0101: 正常查询
 func TestPermList_Normal(t *testing.T) {
-	ctx := context.Background()
+	ctx := ctxhelper.SuperAdminCtx()
 	svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
 	conn := testutil.GetTestSqlConn()
 	now := time.Now().Unix()
@@ -49,9 +49,9 @@ func TestPermList_Normal(t *testing.T) {
 	assert.Equal(t, pc, items[0].ProductCode)
 }
 
-// TC-0081: 默认分页
+// TC-0102: 默认分页
 func TestPermList_DefaultPagination(t *testing.T) {
-	ctx := context.Background()
+	ctx := ctxhelper.SuperAdminCtx()
 	svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
 	conn := testutil.GetTestSqlConn()
 	now := time.Now().Unix()
@@ -76,9 +76,9 @@ func TestPermList_DefaultPagination(t *testing.T) {
 	assert.Equal(t, int64(1), resp.Total)
 }
 
-// TC-0082: pageSize超过上限
+// TC-0103: pageSize超过上限
 func TestPermList_PageSizeExceedsLimit(t *testing.T) {
-	ctx := context.Background()
+	ctx := ctxhelper.SuperAdminCtx()
 	svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
 	conn := testutil.GetTestSqlConn()
 	now := time.Now().Unix()
@@ -103,9 +103,9 @@ func TestPermList_PageSizeExceedsLimit(t *testing.T) {
 	assert.Equal(t, int64(1), resp.Total)
 }
 
-// TC-0083: 不存在的productCode
+// TC-0104: 不存在的productCode
 func TestPermList_NonExistentProductCode(t *testing.T) {
-	ctx := context.Background()
+	ctx := ctxhelper.SuperAdminCtx()
 	svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
 
 	logic := NewPermListLogic(ctx, svcCtx)

+ 10 - 0
internal/logic/product/createProductLogic.go

@@ -40,6 +40,16 @@ func (l *CreateProductLogic) CreateProduct(req *types.CreateProductReq) (resp *t
 		return nil, err
 	}
 
+	if len(req.Code) > 64 {
+		return nil, response.ErrBadRequest("产品编码长度不能超过64个字符")
+	}
+	if len(req.Name) > 64 {
+		return nil, response.ErrBadRequest("产品名称长度不能超过64个字符")
+	}
+	if len(req.Remark) > 255 {
+		return nil, response.ErrBadRequest("备注长度不能超过255个字符")
+	}
+
 	_, findErr := l.svcCtx.SysProductModel.FindOneByCode(l.ctx, req.Code)
 	if findErr == nil {
 		return nil, response.ErrConflict("产品编码已存在")

+ 2 - 2
internal/logic/product/createProductLogic_mock_test.go

@@ -21,7 +21,7 @@ type fakeResult struct{ id int64 }
 func (r fakeResult) LastInsertId() (int64, error) { return r.id, nil }
 func (r fakeResult) RowsAffected() (int64, error) { return 1, nil }
 
-// TC-0046: 事务回滚-用户创建失败
+// TC-0061: 事务回滚-用户创建失败
 func TestCreateProduct_Mock_UserInsertFail(t *testing.T) {
 	ctrl := gomock.NewController(t)
 	defer ctrl.Finish()
@@ -58,7 +58,7 @@ func TestCreateProduct_Mock_UserInsertFail(t *testing.T) {
 	assert.Nil(t, resp)
 }
 
-// TC-0047: 事务回滚-成员创建失败
+// TC-0062: 事务回滚-成员创建失败
 func TestCreateProduct_Mock_MemberInsertFail(t *testing.T) {
 	ctrl := gomock.NewController(t)
 	defer ctrl.Finish()

+ 5 - 5
internal/logic/product/createProductLogic_test.go

@@ -16,7 +16,7 @@ import (
 	"github.com/stretchr/testify/require"
 )
 
-// TC-0045: 正常创建
+// TC-0060: 正常创建
 func TestCreateProduct_Success(t *testing.T) {
 	ctx := ctxhelper.SuperAdminCtx()
 	svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
@@ -46,7 +46,7 @@ func TestCreateProduct_Success(t *testing.T) {
 	assert.NotEmpty(t, resp.AdminPassword)
 }
 
-// TC-0045: 正常创建
+// TC-0060: 正常创建
 func TestCreateProduct_VerifyDB(t *testing.T) {
 	ctx := ctxhelper.SuperAdminCtx()
 	svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
@@ -88,7 +88,7 @@ func TestCreateProduct_VerifyDB(t *testing.T) {
 	assert.Equal(t, int64(1), memberCount)
 }
 
-// TC-0048: 编码已存在
+// TC-0063: 编码已存在
 func TestCreateProduct_DuplicateCode(t *testing.T) {
 	ctx := ctxhelper.SuperAdminCtx()
 	svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
@@ -121,7 +121,7 @@ func TestCreateProduct_DuplicateCode(t *testing.T) {
 	assert.Equal(t, "产品编码已存在", codeErr.Error())
 }
 
-// TC-0049: 并发创建同编码
+// TC-0064: 并发创建同编码
 func TestCreateProduct_ConcurrentSameCode(t *testing.T) {
 	ctx := ctxhelper.SuperAdminCtx()
 	svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
@@ -170,7 +170,7 @@ func TestCreateProduct_ConcurrentSameCode(t *testing.T) {
 	assert.Equal(t, 1, failCount, "exactly one goroutine should fail (409 or DB duplicate)")
 }
 
-// TC-0484: createProduct非超管拒绝
+// TC-0514: createProduct非超管拒绝
 func TestCreateProduct_NonSuperAdminRejected(t *testing.T) {
 	ctx := ctxhelper.AdminCtx("test_product")
 	svcCtx := svc.NewServiceContext(testutil.GetTestConfig())

+ 4 - 4
internal/logic/product/productDetailLogic_test.go

@@ -17,7 +17,7 @@ import (
 	"github.com/stretchr/testify/require"
 )
 
-// TC-0058: 正常查询
+// TC-0073: 正常查询
 func TestProductDetail_Success(t *testing.T) {
 	ctx := context.Background()
 	svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
@@ -56,7 +56,7 @@ func TestProductDetail_Success(t *testing.T) {
 	assert.Equal(t, now, item.CreateTime)
 }
 
-// TC-0059: 不存在
+// TC-0074: 不存在
 func TestProductDetail_NotFound(t *testing.T) {
 	ctx := context.Background()
 	svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
@@ -71,7 +71,7 @@ func TestProductDetail_NotFound(t *testing.T) {
 	assert.Equal(t, "产品不存在", codeErr.Error())
 }
 
-// TC-0528: 非超管AppKey隐藏
+// TC-0077: 非超管AppKey隐藏
 func TestProductDetail_NonSuperAdminAppKeyHidden(t *testing.T) {
 	ctx := ctxhelper.MemberCtx("test_product")
 	svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
@@ -93,7 +93,7 @@ func TestProductDetail_NonSuperAdminAppKeyHidden(t *testing.T) {
 	assert.Empty(t, item.AppKey, "非超管不应看到AppKey")
 }
 
-// TC-0529: 超管可见AppKey
+// TC-0078: 超管可见AppKey
 func TestProductDetail_SuperAdminAppKeyVisible(t *testing.T) {
 	ctx := ctxhelper.SuperAdminCtx()
 	svcCtx := svc.NewServiceContext(testutil.GetTestConfig())

+ 7 - 7
internal/logic/product/productListLogic_test.go

@@ -15,7 +15,7 @@ import (
 	"github.com/stretchr/testify/require"
 )
 
-// TC-0053: 正常分页
+// TC-0068: 正常分页
 func TestProductList_Normal(t *testing.T) {
 	ctx := context.Background()
 	svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
@@ -49,7 +49,7 @@ func TestProductList_Normal(t *testing.T) {
 	assert.True(t, len(items) >= 3)
 }
 
-// TC-0054: 默认分页
+// TC-0069: 默认分页
 func TestProductList_DefaultPagination(t *testing.T) {
 	ctx := context.Background()
 	svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
@@ -64,7 +64,7 @@ func TestProductList_DefaultPagination(t *testing.T) {
 	assert.True(t, len(items) <= 20, "default pageSize should be 20")
 }
 
-// TC-0055: pageSize超过上限
+// TC-0070: pageSize超过上限
 func TestProductList_PageSizeExceedsLimit(t *testing.T) {
 	ctx := context.Background()
 	svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
@@ -79,7 +79,7 @@ func TestProductList_PageSizeExceedsLimit(t *testing.T) {
 	assert.True(t, len(items) <= 100, "pageSize should be capped at 100")
 }
 
-// TC-0056: pageSize=0
+// TC-0071: pageSize=0
 func TestProductList_PageSizeZero_DefaultsTo20(t *testing.T) {
 	ctx := context.Background()
 	svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
@@ -94,7 +94,7 @@ func TestProductList_PageSizeZero_DefaultsTo20(t *testing.T) {
 	assert.True(t, len(items) <= 20, "default pageSize should be 20")
 }
 
-// TC-0057: page负值
+// TC-0072: page负值
 func TestProductList_NegativePage_DefaultsTo1(t *testing.T) {
 	ctx := context.Background()
 	svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
@@ -105,7 +105,7 @@ func TestProductList_NegativePage_DefaultsTo1(t *testing.T) {
 	require.NotNil(t, resp)
 }
 
-// TC-0526: 非超管AppKey隐藏
+// TC-0075: 非超管AppKey隐藏
 func TestProductList_NonSuperAdminAppKeyHidden(t *testing.T) {
 	ctx := ctxhelper.MemberCtx("test_product")
 	svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
@@ -136,7 +136,7 @@ func TestProductList_NonSuperAdminAppKeyHidden(t *testing.T) {
 	t.Fatal("未找到插入的测试产品")
 }
 
-// TC-0527: 超管可见AppKey
+// TC-0076: 超管可见AppKey
 func TestProductList_SuperAdminAppKeyVisible(t *testing.T) {
 	ctx := ctxhelper.SuperAdminCtx()
 	svcCtx := svc.NewServiceContext(testutil.GetTestConfig())

+ 7 - 0
internal/logic/product/updateProductLogic.go

@@ -32,6 +32,13 @@ func (l *UpdateProductLogic) UpdateProduct(req *types.UpdateProductReq) error {
 		return err
 	}
 
+	if len(req.Name) > 64 {
+		return response.ErrBadRequest("产品名称长度不能超过64个字符")
+	}
+	if len(req.Remark) > 255 {
+		return response.ErrBadRequest("备注长度不能超过255个字符")
+	}
+
 	product, err := l.svcCtx.SysProductModel.FindOne(l.ctx, req.Id)
 	if err != nil {
 		return response.ErrNotFound("产品不存在")

+ 4 - 4
internal/logic/product/updateProductLogic_test.go

@@ -54,7 +54,7 @@ func insertTestProduct(t *testing.T, ctx context.Context) *productModel.SysProdu
 	}
 }
 
-// TC-0050: 正常更新
+// TC-0065: 正常更新
 func TestUpdateProduct_Success(t *testing.T) {
 	ctx := ctxhelper.SuperAdminCtx()
 	svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
@@ -76,7 +76,7 @@ func TestUpdateProduct_Success(t *testing.T) {
 	assert.Equal(t, int64(2), updated.Status)
 }
 
-// TC-0051: 不存在
+// TC-0066: 不存在
 func TestUpdateProduct_NotFound(t *testing.T) {
 	ctx := ctxhelper.SuperAdminCtx()
 	svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
@@ -94,7 +94,7 @@ func TestUpdateProduct_NotFound(t *testing.T) {
 	assert.Equal(t, "产品不存在", codeErr.Error())
 }
 
-// TC-0052: 不传status
+// TC-0067: 不传status
 func TestUpdateProduct_StatusZeroUnchanged(t *testing.T) {
 	ctx := ctxhelper.SuperAdminCtx()
 	svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
@@ -116,7 +116,7 @@ func TestUpdateProduct_StatusZeroUnchanged(t *testing.T) {
 	assert.Equal(t, int64(1), updated.Status, "status should remain unchanged when req.Status is 0")
 }
 
-// TC-0485: updateProduct非超管拒绝
+// TC-0515: updateProduct非超管拒绝
 func TestUpdateProduct_NonSuperAdminRejected(t *testing.T) {
 	ctx := ctxhelper.AdminCtx("test_product")
 	svcCtx := svc.NewServiceContext(testutil.GetTestConfig())

+ 8 - 3
internal/logic/pub/adminLoginLogic.go

@@ -3,6 +3,7 @@ package pub
 import (
 	"context"
 	"crypto/subtle"
+	"errors"
 	"time"
 
 	"perms-system-server/internal/consts"
@@ -37,7 +38,7 @@ func (l *AdminLoginLogic) AdminLogin(req *types.AdminLoginReq) (resp *types.Logi
 
 	u, err := l.svcCtx.SysUserModel.FindOneByUsername(l.ctx, req.Username)
 	if err != nil {
-		if err == user.ErrNotFound {
+		if errors.Is(err, user.ErrNotFound) {
 			return nil, response.ErrUnauthorized("用户名或密码错误")
 		}
 		return nil, err
@@ -51,12 +52,16 @@ func (l *AdminLoginLogic) AdminLogin(req *types.AdminLoginReq) (resp *types.Logi
 		return nil, response.ErrUnauthorized("用户名或密码错误")
 	}
 
+	if u.IsSuperAdmin != consts.IsSuperAdminYes {
+		return nil, response.ErrForbidden("仅超级管理员可通过管理后台登录")
+	}
+
 	ud := l.svcCtx.UserDetailsLoader.Load(l.ctx, u.Id, "")
 
 	accessToken, err := authHelper.GenerateAccessToken(
 		l.svcCtx.Config.Auth.AccessSecret,
 		l.svcCtx.Config.Auth.AccessExpire,
-		ud.UserId, ud.Username, ud.ProductCode, ud.MemberType, ud.Perms,
+		ud.UserId, ud.Username, ud.ProductCode, ud.MemberType, ud.Perms, ud.TokenVersion,
 	)
 	if err != nil {
 		return nil, err
@@ -65,7 +70,7 @@ func (l *AdminLoginLogic) AdminLogin(req *types.AdminLoginReq) (resp *types.Logi
 	refreshToken, err := authHelper.GenerateRefreshToken(
 		l.svcCtx.Config.Auth.RefreshSecret,
 		l.svcCtx.Config.Auth.RefreshExpire,
-		ud.UserId, ud.ProductCode,
+		ud.UserId, ud.ProductCode, ud.TokenVersion,
 	)
 	if err != nil {
 		return nil, err

+ 17 - 18
internal/logic/pub/adminLoginLogic_test.go

@@ -14,7 +14,7 @@ import (
 	"github.com/stretchr/testify/require"
 )
 
-// TC-0500: 超管正常登录(管理后台)
+// TC-0015: 超管正常登录(管理后台)
 func TestAdminLogin_SuperAdmin(t *testing.T) {
 	ctx := context.Background()
 	svcCtx := newTestSvcCtx()
@@ -41,8 +41,8 @@ func TestAdminLogin_SuperAdmin(t *testing.T) {
 	assert.Equal(t, "SUPER_ADMIN", resp.UserInfo.MemberType)
 }
 
-// TC-0501: 普通用户正常登录(管理后台)
-func TestAdminLogin_NormalUser(t *testing.T) {
+// TC-0016: 普通用户被拒绝(审计H1修复: 仅超管可通过管理后台登录)
+func TestAdminLogin_NormalUserRejected(t *testing.T) {
 	ctx := context.Background()
 	svcCtx := newTestSvcCtx()
 	username := testutil.UniqueId()
@@ -57,17 +57,16 @@ func TestAdminLogin_NormalUser(t *testing.T) {
 		Password:      password,
 		ManagementKey: svcCtx.Config.Auth.ManagementKey,
 	})
-	require.NoError(t, err)
-	require.NotNil(t, resp)
-	assert.NotEmpty(t, resp.AccessToken)
-	assert.NotEmpty(t, resp.RefreshToken)
-	assert.True(t, resp.Expires > time.Now().Unix(), "expires应为未来的unix时间戳")
-	assert.Equal(t, username, resp.UserInfo.Username)
-	assert.Nil(t, resp.UserInfo.Perms)
-	assert.Empty(t, resp.UserInfo.MemberType)
+	require.Nil(t, resp)
+	require.Error(t, err)
+
+	var codeErr *response.CodeError
+	require.True(t, errors.As(err, &codeErr))
+	assert.Equal(t, 403, codeErr.Code())
+	assert.Equal(t, "仅超级管理员可通过管理后台登录", codeErr.Error())
 }
 
-// TC-0502: managementKey无效
+// TC-0017: managementKey无效
 func TestAdminLogin_InvalidManagementKey(t *testing.T) {
 	ctx := context.Background()
 	svcCtx := newTestSvcCtx()
@@ -87,7 +86,7 @@ func TestAdminLogin_InvalidManagementKey(t *testing.T) {
 	assert.Equal(t, "managementKey无效", codeErr.Error())
 }
 
-// TC-0503: managementKey为空
+// TC-0018: managementKey为空
 func TestAdminLogin_EmptyManagementKey(t *testing.T) {
 	ctx := context.Background()
 	svcCtx := newTestSvcCtx()
@@ -107,7 +106,7 @@ func TestAdminLogin_EmptyManagementKey(t *testing.T) {
 	assert.Equal(t, "managementKey无效", codeErr.Error())
 }
 
-// TC-0504: 用户不存在
+// TC-0019: 用户不存在
 func TestAdminLogin_UserNotFound(t *testing.T) {
 	ctx := context.Background()
 	svcCtx := newTestSvcCtx()
@@ -127,7 +126,7 @@ func TestAdminLogin_UserNotFound(t *testing.T) {
 	assert.Equal(t, "用户名或密码错误", codeErr.Error())
 }
 
-// TC-0505: 密码错误
+// TC-0020: 密码错误
 func TestAdminLogin_WrongPassword(t *testing.T) {
 	ctx := context.Background()
 	svcCtx := newTestSvcCtx()
@@ -151,7 +150,7 @@ func TestAdminLogin_WrongPassword(t *testing.T) {
 	assert.Equal(t, "用户名或密码错误", codeErr.Error())
 }
 
-// TC-0506: 账号冻结
+// TC-0021: 账号冻结
 func TestAdminLogin_AccountFrozen(t *testing.T) {
 	ctx := context.Background()
 	svcCtx := newTestSvcCtx()
@@ -176,7 +175,7 @@ func TestAdminLogin_AccountFrozen(t *testing.T) {
 	assert.Equal(t, "账号已被冻结", codeErr.Error())
 }
 
-// TC-0507: 不带productCode时token无权限(perms为空)
+// TC-0022: 不带productCode时token无权限(perms为空)
 func TestAdminLogin_NoPermsWithoutProductCode(t *testing.T) {
 	ctx := context.Background()
 	svcCtx := newTestSvcCtx()
@@ -198,7 +197,7 @@ func TestAdminLogin_NoPermsWithoutProductCode(t *testing.T) {
 	assert.Equal(t, "SUPER_ADMIN", resp.UserInfo.MemberType, "超管即使不传productCode也会被标记SUPER_ADMIN")
 }
 
-// TC-0509: SQL注入username
+// TC-0024: SQL注入username
 func TestAdminLogin_SQLInjection(t *testing.T) {
 	ctx := context.Background()
 	svcCtx := newTestSvcCtx()

+ 2 - 2
internal/logic/pub/loginLogic_test.go

@@ -363,7 +363,7 @@ func TestLogin_SQLInjection(t *testing.T) {
 	assert.Equal(t, "用户名或密码错误", codeErr.Error())
 }
 
-// TC-0542: 产品成员被禁用时拒绝登录(H-3修复验证)
+// TC-0013: 产品成员被禁用时拒绝登录(H-3修复验证)
 func TestLogin_DisabledMemberRejected(t *testing.T) {
 	ctx := context.Background()
 	svcCtx := newTestSvcCtx()
@@ -401,7 +401,7 @@ func TestLogin_DisabledMemberRejected(t *testing.T) {
 	assert.Equal(t, "您在该产品下的成员资格已被禁用", codeErr2.Error())
 }
 
-// TC-0543: 产品已被禁用时拒绝登录
+// TC-0014: 产品已被禁用时拒绝登录
 func TestLogin_DisabledProductRejected(t *testing.T) {
 	ctx := context.Background()
 	svcCtx := newTestSvcCtx()

+ 4 - 3
internal/logic/pub/loginService.go

@@ -2,6 +2,7 @@ package pub
 
 import (
 	"context"
+	"errors"
 
 	"perms-system-server/internal/consts"
 	"perms-system-server/internal/loaders"
@@ -30,7 +31,7 @@ func (e *LoginError) Error() string {
 func ValidateProductLogin(ctx context.Context, svcCtx *svc.ServiceContext, username, password, productCode string) (*LoginResult, error) {
 	u, err := svcCtx.SysUserModel.FindOneByUsername(ctx, username)
 	if err != nil {
-		if err == user.ErrNotFound {
+		if errors.Is(err, user.ErrNotFound) {
 			return nil, &LoginError{Code: 401, Message: "用户名或密码错误"}
 		}
 		return nil, err
@@ -69,7 +70,7 @@ func ValidateProductLogin(ctx context.Context, svcCtx *svc.ServiceContext, usern
 	accessToken, err := authHelper.GenerateAccessToken(
 		svcCtx.Config.Auth.AccessSecret,
 		svcCtx.Config.Auth.AccessExpire,
-		ud.UserId, ud.Username, ud.ProductCode, ud.MemberType, ud.Perms,
+		ud.UserId, ud.Username, ud.ProductCode, ud.MemberType, ud.Perms, ud.TokenVersion,
 	)
 	if err != nil {
 		return nil, err
@@ -78,7 +79,7 @@ func ValidateProductLogin(ctx context.Context, svcCtx *svc.ServiceContext, usern
 	refreshToken, err := authHelper.GenerateRefreshToken(
 		svcCtx.Config.Auth.RefreshSecret,
 		svcCtx.Config.Auth.RefreshExpire,
-		ud.UserId, ud.ProductCode,
+		ud.UserId, ud.ProductCode, ud.TokenVersion,
 	)
 	if err != nil {
 		return nil, err

+ 5 - 1
internal/logic/pub/refreshTokenLogic.go

@@ -54,10 +54,14 @@ func (l *RefreshTokenLogic) RefreshToken(req *types.RefreshTokenReq) (resp *type
 		return nil, response.ErrForbidden("您已不是该产品的成员")
 	}
 
+	if claims.TokenVersion != ud.TokenVersion {
+		return nil, response.ErrUnauthorized("登录状态已失效,请重新登录")
+	}
+
 	accessToken, err := authHelper.GenerateAccessToken(
 		l.svcCtx.Config.Auth.AccessSecret,
 		l.svcCtx.Config.Auth.AccessExpire,
-		ud.UserId, ud.Username, ud.ProductCode, ud.MemberType, ud.Perms,
+		ud.UserId, ud.Username, ud.ProductCode, ud.MemberType, ud.Perms, ud.TokenVersion,
 	)
 	if err != nil {
 		return nil, err

+ 13 - 13
internal/logic/pub/refreshTokenLogic_test.go

@@ -48,7 +48,7 @@ func insertRefreshTestUser(t *testing.T, ctx context.Context, username, password
 	return id, cleanup
 }
 
-// TC-0013: 正常刷新(refreshToken从header获取,原样返回不重新生成)
+// TC-0025: 正常刷新(refreshToken从header获取,原样返回不重新生成)
 func TestRefreshToken_Normal(t *testing.T) {
 	ctx := context.Background()
 	svcCtx := newTestSvcCtx()
@@ -61,7 +61,7 @@ func TestRefreshToken_Normal(t *testing.T) {
 	refreshToken, err := authHelper.GenerateRefreshToken(
 		svcCtx.Config.Auth.RefreshSecret,
 		svcCtx.Config.Auth.RefreshExpire,
-		userId, "",
+		userId, "", 0,
 	)
 	require.NoError(t, err)
 
@@ -79,7 +79,7 @@ func TestRefreshToken_Normal(t *testing.T) {
 	assert.Equal(t, username, resp.UserInfo.Username)
 }
 
-// TC-0014: 不带productCode(回退)
+// TC-0026: 不带productCode(回退)
 func TestRefreshToken_FallbackToClaimsProductCode(t *testing.T) {
 	ctx := context.Background()
 	svcCtx := newTestSvcCtx()
@@ -113,7 +113,7 @@ func TestRefreshToken_FallbackToClaimsProductCode(t *testing.T) {
 	refreshToken, err := authHelper.GenerateRefreshToken(
 		svcCtx.Config.Auth.RefreshSecret,
 		svcCtx.Config.Auth.RefreshExpire,
-		userId, pc,
+		userId, pc, 0,
 	)
 	require.NoError(t, err)
 
@@ -127,7 +127,7 @@ func TestRefreshToken_FallbackToClaimsProductCode(t *testing.T) {
 	assert.Contains(t, resp.UserInfo.Perms, permCode)
 }
 
-// TC-0015: token无效
+// TC-0027: token无效
 func TestRefreshToken_InvalidToken(t *testing.T) {
 	ctx := context.Background()
 	svcCtx := newTestSvcCtx()
@@ -145,7 +145,7 @@ func TestRefreshToken_InvalidToken(t *testing.T) {
 	assert.Equal(t, "refreshToken无效或已过期", codeErr.Error())
 }
 
-// TC-0016: 用户已删除(UserDetailsLoader 返回 Status=0 → 403 账号已被冻结)
+// TC-0028: 用户已删除(UserDetailsLoader 返回 Status=0 → 403 账号已被冻结)
 func TestRefreshToken_UserDeleted(t *testing.T) {
 	ctx := context.Background()
 	svcCtx := newTestSvcCtx()
@@ -154,7 +154,7 @@ func TestRefreshToken_UserDeleted(t *testing.T) {
 	refreshToken, err := authHelper.GenerateRefreshToken(
 		svcCtx.Config.Auth.RefreshSecret,
 		svcCtx.Config.Auth.RefreshExpire,
-		nonExistentUserId, "",
+		nonExistentUserId, "", 0,
 	)
 	require.NoError(t, err)
 
@@ -171,7 +171,7 @@ func TestRefreshToken_UserDeleted(t *testing.T) {
 	assert.Equal(t, "账号已被冻结", codeErr.Error())
 }
 
-// TC-0017: 账号冻结
+// TC-0029: 账号冻结
 func TestRefreshToken_AccountFrozen(t *testing.T) {
 	ctx := context.Background()
 	svcCtx := newTestSvcCtx()
@@ -184,7 +184,7 @@ func TestRefreshToken_AccountFrozen(t *testing.T) {
 	refreshToken, err := authHelper.GenerateRefreshToken(
 		svcCtx.Config.Auth.RefreshSecret,
 		svcCtx.Config.Auth.RefreshExpire,
-		userId, "",
+		userId, "", 0,
 	)
 	require.NoError(t, err)
 
@@ -201,7 +201,7 @@ func TestRefreshToken_AccountFrozen(t *testing.T) {
 	assert.Equal(t, "账号已被冻结", codeErr.Error())
 }
 
-// TC-0514: 尝试切换产品被拒绝
+// TC-0031: 尝试切换产品被拒绝
 func TestRefreshToken_ProductCodeSwitchRejected(t *testing.T) {
 	ctx := context.Background()
 	svcCtx := newTestSvcCtx()
@@ -214,7 +214,7 @@ func TestRefreshToken_ProductCodeSwitchRejected(t *testing.T) {
 	refreshToken, err := authHelper.GenerateRefreshToken(
 		svcCtx.Config.Auth.RefreshSecret,
 		svcCtx.Config.Auth.RefreshExpire,
-		userId, "product_a",
+		userId, "product_a", 0,
 	)
 	require.NoError(t, err)
 
@@ -232,7 +232,7 @@ func TestRefreshToken_ProductCodeSwitchRejected(t *testing.T) {
 	assert.Equal(t, "刷新令牌不允许切换产品", codeErr.Error())
 }
 
-// TC-0018: 超管+productCode(refreshToken原样返回)
+// TC-0030: 超管+productCode(refreshToken原样返回)
 func TestRefreshToken_SuperAdminWithProductCode(t *testing.T) {
 	ctx := context.Background()
 	svcCtx := newTestSvcCtx()
@@ -259,7 +259,7 @@ func TestRefreshToken_SuperAdminWithProductCode(t *testing.T) {
 	refreshToken, err := authHelper.GenerateRefreshToken(
 		svcCtx.Config.Auth.RefreshSecret,
 		svcCtx.Config.Auth.RefreshExpire,
-		userId, pc,
+		userId, pc, 0,
 	)
 	require.NoError(t, err)
 

+ 1 - 1
internal/logic/pub/syncPermsLogic_mock_test.go

@@ -15,7 +15,7 @@ import (
 	"go.uber.org/mock/gomock"
 )
 
-// TC-0535: 事务保护-中途失败回滚
+// TC-0044: 事务保护-中途失败回滚
 func TestSyncPerms_Mock_TransactionRollbackOnBatchUpdateFail(t *testing.T) {
 	ctrl := gomock.NewController(t)
 	defer ctrl.Finish()

+ 12 - 12
internal/logic/pub/syncPermsLogic_test.go

@@ -39,7 +39,7 @@ func insertSyncTestProduct(t *testing.T, ctx context.Context, code, appKey, appS
 	return id, cleanup
 }
 
-// TC-0019: 全部新增
+// TC-0032: 全部新增
 func TestSyncPerms_AllNew(t *testing.T) {
 	ctx := context.Background()
 	svcCtx := newTestSvcCtx()
@@ -69,7 +69,7 @@ func TestSyncPerms_AllNew(t *testing.T) {
 	assert.Equal(t, int64(0), resp.Disabled)
 }
 
-// TC-0020: 更新已有(名称变更)
+// TC-0033: 更新已有(名称变更)
 func TestSyncPerms_UpdateExisting(t *testing.T) {
 	ctx := context.Background()
 	svcCtx := newTestSvcCtx()
@@ -110,7 +110,7 @@ func TestSyncPerms_UpdateExisting(t *testing.T) {
 	assert.Equal(t, int64(1), updated.Status)
 }
 
-// TC-0021: 无变化
+// TC-0034: 无变化
 func TestSyncPerms_NoChanges(t *testing.T) {
 	ctx := context.Background()
 	svcCtx := newTestSvcCtx()
@@ -145,7 +145,7 @@ func TestSyncPerms_NoChanges(t *testing.T) {
 	assert.Equal(t, int64(0), resp.Disabled)
 }
 
-// TC-0022: 禁用权限重启
+// TC-0035: 禁用权限重启
 func TestSyncPerms_ReEnableDisabled(t *testing.T) {
 	ctx := context.Background()
 	svcCtx := newTestSvcCtx()
@@ -183,7 +183,7 @@ func TestSyncPerms_ReEnableDisabled(t *testing.T) {
 	assert.Equal(t, int64(1), reEnabled.Status)
 }
 
-// TC-0023: 移除不在列表的权限
+// TC-0036: 移除不在列表的权限
 func TestSyncPerms_DisableNotInList(t *testing.T) {
 	ctx := context.Background()
 	svcCtx := newTestSvcCtx()
@@ -235,7 +235,7 @@ func TestSyncPerms_DisableNotInList(t *testing.T) {
 	assert.Equal(t, int64(1), kept.Status)
 }
 
-// TC-0024: 空perms数组
+// TC-0037: 空perms数组
 func TestSyncPerms_EmptyPermsDisablesAll(t *testing.T) {
 	ctx := context.Background()
 	svcCtx := newTestSvcCtx()
@@ -283,7 +283,7 @@ func TestSyncPerms_EmptyPermsDisablesAll(t *testing.T) {
 	assert.Equal(t, int64(2), d2.Status)
 }
 
-// TC-0026: appKey无效
+// TC-0039: appKey无效
 func TestSyncPerms_InvalidAppKey(t *testing.T) {
 	ctx := context.Background()
 	svcCtx := newTestSvcCtx()
@@ -303,7 +303,7 @@ func TestSyncPerms_InvalidAppKey(t *testing.T) {
 	assert.Equal(t, "无效的appKey", codeErr.Error())
 }
 
-// TC-0027: appSecret错误
+// TC-0040: appSecret错误
 func TestSyncPerms_WrongAppSecret(t *testing.T) {
 	ctx := context.Background()
 	svcCtx := newTestSvcCtx()
@@ -329,7 +329,7 @@ func TestSyncPerms_WrongAppSecret(t *testing.T) {
 	assert.Equal(t, "appSecret验证失败", codeErr.Error())
 }
 
-// TC-0028: 产品已禁用
+// TC-0041: 产品已禁用
 func TestSyncPerms_ProductDisabled(t *testing.T) {
 	ctx := context.Background()
 	svcCtx := newTestSvcCtx()
@@ -355,7 +355,7 @@ func TestSyncPerms_ProductDisabled(t *testing.T) {
 	assert.Equal(t, "产品已被禁用", codeErr.Error())
 }
 
-// TC-0029: 大批量(1000条)
+// TC-0042: 大批量(1000条)
 func TestSyncPerms_LargeBatch1000(t *testing.T) {
 	ctx := context.Background()
 	svcCtx := newTestSvcCtx()
@@ -389,7 +389,7 @@ func TestSyncPerms_LargeBatch1000(t *testing.T) {
 	assert.Equal(t, int64(0), resp.Disabled)
 }
 
-// TC-0532: 重复code去重
+// TC-0043: 重复code去重
 func TestSyncPerms_DeduplicateCodes(t *testing.T) {
 	ctx := context.Background()
 	svcCtx := newTestSvcCtx()
@@ -417,7 +417,7 @@ func TestSyncPerms_DeduplicateCodes(t *testing.T) {
 	assert.Equal(t, int64(2), resp.Added, "重复code应被去重,只添加2条")
 }
 
-// TC-0025: 验证disabled返回值
+// TC-0038: 验证disabled返回值
 func TestSyncPerms_VerifyDisabledCount(t *testing.T) {
 	ctx := context.Background()
 	svcCtx := newTestSvcCtx()

+ 12 - 0
internal/logic/role/bindRolePermsLogic.go

@@ -38,6 +38,18 @@ func (l *BindRolePermsLogic) BindRolePerms(req *types.BindPermsReq) error {
 		return err
 	}
 
+	if len(req.PermIds) > 0 {
+		seen := make(map[int64]bool, len(req.PermIds))
+		uniqueIds := make([]int64, 0, len(req.PermIds))
+		for _, id := range req.PermIds {
+			if !seen[id] {
+				seen[id] = true
+				uniqueIds = append(uniqueIds, id)
+			}
+		}
+		req.PermIds = uniqueIds
+	}
+
 	if len(req.PermIds) > 0 {
 		perms, err := l.svcCtx.SysPermModel.FindByIds(l.ctx, req.PermIds)
 		if err != nil {

+ 1 - 1
internal/logic/role/bindRolePermsLogic_mock_test.go

@@ -16,7 +16,7 @@ import (
 	"go.uber.org/mock/gomock"
 )
 
-// TC-0100: 事务回滚
+// TC-0121: 事务回滚
 func TestBindRolePerms_Mock_BatchInsertFail(t *testing.T) {
 	ctrl := gomock.NewController(t)
 	defer ctrl.Finish()

+ 11 - 7
internal/logic/role/bindRolePermsLogic_test.go

@@ -18,7 +18,7 @@ import (
 	"github.com/stretchr/testify/require"
 )
 
-// TC-0096: 正常绑定
+// TC-0117: 正常绑定
 func TestBindRolePerms_Normal(t *testing.T) {
 	ctx := ctxhelper.SuperAdminCtx()
 	svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
@@ -65,7 +65,7 @@ func TestBindRolePerms_Normal(t *testing.T) {
 	assert.ElementsMatch(t, []int64{p1Id, p2Id}, permIds)
 }
 
-// TC-0097: 角色不存在
+// TC-0118: 角色不存在
 func TestBindRolePerms_RoleNotFound(t *testing.T) {
 	ctx := ctxhelper.SuperAdminCtx()
 	svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
@@ -83,7 +83,7 @@ func TestBindRolePerms_RoleNotFound(t *testing.T) {
 	assert.Equal(t, "角色不存在", ce.Error())
 }
 
-// TC-0098: 清空权限
+// TC-0119: 清空权限
 func TestBindRolePerms_EmptyPermIds(t *testing.T) {
 	ctx := ctxhelper.SuperAdminCtx()
 	svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
@@ -129,7 +129,7 @@ func TestBindRolePerms_EmptyPermIds(t *testing.T) {
 	assert.Empty(t, permIds)
 }
 
-// TC-0096: 正常绑定
+// TC-0117: 正常绑定
 func TestBindRolePerms_Rebind(t *testing.T) {
 	ctx := ctxhelper.SuperAdminCtx()
 	svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
@@ -183,7 +183,7 @@ func TestBindRolePerms_Rebind(t *testing.T) {
 	assert.ElementsMatch(t, []int64{permIds[1], permIds[2]}, got)
 }
 
-// TC-0099: 重复permId
+// TC-0120: 重复permId — H-5审计修复后静默去重
 func TestBindRolePerms_DuplicatePermId(t *testing.T) {
 	ctx := ctxhelper.SuperAdminCtx()
 	svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
@@ -216,10 +216,14 @@ func TestBindRolePerms_DuplicatePermId(t *testing.T) {
 		RoleId:  roleId,
 		PermIds: []int64{pId, pId},
 	})
-	assert.Error(t, err)
+	require.NoError(t, err, "重复permId应被静默去重(H-5修复)")
+
+	permIds, err := svcCtx.SysRolePermModel.FindPermIdsByRoleId(ctx, roleId)
+	require.NoError(t, err)
+	assert.Equal(t, []int64{pId}, permIds, "去重后应只绑定1个权限")
 }
 
-// TC-0490: bindRolePerms非管理员拒绝
+// TC-0520: bindRolePerms非管理员拒绝
 func TestBindRolePerms_MemberRejected(t *testing.T) {
 	pc := "test_product"
 	ctx := ctxhelper.MemberCtx(pc)

+ 7 - 0
internal/logic/role/createRoleLogic.go

@@ -34,6 +34,13 @@ func (l *CreateRoleLogic) CreateRole(req *types.CreateRoleReq) (resp *types.IdRe
 		return nil, err
 	}
 
+	if len(req.Name) > 64 {
+		return nil, response.ErrBadRequest("角色名长度不能超过64个字符")
+	}
+	if len(req.Remark) > 255 {
+		return nil, response.ErrBadRequest("备注长度不能超过255个字符")
+	}
+
 	if req.PermsLevel < 1 || req.PermsLevel > 999 {
 		return nil, response.ErrBadRequest("权限级别必须在 1-999 之间")
 	}

+ 4 - 4
internal/logic/role/createRoleLogic_test.go

@@ -17,7 +17,7 @@ import (
 	"github.com/stretchr/testify/require"
 )
 
-// TC-0084: 正常创建
+// TC-0105: 正常创建
 func TestCreateRole_Normal(t *testing.T) {
 	ctx := ctxhelper.SuperAdminCtx()
 	svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
@@ -45,7 +45,7 @@ func TestCreateRole_Normal(t *testing.T) {
 	assert.Equal(t, int64(1), role.PermsLevel)
 }
 
-// TC-0085: 重复角色名
+// TC-0106: 重复角色名
 func TestCreateRole_DuplicateName(t *testing.T) {
 	ctx := ctxhelper.SuperAdminCtx()
 	svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
@@ -80,7 +80,7 @@ func TestCreateRole_DuplicateName(t *testing.T) {
 	assert.Equal(t, "该产品下角色名已存在", ce.Error())
 }
 
-// TC-0086: 并发同名创建
+// TC-0107: 并发同名创建
 func TestCreateRole_ConcurrentSameName(t *testing.T) {
 	ctx := ctxhelper.SuperAdminCtx()
 	svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
@@ -132,7 +132,7 @@ func TestCreateRole_ConcurrentSameName(t *testing.T) {
 	assert.Equal(t, 1, failCount)
 }
 
-// TC-0487: createRole非管理员拒绝
+// TC-0517: createRole非管理员拒绝
 func TestCreateRole_MemberRejected(t *testing.T) {
 	ctx := ctxhelper.MemberCtx("test_product")
 	svcCtx := svc.NewServiceContext(testutil.GetTestConfig())

+ 1 - 1
internal/logic/role/deleteRoleLogic_mock_test.go

@@ -15,7 +15,7 @@ import (
 	"go.uber.org/mock/gomock"
 )
 
-// TC-0094: 事务回滚
+// TC-0115: 事务回滚
 func TestDeleteRole_Mock_UserRoleDeleteFail(t *testing.T) {
 	ctrl := gomock.NewController(t)
 	defer ctrl.Finish()

+ 3 - 3
internal/logic/role/deleteRoleLogic_test.go

@@ -19,7 +19,7 @@ import (
 	"github.com/stretchr/testify/require"
 )
 
-// TC-0093: 正常删除+级联
+// TC-0114: 正常删除+级联
 func TestDeleteRole_WithCascading(t *testing.T) {
 	ctx := ctxhelper.SuperAdminCtx()
 	svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
@@ -76,7 +76,7 @@ func TestDeleteRole_WithCascading(t *testing.T) {
 	assert.NotContains(t, roleIds, roleId)
 }
 
-// TC-0095: 无关联数据
+// TC-0116: 无关联数据
 func TestDeleteRole_NoAssociations(t *testing.T) {
 	ctx := ctxhelper.SuperAdminCtx()
 	svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
@@ -103,7 +103,7 @@ func TestDeleteRole_NoAssociations(t *testing.T) {
 	assert.Error(t, err)
 }
 
-// TC-0489: deleteRole非管理员拒绝
+// TC-0519: deleteRole非管理员拒绝
 func TestDeleteRole_MemberRejected(t *testing.T) {
 	pc := "test_product"
 	ctx := ctxhelper.MemberCtx(pc)

+ 9 - 0
internal/logic/role/roleDetailLogic.go

@@ -3,6 +3,7 @@ package role
 import (
 	"context"
 
+	"perms-system-server/internal/middleware"
 	"perms-system-server/internal/response"
 	"perms-system-server/internal/svc"
 	"perms-system-server/internal/types"
@@ -30,6 +31,14 @@ func (l *RoleDetailLogic) RoleDetail(req *types.RoleDetailReq) (resp *types.Role
 		return nil, response.ErrNotFound("角色不存在")
 	}
 
+	caller := middleware.GetUserDetails(l.ctx)
+	if caller == nil {
+		return nil, response.ErrUnauthorized("未登录")
+	}
+	if !caller.IsSuperAdmin && caller.ProductCode != role.ProductCode {
+		return nil, response.ErrForbidden("无权访问该产品的数据")
+	}
+
 	permIds, err := l.svcCtx.SysRolePermModel.FindPermIdsByRoleId(l.ctx, role.Id)
 	if err != nil {
 		return nil, err

+ 5 - 5
internal/logic/role/roleDetailLogic_test.go

@@ -1,7 +1,6 @@
 package role
 
 import (
-	"context"
 	"errors"
 	"testing"
 	"time"
@@ -12,15 +11,16 @@ import (
 	"perms-system-server/internal/response"
 	"perms-system-server/internal/svc"
 	"perms-system-server/internal/testutil"
+	"perms-system-server/internal/testutil/ctxhelper"
 	"perms-system-server/internal/types"
 
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
 )
 
-// TC-0091: 正常查询
+// TC-0112: 正常查询
 func TestRoleDetail_Normal(t *testing.T) {
-	ctx := context.Background()
+	ctx := ctxhelper.SuperAdminCtx()
 	svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
 	conn := testutil.GetTestSqlConn()
 	now := time.Now().Unix()
@@ -74,9 +74,9 @@ func TestRoleDetail_Normal(t *testing.T) {
 	assert.ElementsMatch(t, []int64{p1Id, p2Id}, resp.PermIds)
 }
 
-// TC-0092: 不存在
+// TC-0113: 不存在
 func TestRoleDetail_NotFound(t *testing.T) {
-	ctx := context.Background()
+	ctx := ctxhelper.SuperAdminCtx()
 	svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
 
 	logic := NewRoleDetailLogic(ctx, svcCtx)

+ 10 - 0
internal/logic/role/roleListLogic.go

@@ -3,6 +3,8 @@ package role
 import (
 	"context"
 
+	"perms-system-server/internal/middleware"
+	"perms-system-server/internal/response"
 	"perms-system-server/internal/svc"
 	"perms-system-server/internal/types"
 	"perms-system-server/internal/util"
@@ -27,6 +29,14 @@ func NewRoleListLogic(ctx context.Context, svcCtx *svc.ServiceContext) *RoleList
 func (l *RoleListLogic) RoleList(req *types.RoleListReq) (resp *types.PageResp, err error) {
 	page, pageSize := util.NormalizePage(req.Page, req.PageSize)
 
+	caller := middleware.GetUserDetails(l.ctx)
+	if caller == nil {
+		return nil, response.ErrUnauthorized("未登录")
+	}
+	if !caller.IsSuperAdmin && caller.ProductCode != req.ProductCode {
+		return nil, response.ErrForbidden("无权访问该产品的数据")
+	}
+
 	list, total, err := l.svcCtx.SysRoleModel.FindListByProductCode(l.ctx, req.ProductCode, page, pageSize)
 	if err != nil {
 		return nil, err

+ 7 - 7
internal/logic/role/roleListLogic_test.go

@@ -1,22 +1,22 @@
 package role
 
 import (
-	"context"
 	"testing"
 	"time"
 
 	roleModel "perms-system-server/internal/model/role"
 	"perms-system-server/internal/svc"
 	"perms-system-server/internal/testutil"
+	"perms-system-server/internal/testutil/ctxhelper"
 	"perms-system-server/internal/types"
 
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
 )
 
-// TC-0089: 正常查询
+// TC-0110: 正常查询
 func TestRoleList_Normal(t *testing.T) {
-	ctx := context.Background()
+	ctx := ctxhelper.SuperAdminCtx()
 	svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
 	conn := testutil.GetTestSqlConn()
 	now := time.Now().Unix()
@@ -48,9 +48,9 @@ func TestRoleList_Normal(t *testing.T) {
 	assert.Len(t, items, 3)
 }
 
-// TC-0089: 正常查询
+// TC-0110: 正常查询
 func TestRoleList_DefaultPagination(t *testing.T) {
-	ctx := context.Background()
+	ctx := ctxhelper.SuperAdminCtx()
 	svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
 	conn := testutil.GetTestSqlConn()
 	now := time.Now().Unix()
@@ -75,9 +75,9 @@ func TestRoleList_DefaultPagination(t *testing.T) {
 	assert.Equal(t, int64(1), resp.Total)
 }
 
-// TC-0090: pageSize超过上限
+// TC-0111: pageSize超过上限
 func TestRoleList_PageSizeExceedsLimit(t *testing.T) {
-	ctx := context.Background()
+	ctx := ctxhelper.SuperAdminCtx()
 	svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
 	conn := testutil.GetTestSqlConn()
 	now := time.Now().Unix()

+ 7 - 0
internal/logic/role/updateRoleLogic.go

@@ -37,6 +37,13 @@ func (l *UpdateRoleLogic) UpdateRole(req *types.UpdateRoleReq) error {
 		return err
 	}
 
+	if len(req.Name) > 64 {
+		return response.ErrBadRequest("角色名长度不能超过64个字符")
+	}
+	if len(req.Remark) > 255 {
+		return response.ErrBadRequest("备注长度不能超过255个字符")
+	}
+
 	if req.PermsLevel < 1 || req.PermsLevel > 999 {
 		return response.ErrBadRequest("权限级别必须在 1-999 之间")
 	}

+ 3 - 3
internal/logic/role/updateRoleLogic_test.go

@@ -16,7 +16,7 @@ import (
 	"github.com/stretchr/testify/require"
 )
 
-// TC-0087: 正常更新
+// TC-0108: 正常更新
 func TestUpdateRole_Normal(t *testing.T) {
 	ctx := ctxhelper.SuperAdminCtx()
 	svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
@@ -54,7 +54,7 @@ func TestUpdateRole_Normal(t *testing.T) {
 	assert.Equal(t, int64(2), updated.Status)
 }
 
-// TC-0088: 不存在
+// TC-0109: 不存在
 func TestUpdateRole_NotFound(t *testing.T) {
 	ctx := ctxhelper.SuperAdminCtx()
 	svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
@@ -73,7 +73,7 @@ func TestUpdateRole_NotFound(t *testing.T) {
 	assert.Equal(t, "角色不存在", ce.Error())
 }
 
-// TC-0488: updateRole非管理员拒绝
+// TC-0518: updateRole非管理员拒绝
 func TestUpdateRole_MemberRejected(t *testing.T) {
 	pc := "test_product"
 	ctx := ctxhelper.MemberCtx(pc)

+ 1 - 1
internal/logic/user/bindRolesLogic_mock_test.go

@@ -17,7 +17,7 @@ import (
 	"go.uber.org/mock/gomock"
 )
 
-// TC-0136: 事务回滚
+// TC-0160: 事务回滚
 func TestBindRoles_Mock_BatchInsertFail(t *testing.T) {
 	ctrl := gomock.NewController(t)
 	defer ctrl.Finish()

+ 8 - 8
internal/logic/user/bindRolesLogic_test.go

@@ -49,7 +49,7 @@ func insertTestRole(t *testing.T, svcCtx *svc.ServiceContext, productCode string
 	return id
 }
 
-// TC-0133: 正常绑定
+// TC-0157: 正常绑定
 func TestBindRoles_Success(t *testing.T) {
 	ctx := ctxhelper.SuperAdminCtx()
 	svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
@@ -81,7 +81,7 @@ func TestBindRoles_Success(t *testing.T) {
 	assert.ElementsMatch(t, []int64{r1, r2}, roleIds)
 }
 
-// TC-0134: 用户不存在
+// TC-0158: 用户不存在
 func TestBindRoles_UserNotFound(t *testing.T) {
 	ctx := ctxhelper.SuperAdminCtx()
 	svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
@@ -99,7 +99,7 @@ func TestBindRoles_UserNotFound(t *testing.T) {
 	assert.Equal(t, "用户不存在", codeErr.Error())
 }
 
-// TC-0135: 清空角色
+// TC-0159: 清空角色
 func TestBindRoles_EmptyRoleIds_ClearsAll(t *testing.T) {
 	ctx := ctxhelper.SuperAdminCtx()
 	svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
@@ -136,7 +136,7 @@ func TestBindRoles_EmptyRoleIds_ClearsAll(t *testing.T) {
 	assert.Empty(t, roleIds)
 }
 
-// TC-0133: 正常重新绑定
+// TC-0157: 正常重新绑定
 func TestBindRoles_Rebind(t *testing.T) {
 	ctx := ctxhelper.SuperAdminCtx()
 	svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
@@ -175,7 +175,7 @@ func TestBindRoles_Rebind(t *testing.T) {
 	assert.ElementsMatch(t, []int64{r2, r3}, roleIds)
 }
 
-// TC-0515: 角色不属于当前产品
+// TC-0161: 角色不属于当前产品
 func TestBindRoles_RoleBelongsToOtherProduct(t *testing.T) {
 	ctx := ctxhelper.SuperAdminCtx()
 	svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
@@ -206,7 +206,7 @@ func TestBindRoles_RoleBelongsToOtherProduct(t *testing.T) {
 	assert.Contains(t, codeErr.Error(), "其他产品的角色")
 }
 
-// TC-0516: 角色已禁用
+// TC-0162: 角色已禁用
 func TestBindRoles_RoleDisabled(t *testing.T) {
 	ctx := ctxhelper.SuperAdminCtx()
 	svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
@@ -237,7 +237,7 @@ func TestBindRoles_RoleDisabled(t *testing.T) {
 	assert.Contains(t, codeErr.Error(), "已禁用的角色")
 }
 
-// TC-0517: 角色不存在
+// TC-0163: 角色不存在
 func TestBindRoles_RoleNotExists(t *testing.T) {
 	ctx := ctxhelper.SuperAdminCtx()
 	svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
@@ -264,7 +264,7 @@ func TestBindRoles_RoleNotExists(t *testing.T) {
 	assert.Contains(t, codeErr.Error(), "无效的角色ID")
 }
 
-// TC-0549: 目标用户不是当前产品成员时拒绝绑定角色(L-4修复验证)
+// TC-0164: 目标用户不是当前产品成员时拒绝绑定角色(L-4修复验证)
 func TestBindRoles_NonMemberRejected(t *testing.T) {
 	ctx := ctxhelper.SuperAdminCtx()
 	svcCtx := svc.NewServiceContext(testutil.GetTestConfig())

+ 9 - 0
internal/logic/user/createUserLogic.go

@@ -44,6 +44,15 @@ func (l *CreateUserLogic) CreateUser(req *types.CreateUserReq) (resp *types.IdRe
 	if len(req.Password) > 72 {
 		return nil, response.ErrBadRequest("密码长度不能超过72个字符")
 	}
+	if len(req.Username) > 64 {
+		return nil, response.ErrBadRequest("用户名长度不能超过64个字符")
+	}
+	if len(req.Nickname) > 64 {
+		return nil, response.ErrBadRequest("昵称长度不能超过64个字符")
+	}
+	if len(req.Remark) > 255 {
+		return nil, response.ErrBadRequest("备注长度不能超过255个字符")
+	}
 
 	if req.Email != "" && !util.IsValidEmail(req.Email) {
 		return nil, response.ErrBadRequest("邮箱格式不正确")

+ 1 - 1
internal/logic/user/createUserLogic_mock_test.go

@@ -15,7 +15,7 @@ import (
 	"go.uber.org/mock/gomock"
 )
 
-// TC-0111: 唯一索引冲突消息(Insert层1062错误)
+// TC-0132: 唯一索引冲突消息(Insert层1062错误)
 func TestCreateUser_Mock_InsertDuplicate1062(t *testing.T) {
 	ctrl := gomock.NewController(t)
 	defer ctrl.Finish()

+ 16 - 16
internal/logic/user/createUserLogic_test.go

@@ -65,7 +65,7 @@ func strPtr(s string) *string { return &s }
 
 func int64Ptr(i int64) *int64 { return &i }
 
-// TC-0101: 正常创建
+// TC-0122: 正常创建
 func TestCreateUser_Success(t *testing.T) {
 	ctx := ctxhelper.SuperAdminCtx()
 	svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
@@ -96,7 +96,7 @@ func TestCreateUser_Success(t *testing.T) {
 	assert.Equal(t, int64(2), user.IsSuperAdmin)
 }
 
-// TC-0102: 用户名已存在(预检)
+// TC-0123: 用户名已存在(预检)
 func TestCreateUser_UsernameExists(t *testing.T) {
 	ctx := ctxhelper.SuperAdminCtx()
 	svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
@@ -120,7 +120,7 @@ func TestCreateUser_UsernameExists(t *testing.T) {
 	assert.Equal(t, "用户名已存在", codeErr.Error())
 }
 
-// TC-0104: 非法email格式
+// TC-0125: 非法email格式
 func TestCreateUser_InvalidEmail(t *testing.T) {
 	ctx := ctxhelper.SuperAdminCtx()
 	svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
@@ -139,7 +139,7 @@ func TestCreateUser_InvalidEmail(t *testing.T) {
 	assert.Equal(t, "邮箱格式不正确", codeErr.Error())
 }
 
-// TC-0105: 合法email
+// TC-0126: 合法email
 func TestCreateUser_ValidEmail(t *testing.T) {
 	ctx := ctxhelper.SuperAdminCtx()
 	svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
@@ -162,7 +162,7 @@ func TestCreateUser_ValidEmail(t *testing.T) {
 	assert.Equal(t, username+"@example.com", user.Email)
 }
 
-// TC-0106: email为空(可选)
+// TC-0127: email为空(可选)
 func TestCreateUser_EmptyEmailSkipsValidation(t *testing.T) {
 	ctx := ctxhelper.SuperAdminCtx()
 	svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
@@ -185,7 +185,7 @@ func TestCreateUser_EmptyEmailSkipsValidation(t *testing.T) {
 	assert.Equal(t, "", user.Email)
 }
 
-// TC-0107: 非法phone格式
+// TC-0128: 非法phone格式
 func TestCreateUser_InvalidPhone(t *testing.T) {
 	ctx := ctxhelper.SuperAdminCtx()
 	svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
@@ -204,7 +204,7 @@ func TestCreateUser_InvalidPhone(t *testing.T) {
 	assert.Equal(t, "手机号格式不正确", codeErr.Error())
 }
 
-// TC-0108: 合法phone(国际)
+// TC-0129: 合法phone(国际)
 func TestCreateUser_ValidPhone(t *testing.T) {
 	ctx := ctxhelper.SuperAdminCtx()
 	svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
@@ -227,7 +227,7 @@ func TestCreateUser_ValidPhone(t *testing.T) {
 	assert.Equal(t, "13900139000", user.Phone)
 }
 
-// TC-0109: phone为空(可选)
+// TC-0130: phone为空(可选)
 func TestCreateUser_EmptyPhoneSkipsValidation(t *testing.T) {
 	ctx := ctxhelper.SuperAdminCtx()
 	svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
@@ -250,7 +250,7 @@ func TestCreateUser_EmptyPhoneSkipsValidation(t *testing.T) {
 	assert.Equal(t, "", user.Phone)
 }
 
-// TC-0110: 并发同username(TOCTOU)
+// TC-0131: 并发同username(TOCTOU)
 func TestCreateUser_ConcurrentSameUsername(t *testing.T) {
 	ctx := ctxhelper.SuperAdminCtx()
 	svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
@@ -302,7 +302,7 @@ func TestCreateUser_ConcurrentSameUsername(t *testing.T) {
 	assert.Equal(t, 1, failCount)
 }
 
-// TC-0108: 合法phone(国际)
+// TC-0129: 合法phone(国际)
 func TestCreateUser_ValidInternationalPhone(t *testing.T) {
 	ctx := ctxhelper.SuperAdminCtx()
 	svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
@@ -325,7 +325,7 @@ func TestCreateUser_ValidInternationalPhone(t *testing.T) {
 	assert.Equal(t, "+8613800138000", user.Phone)
 }
 
-// TC-0524: 密码少于6字符
+// TC-0133: 密码少于6字符
 func TestCreateUser_PasswordTooShort(t *testing.T) {
 	ctx := ctxhelper.SuperAdminCtx()
 	svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
@@ -343,7 +343,7 @@ func TestCreateUser_PasswordTooShort(t *testing.T) {
 	assert.Equal(t, "密码长度不能少于6个字符", codeErr.Error())
 }
 
-// TC-0525: 密码超过72字符
+// TC-0134: 密码超过72字符
 func TestCreateUser_PasswordTooLong(t *testing.T) {
 	ctx := ctxhelper.SuperAdminCtx()
 	svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
@@ -362,7 +362,7 @@ func TestCreateUser_PasswordTooLong(t *testing.T) {
 	assert.Equal(t, "密码长度不能超过72个字符", codeErr.Error())
 }
 
-// TC-0486: createUser非管理员拒绝
+// TC-0516: createUser非管理员拒绝
 func TestCreateUser_MemberRejected(t *testing.T) {
 	ctx := ctxhelper.MemberCtx("test_product")
 	svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
@@ -374,7 +374,7 @@ func TestCreateUser_MemberRejected(t *testing.T) {
 	assert.Equal(t, 403, ce.Code())
 }
 
-// TC-0103: 带完整可选字段
+// TC-0124: 带完整可选字段
 func TestCreateUser_AllOptionalFields(t *testing.T) {
 	ctx := ctxhelper.SuperAdminCtx()
 	svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
@@ -405,7 +405,7 @@ func TestCreateUser_AllOptionalFields(t *testing.T) {
 		Nickname: "全字段用户",
 		Email:    username + "@example.com",
 		Phone:    "13900001111",
-		Remark:   "TC-0103完整字段",
+		Remark:   "TC-0124完整字段",
 		DeptId:   deptId,
 	})
 	require.NoError(t, err)
@@ -420,7 +420,7 @@ func TestCreateUser_AllOptionalFields(t *testing.T) {
 	assert.Equal(t, "全字段用户", user.Nickname)
 	assert.Equal(t, username+"@example.com", user.Email)
 	assert.Equal(t, "13900001111", user.Phone)
-	assert.Equal(t, "TC-0103完整字段", user.Remark)
+	assert.Equal(t, "TC-0124完整字段", user.Remark)
 	assert.Equal(t, deptId, user.DeptId)
 	assert.Equal(t, int64(1), user.Status)
 	assert.Equal(t, int64(2), user.IsSuperAdmin)

+ 8 - 8
internal/logic/user/setUserPermsLogic_test.go

@@ -32,7 +32,7 @@ func insertTestPerm(t *testing.T, svcCtx *svc.ServiceContext, productCode string
 	return id
 }
 
-// TC-0137: 正常ALLOW
+// TC-0165: 正常ALLOW
 func TestSetUserPerms_Allow(t *testing.T) {
 	ctx := ctxhelper.SuperAdminCtx()
 	svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
@@ -70,7 +70,7 @@ func TestSetUserPerms_Allow(t *testing.T) {
 	}
 }
 
-// TC-0139: DENY权限
+// TC-0167: DENY权限
 func TestSetUserPerms_Deny(t *testing.T) {
 	ctx := ctxhelper.SuperAdminCtx()
 	svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
@@ -105,7 +105,7 @@ func TestSetUserPerms_Deny(t *testing.T) {
 	assert.Equal(t, p1, perms[0].PermId)
 }
 
-// TC-0138: 用户不存在
+// TC-0166: 用户不存在
 func TestSetUserPerms_UserNotFound(t *testing.T) {
 	ctx := ctxhelper.SuperAdminCtx()
 	svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
@@ -125,7 +125,7 @@ func TestSetUserPerms_UserNotFound(t *testing.T) {
 	assert.Equal(t, "用户不存在", codeErr.Error())
 }
 
-// TC-0140: 清空权限
+// TC-0168: 清空权限
 func TestSetUserPerms_EmptyPerms_ClearsAll(t *testing.T) {
 	ctx := ctxhelper.SuperAdminCtx()
 	svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
@@ -164,7 +164,7 @@ func TestSetUserPerms_EmptyPerms_ClearsAll(t *testing.T) {
 	assert.Empty(t, perms)
 }
 
-// TC-0518: 无效Effect值
+// TC-0169: 无效Effect值
 func TestSetUserPerms_InvalidEffect(t *testing.T) {
 	ctx := ctxhelper.SuperAdminCtx()
 	svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
@@ -193,7 +193,7 @@ func TestSetUserPerms_InvalidEffect(t *testing.T) {
 	assert.Contains(t, codeErr.Error(), "effect值无效")
 }
 
-// TC-0519: PermId不存在
+// TC-0170: PermId不存在
 func TestSetUserPerms_PermNotExists(t *testing.T) {
 	ctx := ctxhelper.SuperAdminCtx()
 	svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
@@ -222,7 +222,7 @@ func TestSetUserPerms_PermNotExists(t *testing.T) {
 	assert.Contains(t, codeErr.Error(), "无效的权限ID")
 }
 
-// TC-0520: 权限不属于当前产品
+// TC-0171: 权限不属于当前产品
 func TestSetUserPerms_PermBelongsToOtherProduct(t *testing.T) {
 	ctx := ctxhelper.SuperAdminCtx()
 	svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
@@ -255,7 +255,7 @@ func TestSetUserPerms_PermBelongsToOtherProduct(t *testing.T) {
 	assert.Contains(t, codeErr.Error(), "其他产品的权限")
 }
 
-// TC-0550: 目标用户不是当前产品成员时拒绝设置权限(L-5修复验证)
+// TC-0172: 目标用户不是当前产品成员时拒绝设置权限(L-5修复验证)
 func TestSetUserPerms_NonMemberRejected(t *testing.T) {
 	ctx := ctxhelper.SuperAdminCtx()
 	svcCtx := svc.NewServiceContext(testutil.GetTestConfig())

+ 17 - 2
internal/logic/user/updateUserLogic.go

@@ -49,8 +49,23 @@ func (l *UpdateUserLogic) UpdateUser(req *types.UpdateUserReq) error {
 		return response.ErrNotFound("用户不存在")
 	}
 
-	if caller.UserId != req.Id && req.Status != 0 && user.IsSuperAdmin == consts.IsSuperAdminYes {
-		return response.ErrForbidden("不能通过此接口修改超级管理员的状态")
+	if caller.UserId != req.Id && user.IsSuperAdmin == consts.IsSuperAdminYes {
+		if req.Status != 0 || req.DeptId != nil {
+			return response.ErrForbidden("不能通过此接口修改其他超级管理员的状态和部门")
+		}
+	}
+
+	if req.Nickname != nil && len(*req.Nickname) > 64 {
+		return response.ErrBadRequest("昵称长度不能超过64个字符")
+	}
+	if req.Email != nil && len(*req.Email) > 64 {
+		return response.ErrBadRequest("邮箱长度不能超过64个字符")
+	}
+	if req.Phone != nil && len(*req.Phone) > 32 {
+		return response.ErrBadRequest("手机号长度不能超过32个字符")
+	}
+	if req.Remark != nil && len(*req.Remark) > 255 {
+		return response.ErrBadRequest("备注长度不能超过255个字符")
 	}
 
 	if req.Nickname != nil {

+ 19 - 19
internal/logic/user/updateUserLogic_test.go

@@ -38,7 +38,7 @@ func insertTestDept(t *testing.T, ctx context.Context, svcCtx *svc.ServiceContex
 	return id
 }
 
-// TC-0112: 正常更新
+// TC-0135: 正常更新
 func TestUpdateUser_Success(t *testing.T) {
 	ctx := ctxhelper.SuperAdminCtx()
 	svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
@@ -71,7 +71,7 @@ func TestUpdateUser_Success(t *testing.T) {
 	assert.Equal(t, deptId, user.DeptId)
 }
 
-// TC-0113: 不存在
+// TC-0136: 不存在
 func TestUpdateUser_NotFound(t *testing.T) {
 	ctx := ctxhelper.SuperAdminCtx()
 	svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
@@ -89,7 +89,7 @@ func TestUpdateUser_NotFound(t *testing.T) {
 	assert.Equal(t, "用户不存在", codeErr.Error())
 }
 
-// TC-0114: 仅传id
+// TC-0137: 仅传id
 func TestUpdateUser_OnlyId_NothingChanges(t *testing.T) {
 	ctx := ctxhelper.SuperAdminCtx()
 	svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
@@ -114,7 +114,7 @@ func TestUpdateUser_OnlyId_NothingChanges(t *testing.T) {
 	assert.Equal(t, before.DeptId, after.DeptId)
 }
 
-// TC-0115: 清空nickname
+// TC-0138: 清空nickname
 func TestUpdateUser_ClearNickname(t *testing.T) {
 	ctx := ctxhelper.SuperAdminCtx()
 	svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
@@ -136,7 +136,7 @@ func TestUpdateUser_ClearNickname(t *testing.T) {
 	assert.Equal(t, "", user.Nickname)
 }
 
-// TC-0116: 清空email
+// TC-0139: 清空email
 func TestUpdateUser_ClearEmail(t *testing.T) {
 	ctx := ctxhelper.SuperAdminCtx()
 	svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
@@ -158,7 +158,7 @@ func TestUpdateUser_ClearEmail(t *testing.T) {
 	assert.Equal(t, "", user.Email)
 }
 
-// TC-0118: 非法email格式
+// TC-0141: 非法email格式
 func TestUpdateUser_InvalidEmail(t *testing.T) {
 	ctx := ctxhelper.SuperAdminCtx()
 	svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
@@ -181,7 +181,7 @@ func TestUpdateUser_InvalidEmail(t *testing.T) {
 	assert.Equal(t, "邮箱格式不正确", codeErr.Error())
 }
 
-// TC-0122: DeptId设为0(取消部门)
+// TC-0145: DeptId设为0(取消部门)
 func TestUpdateUser_DeptIdZero_Clear(t *testing.T) {
 	ctx := ctxhelper.SuperAdminCtx()
 	svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
@@ -212,7 +212,7 @@ func TestUpdateUser_DeptIdZero_Clear(t *testing.T) {
 	assert.Equal(t, int64(0), user.DeptId)
 }
 
-// TC-0123: DeptId设为正值
+// TC-0146: DeptId设为正值
 func TestUpdateUser_DeptIdSet(t *testing.T) {
 	ctx := ctxhelper.SuperAdminCtx()
 	svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
@@ -237,7 +237,7 @@ func TestUpdateUser_DeptIdSet(t *testing.T) {
 	assert.Equal(t, deptId, user.DeptId)
 }
 
-// TC-0124: DeptId不传(nil)
+// TC-0147: DeptId不传(nil)
 func TestUpdateUser_NilDeptId_Unchanged(t *testing.T) {
 	ctx := ctxhelper.SuperAdminCtx()
 	svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
@@ -269,7 +269,7 @@ func TestUpdateUser_NilDeptId_Unchanged(t *testing.T) {
 	assert.Equal(t, "changed", user.Nickname)
 }
 
-// TC-0117: 清空remark
+// TC-0140: 清空remark
 func TestUpdateUser_ClearRemark(t *testing.T) {
 	ctx := ctxhelper.SuperAdminCtx()
 	svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
@@ -297,7 +297,7 @@ func TestUpdateUser_ClearRemark(t *testing.T) {
 	assert.Equal(t, "", user.Remark)
 }
 
-// TC-0120: 合法phone
+// TC-0143: 合法phone
 func TestUpdateUser_ValidInternationalPhone(t *testing.T) {
 	ctx := ctxhelper.SuperAdminCtx()
 	svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
@@ -319,7 +319,7 @@ func TestUpdateUser_ValidInternationalPhone(t *testing.T) {
 	assert.Equal(t, "+8613800138000", user.Phone)
 }
 
-// TC-0119: 非法phone格式
+// TC-0142: 非法phone格式
 func TestUpdateUser_InvalidPhone(t *testing.T) {
 	ctx := ctxhelper.SuperAdminCtx()
 	svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
@@ -342,7 +342,7 @@ func TestUpdateUser_InvalidPhone(t *testing.T) {
 	assert.Equal(t, "手机号格式不正确", codeErr.Error())
 }
 
-// TC-0121: 不传email(nil)
+// TC-0144: 不传email(nil)
 func TestUpdateUser_NilEmail_Unchanged(t *testing.T) {
 	ctx := ctxhelper.SuperAdminCtx()
 	svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
@@ -369,7 +369,7 @@ func TestUpdateUser_NilEmail_Unchanged(t *testing.T) {
 	assert.Equal(t, "changed-nick", after.Nickname)
 }
 
-// TC-0491: 非本人非超管修改拒绝
+// TC-0521: 非本人非超管修改拒绝
 func TestUpdateUser_NonSelfNonSuperAdminRejected(t *testing.T) {
 	ctx := ctxhelper.MemberCtx("test_product")
 	svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
@@ -382,7 +382,7 @@ func TestUpdateUser_NonSelfNonSuperAdminRejected(t *testing.T) {
 	assert.Contains(t, ce.Error(), "仅允许修改自己的信息或超管操作")
 }
 
-// TC-0511: updateUser自己修改DeptId被拒绝
+// TC-0522: updateUser自己修改DeptId被拒绝
 func TestUpdateUser_SelfEditDeptIdRejected(t *testing.T) {
 	ctx := ctxhelper.CustomCtx(&loaders.UserDetails{
 		UserId:   100,
@@ -399,7 +399,7 @@ func TestUpdateUser_SelfEditDeptIdRejected(t *testing.T) {
 	assert.Equal(t, "不允许修改自己的部门和状态", ce.Error())
 }
 
-// TC-0512: updateUser自己修改Status被拒绝
+// TC-0523: updateUser自己修改Status被拒绝
 func TestUpdateUser_SelfEditStatusRejected(t *testing.T) {
 	ctx := ctxhelper.CustomCtx(&loaders.UserDetails{
 		UserId:   100,
@@ -416,7 +416,7 @@ func TestUpdateUser_SelfEditStatusRejected(t *testing.T) {
 	assert.Equal(t, "不允许修改自己的部门和状态", ce.Error())
 }
 
-// TC-0513: updateUser未登录被拒绝
+// TC-0524: updateUser未登录被拒绝
 func TestUpdateUser_NotLoggedInRejected(t *testing.T) {
 	ctx := context.Background()
 	svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
@@ -429,7 +429,7 @@ func TestUpdateUser_NotLoggedInRejected(t *testing.T) {
 	assert.Equal(t, "未登录", ce.Error())
 }
 
-// TC-0544: 超管A通过updateUser修改超管B的状态被拒绝(H-2修复验证)
+// TC-0148: 超管A通过updateUser修改超管B的状态被拒绝(H-2修复验证)
 func TestUpdateUser_SuperAdminCannotFreezeOtherSuperAdmin(t *testing.T) {
 	ctx := ctxhelper.SuperAdminCtx()
 	svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
@@ -460,7 +460,7 @@ func TestUpdateUser_SuperAdminCannotFreezeOtherSuperAdmin(t *testing.T) {
 	var ce *response.CodeError
 	require.True(t, errors.As(err, &ce))
 	assert.Equal(t, 403, ce.Code())
-	assert.Equal(t, "不能通过此接口修改超级管理员的状态", ce.Error())
+	assert.Equal(t, "不能通过此接口修改其他超级管理员的状态和部门", ce.Error())
 
 	user, err := svcCtx.SysUserModel.FindOne(ctx, superBId)
 	require.NoError(t, err)

+ 6 - 6
internal/logic/user/updateUserStatusLogic_test.go

@@ -24,7 +24,7 @@ func ctxWithUserId(userId int64) context.Context {
 	return middleware.WithUserDetails(context.Background(), &loaders.UserDetails{UserId: userId})
 }
 
-// TC-0141: 正常冻结
+// TC-0173: 正常冻结
 func TestUpdateUserStatus_Freeze(t *testing.T) {
 	ctx := context.Background()
 	svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
@@ -47,7 +47,7 @@ func TestUpdateUserStatus_Freeze(t *testing.T) {
 	assert.Equal(t, int64(2), user.Status)
 }
 
-// TC-0142: 正常解冻
+// TC-0174: 正常解冻
 func TestUpdateUserStatus_Unfreeze(t *testing.T) {
 	ctx := context.Background()
 	svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
@@ -78,7 +78,7 @@ func TestUpdateUserStatus_Unfreeze(t *testing.T) {
 	assert.Equal(t, int64(1), user.Status)
 }
 
-// TC-0143: 非法status(0)
+// TC-0175: 非法status(0)
 func TestUpdateUserStatus_InvalidStatus(t *testing.T) {
 	svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
 
@@ -95,7 +95,7 @@ func TestUpdateUserStatus_InvalidStatus(t *testing.T) {
 	assert.Contains(t, codeErr.Error(), "状态值无效")
 }
 
-// TC-0144: 冻结自己
+// TC-0176: 冻结自己
 func TestUpdateUserStatus_FreezeSelf(t *testing.T) {
 	ctx := context.Background()
 	svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
@@ -118,7 +118,7 @@ func TestUpdateUserStatus_FreezeSelf(t *testing.T) {
 	assert.Equal(t, "不能修改自己的状态", codeErr.Error())
 }
 
-// TC-0145: 冻结超管
+// TC-0177: 冻结超管
 func TestUpdateUserStatus_FreezeSuperAdmin(t *testing.T) {
 	ctx := context.Background()
 	svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
@@ -153,7 +153,7 @@ func TestUpdateUserStatus_FreezeSuperAdmin(t *testing.T) {
 	assert.Equal(t, "不能修改超级管理员的状态", codeErr.Error())
 }
 
-// TC-0145: 冻结超管
+// TC-0177: 冻结超管
 func TestUpdateUserStatus_NotFound(t *testing.T) {
 	svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
 

+ 11 - 0
internal/logic/user/userDetailLogic.go

@@ -3,6 +3,7 @@ package user
 import (
 	"context"
 
+	"perms-system-server/internal/middleware"
 	"perms-system-server/internal/response"
 	"perms-system-server/internal/svc"
 	"perms-system-server/internal/types"
@@ -25,6 +26,16 @@ func NewUserDetailLogic(ctx context.Context, svcCtx *svc.ServiceContext) *UserDe
 }
 
 func (l *UserDetailLogic) UserDetail(req *types.UserDetailReq) (resp *types.UserItem, err error) {
+	caller := middleware.GetUserDetails(l.ctx)
+	if caller == nil {
+		return nil, response.ErrUnauthorized("未登录")
+	}
+	if !caller.IsSuperAdmin && caller.ProductCode != "" {
+		if _, err := l.svcCtx.SysProductMemberModel.FindOneByProductCodeUserId(l.ctx, caller.ProductCode, req.Id); err != nil {
+			return nil, response.ErrForbidden("无权查看非本产品成员的用户信息")
+		}
+	}
+
 	user, err := l.svcCtx.SysUserModel.FindOne(l.ctx, req.Id)
 	if err != nil {
 		return nil, response.ErrNotFound("用户不存在")

+ 7 - 7
internal/logic/user/userDetailLogic_test.go

@@ -1,7 +1,6 @@
 package user
 
 import (
-	"context"
 	"database/sql"
 	"errors"
 	"testing"
@@ -12,15 +11,16 @@ import (
 	"perms-system-server/internal/response"
 	"perms-system-server/internal/svc"
 	"perms-system-server/internal/testutil"
+	"perms-system-server/internal/testutil/ctxhelper"
 	"perms-system-server/internal/types"
 
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
 )
 
-// TC-0130: 正常查询
+// TC-0154: 正常查询
 func TestUserDetail_Success(t *testing.T) {
-	ctx := context.Background()
+	ctx := ctxhelper.SuperAdminCtx()
 	svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
 	conn := testutil.GetTestSqlConn()
 
@@ -56,9 +56,9 @@ func TestUserDetail_Success(t *testing.T) {
 	assert.ElementsMatch(t, []int64{10, 20}, resp.RoleIds)
 }
 
-// TC-0131: 正常查询-含Avatar
+// TC-0155: 正常查询-含Avatar
 func TestUserDetail_WithAvatar(t *testing.T) {
-	ctx := context.Background()
+	ctx := ctxhelper.SuperAdminCtx()
 	svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
 	conn := testutil.GetTestSqlConn()
 
@@ -80,9 +80,9 @@ func TestUserDetail_WithAvatar(t *testing.T) {
 	assert.Equal(t, "https://example.com/avatar.png", resp.Avatar)
 }
 
-// TC-0132: 不存在
+// TC-0156: 不存在
 func TestUserDetail_NotFound(t *testing.T) {
-	ctx := context.Background()
+	ctx := ctxhelper.SuperAdminCtx()
 	svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
 
 	logic := NewUserDetailLogic(ctx, svcCtx)

+ 15 - 0
internal/logic/user/userListLogic.go

@@ -3,6 +3,8 @@ package user
 import (
 	"context"
 
+	"perms-system-server/internal/middleware"
+	"perms-system-server/internal/response"
 	"perms-system-server/internal/svc"
 	"perms-system-server/internal/types"
 	"perms-system-server/internal/util"
@@ -27,6 +29,19 @@ func NewUserListLogic(ctx context.Context, svcCtx *svc.ServiceContext) *UserList
 func (l *UserListLogic) UserList(req *types.UserListReq) (resp *types.PageResp, err error) {
 	page, pageSize := util.NormalizePage(req.Page, req.PageSize)
 
+	caller := middleware.GetUserDetails(l.ctx)
+	if caller == nil {
+		return nil, response.ErrUnauthorized("未登录")
+	}
+	if !caller.IsSuperAdmin {
+		if req.ProductCode == "" {
+			return nil, response.ErrForbidden("非超管用户必须指定产品编码")
+		}
+		if caller.ProductCode != req.ProductCode {
+			return nil, response.ErrForbidden("无权访问该产品的数据")
+		}
+	}
+
 	list, total, err := l.svcCtx.SysUserModel.FindListByPage(l.ctx, page, pageSize)
 	if err != nil {
 		return nil, err

+ 3 - 3
internal/logic/user/userListLogic_mock_test.go

@@ -1,11 +1,11 @@
 package user
 
 import (
-	"context"
 	"errors"
 	"testing"
 
 	userModel "perms-system-server/internal/model/user"
+	"perms-system-server/internal/testutil/ctxhelper"
 	"perms-system-server/internal/testutil/mocks"
 	"perms-system-server/internal/types"
 
@@ -13,7 +13,7 @@ import (
 	"go.uber.org/mock/gomock"
 )
 
-// TC-0129: 批量查询DB异常
+// TC-0153: 批量查询DB异常
 func TestUserList_Mock_FindMapError(t *testing.T) {
 	ctrl := gomock.NewController(t)
 	defer ctrl.Finish()
@@ -36,7 +36,7 @@ func TestUserList_Mock_FindMapError(t *testing.T) {
 		ProductMember: mockPM,
 	})
 
-	logic := NewUserListLogic(context.Background(), svcCtx)
+	logic := NewUserListLogic(ctxhelper.SuperAdminCtx(), svcCtx)
 	resp, err := logic.UserList(&types.UserListReq{
 		ProductCode: "pc",
 		Page:        1,

+ 9 - 9
internal/logic/user/userListLogic_test.go

@@ -1,22 +1,22 @@
 package user
 
 import (
-	"context"
 	"testing"
 	"time"
 
 	productMemberModel "perms-system-server/internal/model/productmember"
 	"perms-system-server/internal/svc"
 	"perms-system-server/internal/testutil"
+	"perms-system-server/internal/testutil/ctxhelper"
 	"perms-system-server/internal/types"
 
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
 )
 
-// TC-0125: 含productCode
+// TC-0149: 含productCode
 func TestUserList_WithProductCode(t *testing.T) {
-	ctx := context.Background()
+	ctx := ctxhelper.SuperAdminCtx()
 	svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
 	conn := testutil.GetTestSqlConn()
 
@@ -63,9 +63,9 @@ func TestUserList_WithProductCode(t *testing.T) {
 	assert.True(t, found, "should find the inserted user in the list")
 }
 
-// TC-0126: 不含productCode
+// TC-0150: 不含productCode
 func TestUserList_WithoutProductCode(t *testing.T) {
-	ctx := context.Background()
+	ctx := ctxhelper.SuperAdminCtx()
 	svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
 	conn := testutil.GetTestSqlConn()
 
@@ -91,9 +91,9 @@ func TestUserList_WithoutProductCode(t *testing.T) {
 	t.Fatal("should find inserted user in the list")
 }
 
-// TC-0127: pageSize超过上限
+// TC-0151: pageSize超过上限
 func TestUserList_PageSizeOver100_Capped(t *testing.T) {
-	ctx := context.Background()
+	ctx := ctxhelper.SuperAdminCtx()
 	svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
 
 	logic := NewUserListLogic(ctx, svcCtx)
@@ -108,9 +108,9 @@ func TestUserList_PageSizeOver100_Capped(t *testing.T) {
 	assert.LessOrEqual(t, len(items), 100)
 }
 
-// TC-0128: 用户不在产品中
+// TC-0152: 用户不在产品中
 func TestUserList_PartialNonMember(t *testing.T) {
-	ctx := context.Background()
+	ctx := ctxhelper.SuperAdminCtx()
 	svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
 	conn := testutil.GetTestSqlConn()
 	now := time.Now().Unix()

+ 11 - 6
internal/middleware/jwtauthMiddleware.go

@@ -21,12 +21,13 @@ const (
 
 // Claims JWT access token 的 Claims 结构。
 type Claims struct {
-	TokenType   string   `json:"tokenType"`
-	UserId      int64    `json:"userId"`
-	Username    string   `json:"username"`
-	ProductCode string   `json:"productCode"`
-	MemberType  string   `json:"memberType"`
-	Perms       []string `json:"perms"`
+	TokenType    string   `json:"tokenType"`
+	UserId       int64    `json:"userId"`
+	Username     string   `json:"username"`
+	ProductCode  string   `json:"productCode"`
+	MemberType   string   `json:"memberType"`
+	TokenVersion int64    `json:"tokenVersion"`
+	Perms        []string `json:"perms"`
 	jwt.RegisteredClaims
 }
 
@@ -75,6 +76,10 @@ func (m *JwtAuthMiddleware) Handle(next http.HandlerFunc) http.HandlerFunc {
 			httpx.ErrorCtx(r.Context(), w, response.NewCodeError(403, "账号已被冻结"))
 			return
 		}
+		if claims.TokenVersion != ud.TokenVersion {
+			httpx.ErrorCtx(r.Context(), w, response.NewCodeError(401, "登录状态已失效,请重新登录"))
+			return
+		}
 		ctx := context.WithValue(r.Context(), ctxKeyUserDetails, ud)
 		next(w, r.WithContext(ctx))
 	}

+ 8 - 8
internal/middleware/jwtauthMiddleware_test.go

@@ -84,7 +84,7 @@ func init() {
 	response.Setup()
 }
 
-// TC-0184: `Authorization: Bearer {valid}`
+// TC-0223: `Authorization: Bearer {valid}`
 func TestJwtAuthMiddleware_Handle(t *testing.T) {
 	m, _ := newTestMiddleware()
 
@@ -211,7 +211,7 @@ func TestJwtAuthMiddleware_Handle(t *testing.T) {
 		assert.Equal(t, "token无效或已过期", body.Msg)
 	})
 
-	// TC-0434: refresh token 不应被中间件接受
+	// TC-0229: refresh token 不应被中间件接受
 	t.Run("refresh token rejected", func(t *testing.T) {
 		tokenStr := generateTestToken(testAccessSecret, 3600, &middleware.Claims{
 			TokenType: consts.TokenTypeRefresh,
@@ -283,7 +283,7 @@ func TestJwtAuthMiddleware_Handle(t *testing.T) {
 	})
 }
 
-// TC-0258: ctx含userId=100
+// TC-0272: ctx含userId=100
 func TestGetUserId(t *testing.T) {
 	ctx := context.Background()
 	assert.Equal(t, int64(0), middleware.GetUserId(ctx))
@@ -295,7 +295,7 @@ func TestGetUserId(t *testing.T) {
 	assert.Equal(t, int64(0), middleware.GetUserId(ctx2))
 }
 
-// TC-0260: ctx含username="admin"
+// TC-0274: ctx含username="admin"
 func TestGetUsername(t *testing.T) {
 	ctx := context.Background()
 	assert.Equal(t, "", middleware.GetUsername(ctx))
@@ -304,7 +304,7 @@ func TestGetUsername(t *testing.T) {
 	assert.Equal(t, "admin", middleware.GetUsername(ctx))
 }
 
-// TC-0261: 空ctx
+// TC-0275: 空ctx
 func TestGetProductCode(t *testing.T) {
 	ctx := context.Background()
 	assert.Equal(t, "", middleware.GetProductCode(ctx))
@@ -313,7 +313,7 @@ func TestGetProductCode(t *testing.T) {
 	assert.Equal(t, "p1", middleware.GetProductCode(ctx))
 }
 
-// TC-0262: ctx含productCode="p1"
+// TC-0276: ctx含productCode="p1"
 func TestGetMemberType(t *testing.T) {
 	ctx := context.Background()
 	assert.Equal(t, "", middleware.GetMemberType(ctx))
@@ -322,7 +322,7 @@ func TestGetMemberType(t *testing.T) {
 	assert.Equal(t, "ADMIN", middleware.GetMemberType(ctx))
 }
 
-// TC-0263: ctx含memberType="ADMIN"
+// TC-0277: ctx含memberType="ADMIN"
 func TestIsSuperAdmin(t *testing.T) {
 	tests := []struct {
 		name         string
@@ -345,7 +345,7 @@ func TestIsSuperAdmin(t *testing.T) {
 	})
 }
 
-// TC-0189: claims类型断言失败(防御性分支)
+// TC-0228: claims类型断言失败(防御性分支)
 // jwt.ParseWithClaims(tokenStr, &Claims{}, keyFunc) 始终将 token.Claims 设为 *Claims,
 // 且解析失败时 Handle 已在 err!=nil 分支提前返回,因此 !ok 分支不可达。
 func TestJwtAuthMiddleware_Handle_ClaimsTypeAssertionUnreachable(t *testing.T) {

+ 10 - 2
internal/middleware/ratelimitMiddleware.go

@@ -2,6 +2,7 @@ package middleware
 
 import (
 	"fmt"
+	"net"
 	"net/http"
 
 	"perms-system-server/internal/response"
@@ -22,8 +23,7 @@ func NewRateLimitMiddleware(rds *redis.Redis, period int, quota int, keyPrefix s
 
 func (m *RateLimitMiddleware) Handle(next http.HandlerFunc) http.HandlerFunc {
 	return func(w http.ResponseWriter, r *http.Request) {
-		ip := r.RemoteAddr
-
+		ip := extractClientIP(r)
 		key := fmt.Sprintf("ip:%s", ip)
 		code, _ := m.limiter.Take(key)
 		if code == limit.OverQuota {
@@ -33,3 +33,11 @@ func (m *RateLimitMiddleware) Handle(next http.HandlerFunc) http.HandlerFunc {
 		next(w, r)
 	}
 }
+
+func extractClientIP(r *http.Request) string {
+	host, _, err := net.SplitHostPort(r.RemoteAddr)
+	if err != nil {
+		return r.RemoteAddr
+	}
+	return host
+}

+ 6 - 6
internal/middleware/ratelimitMiddleware_test.go

@@ -37,7 +37,7 @@ func newTestMiddleware(rds *redis.Redis, quota int) *RateLimitMiddleware {
 	return NewRateLimitMiddleware(rds, 60, quota, prefix)
 }
 
-// TC-0536: 正常请求(未超限)
+// TC-0525: 正常请求(未超限)
 func TestRateLimit_NormalRequest(t *testing.T) {
 	rds := newTestRedis()
 	m := newTestMiddleware(rds, 10)
@@ -57,7 +57,7 @@ func TestRateLimit_NormalRequest(t *testing.T) {
 	assert.Equal(t, http.StatusOK, w.Code)
 }
 
-// TC-0537: 超限请求被拒绝
+// TC-0526: 超限请求被拒绝
 func TestRateLimit_OverQuotaRejected(t *testing.T) {
 	rds := newTestRedis()
 	m := newTestMiddleware(rds, 2)
@@ -86,7 +86,7 @@ func TestRateLimit_OverQuotaRejected(t *testing.T) {
 	assert.Equal(t, "请求过于频繁,请稍后再试", body.Msg)
 }
 
-// TC-0538: X-Forwarded-For被忽略(M-1安全修复验证)
+// TC-0527: X-Forwarded-For被忽略(M-1安全修复验证)
 func TestRateLimit_XForwardedForIgnored(t *testing.T) {
 	rds := newTestRedis()
 	m := newTestMiddleware(rds, 1)
@@ -112,7 +112,7 @@ func TestRateLimit_XForwardedForIgnored(t *testing.T) {
 	assert.Equal(t, 1, nextCount, "different X-Forwarded-For should NOT bypass rate limit; RemoteAddr is used")
 }
 
-// TC-0539: X-Real-IP被忽略(M-1安全修复验证)
+// TC-0528: X-Real-IP被忽略(M-1安全修复验证)
 func TestRateLimit_XRealIPIgnored(t *testing.T) {
 	rds := newTestRedis()
 	m := newTestMiddleware(rds, 1)
@@ -138,7 +138,7 @@ func TestRateLimit_XRealIPIgnored(t *testing.T) {
 	assert.Equal(t, 1, nextCount, "different X-Real-IP should NOT bypass rate limit; RemoteAddr is used")
 }
 
-// TC-0540: IP从RemoteAddr获取
+// TC-0529: IP从RemoteAddr获取
 func TestRateLimit_IPFromRemoteAddr(t *testing.T) {
 	rds := newTestRedis()
 	m := newTestMiddleware(rds, 1)
@@ -165,7 +165,7 @@ func TestRateLimit_IPFromRemoteAddr(t *testing.T) {
 	assert.False(t, gotNext, "should be rate limited by RemoteAddr")
 }
 
-// TC-0541: 不同RemoteAddr独立限流
+// TC-0530: 不同RemoteAddr独立限流
 func TestRateLimit_DifferentIPsIndependent(t *testing.T) {
 	rds := newTestRedis()
 	m := newTestMiddleware(rds, 1)

+ 32 - 32
internal/model/dept/sysDeptModel_test.go

@@ -14,7 +14,7 @@ import (
 	"github.com/zeromicro/go-zero/core/stores/sqlx"
 )
 
-// TC-0267: 正常插入
+// TC-0281: 正常插入
 func TestSysDeptModel_CRUD(t *testing.T) {
 	ctx := context.Background()
 	conn := testutil.GetTestSqlConn()
@@ -60,7 +60,7 @@ func TestSysDeptModel_CRUD(t *testing.T) {
 	assert.True(t, errors.Is(err, ErrNotFound))
 }
 
-// TC-0388: 正常查询
+// TC-0411: 正常查询
 func TestSysDeptModel_FindAll_OrderBySortAscIdAsc(t *testing.T) {
 	ctx := context.Background()
 	conn := testutil.GetTestSqlConn()
@@ -107,7 +107,7 @@ func TestSysDeptModel_FindAll_OrderBySortAscIdAsc(t *testing.T) {
 	assert.Equal(t, int64(30), picked[2].Sort)
 }
 
-// TC-0390: 正常查询
+// TC-0413: 正常查询
 func TestSysDeptModel_FindByParentId_FoundAndNotFound(t *testing.T) {
 	ctx := context.Background()
 	conn := testutil.GetTestSqlConn()
@@ -158,7 +158,7 @@ func TestSysDeptModel_FindByParentId_FoundAndNotFound(t *testing.T) {
 	assert.Len(t, empty, 0)
 }
 
-// TC-0392: 正常查询
+// TC-0415: 正常查询
 func TestSysDeptModel_FindByPathPrefix_LikePrefix(t *testing.T) {
 	ctx := context.Background()
 	conn := testutil.GetTestSqlConn()
@@ -190,7 +190,7 @@ func TestSysDeptModel_FindByPathPrefix_LikePrefix(t *testing.T) {
 	assert.Contains(t, names, d2.Name)
 }
 
-// TC-0290: 多条记录(3条)
+// TC-0307: 多条记录(3条)
 func TestSysDeptModel_BatchInsert_BatchDelete(t *testing.T) {
 	ctx := context.Background()
 	conn := testutil.GetTestSqlConn()
@@ -219,7 +219,7 @@ func TestSysDeptModel_BatchInsert_BatchDelete(t *testing.T) {
 	}
 }
 
-// TC-0281: 事务内更新
+// TC-0298: 事务内更新
 func TestSysDeptModel_TransactCtx_InsertWithTx_UpdateWithTx(t *testing.T) {
 	ctx := context.Background()
 	conn := testutil.GetTestSqlConn()
@@ -262,14 +262,14 @@ func TestSysDeptModel_TransactCtx_InsertWithTx_UpdateWithTx(t *testing.T) {
 	assert.Equal(t, "after_tx", out.Remark)
 }
 
-// TC-0287: 获取表名
+// TC-0304: 获取表名
 func TestSysDeptModel_TableName(t *testing.T) {
 	conn := testutil.GetTestSqlConn()
 	m := NewSysDeptModel(conn, testutil.GetTestCacheConf(), testutil.GetTestCachePrefix())
 	assert.Equal(t, "`sys_dept`", m.TableName())
 }
 
-// TC-0274: 记录不存在
+// TC-0290: 记录不存在
 func TestSysDeptModel_FindOne_NotFound(t *testing.T) {
 	conn := testutil.GetTestSqlConn()
 	m := NewSysDeptModel(conn, testutil.GetTestCacheConf(), testutil.GetTestCachePrefix())
@@ -277,7 +277,7 @@ func TestSysDeptModel_FindOne_NotFound(t *testing.T) {
 	require.ErrorIs(t, err, ErrNotFound)
 }
 
-// TC-0280: 记录不存在
+// TC-0297: 记录不存在
 func TestSysDeptModel_Update_NonExistentRow_NoError(t *testing.T) {
 	conn := testutil.GetTestSqlConn()
 	m := NewSysDeptModel(conn, testutil.GetTestCacheConf(), testutil.GetTestCachePrefix())
@@ -288,7 +288,7 @@ func TestSysDeptModel_Update_NonExistentRow_NoError(t *testing.T) {
 	require.NoError(t, err)
 }
 
-// TC-0283: 记录不存在
+// TC-0300: 记录不存在
 func TestSysDeptModel_Delete_NonExistentRow_NoError(t *testing.T) {
 	conn := testutil.GetTestSqlConn()
 	m := NewSysDeptModel(conn, testutil.GetTestCacheConf(), testutil.GetTestCachePrefix())
@@ -296,7 +296,7 @@ func TestSysDeptModel_Delete_NonExistentRow_NoError(t *testing.T) {
 	require.NoError(t, err)
 }
 
-// TC-0288: 空列表
+// TC-0305: 空列表
 func TestSysDeptModel_BatchInsert_Empty(t *testing.T) {
 	conn := testutil.GetTestSqlConn()
 	m := NewSysDeptModel(conn, testutil.GetTestCacheConf(), testutil.GetTestCachePrefix())
@@ -304,7 +304,7 @@ func TestSysDeptModel_BatchInsert_Empty(t *testing.T) {
 	require.NoError(t, m.BatchInsert(context.Background(), []*SysDept{}))
 }
 
-// TC-0305: 空ids
+// TC-0324: 空ids
 func TestSysDeptModel_BatchDelete_Empty(t *testing.T) {
 	conn := testutil.GetTestSqlConn()
 	m := NewSysDeptModel(conn, testutil.GetTestCacheConf(), testutil.GetTestCachePrefix())
@@ -312,7 +312,7 @@ func TestSysDeptModel_BatchDelete_Empty(t *testing.T) {
 	require.NoError(t, m.BatchDelete(context.Background(), []int64{}))
 }
 
-// TC-0394: 无匹配
+// TC-0417: 无匹配
 func TestSysDeptModel_FindByPathPrefix_NoMatch(t *testing.T) {
 	conn := testutil.GetTestSqlConn()
 	m := NewSysDeptModel(conn, testutil.GetTestCacheConf(), testutil.GetTestCachePrefix())
@@ -321,7 +321,7 @@ func TestSysDeptModel_FindByPathPrefix_NoMatch(t *testing.T) {
 	assert.Empty(t, list)
 }
 
-// TC-0271: 事务回滚后无数据
+// TC-0287: 事务回滚后无数据
 func TestSysDeptModel_InsertWithTx_Rollback(t *testing.T) {
 	ctx := context.Background()
 	conn := testutil.GetTestSqlConn()
@@ -352,7 +352,7 @@ func TestSysDeptModel_InsertWithTx_Rollback(t *testing.T) {
 	assert.Empty(t, list)
 }
 
-// TC-0284: 事务内删除
+// TC-0301: 事务内删除
 func TestSysDeptModel_DeleteWithTx(t *testing.T) {
 	ctx := context.Background()
 	conn := testutil.GetTestSqlConn()
@@ -383,7 +383,7 @@ func TestSysDeptModel_DeleteWithTx(t *testing.T) {
 	require.ErrorIs(t, err, ErrNotFound)
 }
 
-// TC-0286: fn返回错误
+// TC-0303: fn返回错误
 func TestSysDeptModel_TransactCtx_Rollback(t *testing.T) {
 	ctx := context.Background()
 	conn := testutil.GetTestSqlConn()
@@ -415,7 +415,7 @@ func TestSysDeptModel_TransactCtx_Rollback(t *testing.T) {
 	assert.Empty(t, list)
 }
 
-// TC-0296: 空列表
+// TC-0314: 空列表
 func TestSysDeptModel_BatchUpdate_Empty(t *testing.T) {
 	conn := testutil.GetTestSqlConn()
 	m := NewSysDeptModel(conn, testutil.GetTestCacheConf(), testutil.GetTestCachePrefix())
@@ -423,7 +423,7 @@ func TestSysDeptModel_BatchUpdate_Empty(t *testing.T) {
 	require.NoError(t, m.BatchUpdate(context.Background(), []*SysDept{}))
 }
 
-// TC-0298: 多条记录(3条)
+// TC-0316: 多条记录(3条)
 func TestSysDeptModel_BatchUpdate_Multi(t *testing.T) {
 	ctx := context.Background()
 	conn := testutil.GetTestSqlConn()
@@ -461,7 +461,7 @@ func TestSysDeptModel_BatchUpdate_Multi(t *testing.T) {
 	assert.Equal(t, int64(2), g2.Status)
 }
 
-// TC-0289: 单条记录
+// TC-0306: 单条记录
 func TestSysDeptModel_BatchInsert_Single(t *testing.T) {
 	ctx := context.Background()
 	conn := testutil.GetTestSqlConn()
@@ -481,7 +481,7 @@ func TestSysDeptModel_BatchInsert_Single(t *testing.T) {
 	assert.Equal(t, d.Name, list[0].Name)
 }
 
-// TC-0294: 正常多条
+// TC-0312: 正常多条
 func TestSysDeptModel_BatchInsertWithTx_Normal(t *testing.T) {
 	ctx := context.Background()
 	conn := testutil.GetTestSqlConn()
@@ -506,7 +506,7 @@ func TestSysDeptModel_BatchInsertWithTx_Normal(t *testing.T) {
 	t.Cleanup(func() { testutil.CleanTable(ctx, conn, tbl, list[0].Id, list[1].Id) })
 }
 
-// TC-0293: 空列表
+// TC-0311: 空列表
 func TestSysDeptModel_BatchInsertWithTx_Empty(t *testing.T) {
 	conn := testutil.GetTestSqlConn()
 	m := NewSysDeptModel(conn, testutil.GetTestCacheConf(), testutil.GetTestCachePrefix())
@@ -516,7 +516,7 @@ func TestSysDeptModel_BatchInsertWithTx_Empty(t *testing.T) {
 	require.NoError(t, err)
 }
 
-// TC-0295: 事务回滚
+// TC-0313: 事务回滚
 func TestSysDeptModel_BatchInsertWithTx_Rollback(t *testing.T) {
 	ctx := context.Background()
 	conn := testutil.GetTestSqlConn()
@@ -541,7 +541,7 @@ func TestSysDeptModel_BatchInsertWithTx_Rollback(t *testing.T) {
 	assert.Empty(t, list)
 }
 
-// TC-0301: 正常多条
+// TC-0320: 正常多条
 func TestSysDeptModel_BatchUpdateWithTx_Normal(t *testing.T) {
 	ctx := context.Background()
 	conn := testutil.GetTestSqlConn()
@@ -580,7 +580,7 @@ func TestSysDeptModel_BatchUpdateWithTx_Normal(t *testing.T) {
 	assert.Equal(t, "butx2_new", g2.Name)
 }
 
-// TC-0300: 空列表
+// TC-0319: 空列表
 func TestSysDeptModel_BatchUpdateWithTx_Empty(t *testing.T) {
 	conn := testutil.GetTestSqlConn()
 	m := NewSysDeptModel(conn, testutil.GetTestCacheConf(), testutil.GetTestCachePrefix())
@@ -590,7 +590,7 @@ func TestSysDeptModel_BatchUpdateWithTx_Empty(t *testing.T) {
 	require.NoError(t, err)
 }
 
-// TC-0306: 单个id
+// TC-0325: 单个id
 func TestSysDeptModel_BatchDelete_Single(t *testing.T) {
 	ctx := context.Background()
 	conn := testutil.GetTestSqlConn()
@@ -610,7 +610,7 @@ func TestSysDeptModel_BatchDelete_Single(t *testing.T) {
 	require.ErrorIs(t, err, ErrNotFound)
 }
 
-// TC-0308: 包含不存在id
+// TC-0327: 包含不存在id
 func TestSysDeptModel_BatchDelete_ContainsNonExist(t *testing.T) {
 	ctx := context.Background()
 	conn := testutil.GetTestSqlConn()
@@ -630,7 +630,7 @@ func TestSysDeptModel_BatchDelete_ContainsNonExist(t *testing.T) {
 	require.ErrorIs(t, err, ErrNotFound)
 }
 
-// TC-0310: 正常多条
+// TC-0329: 正常多条
 func TestSysDeptModel_BatchDeleteWithTx_Normal(t *testing.T) {
 	ctx := context.Background()
 	conn := testutil.GetTestSqlConn()
@@ -661,7 +661,7 @@ func TestSysDeptModel_BatchDeleteWithTx_Normal(t *testing.T) {
 	require.ErrorIs(t, err, ErrNotFound)
 }
 
-// TC-0309: 空ids
+// TC-0328: 空ids
 func TestSysDeptModel_BatchDeleteWithTx_Empty(t *testing.T) {
 	conn := testutil.GetTestSqlConn()
 	m := NewSysDeptModel(conn, testutil.GetTestCacheConf(), testutil.GetTestCachePrefix())
@@ -671,7 +671,7 @@ func TestSysDeptModel_BatchDeleteWithTx_Empty(t *testing.T) {
 	require.NoError(t, err)
 }
 
-// TC-0393: LIKE注入已修复 — % 和 _ 被转义,不再作为通配符
+// TC-0416: LIKE注入已修复 — % 和 _ 被转义,不再作为通配符
 func TestSysDeptModel_FindByPathPrefix_LikeInjection(t *testing.T) {
 	ctx := context.Background()
 	conn := testutil.GetTestSqlConn()
@@ -694,7 +694,7 @@ func TestSysDeptModel_FindByPathPrefix_LikeInjection(t *testing.T) {
 	assert.GreaterOrEqual(t, len(list2), 1, "正常前缀仍应匹配")
 }
 
-// TC-0278: 事务内可见性
+// TC-0294: 事务内可见性
 func TestSysDeptModel_FindOneWithTx_InsertThenFind(t *testing.T) {
 	ctx := context.Background()
 	conn := testutil.GetTestSqlConn()
@@ -724,7 +724,7 @@ func TestSysDeptModel_FindOneWithTx_InsertThenFind(t *testing.T) {
 	assert.Equal(t, insertedId, foundInTx.Id)
 }
 
-// TC-0277: 事务内记录不存在
+// TC-0293: 事务内记录不存在
 func TestSysDeptModel_FindOneWithTx_NotFound(t *testing.T) {
 	ctx := context.Background()
 	conn := testutil.GetTestSqlConn()
@@ -737,7 +737,7 @@ func TestSysDeptModel_FindOneWithTx_NotFound(t *testing.T) {
 	require.ErrorIs(t, err, ErrNotFound)
 }
 
-// TC-0391: FindByParentId 无子部门
+// TC-0414: FindByParentId 无子部门
 func TestSysDeptModel_FindByParentId_NoChildren(t *testing.T) {
 	ctx := context.Background()
 	conn := testutil.GetTestSqlConn()

+ 48 - 48
internal/model/perm/sysPermModel_test.go

@@ -34,7 +34,7 @@ func isDuplicateKeyError(err error) bool {
 	return errors.As(err, &me) && me.Number == 1062
 }
 
-// TC-0267: 正常插入
+// TC-0281: 正常插入
 func TestSysPermModel_CRUD(t *testing.T) {
 	ctx := context.Background()
 	conn := testutil.GetTestSqlConn()
@@ -92,7 +92,7 @@ func TestSysPermModel_CRUD(t *testing.T) {
 	require.ErrorIs(t, err, perm.ErrNotFound)
 }
 
-// TC-0323: FindOneByProductCodeCode
+// TC-0342: FindOneByProductCodeCode
 func TestSysPermModel_FindOneByProductCodeCode(t *testing.T) {
 	ctx := context.Background()
 	conn := testutil.GetTestSqlConn()
@@ -129,7 +129,7 @@ func TestSysPermModel_FindOneByProductCodeCode(t *testing.T) {
 	require.ErrorIs(t, err, perm.ErrNotFound)
 }
 
-// TC-0373: 正常分页
+// TC-0394: 正常分页
 func TestSysPermModel_FindListByProductCode_Pagination(t *testing.T) {
 	ctx := context.Background()
 	conn := testutil.GetTestSqlConn()
@@ -181,7 +181,7 @@ func TestSysPermModel_FindListByProductCode_Pagination(t *testing.T) {
 	require.Len(t, page3, 1)
 }
 
-// TC-0375: 正常查询(仅status=1)
+// TC-0396: 正常查询(仅status=1)
 func TestSysPermModel_FindAllByProductCode_OnlyEnabled(t *testing.T) {
 	ctx := context.Background()
 	conn := testutil.GetTestSqlConn()
@@ -220,7 +220,7 @@ func TestSysPermModel_FindAllByProductCode_OnlyEnabled(t *testing.T) {
 	}
 }
 
-// TC-0377: 正常查询
+// TC-0398: 正常查询
 func TestSysPermModel_FindAllCodesByProductCode_OnlyEnabled(t *testing.T) {
 	ctx := context.Background()
 	conn := testutil.GetTestSqlConn()
@@ -265,7 +265,7 @@ func TestSysPermModel_FindAllCodesByProductCode_OnlyEnabled(t *testing.T) {
 	require.Equal(t, []string{codeOn}, codes)
 }
 
-// TC-0379: 正常
+// TC-0400: 正常
 func TestSysPermModel_FindByIds(t *testing.T) {
 	ctx := context.Background()
 	conn := testutil.GetTestSqlConn()
@@ -326,7 +326,7 @@ func TestSysPermModel_FindByIds(t *testing.T) {
 	})
 }
 
-// TC-0381: 正常查询
+// TC-0402: 正常查询
 func TestSysPermModel_FindMapByProductCode(t *testing.T) {
 	ctx := context.Background()
 	conn := testutil.GetTestSqlConn()
@@ -368,7 +368,7 @@ func TestSysPermModel_FindMapByProductCode(t *testing.T) {
 	require.Equal(t, productCode, mp[c2].ProductCode)
 }
 
-// TC-0384: codes非空-正常
+// TC-0405: codes非空-正常
 func TestSysPermModel_DisableNotInCodes(t *testing.T) {
 	ctx := context.Background()
 	conn := testutil.GetTestSqlConn()
@@ -492,7 +492,7 @@ func TestSysPermModel_DisableNotInCodes(t *testing.T) {
 	})
 }
 
-// TC-0290: 多条记录(3条)
+// TC-0307: 多条记录(3条)
 func TestSysPermModel_BatchInsert_BatchDelete(t *testing.T) {
 	ctx := context.Background()
 	conn := testutil.GetTestSqlConn()
@@ -543,7 +543,7 @@ func TestSysPermModel_BatchInsert_BatchDelete(t *testing.T) {
 	}
 }
 
-// TC-0268: 唯一索引冲突
+// TC-0283: 唯一索引冲突
 func TestSysPermModel_InsertDuplicateProductCodeCode(t *testing.T) {
 	ctx := context.Background()
 	conn := testutil.GetTestSqlConn()
@@ -585,14 +585,14 @@ func TestSysPermModel_InsertDuplicateProductCodeCode(t *testing.T) {
 	assert.True(t, isDuplicateKeyError(err), "expected MySQL duplicate key error, got: %v", err)
 }
 
-// TC-0274: 记录不存在
+// TC-0290: 记录不存在
 func TestSysPermModel_FindOne_NotFound(t *testing.T) {
 	m := newTestSysPermModel(t)
 	_, err := m.FindOne(context.Background(), 999999999999)
 	require.ErrorIs(t, err, perm.ErrNotFound)
 }
 
-// TC-0280: 记录不存在
+// TC-0297: 记录不存在
 func TestSysPermModel_Update_NotFound(t *testing.T) {
 	m := newTestSysPermModel(t)
 	err := m.Update(context.Background(), &perm.SysPerm{
@@ -602,28 +602,28 @@ func TestSysPermModel_Update_NotFound(t *testing.T) {
 	require.ErrorIs(t, err, perm.ErrNotFound)
 }
 
-// TC-0283: 记录不存在
+// TC-0300: 记录不存在
 func TestSysPermModel_Delete_NotFound(t *testing.T) {
 	m := newTestSysPermModel(t)
 	err := m.Delete(context.Background(), 999999999999)
 	require.ErrorIs(t, err, perm.ErrNotFound)
 }
 
-// TC-0288: 空列表
+// TC-0305: 空列表
 func TestSysPermModel_BatchInsert_Empty(t *testing.T) {
 	m := newTestSysPermModel(t)
 	require.NoError(t, m.BatchInsert(context.Background(), nil))
 	require.NoError(t, m.BatchInsert(context.Background(), []*perm.SysPerm{}))
 }
 
-// TC-0305: 空ids
+// TC-0324: 空ids
 func TestSysPermModel_BatchDelete_Empty(t *testing.T) {
 	m := newTestSysPermModel(t)
 	require.NoError(t, m.BatchDelete(context.Background(), nil))
 	require.NoError(t, m.BatchDelete(context.Background(), []int64{}))
 }
 
-// TC-0374: 不存在的productCode
+// TC-0395: 不存在的productCode
 func TestSysPermModel_FindListByProductCode_NotExistProduct(t *testing.T) {
 	m := newTestSysPermModel(t)
 	list, total, err := m.FindListByProductCode(context.Background(), "notexist_"+testutil.UniqueId(), 1, 10)
@@ -632,7 +632,7 @@ func TestSysPermModel_FindListByProductCode_NotExistProduct(t *testing.T) {
 	require.Len(t, list, 0)
 }
 
-// TC-0378: 空结果
+// TC-0399: 空结果
 func TestSysPermModel_FindAllCodesByProductCode_Empty(t *testing.T) {
 	m := newTestSysPermModel(t)
 	codes, err := m.FindAllCodesByProductCode(context.Background(), "empty_"+testutil.UniqueId())
@@ -640,7 +640,7 @@ func TestSysPermModel_FindAllCodesByProductCode_Empty(t *testing.T) {
 	require.Empty(t, codes)
 }
 
-// TC-0382: 空结果
+// TC-0403: 空结果
 func TestSysPermModel_FindMapByProductCode_Empty(t *testing.T) {
 	m := newTestSysPermModel(t)
 	mp, err := m.FindMapByProductCode(context.Background(), "empty_"+testutil.UniqueId())
@@ -648,7 +648,7 @@ func TestSysPermModel_FindMapByProductCode_Empty(t *testing.T) {
 	require.Empty(t, mp)
 }
 
-// TC-0270: 事务内插入
+// TC-0285: 事务内插入
 func TestSysPermModel_InsertWithTx_Normal(t *testing.T) {
 	ctx := context.Background()
 	conn := testutil.GetTestSqlConn()
@@ -686,7 +686,7 @@ func TestSysPermModel_InsertWithTx_Normal(t *testing.T) {
 	require.Equal(t, code, got.Code)
 }
 
-// TC-0271: 事务回滚后无数据
+// TC-0287: 事务回滚后无数据
 func TestSysPermModel_InsertWithTx_Rollback(t *testing.T) {
 	ctx := context.Background()
 	m := newTestSysPermModel(t)
@@ -716,7 +716,7 @@ func TestSysPermModel_InsertWithTx_Rollback(t *testing.T) {
 	require.ErrorIs(t, err, perm.ErrNotFound)
 }
 
-// TC-0281: 事务内更新
+// TC-0298: 事务内更新
 func TestSysPermModel_UpdateWithTx(t *testing.T) {
 	ctx := context.Background()
 	conn := testutil.GetTestSqlConn()
@@ -753,7 +753,7 @@ func TestSysPermModel_UpdateWithTx(t *testing.T) {
 	require.Equal(t, "after", got.Name)
 }
 
-// TC-0284: 事务内删除
+// TC-0301: 事务内删除
 func TestSysPermModel_DeleteWithTx(t *testing.T) {
 	ctx := context.Background()
 	conn := testutil.GetTestSqlConn()
@@ -786,7 +786,7 @@ func TestSysPermModel_DeleteWithTx(t *testing.T) {
 	require.ErrorIs(t, err, perm.ErrNotFound)
 }
 
-// TC-0285: 正常事务
+// TC-0302: 正常事务
 func TestSysPermModel_TransactCtx_CommitAndRollback(t *testing.T) {
 	ctx := context.Background()
 	conn := testutil.GetTestSqlConn()
@@ -829,13 +829,13 @@ func TestSysPermModel_TransactCtx_CommitAndRollback(t *testing.T) {
 	require.ErrorIs(t, err, perm.ErrNotFound)
 }
 
-// TC-0287: 获取表名
+// TC-0304: 获取表名
 func TestSysPermModel_TableName(t *testing.T) {
 	m := newTestSysPermModel(t)
 	require.Equal(t, "`sys_perm`", m.TableName())
 }
 
-// TC-0289: 单条记录
+// TC-0306: 单条记录
 func TestSysPermModel_BatchInsert_Single(t *testing.T) {
 	ctx := context.Background()
 	conn := testutil.GetTestSqlConn()
@@ -855,7 +855,7 @@ func TestSysPermModel_BatchInsert_Single(t *testing.T) {
 	require.Equal(t, code, found.Code)
 }
 
-// TC-0291: 唯一索引冲突
+// TC-0309: 唯一索引冲突
 func TestSysPermModel_BatchInsert_UniqueConflict(t *testing.T) {
 	ctx := context.Background()
 	conn := testutil.GetTestSqlConn()
@@ -880,14 +880,14 @@ func TestSysPermModel_BatchInsert_UniqueConflict(t *testing.T) {
 	})
 }
 
-// TC-0296: 空列表
+// TC-0314: 空列表
 func TestSysPermModel_BatchUpdate_Empty(t *testing.T) {
 	m := newTestSysPermModel(t)
 	require.NoError(t, m.BatchUpdate(context.Background(), nil))
 	require.NoError(t, m.BatchUpdate(context.Background(), []*perm.SysPerm{}))
 }
 
-// TC-0298: 多条记录(3条)
+// TC-0316: 多条记录(3条)
 func TestSysPermModel_BatchUpdate_Multi(t *testing.T) {
 	ctx := context.Background()
 	conn := testutil.GetTestSqlConn()
@@ -923,7 +923,7 @@ func TestSysPermModel_BatchUpdate_Multi(t *testing.T) {
 	require.Equal(t, "u2_new", g2.Name)
 }
 
-// TC-0294: 正常多条
+// TC-0312: 正常多条
 func TestSysPermModel_BatchInsertWithTx_Normal(t *testing.T) {
 	ctx := context.Background()
 	conn := testutil.GetTestSqlConn()
@@ -952,7 +952,7 @@ func TestSysPermModel_BatchInsertWithTx_Normal(t *testing.T) {
 	require.Equal(t, c2, f2.Code)
 }
 
-// TC-0293: 空列表
+// TC-0311: 空列表
 func TestSysPermModel_BatchInsertWithTx_Empty(t *testing.T) {
 	m := newTestSysPermModel(t)
 	err := m.TransactCtx(context.Background(), func(c context.Context, session sqlx.Session) error {
@@ -961,7 +961,7 @@ func TestSysPermModel_BatchInsertWithTx_Empty(t *testing.T) {
 	require.NoError(t, err)
 }
 
-// TC-0295: 事务回滚
+// TC-0313: 事务回滚
 func TestSysPermModel_BatchInsertWithTx_Rollback(t *testing.T) {
 	ctx := context.Background()
 	m := newTestSysPermModel(t)
@@ -984,7 +984,7 @@ func TestSysPermModel_BatchInsertWithTx_Rollback(t *testing.T) {
 	require.ErrorIs(t, err, perm.ErrNotFound)
 }
 
-// TC-0301: 正常多条
+// TC-0320: 正常多条
 func TestSysPermModel_BatchUpdateWithTx_Normal(t *testing.T) {
 	ctx := context.Background()
 	conn := testutil.GetTestSqlConn()
@@ -1021,7 +1021,7 @@ func TestSysPermModel_BatchUpdateWithTx_Normal(t *testing.T) {
 	require.Equal(t, int64(2), g2.Status)
 }
 
-// TC-0300: 空列表
+// TC-0319: 空列表
 func TestSysPermModel_BatchUpdateWithTx_Empty(t *testing.T) {
 	m := newTestSysPermModel(t)
 	err := m.TransactCtx(context.Background(), func(c context.Context, session sqlx.Session) error {
@@ -1030,7 +1030,7 @@ func TestSysPermModel_BatchUpdateWithTx_Empty(t *testing.T) {
 	require.NoError(t, err)
 }
 
-// TC-0306: 单个id
+// TC-0325: 单个id
 func TestSysPermModel_BatchDelete_Single(t *testing.T) {
 	ctx := context.Background()
 	conn := testutil.GetTestSqlConn()
@@ -1050,7 +1050,7 @@ func TestSysPermModel_BatchDelete_Single(t *testing.T) {
 	require.ErrorIs(t, err, perm.ErrNotFound)
 }
 
-// TC-0308: 包含不存在id
+// TC-0327: 包含不存在id
 func TestSysPermModel_BatchDelete_ContainsNonExist(t *testing.T) {
 	ctx := context.Background()
 	conn := testutil.GetTestSqlConn()
@@ -1070,7 +1070,7 @@ func TestSysPermModel_BatchDelete_ContainsNonExist(t *testing.T) {
 	require.ErrorIs(t, err, perm.ErrNotFound)
 }
 
-// TC-0310: 正常多条
+// TC-0329: 正常多条
 func TestSysPermModel_BatchDeleteWithTx_Normal(t *testing.T) {
 	ctx := context.Background()
 	conn := testutil.GetTestSqlConn()
@@ -1099,7 +1099,7 @@ func TestSysPermModel_BatchDeleteWithTx_Normal(t *testing.T) {
 	require.ErrorIs(t, err, perm.ErrNotFound)
 }
 
-// TC-0309: 空ids
+// TC-0328: 空ids
 func TestSysPermModel_BatchDeleteWithTx_Empty(t *testing.T) {
 	m := newTestSysPermModel(t)
 	err := m.TransactCtx(context.Background(), func(c context.Context, session sqlx.Session) error {
@@ -1108,7 +1108,7 @@ func TestSysPermModel_BatchDeleteWithTx_Empty(t *testing.T) {
 	require.NoError(t, err)
 }
 
-// TC-0376: 无启用权限
+// TC-0397: 无启用权限
 func TestSysPermModel_FindAllByProductCode_AllDisabled(t *testing.T) {
 	ctx := context.Background()
 	conn := testutil.GetTestSqlConn()
@@ -1139,7 +1139,7 @@ func TestSysPermModel_FindAllByProductCode_AllDisabled(t *testing.T) {
 	require.Empty(t, list)
 }
 
-// TC-0278: 事务内可见性
+// TC-0294: 事务内可见性
 func TestSysPermModel_FindOneWithTx_InsertThenFind(t *testing.T) {
 	ctx := context.Background()
 	conn := testutil.GetTestSqlConn()
@@ -1177,7 +1177,7 @@ func TestSysPermModel_FindOneWithTx_InsertThenFind(t *testing.T) {
 	assert.Equal(t, code, foundInTx.Code)
 }
 
-// TC-0277: 事务内记录不存在
+// TC-0293: 事务内记录不存在
 func TestSysPermModel_FindOneWithTx_NotFound(t *testing.T) {
 	ctx := context.Background()
 	m := newTestSysPermModel(t)
@@ -1189,7 +1189,7 @@ func TestSysPermModel_FindOneWithTx_NotFound(t *testing.T) {
 	require.ErrorIs(t, err, perm.ErrNotFound)
 }
 
-// TC-0325: FindOneByProductCodeCodeWithTx
+// TC-0344: FindOneByProductCodeCodeWithTx
 func TestSysPermModel_FindOneByProductCodeCodeWithTx_InsertThenFind(t *testing.T) {
 	ctx := context.Background()
 	conn := testutil.GetTestSqlConn()
@@ -1227,7 +1227,7 @@ func TestSysPermModel_FindOneByProductCodeCodeWithTx_InsertThenFind(t *testing.T
 	assert.Equal(t, code, foundByKey.Code)
 }
 
-// TC-0326: FindOneByProductCodeCodeWithTx
+// TC-0345: FindOneByProductCodeCodeWithTx
 func TestSysPermModel_FindOneByProductCodeCodeWithTx_NotFound(t *testing.T) {
 	ctx := context.Background()
 	m := newTestSysPermModel(t)
@@ -1239,7 +1239,7 @@ func TestSysPermModel_FindOneByProductCodeCodeWithTx_NotFound(t *testing.T) {
 	require.ErrorIs(t, err, perm.ErrNotFound)
 }
 
-// TC-0494: FindAllCodesByProductCode 返回仅启用的权限code
+// TC-0409: FindAllCodesByProductCode 返回仅启用的权限code
 func TestSysPermModel_FindAllCodesByProductCode_OnlyEnabledCodes(t *testing.T) {
 	ctx := context.Background()
 	conn := testutil.GetTestSqlConn()
@@ -1286,7 +1286,7 @@ func TestSysPermModel_FindAllCodesByProductCode_OnlyEnabledCodes(t *testing.T) {
 	assert.ElementsMatch(t, []string{codeA, codeB}, codes)
 }
 
-// TC-0495: FindAllCodesByProductCode 不存在的product返回空
+// TC-0410: FindAllCodesByProductCode 不存在的product返回空
 func TestSysPermModel_FindAllCodesByProductCode_NonExistentProduct(t *testing.T) {
 	m := newTestSysPermModel(t)
 	codes, err := m.FindAllCodesByProductCode(context.Background(), "nonexist_"+testutil.UniqueId())
@@ -1294,7 +1294,7 @@ func TestSysPermModel_FindAllCodesByProductCode_NonExistentProduct(t *testing.T)
 	require.Empty(t, codes)
 }
 
-// TC-0292: BatchInsert 大批量(1000条)
+// TC-0310: BatchInsert 大批量(1000条)
 func TestSysPermModel_BatchInsert_Bulk1000(t *testing.T) {
 	ctx := context.Background()
 	conn := testutil.GetTestSqlConn()
@@ -1327,7 +1327,7 @@ func TestSysPermModel_BatchInsert_Bulk1000(t *testing.T) {
 	require.Equal(t, int64(1000), cnt)
 }
 
-// TC-0297: BatchUpdate 单条记录
+// TC-0315: BatchUpdate 单条记录
 func TestSysPermModel_BatchUpdate_Single(t *testing.T) {
 	ctx := context.Background()
 	conn := testutil.GetTestSqlConn()
@@ -1356,7 +1356,7 @@ func TestSysPermModel_BatchUpdate_Single(t *testing.T) {
 	require.Equal(t, now2, got.UpdateTime)
 }
 
-// TC-0299: BatchUpdate 部分id不存在
+// TC-0318: BatchUpdate 部分id不存在
 func TestSysPermModel_BatchUpdate_PartialIdNotExist(t *testing.T) {
 	ctx := context.Background()
 	conn := testutil.GetTestSqlConn()
@@ -1392,7 +1392,7 @@ func TestSysPermModel_BatchUpdate_PartialIdNotExist(t *testing.T) {
 	require.Equal(t, "bu_p2_new", g2.Name)
 }
 
-// TC-0383: FindMapByProductCode key唯一性
+// TC-0404: FindMapByProductCode key唯一性
 func TestSysPermModel_FindMapByProductCode_KeyUniqueness(t *testing.T) {
 	ctx := context.Background()
 	conn := testutil.GetTestSqlConn()

+ 28 - 28
internal/model/product/sysProductModel_test.go

@@ -39,7 +39,7 @@ func newSysProduct() *SysProduct {
 	}
 }
 
-// TC-0370: 正常分页
+// TC-0391: 正常分页
 func TestSysProductModel_Integration(t *testing.T) {
 	ctx := context.Background()
 	conn := testutil.GetTestSqlConn()
@@ -254,14 +254,14 @@ func TestSysProductModel_Integration(t *testing.T) {
 	})
 }
 
-// TC-0274: 记录不存在
+// TC-0290: 记录不存在
 func TestSysProductModel_FindOne_NotFound(t *testing.T) {
 	m := newTestModel(t)
 	_, err := m.FindOne(context.Background(), 999999999999)
 	require.ErrorIs(t, err, ErrNotFound)
 }
 
-// TC-0280: 记录不存在
+// TC-0297: 记录不存在
 func TestSysProductModel_Update_NotFound(t *testing.T) {
 	m := newTestModel(t)
 	err := m.Update(context.Background(), &SysProduct{
@@ -271,28 +271,28 @@ func TestSysProductModel_Update_NotFound(t *testing.T) {
 	require.ErrorIs(t, err, ErrNotFound)
 }
 
-// TC-0283: 记录不存在
+// TC-0300: 记录不存在
 func TestSysProductModel_Delete_NotFound(t *testing.T) {
 	m := newTestModel(t)
 	err := m.Delete(context.Background(), 999999999999)
 	require.ErrorIs(t, err, ErrNotFound)
 }
 
-// TC-0288: 空列表
+// TC-0305: 空列表
 func TestSysProductModel_BatchInsert_Empty(t *testing.T) {
 	m := newTestModel(t)
 	require.NoError(t, m.BatchInsert(context.Background(), nil))
 	require.NoError(t, m.BatchInsert(context.Background(), []*SysProduct{}))
 }
 
-// TC-0305: 空ids
+// TC-0324: 空ids
 func TestSysProductModel_BatchDelete_Empty(t *testing.T) {
 	m := newTestModel(t)
 	require.NoError(t, m.BatchDelete(context.Background(), nil))
 	require.NoError(t, m.BatchDelete(context.Background(), []int64{}))
 }
 
-// TC-0281: 事务内更新
+// TC-0298: 事务内更新
 func TestSysProductModel_UpdateWithTx(t *testing.T) {
 	ctx := context.Background()
 	conn := testutil.GetTestSqlConn()
@@ -318,7 +318,7 @@ func TestSysProductModel_UpdateWithTx(t *testing.T) {
 	require.Equal(t, "tx_updated_name", got.Name)
 }
 
-// TC-0284: 事务内删除
+// TC-0301: 事务内删除
 func TestSysProductModel_DeleteWithTx(t *testing.T) {
 	ctx := context.Background()
 	conn := testutil.GetTestSqlConn()
@@ -340,7 +340,7 @@ func TestSysProductModel_DeleteWithTx(t *testing.T) {
 	require.ErrorIs(t, err, ErrNotFound)
 }
 
-// TC-0289: 单条记录
+// TC-0306: 单条记录
 func TestSysProductModel_BatchInsert_Single(t *testing.T) {
 	ctx := context.Background()
 	conn := testutil.GetTestSqlConn()
@@ -355,7 +355,7 @@ func TestSysProductModel_BatchInsert_Single(t *testing.T) {
 	require.Equal(t, p.Code, found.Code)
 }
 
-// TC-0291: 唯一索引冲突
+// TC-0309: 唯一索引冲突
 func TestSysProductModel_BatchInsert_UniqueConflict(t *testing.T) {
 	ctx := context.Background()
 	conn := testutil.GetTestSqlConn()
@@ -381,14 +381,14 @@ func TestSysProductModel_BatchInsert_UniqueConflict(t *testing.T) {
 	require.Equal(t, uint16(1062), me.Number)
 }
 
-// TC-0296: 空列表
+// TC-0314: 空列表
 func TestSysProductModel_BatchUpdate_Empty(t *testing.T) {
 	m := newTestModel(t)
 	require.NoError(t, m.BatchUpdate(context.Background(), nil))
 	require.NoError(t, m.BatchUpdate(context.Background(), []*SysProduct{}))
 }
 
-// TC-0298: 多条记录(3条)
+// TC-0316: 多条记录(3条)
 func TestSysProductModel_BatchUpdate_Multi(t *testing.T) {
 	ctx := context.Background()
 	conn := testutil.GetTestSqlConn()
@@ -421,7 +421,7 @@ func TestSysProductModel_BatchUpdate_Multi(t *testing.T) {
 	require.Equal(t, int64(2), g2.Status)
 }
 
-// TC-0294: 正常多条
+// TC-0312: 正常多条
 func TestSysProductModel_BatchInsertWithTx_Normal(t *testing.T) {
 	ctx := context.Background()
 	conn := testutil.GetTestSqlConn()
@@ -444,7 +444,7 @@ func TestSysProductModel_BatchInsertWithTx_Normal(t *testing.T) {
 	require.Equal(t, p2.Code, f2.Code)
 }
 
-// TC-0293: 空列表
+// TC-0311: 空列表
 func TestSysProductModel_BatchInsertWithTx_Empty(t *testing.T) {
 	m := newTestModel(t)
 	err := m.TransactCtx(context.Background(), func(c context.Context, session sqlx.Session) error {
@@ -453,7 +453,7 @@ func TestSysProductModel_BatchInsertWithTx_Empty(t *testing.T) {
 	require.NoError(t, err)
 }
 
-// TC-0295: 事务回滚
+// TC-0313: 事务回滚
 func TestSysProductModel_BatchInsertWithTx_Rollback(t *testing.T) {
 	ctx := context.Background()
 	m := newTestModel(t)
@@ -471,7 +471,7 @@ func TestSysProductModel_BatchInsertWithTx_Rollback(t *testing.T) {
 	require.ErrorIs(t, err, ErrNotFound)
 }
 
-// TC-0301: 正常多条
+// TC-0320: 正常多条
 func TestSysProductModel_BatchUpdateWithTx_Normal(t *testing.T) {
 	ctx := context.Background()
 	conn := testutil.GetTestSqlConn()
@@ -505,7 +505,7 @@ func TestSysProductModel_BatchUpdateWithTx_Normal(t *testing.T) {
 	require.Equal(t, "tx_upd2", g2.Name)
 }
 
-// TC-0300: 空列表
+// TC-0319: 空列表
 func TestSysProductModel_BatchUpdateWithTx_Empty(t *testing.T) {
 	m := newTestModel(t)
 	err := m.TransactCtx(context.Background(), func(c context.Context, session sqlx.Session) error {
@@ -514,7 +514,7 @@ func TestSysProductModel_BatchUpdateWithTx_Empty(t *testing.T) {
 	require.NoError(t, err)
 }
 
-// TC-0306: 单个id
+// TC-0325: 单个id
 func TestSysProductModel_BatchDelete_Single(t *testing.T) {
 	ctx := context.Background()
 	conn := testutil.GetTestSqlConn()
@@ -532,7 +532,7 @@ func TestSysProductModel_BatchDelete_Single(t *testing.T) {
 	require.ErrorIs(t, err, ErrNotFound)
 }
 
-// TC-0308: 包含不存在id
+// TC-0327: 包含不存在id
 func TestSysProductModel_BatchDelete_ContainsNonExist(t *testing.T) {
 	ctx := context.Background()
 	conn := testutil.GetTestSqlConn()
@@ -550,7 +550,7 @@ func TestSysProductModel_BatchDelete_ContainsNonExist(t *testing.T) {
 	require.ErrorIs(t, err, ErrNotFound)
 }
 
-// TC-0310: 正常多条
+// TC-0329: 正常多条
 func TestSysProductModel_BatchDeleteWithTx_Normal(t *testing.T) {
 	ctx := context.Background()
 	conn := testutil.GetTestSqlConn()
@@ -577,7 +577,7 @@ func TestSysProductModel_BatchDeleteWithTx_Normal(t *testing.T) {
 	require.ErrorIs(t, err, ErrNotFound)
 }
 
-// TC-0309: 空ids
+// TC-0328: 空ids
 func TestSysProductModel_BatchDeleteWithTx_Empty(t *testing.T) {
 	m := newTestModel(t)
 	err := m.TransactCtx(context.Background(), func(c context.Context, session sqlx.Session) error {
@@ -586,7 +586,7 @@ func TestSysProductModel_BatchDeleteWithTx_Empty(t *testing.T) {
 	require.NoError(t, err)
 }
 
-// TC-0278: 事务内可见性
+// TC-0294: 事务内可见性
 func TestSysProductModel_FindOneWithTx_InsertThenFind(t *testing.T) {
 	ctx := context.Background()
 	conn := testutil.GetTestSqlConn()
@@ -621,7 +621,7 @@ func TestSysProductModel_FindOneWithTx_InsertThenFind(t *testing.T) {
 	assert.Equal(t, insertedId, foundInTx.Id)
 }
 
-// TC-0277: 事务内记录不存在
+// TC-0293: 事务内记录不存在
 func TestSysProductModel_FindOneWithTx_NotFound(t *testing.T) {
 	ctx := context.Background()
 	m := newTestModel(t)
@@ -633,7 +633,7 @@ func TestSysProductModel_FindOneWithTx_NotFound(t *testing.T) {
 	require.ErrorIs(t, err, ErrNotFound)
 }
 
-// TC-0317: FindOneByAppKeyWithTx
+// TC-0336: FindOneByAppKeyWithTx
 func TestSysProductModel_FindOneByAppKeyWithTx_InsertThenFind(t *testing.T) {
 	ctx := context.Background()
 	conn := testutil.GetTestSqlConn()
@@ -671,7 +671,7 @@ func TestSysProductModel_FindOneByAppKeyWithTx_InsertThenFind(t *testing.T) {
 	assert.Equal(t, appKey, foundByKey.AppKey)
 }
 
-// TC-0318: FindOneByAppKeyWithTx
+// TC-0337: FindOneByAppKeyWithTx
 func TestSysProductModel_FindOneByAppKeyWithTx_NotFound(t *testing.T) {
 	ctx := context.Background()
 	m := newTestModel(t)
@@ -683,7 +683,7 @@ func TestSysProductModel_FindOneByAppKeyWithTx_NotFound(t *testing.T) {
 	require.ErrorIs(t, err, ErrNotFound)
 }
 
-// TC-0321: FindOneByCodeWithTx
+// TC-0340: FindOneByCodeWithTx
 func TestSysProductModel_FindOneByCodeWithTx_InsertThenFind(t *testing.T) {
 	ctx := context.Background()
 	conn := testutil.GetTestSqlConn()
@@ -720,7 +720,7 @@ func TestSysProductModel_FindOneByCodeWithTx_InsertThenFind(t *testing.T) {
 	assert.Equal(t, code, foundByCode.Code)
 }
 
-// TC-0322: FindOneByCodeWithTx
+// TC-0341: FindOneByCodeWithTx
 func TestSysProductModel_FindOneByCodeWithTx_NotFound(t *testing.T) {
 	ctx := context.Background()
 	m := newTestModel(t)
@@ -732,7 +732,7 @@ func TestSysProductModel_FindOneByCodeWithTx_NotFound(t *testing.T) {
 	require.ErrorIs(t, err, ErrNotFound)
 }
 
-// TC-0356: 多唯一索引前缀(SysProduct)
+// TC-0375: 多唯一索引前缀(SysProduct)
 func TestSysProductModel_CachePrefix_MultiUniqueIndex(t *testing.T) {
 	oldId := cacheSysProductIdPrefix
 	oldAppKey := cacheSysProductAppKeyPrefix

+ 40 - 40
internal/model/productmember/sysProductMemberModel_test.go

@@ -19,7 +19,7 @@ func randProductMemberUserId() int64 {
 	return int64(900000 + rand.Intn(100000))
 }
 
-// TC-0267: 正常插入
+// TC-0281: 正常插入
 func TestSysProductMemberModel_CRUD(t *testing.T) {
 	ctx := context.Background()
 	conn := testutil.GetTestSqlConn()
@@ -85,7 +85,7 @@ func TestSysProductMemberModel_CRUD(t *testing.T) {
 	}
 }
 
-// TC-0426: 正常分页
+// TC-0453: 正常分页
 func TestSysProductMemberModel_FindListByProductCode(t *testing.T) {
 	ctx := context.Background()
 	conn := testutil.GetTestSqlConn()
@@ -139,7 +139,7 @@ func TestSysProductMemberModel_FindListByProductCode(t *testing.T) {
 	}
 }
 
-// TC-0428: 正常查询
+// TC-0455: 正常查询
 func TestSysProductMemberModel_FindByUserId(t *testing.T) {
 	ctx := context.Background()
 	conn := testutil.GetTestSqlConn()
@@ -173,7 +173,7 @@ func TestSysProductMemberModel_FindByUserId(t *testing.T) {
 	}
 }
 
-// TC-0430: 正常批量
+// TC-0457: 正常批量
 func TestSysProductMemberModel_FindMapByProductCodeUserIds(t *testing.T) {
 	ctx := context.Background()
 	conn := testutil.GetTestSqlConn()
@@ -231,7 +231,7 @@ func TestSysProductMemberModel_FindMapByProductCodeUserIds(t *testing.T) {
 	}
 }
 
-// TC-0290: 多条记录(3条)
+// TC-0307: 多条记录(3条)
 func TestSysProductMemberModel_BatchInsert(t *testing.T) {
 	ctx := context.Background()
 	conn := testutil.GetTestSqlConn()
@@ -265,7 +265,7 @@ func TestSysProductMemberModel_BatchInsert(t *testing.T) {
 	}
 }
 
-// TC-0268: 唯一索引冲突
+// TC-0283: 唯一索引冲突
 func TestSysProductMemberModel_DuplicateConstraint(t *testing.T) {
 	ctx := context.Background()
 	conn := testutil.GetTestSqlConn()
@@ -292,7 +292,7 @@ func TestSysProductMemberModel_DuplicateConstraint(t *testing.T) {
 	}
 }
 
-// TC-0274: 记录不存在
+// TC-0290: 记录不存在
 func TestSysProductMemberModel_FindOne_NotFound(t *testing.T) {
 	conn := testutil.GetTestSqlConn()
 	m := NewSysProductMemberModel(conn, testutil.GetTestCacheConf(), testutil.GetTestCachePrefix())
@@ -302,7 +302,7 @@ func TestSysProductMemberModel_FindOne_NotFound(t *testing.T) {
 	}
 }
 
-// TC-0344: FindOneByProductCodeUserId
+// TC-0363: FindOneByProductCodeUserId
 func TestSysProductMemberModel_FindOneByProductCodeUserId_NotFound(t *testing.T) {
 	conn := testutil.GetTestSqlConn()
 	m := NewSysProductMemberModel(conn, testutil.GetTestCacheConf(), testutil.GetTestCachePrefix())
@@ -312,7 +312,7 @@ func TestSysProductMemberModel_FindOneByProductCodeUserId_NotFound(t *testing.T)
 	}
 }
 
-// TC-0427: 空结果
+// TC-0454: 空结果
 func TestSysProductMemberModel_FindListByProductCode_Empty(t *testing.T) {
 	conn := testutil.GetTestSqlConn()
 	m := NewSysProductMemberModel(conn, testutil.GetTestCacheConf(), testutil.GetTestCachePrefix())
@@ -325,7 +325,7 @@ func TestSysProductMemberModel_FindListByProductCode_Empty(t *testing.T) {
 	}
 }
 
-// TC-0429: 无成员身份
+// TC-0456: 无成员身份
 func TestSysProductMemberModel_FindByUserId_Empty(t *testing.T) {
 	conn := testutil.GetTestSqlConn()
 	m := NewSysProductMemberModel(conn, testutil.GetTestCacheConf(), testutil.GetTestCachePrefix())
@@ -338,7 +338,7 @@ func TestSysProductMemberModel_FindByUserId_Empty(t *testing.T) {
 	}
 }
 
-// TC-0288: 空列表
+// TC-0305: 空列表
 func TestSysProductMemberModel_BatchInsert_Empty(t *testing.T) {
 	conn := testutil.GetTestSqlConn()
 	m := NewSysProductMemberModel(conn, testutil.GetTestCacheConf(), testutil.GetTestCachePrefix())
@@ -350,7 +350,7 @@ func TestSysProductMemberModel_BatchInsert_Empty(t *testing.T) {
 	}
 }
 
-// TC-0305: 空ids
+// TC-0324: 空ids
 func TestSysProductMemberModel_BatchDelete_Empty(t *testing.T) {
 	conn := testutil.GetTestSqlConn()
 	m := NewSysProductMemberModel(conn, testutil.GetTestCacheConf(), testutil.GetTestCachePrefix())
@@ -362,7 +362,7 @@ func TestSysProductMemberModel_BatchDelete_Empty(t *testing.T) {
 	}
 }
 
-// TC-0270: 事务内插入
+// TC-0285: 事务内插入
 func TestSysProductMemberModel_InsertWithTx_Normal(t *testing.T) {
 	ctx := context.Background()
 	conn := testutil.GetTestSqlConn()
@@ -397,7 +397,7 @@ func TestSysProductMemberModel_InsertWithTx_Normal(t *testing.T) {
 	}
 }
 
-// TC-0271: 事务回滚后无数据
+// TC-0287: 事务回滚后无数据
 func TestSysProductMemberModel_InsertWithTx_Rollback(t *testing.T) {
 	ctx := context.Background()
 	conn := testutil.GetTestSqlConn()
@@ -425,7 +425,7 @@ func TestSysProductMemberModel_InsertWithTx_Rollback(t *testing.T) {
 	}
 }
 
-// TC-0280: 记录不存在
+// TC-0297: 记录不存在
 func TestSysProductMemberModel_Update_NotFound(t *testing.T) {
 	conn := testutil.GetTestSqlConn()
 	m := NewSysProductMemberModel(conn, testutil.GetTestCacheConf(), testutil.GetTestCachePrefix())
@@ -439,7 +439,7 @@ func TestSysProductMemberModel_Update_NotFound(t *testing.T) {
 	}
 }
 
-// TC-0281: 事务内更新
+// TC-0298: 事务内更新
 func TestSysProductMemberModel_UpdateWithTx(t *testing.T) {
 	ctx := context.Background()
 	conn := testutil.GetTestSqlConn()
@@ -476,7 +476,7 @@ func TestSysProductMemberModel_UpdateWithTx(t *testing.T) {
 	}
 }
 
-// TC-0283: 记录不存在
+// TC-0300: 记录不存在
 func TestSysProductMemberModel_Delete_NotFound(t *testing.T) {
 	conn := testutil.GetTestSqlConn()
 	m := NewSysProductMemberModel(conn, testutil.GetTestCacheConf(), testutil.GetTestCachePrefix())
@@ -486,7 +486,7 @@ func TestSysProductMemberModel_Delete_NotFound(t *testing.T) {
 	}
 }
 
-// TC-0284: 事务内删除
+// TC-0301: 事务内删除
 func TestSysProductMemberModel_DeleteWithTx(t *testing.T) {
 	ctx := context.Background()
 	conn := testutil.GetTestSqlConn()
@@ -516,7 +516,7 @@ func TestSysProductMemberModel_DeleteWithTx(t *testing.T) {
 	}
 }
 
-// TC-0285: 正常事务
+// TC-0302: 正常事务
 func TestSysProductMemberModel_TransactCtx_CommitAndRollback(t *testing.T) {
 	ctx := context.Background()
 	conn := testutil.GetTestSqlConn()
@@ -570,7 +570,7 @@ func TestSysProductMemberModel_TransactCtx_CommitAndRollback(t *testing.T) {
 	}
 }
 
-// TC-0287: 获取表名
+// TC-0304: 获取表名
 func TestSysProductMemberModel_TableName(t *testing.T) {
 	conn := testutil.GetTestSqlConn()
 	m := NewSysProductMemberModel(conn, testutil.GetTestCacheConf(), testutil.GetTestCachePrefix())
@@ -579,7 +579,7 @@ func TestSysProductMemberModel_TableName(t *testing.T) {
 	}
 }
 
-// TC-0289: 单条记录
+// TC-0306: 单条记录
 func TestSysProductMemberModel_BatchInsert_Single(t *testing.T) {
 	ctx := context.Background()
 	conn := testutil.GetTestSqlConn()
@@ -604,7 +604,7 @@ func TestSysProductMemberModel_BatchInsert_Single(t *testing.T) {
 	}
 }
 
-// TC-0291: 唯一索引冲突
+// TC-0309: 唯一索引冲突
 func TestSysProductMemberModel_BatchInsert_UniqueConflict(t *testing.T) {
 	ctx := context.Background()
 	conn := testutil.GetTestSqlConn()
@@ -627,7 +627,7 @@ func TestSysProductMemberModel_BatchInsert_UniqueConflict(t *testing.T) {
 	}
 }
 
-// TC-0296: 空列表
+// TC-0314: 空列表
 func TestSysProductMemberModel_BatchUpdate_Empty(t *testing.T) {
 	conn := testutil.GetTestSqlConn()
 	m := NewSysProductMemberModel(conn, testutil.GetTestCacheConf(), testutil.GetTestCachePrefix())
@@ -639,7 +639,7 @@ func TestSysProductMemberModel_BatchUpdate_Empty(t *testing.T) {
 	}
 }
 
-// TC-0298: 多条记录(3条)
+// TC-0316: 多条记录(3条)
 func TestSysProductMemberModel_BatchUpdate_Multi(t *testing.T) {
 	ctx := context.Background()
 	conn := testutil.GetTestSqlConn()
@@ -685,7 +685,7 @@ func TestSysProductMemberModel_BatchUpdate_Multi(t *testing.T) {
 	}
 }
 
-// TC-0307: 多个id(3个)
+// TC-0326: 多个id(3个)
 func TestSysProductMemberModel_BatchDelete_Multi(t *testing.T) {
 	ctx := context.Background()
 	conn := testutil.GetTestSqlConn()
@@ -720,7 +720,7 @@ func TestSysProductMemberModel_BatchDelete_Multi(t *testing.T) {
 	}
 }
 
-// TC-0306: 单个id
+// TC-0325: 单个id
 func TestSysProductMemberModel_BatchDelete_Single(t *testing.T) {
 	ctx := context.Background()
 	conn := testutil.GetTestSqlConn()
@@ -747,7 +747,7 @@ func TestSysProductMemberModel_BatchDelete_Single(t *testing.T) {
 	}
 }
 
-// TC-0308: 包含不存在id
+// TC-0327: 包含不存在id
 func TestSysProductMemberModel_BatchDelete_ContainsNonExist(t *testing.T) {
 	ctx := context.Background()
 	conn := testutil.GetTestSqlConn()
@@ -774,7 +774,7 @@ func TestSysProductMemberModel_BatchDelete_ContainsNonExist(t *testing.T) {
 	}
 }
 
-// TC-0294: 正常多条
+// TC-0312: 正常多条
 func TestSysProductMemberModel_BatchInsertWithTx_Normal(t *testing.T) {
 	ctx := context.Background()
 	conn := testutil.GetTestSqlConn()
@@ -804,7 +804,7 @@ func TestSysProductMemberModel_BatchInsertWithTx_Normal(t *testing.T) {
 	defer testutil.CleanTable(ctx, conn, "sys_product_member", got1.Id, got2.Id)
 }
 
-// TC-0293: 空列表
+// TC-0311: 空列表
 func TestSysProductMemberModel_BatchInsertWithTx_Empty(t *testing.T) {
 	ctx := context.Background()
 	conn := testutil.GetTestSqlConn()
@@ -821,7 +821,7 @@ func TestSysProductMemberModel_BatchInsertWithTx_Empty(t *testing.T) {
 	}
 }
 
-// TC-0295: 事务回滚
+// TC-0313: 事务回滚
 func TestSysProductMemberModel_BatchInsertWithTx_Rollback(t *testing.T) {
 	ctx := context.Background()
 	conn := testutil.GetTestSqlConn()
@@ -848,7 +848,7 @@ func TestSysProductMemberModel_BatchInsertWithTx_Rollback(t *testing.T) {
 	}
 }
 
-// TC-0301: 正常多条
+// TC-0320: 正常多条
 func TestSysProductMemberModel_BatchUpdateWithTx_Normal(t *testing.T) {
 	ctx := context.Background()
 	conn := testutil.GetTestSqlConn()
@@ -889,7 +889,7 @@ func TestSysProductMemberModel_BatchUpdateWithTx_Normal(t *testing.T) {
 	}
 }
 
-// TC-0300: 空列表
+// TC-0319: 空列表
 func TestSysProductMemberModel_BatchUpdateWithTx_Empty(t *testing.T) {
 	ctx := context.Background()
 	conn := testutil.GetTestSqlConn()
@@ -906,7 +906,7 @@ func TestSysProductMemberModel_BatchUpdateWithTx_Empty(t *testing.T) {
 	}
 }
 
-// TC-0310: 正常多条
+// TC-0329: 正常多条
 func TestSysProductMemberModel_BatchDeleteWithTx_Normal(t *testing.T) {
 	ctx := context.Background()
 	conn := testutil.GetTestSqlConn()
@@ -944,7 +944,7 @@ func TestSysProductMemberModel_BatchDeleteWithTx_Normal(t *testing.T) {
 	}
 }
 
-// TC-0309: 空ids
+// TC-0328: 空ids
 func TestSysProductMemberModel_BatchDeleteWithTx_Empty(t *testing.T) {
 	ctx := context.Background()
 	conn := testutil.GetTestSqlConn()
@@ -961,7 +961,7 @@ func TestSysProductMemberModel_BatchDeleteWithTx_Empty(t *testing.T) {
 	}
 }
 
-// TC-0278: 事务内可见性
+// TC-0294: 事务内可见性
 func TestSysProductMemberModel_FindOneWithTx_InsertThenFind(t *testing.T) {
 	ctx := context.Background()
 	conn := testutil.GetTestSqlConn()
@@ -996,7 +996,7 @@ func TestSysProductMemberModel_FindOneWithTx_InsertThenFind(t *testing.T) {
 	defer testutil.CleanTable(ctx, conn, "sys_product_member", insertedId)
 }
 
-// TC-0277: 事务内记录不存在
+// TC-0293: 事务内记录不存在
 func TestSysProductMemberModel_FindOneWithTx_NotFound(t *testing.T) {
 	ctx := context.Background()
 	conn := testutil.GetTestSqlConn()
@@ -1010,7 +1010,7 @@ func TestSysProductMemberModel_FindOneWithTx_NotFound(t *testing.T) {
 	require.NoError(t, err)
 }
 
-// TC-0345: FindOneByProductCodeUserIdWithTx
+// TC-0364: FindOneByProductCodeUserIdWithTx
 func TestSysProductMemberModel_FindOneByProductCodeUserIdWithTx_InsertThenFind(t *testing.T) {
 	ctx := context.Background()
 	conn := testutil.GetTestSqlConn()
@@ -1045,7 +1045,7 @@ func TestSysProductMemberModel_FindOneByProductCodeUserIdWithTx_InsertThenFind(t
 	defer testutil.CleanTable(ctx, conn, "sys_product_member", insertedId)
 }
 
-// TC-0346: FindOneByProductCodeUserIdWithTx
+// TC-0365: FindOneByProductCodeUserIdWithTx
 func TestSysProductMemberModel_FindOneByProductCodeUserIdWithTx_NotFound(t *testing.T) {
 	ctx := context.Background()
 	conn := testutil.GetTestSqlConn()
@@ -1059,7 +1059,7 @@ func TestSysProductMemberModel_FindOneByProductCodeUserIdWithTx_NotFound(t *test
 	require.NoError(t, err)
 }
 
-// TC-0431: FindMapByProductCodeUserIds - 空userIds
+// TC-0458: FindMapByProductCodeUserIds - 空userIds
 func TestSysProductMemberModel_FindMapByProductCodeUserIds_EmptyUserIds(t *testing.T) {
 	conn := testutil.GetTestSqlConn()
 	m := NewSysProductMemberModel(conn, testutil.GetTestCacheConf(), testutil.GetTestCachePrefix())
@@ -1075,7 +1075,7 @@ func TestSysProductMemberModel_FindMapByProductCodeUserIds_EmptyUserIds(t *testi
 	assert.Empty(t, m2)
 }
 
-// TC-0433: FindMapByProductCodeUserIds - map key正确
+// TC-0460: FindMapByProductCodeUserIds - map key正确
 func TestSysProductMemberModel_FindMapByProductCodeUserIds_MapKeysCorrect(t *testing.T) {
 	ctx := context.Background()
 	conn := testutil.GetTestSqlConn()

+ 50 - 50
internal/model/role/sysRoleModel_test.go

@@ -16,7 +16,7 @@ import (
 	"github.com/zeromicro/go-zero/core/stores/sqlx"
 )
 
-// TC-0267: 正常插入
+// TC-0281: 正常插入
 func TestSysRoleModel_CRUD(t *testing.T) {
 	ctx := context.Background()
 	conn := testutil.GetTestSqlConn()
@@ -62,7 +62,7 @@ func TestSysRoleModel_CRUD(t *testing.T) {
 	assert.True(t, errors.Is(err, ErrNotFound))
 }
 
-// TC-0327: FindOneByProductCodeName
+// TC-0346: FindOneByProductCodeName
 func TestSysRoleModel_FindOneByProductCodeName_FoundAndNotFound(t *testing.T) {
 	ctx := context.Background()
 	conn := testutil.GetTestSqlConn()
@@ -97,7 +97,7 @@ func TestSysRoleModel_FindOneByProductCodeName_FoundAndNotFound(t *testing.T) {
 	assert.True(t, errors.Is(err, ErrNotFound))
 }
 
-// TC-0395: 正常分页
+// TC-0418: 正常分页
 func TestSysRoleModel_FindListByProductCode_Pagination(t *testing.T) {
 	ctx := context.Background()
 	conn := testutil.GetTestSqlConn()
@@ -141,7 +141,7 @@ func TestSysRoleModel_FindListByProductCode_Pagination(t *testing.T) {
 	require.Len(t, list3, 1)
 }
 
-// TC-0397: 正常
+// TC-0420: 正常
 func TestSysRoleModel_FindByIds_NormalEmptyPartial(t *testing.T) {
 	ctx := context.Background()
 	conn := testutil.GetTestSqlConn()
@@ -181,7 +181,7 @@ func TestSysRoleModel_FindByIds_NormalEmptyPartial(t *testing.T) {
 	assert.Equal(t, id1, partial[0].Id)
 }
 
-// TC-0290: 多条记录(3条)
+// TC-0307: 多条记录(3条)
 func TestSysRoleModel_BatchInsert_BatchDelete(t *testing.T) {
 	ctx := context.Background()
 	conn := testutil.GetTestSqlConn()
@@ -218,7 +218,7 @@ func TestSysRoleModel_BatchInsert_BatchDelete(t *testing.T) {
 	}
 }
 
-// TC-0268: 唯一索引冲突
+// TC-0283: 唯一索引冲突
 func TestSysRoleModel_Insert_DuplicateProductCodeName(t *testing.T) {
 	ctx := context.Background()
 	conn := testutil.GetTestSqlConn()
@@ -259,14 +259,14 @@ func TestSysRoleModel_Insert_DuplicateProductCodeName(t *testing.T) {
 	assert.Equal(t, uint16(1062), me.Number)
 }
 
-// TC-0287: 获取表名
+// TC-0304: 获取表名
 func TestSysRoleModel_TableName(t *testing.T) {
 	conn := testutil.GetTestSqlConn()
 	m := NewSysRoleModel(conn, testutil.GetTestCacheConf(), testutil.GetTestCachePrefix())
 	assert.Equal(t, "`sys_role`", m.TableName())
 }
 
-// TC-0274: 记录不存在
+// TC-0290: 记录不存在
 func TestSysRoleModel_FindOne_NotFound(t *testing.T) {
 	conn := testutil.GetTestSqlConn()
 	m := NewSysRoleModel(conn, testutil.GetTestCacheConf(), testutil.GetTestCachePrefix())
@@ -274,7 +274,7 @@ func TestSysRoleModel_FindOne_NotFound(t *testing.T) {
 	require.ErrorIs(t, err, ErrNotFound)
 }
 
-// TC-0280: 记录不存在
+// TC-0297: 记录不存在
 func TestSysRoleModel_Update_NotFound(t *testing.T) {
 	conn := testutil.GetTestSqlConn()
 	m := NewSysRoleModel(conn, testutil.GetTestCacheConf(), testutil.GetTestCachePrefix())
@@ -285,7 +285,7 @@ func TestSysRoleModel_Update_NotFound(t *testing.T) {
 	require.ErrorIs(t, err, ErrNotFound)
 }
 
-// TC-0283: 记录不存在
+// TC-0300: 记录不存在
 func TestSysRoleModel_Delete_NotFound(t *testing.T) {
 	conn := testutil.GetTestSqlConn()
 	m := NewSysRoleModel(conn, testutil.GetTestCacheConf(), testutil.GetTestCachePrefix())
@@ -293,7 +293,7 @@ func TestSysRoleModel_Delete_NotFound(t *testing.T) {
 	require.ErrorIs(t, err, ErrNotFound)
 }
 
-// TC-0288: 空列表
+// TC-0305: 空列表
 func TestSysRoleModel_BatchInsert_Empty(t *testing.T) {
 	conn := testutil.GetTestSqlConn()
 	m := NewSysRoleModel(conn, testutil.GetTestCacheConf(), testutil.GetTestCachePrefix())
@@ -301,7 +301,7 @@ func TestSysRoleModel_BatchInsert_Empty(t *testing.T) {
 	require.NoError(t, m.BatchInsert(context.Background(), []*SysRole{}))
 }
 
-// TC-0305: 空ids
+// TC-0324: 空ids
 func TestSysRoleModel_BatchDelete_Empty(t *testing.T) {
 	conn := testutil.GetTestSqlConn()
 	m := NewSysRoleModel(conn, testutil.GetTestCacheConf(), testutil.GetTestCachePrefix())
@@ -309,7 +309,7 @@ func TestSysRoleModel_BatchDelete_Empty(t *testing.T) {
 	require.NoError(t, m.BatchDelete(context.Background(), []int64{}))
 }
 
-// TC-0396: 空结果
+// TC-0419: 空结果
 func TestSysRoleModel_FindListByProductCode_Empty(t *testing.T) {
 	conn := testutil.GetTestSqlConn()
 	m := NewSysRoleModel(conn, testutil.GetTestCacheConf(), testutil.GetTestCachePrefix())
@@ -319,7 +319,7 @@ func TestSysRoleModel_FindListByProductCode_Empty(t *testing.T) {
 	require.Len(t, list, 0)
 }
 
-// TC-0270: 事务内插入
+// TC-0285: 事务内插入
 func TestSysRoleModel_InsertWithTx_Normal(t *testing.T) {
 	ctx := context.Background()
 	conn := testutil.GetTestSqlConn()
@@ -358,7 +358,7 @@ func TestSysRoleModel_InsertWithTx_Normal(t *testing.T) {
 	assert.Equal(t, name, got.Name)
 }
 
-// TC-0271: 事务回滚后无数据
+// TC-0287: 事务回滚后无数据
 func TestSysRoleModel_InsertWithTx_Rollback(t *testing.T) {
 	ctx := context.Background()
 	conn := testutil.GetTestSqlConn()
@@ -388,7 +388,7 @@ func TestSysRoleModel_InsertWithTx_Rollback(t *testing.T) {
 	require.ErrorIs(t, err, ErrNotFound)
 }
 
-// TC-0281: 事务内更新
+// TC-0298: 事务内更新
 func TestSysRoleModel_UpdateWithTx(t *testing.T) {
 	ctx := context.Background()
 	conn := testutil.GetTestSqlConn()
@@ -425,7 +425,7 @@ func TestSysRoleModel_UpdateWithTx(t *testing.T) {
 	assert.Equal(t, "after_tx", got.Remark)
 }
 
-// TC-0284: 事务内删除
+// TC-0301: 事务内删除
 func TestSysRoleModel_DeleteWithTx(t *testing.T) {
 	ctx := context.Background()
 	conn := testutil.GetTestSqlConn()
@@ -456,7 +456,7 @@ func TestSysRoleModel_DeleteWithTx(t *testing.T) {
 	require.ErrorIs(t, err, ErrNotFound)
 }
 
-// TC-0285: 正常事务
+// TC-0302: 正常事务
 func TestSysRoleModel_TransactCtx_CommitAndRollback(t *testing.T) {
 	ctx := context.Background()
 	conn := testutil.GetTestSqlConn()
@@ -500,7 +500,7 @@ func TestSysRoleModel_TransactCtx_CommitAndRollback(t *testing.T) {
 	require.ErrorIs(t, err, ErrNotFound)
 }
 
-// TC-0289: 单条记录
+// TC-0306: 单条记录
 func TestSysRoleModel_BatchInsert_Single(t *testing.T) {
 	ctx := context.Background()
 	conn := testutil.GetTestSqlConn()
@@ -520,7 +520,7 @@ func TestSysRoleModel_BatchInsert_Single(t *testing.T) {
 	assert.Equal(t, name, found.Name)
 }
 
-// TC-0291: 唯一索引冲突
+// TC-0309: 唯一索引冲突
 func TestSysRoleModel_BatchInsert_UniqueConflict(t *testing.T) {
 	ctx := context.Background()
 	conn := testutil.GetTestSqlConn()
@@ -549,7 +549,7 @@ func TestSysRoleModel_BatchInsert_UniqueConflict(t *testing.T) {
 	}
 }
 
-// TC-0296: 空列表
+// TC-0314: 空列表
 func TestSysRoleModel_BatchUpdate_Empty(t *testing.T) {
 	conn := testutil.GetTestSqlConn()
 	m := NewSysRoleModel(conn, testutil.GetTestCacheConf(), testutil.GetTestCachePrefix())
@@ -557,7 +557,7 @@ func TestSysRoleModel_BatchUpdate_Empty(t *testing.T) {
 	require.NoError(t, m.BatchUpdate(context.Background(), []*SysRole{}))
 }
 
-// TC-0298: 多条记录(3条)
+// TC-0316: 多条记录(3条)
 func TestSysRoleModel_BatchUpdate_Multi(t *testing.T) {
 	ctx := context.Background()
 	conn := testutil.GetTestSqlConn()
@@ -593,7 +593,7 @@ func TestSysRoleModel_BatchUpdate_Multi(t *testing.T) {
 	assert.Equal(t, int64(2), g2.Status)
 }
 
-// TC-0294: 正常多条
+// TC-0312: 正常多条
 func TestSysRoleModel_BatchInsertWithTx_Normal(t *testing.T) {
 	ctx := context.Background()
 	conn := testutil.GetTestSqlConn()
@@ -623,7 +623,7 @@ func TestSysRoleModel_BatchInsertWithTx_Normal(t *testing.T) {
 	assert.Equal(t, n2, f2.Name)
 }
 
-// TC-0293: 空列表
+// TC-0311: 空列表
 func TestSysRoleModel_BatchInsertWithTx_Empty(t *testing.T) {
 	conn := testutil.GetTestSqlConn()
 	m := NewSysRoleModel(conn, testutil.GetTestCacheConf(), testutil.GetTestCachePrefix())
@@ -633,7 +633,7 @@ func TestSysRoleModel_BatchInsertWithTx_Empty(t *testing.T) {
 	require.NoError(t, err)
 }
 
-// TC-0295: 事务回滚
+// TC-0313: 事务回滚
 func TestSysRoleModel_BatchInsertWithTx_Rollback(t *testing.T) {
 	ctx := context.Background()
 	conn := testutil.GetTestSqlConn()
@@ -657,7 +657,7 @@ func TestSysRoleModel_BatchInsertWithTx_Rollback(t *testing.T) {
 	require.ErrorIs(t, err, ErrNotFound)
 }
 
-// TC-0301: 正常多条
+// TC-0320: 正常多条
 func TestSysRoleModel_BatchUpdateWithTx_Normal(t *testing.T) {
 	ctx := context.Background()
 	conn := testutil.GetTestSqlConn()
@@ -694,7 +694,7 @@ func TestSysRoleModel_BatchUpdateWithTx_Normal(t *testing.T) {
 	assert.Equal(t, "tx_new2", g2.Remark)
 }
 
-// TC-0300: 空列表
+// TC-0319: 空列表
 func TestSysRoleModel_BatchUpdateWithTx_Empty(t *testing.T) {
 	conn := testutil.GetTestSqlConn()
 	m := NewSysRoleModel(conn, testutil.GetTestCacheConf(), testutil.GetTestCachePrefix())
@@ -704,7 +704,7 @@ func TestSysRoleModel_BatchUpdateWithTx_Empty(t *testing.T) {
 	require.NoError(t, err)
 }
 
-// TC-0306: 单个id
+// TC-0325: 单个id
 func TestSysRoleModel_BatchDelete_Single(t *testing.T) {
 	ctx := context.Background()
 	conn := testutil.GetTestSqlConn()
@@ -725,7 +725,7 @@ func TestSysRoleModel_BatchDelete_Single(t *testing.T) {
 	require.ErrorIs(t, err, ErrNotFound)
 }
 
-// TC-0308: 包含不存在id
+// TC-0327: 包含不存在id
 func TestSysRoleModel_BatchDelete_ContainsNonExist(t *testing.T) {
 	ctx := context.Background()
 	conn := testutil.GetTestSqlConn()
@@ -746,7 +746,7 @@ func TestSysRoleModel_BatchDelete_ContainsNonExist(t *testing.T) {
 	require.ErrorIs(t, err, ErrNotFound)
 }
 
-// TC-0310: 正常多条
+// TC-0329: 正常多条
 func TestSysRoleModel_BatchDeleteWithTx_Normal(t *testing.T) {
 	ctx := context.Background()
 	conn := testutil.GetTestSqlConn()
@@ -776,7 +776,7 @@ func TestSysRoleModel_BatchDeleteWithTx_Normal(t *testing.T) {
 	require.ErrorIs(t, err, ErrNotFound)
 }
 
-// TC-0309: 空ids
+// TC-0328: 空ids
 func TestSysRoleModel_BatchDeleteWithTx_Empty(t *testing.T) {
 	conn := testutil.GetTestSqlConn()
 	m := NewSysRoleModel(conn, testutil.GetTestCacheConf(), testutil.GetTestCachePrefix())
@@ -786,7 +786,7 @@ func TestSysRoleModel_BatchDeleteWithTx_Empty(t *testing.T) {
 	require.NoError(t, err)
 }
 
-// TC-0278: 事务内可见性
+// TC-0294: 事务内可见性
 func TestSysRoleModel_FindOneWithTx_InsertThenFind(t *testing.T) {
 	ctx := context.Background()
 	conn := testutil.GetTestSqlConn()
@@ -821,7 +821,7 @@ func TestSysRoleModel_FindOneWithTx_InsertThenFind(t *testing.T) {
 	t.Cleanup(func() { testutil.CleanTable(ctx, conn, tbl, insertedID) })
 }
 
-// TC-0277: 事务内记录不存在
+// TC-0293: 事务内记录不存在
 func TestSysRoleModel_FindOneWithTx_NotFound(t *testing.T) {
 	ctx := context.Background()
 	conn := testutil.GetTestSqlConn()
@@ -835,7 +835,7 @@ func TestSysRoleModel_FindOneWithTx_NotFound(t *testing.T) {
 	require.NoError(t, err)
 }
 
-// TC-0329: FindOneByProductCodeNameWithTx
+// TC-0348: FindOneByProductCodeNameWithTx
 func TestSysRoleModel_FindOneByProductCodeNameWithTx_InsertThenFind(t *testing.T) {
 	ctx := context.Background()
 	conn := testutil.GetTestSqlConn()
@@ -870,7 +870,7 @@ func TestSysRoleModel_FindOneByProductCodeNameWithTx_InsertThenFind(t *testing.T
 	t.Cleanup(func() { testutil.CleanTable(ctx, conn, tbl, insertedID) })
 }
 
-// TC-0330: FindOneByProductCodeNameWithTx
+// TC-0349: FindOneByProductCodeNameWithTx
 func TestSysRoleModel_FindOneByProductCodeNameWithTx_NotFound(t *testing.T) {
 	ctx := context.Background()
 	conn := testutil.GetTestSqlConn()
@@ -884,7 +884,7 @@ func TestSysRoleModel_FindOneByProductCodeNameWithTx_NotFound(t *testing.T) {
 	require.NoError(t, err)
 }
 
-// TC-0492: FindMinPermsLevelByUserIdAndProductCode 正常返回最小权限级别
+// TC-0422: FindMinPermsLevelByUserIdAndProductCode 正常返回最小权限级别
 func TestSysRoleModel_FindMinPermsLevelByUserIdAndProductCode_Normal(t *testing.T) {
 	ctx := context.Background()
 	conn := testutil.GetTestSqlConn()
@@ -923,7 +923,7 @@ func TestSysRoleModel_FindMinPermsLevelByUserIdAndProductCode_Normal(t *testing.
 	assert.Equal(t, int64(5), level)
 }
 
-// TC-0493: FindMinPermsLevelByUserIdAndProductCode 无角色返回ErrNotFound
+// TC-0423: FindMinPermsLevelByUserIdAndProductCode 无角色返回ErrNotFound
 func TestSysRoleModel_FindMinPermsLevelByUserIdAndProductCode_NoRoles(t *testing.T) {
 	conn := testutil.GetTestSqlConn()
 	m := NewSysRoleModel(conn, testutil.GetTestCacheConf(), testutil.GetTestCachePrefix())
@@ -933,7 +933,7 @@ func TestSysRoleModel_FindMinPermsLevelByUserIdAndProductCode_NoRoles(t *testing
 	require.ErrorIs(t, err, ErrNotFound)
 }
 
-// TC-0302: buildBatchUpdateQuery 单条
+// TC-0321: buildBatchUpdateQuery 单条
 func TestSysRoleModel_buildBatchUpdateQuery_Single(t *testing.T) {
 	conn := testutil.GetTestSqlConn()
 	dm := newSysRoleModel(conn, testutil.GetTestCacheConf(), testutil.GetTestCachePrefix())
@@ -951,7 +951,7 @@ func TestSysRoleModel_buildBatchUpdateQuery_Single(t *testing.T) {
 	assert.Equal(t, 15, len(vals))
 }
 
-// TC-0303: buildBatchUpdateQuery 多条
+// TC-0322: buildBatchUpdateQuery 多条
 func TestSysRoleModel_buildBatchUpdateQuery_Multi(t *testing.T) {
 	conn := testutil.GetTestSqlConn()
 	dm := newSysRoleModel(conn, testutil.GetTestCacheConf(), testutil.GetTestCachePrefix())
@@ -969,7 +969,7 @@ func TestSysRoleModel_buildBatchUpdateQuery_Multi(t *testing.T) {
 	assert.Equal(t, 45, len(vals))
 }
 
-// TC-0304: buildBatchUpdateQuery vals数量正确
+// TC-0323: buildBatchUpdateQuery vals数量正确
 func TestSysRoleModel_buildBatchUpdateQuery_ValsCount(t *testing.T) {
 	conn := testutil.GetTestSqlConn()
 	dm := newSysRoleModel(conn, testutil.GetTestCacheConf(), testutil.GetTestCachePrefix())
@@ -989,7 +989,7 @@ func TestSysRoleModel_buildBatchUpdateQuery_ValsCount(t *testing.T) {
 	}
 }
 
-// TC-0347: findListByPrimaryKeys 空ids
+// TC-0366: findListByPrimaryKeys 空ids
 func TestSysRoleModel_findListByPrimaryKeys_Empty(t *testing.T) {
 	conn := testutil.GetTestSqlConn()
 	dm := newSysRoleModel(conn, testutil.GetTestCacheConf(), testutil.GetTestCachePrefix())
@@ -999,7 +999,7 @@ func TestSysRoleModel_findListByPrimaryKeys_Empty(t *testing.T) {
 	require.Empty(t, list)
 }
 
-// TC-0348: findListByPrimaryKeys 正常ids
+// TC-0367: findListByPrimaryKeys 正常ids
 func TestSysRoleModel_findListByPrimaryKeys_Normal(t *testing.T) {
 	ctx := context.Background()
 	conn := testutil.GetTestSqlConn()
@@ -1021,7 +1021,7 @@ func TestSysRoleModel_findListByPrimaryKeys_Normal(t *testing.T) {
 	require.Len(t, list, 2)
 }
 
-// TC-0349: findListByPrimaryKeys 部分不存在
+// TC-0368: findListByPrimaryKeys 部分不存在
 func TestSysRoleModel_findListByPrimaryKeys_PartialNotExist(t *testing.T) {
 	ctx := context.Background()
 	conn := testutil.GetTestSqlConn()
@@ -1041,7 +1041,7 @@ func TestSysRoleModel_findListByPrimaryKeys_PartialNotExist(t *testing.T) {
 	assert.Equal(t, id, list[0].Id)
 }
 
-// TC-0350: findListByPrimaryKeys DB异常
+// TC-0369: findListByPrimaryKeys DB异常
 func TestSysRoleModel_findListByPrimaryKeys_DBError(t *testing.T) {
 	badConn := sqlx.NewMysql("root:bad@tcp(127.0.0.1:1)/nodb")
 	dm := newSysRoleModel(badConn, testutil.GetTestCacheConf(), testutil.GetTestCachePrefix())
@@ -1050,7 +1050,7 @@ func TestSysRoleModel_findListByPrimaryKeys_DBError(t *testing.T) {
 	require.Error(t, err)
 }
 
-// TC-0351: getPrimaryKeyValue 正常
+// TC-0370: getPrimaryKeyValue 正常
 func TestSysRoleModel_getPrimaryKeyValue_Normal(t *testing.T) {
 	conn := testutil.GetTestSqlConn()
 	dm := newSysRoleModel(conn, testutil.GetTestCacheConf(), testutil.GetTestCachePrefix())
@@ -1060,7 +1060,7 @@ func TestSysRoleModel_getPrimaryKeyValue_Normal(t *testing.T) {
 	assert.Equal(t, int64(42), val)
 }
 
-// TC-0352: formatPrimary 正常
+// TC-0371: formatPrimary 正常
 func TestSysRoleModel_formatPrimary_Normal(t *testing.T) {
 	conn := testutil.GetTestSqlConn()
 	dm := newSysRoleModel(conn, testutil.GetTestCacheConf(), testutil.GetTestCachePrefix())
@@ -1070,7 +1070,7 @@ func TestSysRoleModel_formatPrimary_Normal(t *testing.T) {
 	assert.Contains(t, key, "123")
 }
 
-// TC-0353: queryPrimary 正常
+// TC-0372: queryPrimary 正常
 func TestSysRoleModel_queryPrimary_Normal(t *testing.T) {
 	ctx := context.Background()
 	conn := testutil.GetTestSqlConn()
@@ -1093,7 +1093,7 @@ func TestSysRoleModel_queryPrimary_Normal(t *testing.T) {
 	assert.Equal(t, name, result.Name)
 }
 
-// TC-0354: cachePrefix为空
+// TC-0373: cachePrefix为空
 func TestSysRoleModel_CachePrefix_Empty(t *testing.T) {
 	oldId := cacheSysRoleIdPrefix
 	oldName := cacheSysRoleProductCodeNamePrefix
@@ -1110,7 +1110,7 @@ func TestSysRoleModel_CachePrefix_Empty(t *testing.T) {
 	assert.Equal(t, "cache:sysRole:id:123", key)
 }
 
-// TC-0355: cachePrefix非空
+// TC-0374: cachePrefix非空
 func TestSysRoleModel_CachePrefix_NonEmpty(t *testing.T) {
 	oldId := cacheSysRoleIdPrefix
 	oldName := cacheSysRoleProductCodeNamePrefix
@@ -1125,7 +1125,7 @@ func TestSysRoleModel_CachePrefix_NonEmpty(t *testing.T) {
 	assert.True(t, strings.HasPrefix(key, "test:"))
 }
 
-// TC-0372: FindListByProductCode count查询失败(DB异常)
+// TC-0393: FindListByProductCode count查询失败(DB异常)
 func TestSysRoleModel_FindListByProductCode_DBError(t *testing.T) {
 	badConn := sqlx.NewMysql("root:bad@tcp(127.0.0.1:1)/bad?timeout=1s")
 	m := NewSysRoleModel(badConn, testutil.GetTestCacheConf(), testutil.GetTestCachePrefix())

+ 40 - 40
internal/model/roleperm/sysRolePermModel_test.go

@@ -39,7 +39,7 @@ func int64SliceEqualIgnoreOrder(a, b []int64) bool {
 	return true
 }
 
-// TC-0267: 正常插入
+// TC-0281: 正常插入
 func TestSysRolePermModel_CRUD(t *testing.T) {
 	ctx := context.Background()
 	conn := testutil.GetTestSqlConn()
@@ -103,7 +103,7 @@ func TestSysRolePermModel_CRUD(t *testing.T) {
 	}
 }
 
-// TC-0399: 正常查询
+// TC-0424: 正常查询
 func TestSysRolePermModel_FindPermIdsByRoleId(t *testing.T) {
 	ctx := context.Background()
 	conn := testutil.GetTestSqlConn()
@@ -137,7 +137,7 @@ func TestSysRolePermModel_FindPermIdsByRoleId(t *testing.T) {
 	}
 }
 
-// TC-0401: 正常查询
+// TC-0426: 正常查询
 func TestSysRolePermModel_FindPermIdsByRoleIds(t *testing.T) {
 	ctx := context.Background()
 	conn := testutil.GetTestSqlConn()
@@ -202,7 +202,7 @@ func TestSysRolePermModel_FindPermIdsByRoleIds(t *testing.T) {
 	}
 }
 
-// TC-0404: 正常删除
+// TC-0429: 正常删除
 func TestSysRolePermModel_DeleteByRoleId(t *testing.T) {
 	ctx := context.Background()
 	conn := testutil.GetTestSqlConn()
@@ -238,7 +238,7 @@ func TestSysRolePermModel_DeleteByRoleId(t *testing.T) {
 	}
 }
 
-// TC-0406: 正常事务内删除
+// TC-0431: 正常事务内删除
 func TestSysRolePermModel_DeleteByRoleIdTx(t *testing.T) {
 	ctx := context.Background()
 	conn := testutil.GetTestSqlConn()
@@ -266,7 +266,7 @@ func TestSysRolePermModel_DeleteByRoleIdTx(t *testing.T) {
 	}
 }
 
-// TC-0290: 多条记录(3条)
+// TC-0307: 多条记录(3条)
 func TestSysRolePermModel_BatchInsert(t *testing.T) {
 	ctx := context.Background()
 	conn := testutil.GetTestSqlConn()
@@ -307,7 +307,7 @@ func TestSysRolePermModel_BatchInsert(t *testing.T) {
 	}
 }
 
-// TC-0274: 记录不存在
+// TC-0290: 记录不存在
 func TestSysRolePermModel_FindOne_NotFound(t *testing.T) {
 	conn := testutil.GetTestSqlConn()
 	m := NewSysRolePermModel(conn, testutil.GetTestCacheConf(), testutil.GetTestCachePrefix())
@@ -317,7 +317,7 @@ func TestSysRolePermModel_FindOne_NotFound(t *testing.T) {
 	}
 }
 
-// TC-0332: FindOneByRoleIdPermId
+// TC-0351: FindOneByRoleIdPermId
 func TestSysRolePermModel_FindOneByRoleIdPermId_NotFound(t *testing.T) {
 	conn := testutil.GetTestSqlConn()
 	m := NewSysRolePermModel(conn, testutil.GetTestCacheConf(), testutil.GetTestCachePrefix())
@@ -327,7 +327,7 @@ func TestSysRolePermModel_FindOneByRoleIdPermId_NotFound(t *testing.T) {
 	}
 }
 
-// TC-0400: 无绑定
+// TC-0425: 无绑定
 func TestSysRolePermModel_FindPermIdsByRoleId_Empty(t *testing.T) {
 	conn := testutil.GetTestSqlConn()
 	m := NewSysRolePermModel(conn, testutil.GetTestCacheConf(), testutil.GetTestCachePrefix())
@@ -340,7 +340,7 @@ func TestSysRolePermModel_FindPermIdsByRoleId_Empty(t *testing.T) {
 	}
 }
 
-// TC-0405: 无绑定
+// TC-0430: 无绑定
 func TestSysRolePermModel_DeleteByRoleId_NoRows(t *testing.T) {
 	conn := testutil.GetTestSqlConn()
 	m := NewSysRolePermModel(conn, testutil.GetTestCacheConf(), testutil.GetTestCachePrefix())
@@ -349,7 +349,7 @@ func TestSysRolePermModel_DeleteByRoleId_NoRows(t *testing.T) {
 	}
 }
 
-// TC-0288: 空列表
+// TC-0305: 空列表
 func TestSysRolePermModel_BatchInsert_Empty(t *testing.T) {
 	conn := testutil.GetTestSqlConn()
 	m := NewSysRolePermModel(conn, testutil.GetTestCacheConf(), testutil.GetTestCachePrefix())
@@ -361,7 +361,7 @@ func TestSysRolePermModel_BatchInsert_Empty(t *testing.T) {
 	}
 }
 
-// TC-0305: 空ids
+// TC-0324: 空ids
 func TestSysRolePermModel_BatchDelete_Empty(t *testing.T) {
 	conn := testutil.GetTestSqlConn()
 	m := NewSysRolePermModel(conn, testutil.GetTestCacheConf(), testutil.GetTestCachePrefix())
@@ -373,7 +373,7 @@ func TestSysRolePermModel_BatchDelete_Empty(t *testing.T) {
 	}
 }
 
-// TC-0268: 唯一索引冲突
+// TC-0283: 唯一索引冲突
 func TestSysRolePermModel_Insert_UniqueConflict(t *testing.T) {
 	ctx := context.Background()
 	conn := testutil.GetTestSqlConn()
@@ -400,7 +400,7 @@ func TestSysRolePermModel_Insert_UniqueConflict(t *testing.T) {
 	}
 }
 
-// TC-0270: 事务内插入
+// TC-0285: 事务内插入
 func TestSysRolePermModel_InsertWithTx_Normal(t *testing.T) {
 	ctx := context.Background()
 	conn := testutil.GetTestSqlConn()
@@ -433,7 +433,7 @@ func TestSysRolePermModel_InsertWithTx_Normal(t *testing.T) {
 	}
 }
 
-// TC-0271: 事务回滚后无数据
+// TC-0287: 事务回滚后无数据
 func TestSysRolePermModel_InsertWithTx_Rollback(t *testing.T) {
 	ctx := context.Background()
 	conn := testutil.GetTestSqlConn()
@@ -459,7 +459,7 @@ func TestSysRolePermModel_InsertWithTx_Rollback(t *testing.T) {
 	}
 }
 
-// TC-0280: 记录不存在
+// TC-0297: 记录不存在
 func TestSysRolePermModel_Update_NotFound(t *testing.T) {
 	conn := testutil.GetTestSqlConn()
 	m := NewSysRolePermModel(conn, testutil.GetTestCacheConf(), testutil.GetTestCachePrefix())
@@ -473,7 +473,7 @@ func TestSysRolePermModel_Update_NotFound(t *testing.T) {
 	}
 }
 
-// TC-0281: 事务内更新
+// TC-0298: 事务内更新
 func TestSysRolePermModel_UpdateWithTx(t *testing.T) {
 	ctx := context.Background()
 	conn := testutil.GetTestSqlConn()
@@ -508,7 +508,7 @@ func TestSysRolePermModel_UpdateWithTx(t *testing.T) {
 	}
 }
 
-// TC-0283: 记录不存在
+// TC-0300: 记录不存在
 func TestSysRolePermModel_Delete_NotFound(t *testing.T) {
 	conn := testutil.GetTestSqlConn()
 	m := NewSysRolePermModel(conn, testutil.GetTestCacheConf(), testutil.GetTestCachePrefix())
@@ -518,7 +518,7 @@ func TestSysRolePermModel_Delete_NotFound(t *testing.T) {
 	}
 }
 
-// TC-0284: 事务内删除
+// TC-0301: 事务内删除
 func TestSysRolePermModel_DeleteWithTx(t *testing.T) {
 	ctx := context.Background()
 	conn := testutil.GetTestSqlConn()
@@ -546,7 +546,7 @@ func TestSysRolePermModel_DeleteWithTx(t *testing.T) {
 	}
 }
 
-// TC-0286: fn返回错误
+// TC-0303: fn返回错误
 func TestSysRolePermModel_TransactCtx_Rollback(t *testing.T) {
 	ctx := context.Background()
 	conn := testutil.GetTestSqlConn()
@@ -581,7 +581,7 @@ func TestSysRolePermModel_TransactCtx_Rollback(t *testing.T) {
 	}
 }
 
-// TC-0287: 获取表名
+// TC-0304: 获取表名
 func TestSysRolePermModel_TableName(t *testing.T) {
 	conn := testutil.GetTestSqlConn()
 	m := NewSysRolePermModel(conn, testutil.GetTestCacheConf(), testutil.GetTestCachePrefix())
@@ -590,7 +590,7 @@ func TestSysRolePermModel_TableName(t *testing.T) {
 	}
 }
 
-// TC-0289: 单条记录
+// TC-0306: 单条记录
 func TestSysRolePermModel_BatchInsert_Single(t *testing.T) {
 	ctx := context.Background()
 	conn := testutil.GetTestSqlConn()
@@ -615,7 +615,7 @@ func TestSysRolePermModel_BatchInsert_Single(t *testing.T) {
 	}
 }
 
-// TC-0291: 唯一索引冲突
+// TC-0309: 唯一索引冲突
 func TestSysRolePermModel_BatchInsert_UniqueConflict(t *testing.T) {
 	ctx := context.Background()
 	conn := testutil.GetTestSqlConn()
@@ -638,7 +638,7 @@ func TestSysRolePermModel_BatchInsert_UniqueConflict(t *testing.T) {
 	}
 }
 
-// TC-0296: 空列表
+// TC-0314: 空列表
 func TestSysRolePermModel_BatchUpdate_Empty(t *testing.T) {
 	conn := testutil.GetTestSqlConn()
 	m := NewSysRolePermModel(conn, testutil.GetTestCacheConf(), testutil.GetTestCachePrefix())
@@ -650,7 +650,7 @@ func TestSysRolePermModel_BatchUpdate_Empty(t *testing.T) {
 	}
 }
 
-// TC-0298: 多条记录(3条)
+// TC-0316: 多条记录(3条)
 func TestSysRolePermModel_BatchUpdate_Multi(t *testing.T) {
 	ctx := context.Background()
 	conn := testutil.GetTestSqlConn()
@@ -697,7 +697,7 @@ func TestSysRolePermModel_BatchUpdate_Multi(t *testing.T) {
 	}
 }
 
-// TC-0307: 多个id(3个)
+// TC-0326: 多个id(3个)
 func TestSysRolePermModel_BatchDelete_Multi(t *testing.T) {
 	ctx := context.Background()
 	conn := testutil.GetTestSqlConn()
@@ -732,7 +732,7 @@ func TestSysRolePermModel_BatchDelete_Multi(t *testing.T) {
 	}
 }
 
-// TC-0306: 单个id
+// TC-0325: 单个id
 func TestSysRolePermModel_BatchDelete_Single(t *testing.T) {
 	ctx := context.Background()
 	conn := testutil.GetTestSqlConn()
@@ -757,7 +757,7 @@ func TestSysRolePermModel_BatchDelete_Single(t *testing.T) {
 	}
 }
 
-// TC-0308: 包含不存在id
+// TC-0327: 包含不存在id
 func TestSysRolePermModel_BatchDelete_ContainsNonExist(t *testing.T) {
 	ctx := context.Background()
 	conn := testutil.GetTestSqlConn()
@@ -782,7 +782,7 @@ func TestSysRolePermModel_BatchDelete_ContainsNonExist(t *testing.T) {
 	}
 }
 
-// TC-0294: 正常多条
+// TC-0312: 正常多条
 func TestSysRolePermModel_BatchInsertWithTx_Normal(t *testing.T) {
 	ctx := context.Background()
 	conn := testutil.GetTestSqlConn()
@@ -812,7 +812,7 @@ func TestSysRolePermModel_BatchInsertWithTx_Normal(t *testing.T) {
 	defer testutil.CleanTable(ctx, conn, "sys_role_perm", got1.Id, got2.Id)
 }
 
-// TC-0293: 空列表
+// TC-0311: 空列表
 func TestSysRolePermModel_BatchInsertWithTx_Empty(t *testing.T) {
 	ctx := context.Background()
 	conn := testutil.GetTestSqlConn()
@@ -829,7 +829,7 @@ func TestSysRolePermModel_BatchInsertWithTx_Empty(t *testing.T) {
 	}
 }
 
-// TC-0295: 事务回滚
+// TC-0313: 事务回滚
 func TestSysRolePermModel_BatchInsertWithTx_Rollback(t *testing.T) {
 	ctx := context.Background()
 	conn := testutil.GetTestSqlConn()
@@ -856,7 +856,7 @@ func TestSysRolePermModel_BatchInsertWithTx_Rollback(t *testing.T) {
 	}
 }
 
-// TC-0301: 正常多条
+// TC-0320: 正常多条
 func TestSysRolePermModel_BatchUpdateWithTx_Normal(t *testing.T) {
 	ctx := context.Background()
 	conn := testutil.GetTestSqlConn()
@@ -897,7 +897,7 @@ func TestSysRolePermModel_BatchUpdateWithTx_Normal(t *testing.T) {
 	}
 }
 
-// TC-0300: 空列表
+// TC-0319: 空列表
 func TestSysRolePermModel_BatchUpdateWithTx_Empty(t *testing.T) {
 	ctx := context.Background()
 	conn := testutil.GetTestSqlConn()
@@ -914,7 +914,7 @@ func TestSysRolePermModel_BatchUpdateWithTx_Empty(t *testing.T) {
 	}
 }
 
-// TC-0310: 正常多条
+// TC-0329: 正常多条
 func TestSysRolePermModel_BatchDeleteWithTx_Normal(t *testing.T) {
 	ctx := context.Background()
 	conn := testutil.GetTestSqlConn()
@@ -949,7 +949,7 @@ func TestSysRolePermModel_BatchDeleteWithTx_Normal(t *testing.T) {
 	}
 }
 
-// TC-0309: 空ids
+// TC-0328: 空ids
 func TestSysRolePermModel_BatchDeleteWithTx_Empty(t *testing.T) {
 	ctx := context.Background()
 	conn := testutil.GetTestSqlConn()
@@ -966,7 +966,7 @@ func TestSysRolePermModel_BatchDeleteWithTx_Empty(t *testing.T) {
 	}
 }
 
-// TC-0278: 事务内可见性
+// TC-0294: 事务内可见性
 func TestSysRolePermModel_FindOneWithTx_InsertThenFind(t *testing.T) {
 	ctx := context.Background()
 	conn := testutil.GetTestSqlConn()
@@ -998,7 +998,7 @@ func TestSysRolePermModel_FindOneWithTx_InsertThenFind(t *testing.T) {
 	defer testutil.CleanTable(ctx, conn, "sys_role_perm", insertedId)
 }
 
-// TC-0277: 事务内记录不存在
+// TC-0293: 事务内记录不存在
 func TestSysRolePermModel_FindOneWithTx_NotFound(t *testing.T) {
 	ctx := context.Background()
 	conn := testutil.GetTestSqlConn()
@@ -1012,7 +1012,7 @@ func TestSysRolePermModel_FindOneWithTx_NotFound(t *testing.T) {
 	require.NoError(t, err)
 }
 
-// TC-0333: FindOneByRoleIdPermIdWithTx
+// TC-0352: FindOneByRoleIdPermIdWithTx
 func TestSysRolePermModel_FindOneByRoleIdPermIdWithTx_InsertThenFind(t *testing.T) {
 	ctx := context.Background()
 	conn := testutil.GetTestSqlConn()
@@ -1045,7 +1045,7 @@ func TestSysRolePermModel_FindOneByRoleIdPermIdWithTx_InsertThenFind(t *testing.
 	defer testutil.CleanTable(ctx, conn, "sys_role_perm", insertedId)
 }
 
-// TC-0334: FindOneByRoleIdPermIdWithTx
+// TC-0353: FindOneByRoleIdPermIdWithTx
 func TestSysRolePermModel_FindOneByRoleIdPermIdWithTx_NotFound(t *testing.T) {
 	ctx := context.Background()
 	conn := testutil.GetTestSqlConn()
@@ -1059,7 +1059,7 @@ func TestSysRolePermModel_FindOneByRoleIdPermIdWithTx_NotFound(t *testing.T) {
 	require.NoError(t, err)
 }
 
-// TC-0403: FindPermIdsByRoleIds 去重验证
+// TC-0428: FindPermIdsByRoleIds 去重验证
 func TestSysRolePermModel_FindPermIdsByRoleIds_Dedup(t *testing.T) {
 	ctx := context.Background()
 	conn := testutil.GetTestSqlConn()

+ 2 - 2
internal/model/user/sysUserModel.go

@@ -99,7 +99,7 @@ func (m *customSysUserModel) UpdatePassword(ctx context.Context, id int64, passw
 	sysUserIdKey := fmt.Sprintf("%s%v", cacheSysUserIdPrefix, id)
 	sysUserUsernameKey := fmt.Sprintf("%s%v", cacheSysUserUsernamePrefix, data.Username)
 	_, err = m.ExecCtx(ctx, func(ctx context.Context, conn sqlx.SqlConn) (sql.Result, error) {
-		query := fmt.Sprintf("UPDATE %s SET `password` = ?, `mustChangePassword` = ?, `updateTime` = ? WHERE `id` = ?", m.table)
+		query := fmt.Sprintf("UPDATE %s SET `password` = ?, `mustChangePassword` = ?, `tokenVersion` = `tokenVersion` + 1, `updateTime` = ? WHERE `id` = ?", m.table)
 		return conn.ExecCtx(ctx, query, password, mustChangePassword, time.Now().Unix(), id)
 	}, sysUserIdKey, sysUserUsernameKey)
 	return err
@@ -114,7 +114,7 @@ func (m *customSysUserModel) UpdateStatus(ctx context.Context, id int64, status
 	sysUserIdKey := fmt.Sprintf("%s%v", cacheSysUserIdPrefix, id)
 	sysUserUsernameKey := fmt.Sprintf("%s%v", cacheSysUserUsernamePrefix, data.Username)
 	_, err = m.ExecCtx(ctx, func(ctx context.Context, conn sqlx.SqlConn) (sql.Result, error) {
-		query := fmt.Sprintf("UPDATE %s SET `status` = ?, `updateTime` = ? WHERE `id` = ?", m.table)
+		query := fmt.Sprintf("UPDATE %s SET `status` = ?, `tokenVersion` = `tokenVersion` + 1, `updateTime` = ? WHERE `id` = ?", m.table)
 		return conn.ExecCtx(ctx, query, status, time.Now().Unix(), id)
 	}, sysUserIdKey, sysUserUsernameKey)
 	return err

+ 12 - 11
internal/model/user/sysUserModel_gen.go

@@ -67,6 +67,7 @@ type (
 		IsSuperAdmin       int64          `db:"isSuperAdmin"`       // 是否为超级管理员 1是 2否
 		MustChangePassword int64          `db:"mustChangePassword"` // 是否需要强制修改密码 1是 2否
 		Status             int64          `db:"status"`             // 状态 1正常 2冻结
+		TokenVersion       int64          `db:"tokenVersion"`       // Token版本号,修改密码/冻结时递增
 		CreateTime         int64          `db:"createTime"`         // 创建时间
 		UpdateTime         int64          `db:"updateTime"`         // 修改时间
 	}
@@ -255,8 +256,8 @@ func (m *defaultSysUserModel) Insert(ctx context.Context, data *SysUser) (sql.Re
 	sysUserIdKey := fmt.Sprintf("%s%v", cacheSysUserIdPrefix, data.Id)
 	sysUserUsernameKey := fmt.Sprintf("%s%v", cacheSysUserUsernamePrefix, data.Username)
 	ret, err := m.ExecCtx(ctx, func(ctx context.Context, conn sqlx.SqlConn) (result sql.Result, err error) {
-		query := fmt.Sprintf("INSERT INTO %s (%s) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", m.table, sysUserRowsExpectAutoSet)
-		return conn.ExecCtx(ctx, query, data.Username, data.Password, data.Nickname, data.Avatar, data.Email, data.Phone, data.Remark, data.DeptId, data.IsSuperAdmin, data.MustChangePassword, data.Status, data.CreateTime, data.UpdateTime)
+		query := fmt.Sprintf("INSERT INTO %s (%s) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", m.table, sysUserRowsExpectAutoSet)
+		return conn.ExecCtx(ctx, query, data.Username, data.Password, data.Nickname, data.Avatar, data.Email, data.Phone, data.Remark, data.DeptId, data.IsSuperAdmin, data.MustChangePassword, data.Status, data.TokenVersion, data.CreateTime, data.UpdateTime)
 	}, sysUserIdKey, sysUserUsernameKey)
 	return ret, err
 }
@@ -265,8 +266,8 @@ func (m *defaultSysUserModel) InsertWithTx(ctx context.Context, session sqlx.Ses
 	sysUserIdKey := fmt.Sprintf("%s%v", cacheSysUserIdPrefix, data.Id)
 	sysUserUsernameKey := fmt.Sprintf("%s%v", cacheSysUserUsernamePrefix, data.Username)
 	ret, err := m.ExecCtx(ctx, func(ctx context.Context, conn sqlx.SqlConn) (result sql.Result, err error) {
-		query := fmt.Sprintf("INSERT INTO %s (%s) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", m.table, sysUserRowsExpectAutoSet)
-		return session.ExecCtx(ctx, query, data.Username, data.Password, data.Nickname, data.Avatar, data.Email, data.Phone, data.Remark, data.DeptId, data.IsSuperAdmin, data.MustChangePassword, data.Status, data.CreateTime, data.UpdateTime)
+		query := fmt.Sprintf("INSERT INTO %s (%s) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", m.table, sysUserRowsExpectAutoSet)
+		return session.ExecCtx(ctx, query, data.Username, data.Password, data.Nickname, data.Avatar, data.Email, data.Phone, data.Remark, data.DeptId, data.IsSuperAdmin, data.MustChangePassword, data.Status, data.TokenVersion, data.CreateTime, data.UpdateTime)
 	}, sysUserIdKey, sysUserUsernameKey)
 	return ret, err
 }
@@ -279,8 +280,8 @@ func (m *defaultSysUserModel) BatchInsert(ctx context.Context, dataList []*SysUs
 	valueSets := make([]string, 0, len(dataList))
 	args := make([]interface{}, 0)
 	for _, data := range dataList {
-		valueSets = append(valueSets, "(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)")
-		args = append(args, data.Username, data.Password, data.Nickname, data.Avatar, data.Email, data.Phone, data.Remark, data.DeptId, data.IsSuperAdmin, data.MustChangePassword, data.Status, data.CreateTime, data.UpdateTime)
+		valueSets = append(valueSets, "(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)")
+		args = append(args, data.Username, data.Password, data.Nickname, data.Avatar, data.Email, data.Phone, data.Remark, data.DeptId, data.IsSuperAdmin, data.MustChangePassword, data.Status, data.TokenVersion, data.CreateTime, data.UpdateTime)
 		sysUserIdKey := fmt.Sprintf("%s%v", cacheSysUserIdPrefix, data.Id)
 		sysUserUsernameKey := fmt.Sprintf("%s%v", cacheSysUserUsernamePrefix, data.Username)
 		keys = append(keys, sysUserIdKey, sysUserUsernameKey)
@@ -301,8 +302,8 @@ func (m *defaultSysUserModel) BatchInsertWithTx(ctx context.Context, session sql
 	valueSets := make([]string, 0, len(dataList))
 	args := make([]interface{}, 0)
 	for _, data := range dataList {
-		valueSets = append(valueSets, "(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)")
-		args = append(args, data.Username, data.Password, data.Nickname, data.Avatar, data.Email, data.Phone, data.Remark, data.DeptId, data.IsSuperAdmin, data.MustChangePassword, data.Status, data.CreateTime, data.UpdateTime)
+		valueSets = append(valueSets, "(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)")
+		args = append(args, data.Username, data.Password, data.Nickname, data.Avatar, data.Email, data.Phone, data.Remark, data.DeptId, data.IsSuperAdmin, data.MustChangePassword, data.Status, data.TokenVersion, data.CreateTime, data.UpdateTime)
 		sysUserIdKey := fmt.Sprintf("%s%v", cacheSysUserIdPrefix, data.Id)
 		sysUserUsernameKey := fmt.Sprintf("%s%v", cacheSysUserUsernamePrefix, data.Username)
 		keys = append(keys, sysUserIdKey, sysUserUsernameKey)
@@ -325,7 +326,7 @@ func (m *defaultSysUserModel) Update(ctx context.Context, newData *SysUser) erro
 	sysUserUsernameKey := fmt.Sprintf("%s%v", cacheSysUserUsernamePrefix, data.Username)
 	_, err = m.ExecCtx(ctx, func(ctx context.Context, conn sqlx.SqlConn) (result sql.Result, err error) {
 		query := fmt.Sprintf("UPDATE %s SET %s WHERE `id` = ?", m.table, sysUserRowsWithPlaceHolder)
-		return conn.ExecCtx(ctx, query, newData.Username, newData.Password, newData.Nickname, newData.Avatar, newData.Email, newData.Phone, newData.Remark, newData.DeptId, newData.IsSuperAdmin, newData.MustChangePassword, newData.Status, newData.CreateTime, newData.UpdateTime, newData.Id)
+		return conn.ExecCtx(ctx, query, newData.Username, newData.Password, newData.Nickname, newData.Avatar, newData.Email, newData.Phone, newData.Remark, newData.DeptId, newData.IsSuperAdmin, newData.MustChangePassword, newData.Status, newData.TokenVersion, newData.CreateTime, newData.UpdateTime, newData.Id)
 	}, sysUserIdKey, sysUserUsernameKey)
 	return err
 }
@@ -340,7 +341,7 @@ func (m *defaultSysUserModel) UpdateWithTx(ctx context.Context, session sqlx.Ses
 	sysUserUsernameKey := fmt.Sprintf("%s%v", cacheSysUserUsernamePrefix, data.Username)
 	_, err = m.ExecCtx(ctx, func(ctx context.Context, conn sqlx.SqlConn) (result sql.Result, err error) {
 		query := fmt.Sprintf("UPDATE %s SET %s WHERE `id` = ?", m.table, sysUserRowsWithPlaceHolder)
-		return session.ExecCtx(ctx, query, newData.Username, newData.Password, newData.Nickname, newData.Avatar, newData.Email, newData.Phone, newData.Remark, newData.DeptId, newData.IsSuperAdmin, newData.MustChangePassword, newData.Status, newData.CreateTime, newData.UpdateTime, newData.Id)
+		return session.ExecCtx(ctx, query, newData.Username, newData.Password, newData.Nickname, newData.Avatar, newData.Email, newData.Phone, newData.Remark, newData.DeptId, newData.IsSuperAdmin, newData.MustChangePassword, newData.Status, newData.TokenVersion, newData.CreateTime, newData.UpdateTime, newData.Id)
 	}, sysUserIdKey, sysUserUsernameKey)
 	return err
 }
@@ -409,7 +410,7 @@ func (m *defaultSysUserModel) buildBatchUpdateQuery(dataList []*SysUser) (string
 	listValues := make([][]interface{}, 0, len(dataList))
 	for _, newData := range dataList {
 		values := make([]interface{}, 0, len(fields)+1)
-		values = append(values, newData.Username, newData.Password, newData.Nickname, newData.Avatar, newData.Email, newData.Phone, newData.Remark, newData.DeptId, newData.IsSuperAdmin, newData.MustChangePassword, newData.Status, newData.CreateTime, newData.UpdateTime, newData.Id)
+		values = append(values, newData.Username, newData.Password, newData.Nickname, newData.Avatar, newData.Email, newData.Phone, newData.Remark, newData.DeptId, newData.IsSuperAdmin, newData.MustChangePassword, newData.Status, newData.TokenVersion, newData.CreateTime, newData.UpdateTime, newData.Id)
 		listValues = append(listValues, values)
 	}
 

+ 172 - 43
internal/model/user/sysUserModel_test.go

@@ -44,13 +44,13 @@ func newModel(t *testing.T) (user.SysUserModel, sqlx.SqlConn) {
 	return m, conn
 }
 
-// TC-0287: 获取表名
+// TC-0304: 获取表名
 func TestSysUserModel_TableName(t *testing.T) {
 	m, _ := newModel(t)
 	require.Equal(t, "`sys_user`", m.TableName())
 }
 
-// TC-0267: 正常插入
+// TC-0281: 正常插入
 func TestSysUserModel_CRUD(t *testing.T) {
 	ctx := context.Background()
 	m, conn := newModel(t)
@@ -84,7 +84,7 @@ func TestSysUserModel_CRUD(t *testing.T) {
 	require.ErrorIs(t, err, user.ErrNotFound)
 }
 
-// TC-0311: FindOneByUsername
+// TC-0330: FindOneByUsername
 func TestSysUserModel_FindOneByUsername(t *testing.T) {
 	ctx := context.Background()
 	m, conn := newModel(t)
@@ -106,7 +106,7 @@ func TestSysUserModel_FindOneByUsername(t *testing.T) {
 	require.ErrorIs(t, err, user.ErrNotFound)
 }
 
-// TC-0290: 多条记录(3条)
+// TC-0307: 多条记录(3条)
 func TestSysUserModel_BatchInsert_BatchDelete(t *testing.T) {
 	ctx := context.Background()
 	m, conn := newModel(t)
@@ -138,7 +138,7 @@ func TestSysUserModel_BatchInsert_BatchDelete(t *testing.T) {
 	}
 }
 
-// TC-0298: 多条记录(3条)
+// TC-0316: 多条记录(3条)
 func TestSysUserModel_BatchUpdate(t *testing.T) {
 	ctx := context.Background()
 	m, conn := newModel(t)
@@ -176,7 +176,7 @@ func TestSysUserModel_BatchUpdate(t *testing.T) {
 	require.Equal(t, int64(2), g2.Status)
 }
 
-// TC-0285: 正常事务
+// TC-0302: 正常事务
 func TestSysUserModel_TransactCtx_Commit(t *testing.T) {
 	ctx := context.Background()
 	m, conn := newModel(t)
@@ -201,7 +201,7 @@ func TestSysUserModel_TransactCtx_Commit(t *testing.T) {
 	require.Equal(t, username, got.Username)
 }
 
-// TC-0286: fn返回错误
+// TC-0303: fn返回错误
 func TestSysUserModel_TransactCtx_Rollback(t *testing.T) {
 	ctx := context.Background()
 	m, _ := newModel(t)
@@ -221,7 +221,7 @@ func TestSysUserModel_TransactCtx_Rollback(t *testing.T) {
 	require.ErrorIs(t, err, user.ErrNotFound)
 }
 
-// TC-0270: 事务内插入
+// TC-0285: 事务内插入
 func TestSysUserModel_InsertWithTx_DeleteWithTx_SameTransaction(t *testing.T) {
 	ctx := context.Background()
 	m, conn := newModel(t)
@@ -252,7 +252,7 @@ func TestSysUserModel_InsertWithTx_DeleteWithTx_SameTransaction(t *testing.T) {
 	require.ErrorIs(t, err, user.ErrNotFound)
 }
 
-// TC-0357: 正常分页
+// TC-0376: 正常分页
 func TestSysUserModel_FindListByPage(t *testing.T) {
 	ctx := context.Background()
 	m, conn := newModel(t)
@@ -282,7 +282,7 @@ func TestSysUserModel_FindListByPage(t *testing.T) {
 	require.Len(t, list2, 1)
 }
 
-// TC-0362: 正常查询
+// TC-0381: 正常查询
 func TestSysUserModel_FindListByDeptIds(t *testing.T) {
 	ctx := context.Background()
 	m, conn := newModel(t)
@@ -333,7 +333,7 @@ func TestSysUserModel_FindListByDeptIds(t *testing.T) {
 	require.GreaterOrEqual(t, len(list2), 3)
 }
 
-// TC-0366: 正常批量查询
+// TC-0385: 正常批量查询
 func TestSysUserModel_FindByIds(t *testing.T) {
 	ctx := context.Background()
 	m, conn := newModel(t)
@@ -370,7 +370,7 @@ func TestSysUserModel_FindByIds(t *testing.T) {
 	require.Equal(t, id1, list[0].Id)
 }
 
-// TC-0268: 唯一索引冲突
+// TC-0283: 唯一索引冲突
 func TestSysUserModel_Insert_DuplicateUsername(t *testing.T) {
 	ctx := context.Background()
 	m, conn := newModel(t)
@@ -394,14 +394,14 @@ func TestSysUserModel_Insert_DuplicateUsername(t *testing.T) {
 	}
 }
 
-// TC-0274: 记录不存在
+// TC-0290: 记录不存在
 func TestSysUserModel_FindOne_NotFound(t *testing.T) {
 	m, _ := newModel(t)
 	_, err := m.FindOne(context.Background(), 999999999999)
 	require.ErrorIs(t, err, user.ErrNotFound)
 }
 
-// TC-0280: 记录不存在
+// TC-0297: 记录不存在
 func TestSysUserModel_Update_NotFound(t *testing.T) {
 	m, _ := newModel(t)
 	err := m.Update(context.Background(), &user.SysUser{
@@ -413,35 +413,35 @@ func TestSysUserModel_Update_NotFound(t *testing.T) {
 	require.ErrorIs(t, err, user.ErrNotFound)
 }
 
-// TC-0283: 记录不存在
+// TC-0300: 记录不存在
 func TestSysUserModel_Delete_NotFound(t *testing.T) {
 	m, _ := newModel(t)
 	err := m.Delete(context.Background(), 999999999999)
 	require.ErrorIs(t, err, user.ErrNotFound)
 }
 
-// TC-0288: 空列表
+// TC-0305: 空列表
 func TestSysUserModel_BatchInsert_Empty(t *testing.T) {
 	m, _ := newModel(t)
 	require.NoError(t, m.BatchInsert(context.Background(), nil))
 	require.NoError(t, m.BatchInsert(context.Background(), []*user.SysUser{}))
 }
 
-// TC-0296: 空列表
+// TC-0314: 空列表
 func TestSysUserModel_BatchUpdate_Empty(t *testing.T) {
 	m, _ := newModel(t)
 	require.NoError(t, m.BatchUpdate(context.Background(), nil))
 	require.NoError(t, m.BatchUpdate(context.Background(), []*user.SysUser{}))
 }
 
-// TC-0305: 空ids
+// TC-0324: 空ids
 func TestSysUserModel_BatchDelete_Empty(t *testing.T) {
 	m, _ := newModel(t)
 	require.NoError(t, m.BatchDelete(context.Background(), nil))
 	require.NoError(t, m.BatchDelete(context.Background(), []int64{}))
 }
 
-// TC-0358: 第二页
+// TC-0377: 第二页
 func TestSysUserModel_FindListByPage_SecondPage(t *testing.T) {
 	ctx := context.Background()
 	m, conn := newModel(t)
@@ -465,7 +465,7 @@ func TestSysUserModel_FindListByPage_SecondPage(t *testing.T) {
 	}
 }
 
-// TC-0365: deptId不存在
+// TC-0384: deptId不存在
 func TestSysUserModel_FindListByDeptIds_NotExistDept(t *testing.T) {
 	m, _ := newModel(t)
 	list, total, err := m.FindListByDeptIds(context.Background(), []int64{999999999}, 1, 10)
@@ -474,7 +474,7 @@ func TestSysUserModel_FindListByDeptIds_NotExistDept(t *testing.T) {
 	require.Len(t, list, 0)
 }
 
-// TC-0281: 事务内更新
+// TC-0298: 事务内更新
 func TestSysUserModel_UpdateWithTx(t *testing.T) {
 	ctx := context.Background()
 	m, conn := newModel(t)
@@ -500,7 +500,7 @@ func TestSysUserModel_UpdateWithTx(t *testing.T) {
 	require.Equal(t, "tx_updated", got.Nickname)
 }
 
-// TC-0289: 单条记录
+// TC-0306: 单条记录
 func TestSysUserModel_BatchInsert_Single(t *testing.T) {
 	ctx := context.Background()
 	m, conn := newModel(t)
@@ -514,7 +514,7 @@ func TestSysUserModel_BatchInsert_Single(t *testing.T) {
 	require.Equal(t, username, found.Username)
 }
 
-// TC-0291: 唯一索引冲突
+// TC-0309: 唯一索引冲突
 func TestSysUserModel_BatchInsert_UniqueConflict(t *testing.T) {
 	ctx := context.Background()
 	m, conn := newModel(t)
@@ -540,7 +540,7 @@ func TestSysUserModel_BatchInsert_UniqueConflict(t *testing.T) {
 	}
 }
 
-// TC-0294: 正常多条
+// TC-0312: 正常多条
 func TestSysUserModel_BatchInsertWithTx_Normal(t *testing.T) {
 	ctx := context.Background()
 	m, conn := newModel(t)
@@ -566,7 +566,7 @@ func TestSysUserModel_BatchInsertWithTx_Normal(t *testing.T) {
 	require.Equal(t, u2, f2.Username)
 }
 
-// TC-0293: 空列表
+// TC-0311: 空列表
 func TestSysUserModel_BatchInsertWithTx_Empty(t *testing.T) {
 	ctx := context.Background()
 	m, _ := newModel(t)
@@ -576,7 +576,7 @@ func TestSysUserModel_BatchInsertWithTx_Empty(t *testing.T) {
 	require.NoError(t, err)
 }
 
-// TC-0295: 事务回滚
+// TC-0313: 事务回滚
 func TestSysUserModel_BatchInsertWithTx_Rollback(t *testing.T) {
 	ctx := context.Background()
 	m, _ := newModel(t)
@@ -601,7 +601,7 @@ func TestSysUserModel_BatchInsertWithTx_Rollback(t *testing.T) {
 	require.ErrorIs(t, err, user.ErrNotFound)
 }
 
-// TC-0301: 正常多条
+// TC-0320: 正常多条
 func TestSysUserModel_BatchUpdateWithTx_Normal(t *testing.T) {
 	ctx := context.Background()
 	m, conn := newModel(t)
@@ -633,7 +633,7 @@ func TestSysUserModel_BatchUpdateWithTx_Normal(t *testing.T) {
 	require.Equal(t, "new2", g2.Nickname)
 }
 
-// TC-0300: 空列表
+// TC-0319: 空列表
 func TestSysUserModel_BatchUpdateWithTx_Empty(t *testing.T) {
 	ctx := context.Background()
 	m, _ := newModel(t)
@@ -643,7 +643,7 @@ func TestSysUserModel_BatchUpdateWithTx_Empty(t *testing.T) {
 	require.NoError(t, err)
 }
 
-// TC-0306: 单个id
+// TC-0325: 单个id
 func TestSysUserModel_BatchDelete_Single(t *testing.T) {
 	ctx := context.Background()
 	m, conn := newModel(t)
@@ -659,7 +659,7 @@ func TestSysUserModel_BatchDelete_Single(t *testing.T) {
 	require.ErrorIs(t, err, user.ErrNotFound)
 }
 
-// TC-0308: 包含不存在id
+// TC-0327: 包含不存在id
 func TestSysUserModel_BatchDelete_ContainsNonExist(t *testing.T) {
 	ctx := context.Background()
 	m, conn := newModel(t)
@@ -675,7 +675,7 @@ func TestSysUserModel_BatchDelete_ContainsNonExist(t *testing.T) {
 	require.ErrorIs(t, err, user.ErrNotFound)
 }
 
-// TC-0310: 正常多条
+// TC-0329: 正常多条
 func TestSysUserModel_BatchDeleteWithTx_Normal(t *testing.T) {
 	ctx := context.Background()
 	m, conn := newModel(t)
@@ -701,7 +701,7 @@ func TestSysUserModel_BatchDeleteWithTx_Normal(t *testing.T) {
 	require.ErrorIs(t, err, user.ErrNotFound)
 }
 
-// TC-0309: 空ids
+// TC-0328: 空ids
 func TestSysUserModel_BatchDeleteWithTx_Empty(t *testing.T) {
 	ctx := context.Background()
 	m, _ := newModel(t)
@@ -711,7 +711,7 @@ func TestSysUserModel_BatchDeleteWithTx_Empty(t *testing.T) {
 	require.NoError(t, err)
 }
 
-// TC-0278: 事务内可见性
+// TC-0294: 事务内可见性
 func TestSysUserModel_FindOneWithTx_InsertThenFind(t *testing.T) {
 	ctx := context.Background()
 	m, conn := newModel(t)
@@ -743,7 +743,7 @@ func TestSysUserModel_FindOneWithTx_InsertThenFind(t *testing.T) {
 	defer testutil.CleanTable(ctx, conn, m.TableName(), insertedID)
 }
 
-// TC-0277: 事务内记录不存在
+// TC-0293: 事务内记录不存在
 func TestSysUserModel_FindOneWithTx_NotFound(t *testing.T) {
 	ctx := context.Background()
 	m, _ := newModel(t)
@@ -755,7 +755,7 @@ func TestSysUserModel_FindOneWithTx_NotFound(t *testing.T) {
 	require.NoError(t, err)
 }
 
-// TC-0313: FindOneByUsernameWithTx
+// TC-0332: FindOneByUsernameWithTx
 func TestSysUserModel_FindOneByUsernameWithTx_InsertThenFind(t *testing.T) {
 	ctx := context.Background()
 	m, conn := newModel(t)
@@ -785,7 +785,7 @@ func TestSysUserModel_FindOneByUsernameWithTx_InsertThenFind(t *testing.T) {
 	defer testutil.CleanTable(ctx, conn, m.TableName(), insertedID)
 }
 
-// TC-0314: FindOneByUsernameWithTx
+// TC-0333: FindOneByUsernameWithTx
 func TestSysUserModel_FindOneByUsernameWithTx_NotFound(t *testing.T) {
 	ctx := context.Background()
 	m, _ := newModel(t)
@@ -797,7 +797,7 @@ func TestSysUserModel_FindOneByUsernameWithTx_NotFound(t *testing.T) {
 	require.NoError(t, err)
 }
 
-// TC-0496: FindIdsByDeptId 正常返回部门下用户ID列表
+// TC-0389: FindIdsByDeptId 正常返回部门下用户ID列表
 func TestSysUserModel_FindIdsByDeptId_Normal(t *testing.T) {
 	ctx := context.Background()
 	m, conn := newModel(t)
@@ -822,7 +822,7 @@ func TestSysUserModel_FindIdsByDeptId_Normal(t *testing.T) {
 	assert.ElementsMatch(t, []int64{id1, id2}, ids)
 }
 
-// TC-0497: FindIdsByDeptId 部门无用户返回空
+// TC-0390: FindIdsByDeptId 部门无用户返回空
 func TestSysUserModel_FindIdsByDeptId_Empty(t *testing.T) {
 	m, _ := newModel(t)
 	deptId := time.Now().UnixNano()%100_000_000 + 700_000_000
@@ -831,7 +831,7 @@ func TestSysUserModel_FindIdsByDeptId_Empty(t *testing.T) {
 	require.Empty(t, ids)
 }
 
-// TC-0361: FindListByPage list查询失败(DB异常)
+// TC-0380: FindListByPage list查询失败(DB异常)
 func TestSysUserModel_FindListByPage_DBError(t *testing.T) {
 	badConn := sqlx.NewMysql("root:bad@tcp(127.0.0.1:1)/bad?timeout=1s")
 	m := user.NewSysUserModel(badConn, testutil.GetTestCacheConf(), testutil.GetTestCachePrefix())
@@ -840,7 +840,7 @@ func TestSysUserModel_FindListByPage_DBError(t *testing.T) {
 	require.Error(t, err)
 }
 
-// TC-0369: FindByIds DB异常
+// TC-0388: FindByIds DB异常
 func TestSysUserModel_FindByIds_DBError(t *testing.T) {
 	badConn := sqlx.NewMysql("root:bad@tcp(127.0.0.1:1)/bad?timeout=1s")
 	m := user.NewSysUserModel(badConn, testutil.GetTestCacheConf(), testutil.GetTestCachePrefix())
@@ -850,7 +850,7 @@ func TestSysUserModel_FindByIds_DBError(t *testing.T) {
 	require.Nil(t, list)
 }
 
-// TC-0359: FindListByPage - 空结果页
+// TC-0378: FindListByPage - 空结果页
 func TestSysUserModel_FindListByPage_EmptyPage(t *testing.T) {
 	ctx := context.Background()
 	m, _ := newModel(t)
@@ -861,7 +861,7 @@ func TestSysUserModel_FindListByPage_EmptyPage(t *testing.T) {
 	require.Empty(t, list)
 }
 
-// TC-0363: FindListByDeptIds - 空deptIds
+// TC-0382: FindListByDeptIds - 空deptIds
 func TestSysUserModel_FindListByDeptIds_EmptyDeptIds(t *testing.T) {
 	ctx := context.Background()
 	m, _ := newModel(t)
@@ -872,7 +872,7 @@ func TestSysUserModel_FindListByDeptIds_EmptyDeptIds(t *testing.T) {
 	require.Nil(t, list)
 }
 
-// TC-0364: FindListByDeptIds - 单个deptId
+// TC-0383: FindListByDeptIds - 单个deptId
 func TestSysUserModel_FindListByDeptIds_SingleDeptId(t *testing.T) {
 	ctx := context.Background()
 	m, conn := newModel(t)
@@ -902,3 +902,132 @@ func TestSysUserModel_FindListByDeptIds_SingleDeptId(t *testing.T) {
 	}
 	require.True(t, found, "expected user with id %d in result set", id)
 }
+
+// TC-0282: Insert 正常插入含TokenVersion
+func TestSysUserModel_Insert_WithTokenVersion(t *testing.T) {
+	ctx := context.Background()
+	m, conn := newModel(t)
+	username := "tv_insert_" + testutil.UniqueId()
+	data := newTestSysUser(username, 0)
+
+	res, err := m.Insert(ctx, data)
+	require.NoError(t, err, "Insert should include tokenVersion in SQL parameters")
+
+	id, err := res.LastInsertId()
+	require.NoError(t, err)
+	defer testutil.CleanTable(ctx, conn, m.TableName(), id)
+
+	got, err := m.FindOne(ctx, id)
+	require.NoError(t, err)
+	assert.Equal(t, int64(0), got.TokenVersion, "default tokenVersion should be 0")
+}
+
+// TC-0286: InsertWithTx 事务内插入含TokenVersion
+func TestSysUserModel_InsertWithTx_WithTokenVersion(t *testing.T) {
+	ctx := context.Background()
+	m, conn := newModel(t)
+	username := "tv_instx_" + testutil.UniqueId()
+	data := newTestSysUser(username, 0)
+
+	var insertedId int64
+	err := m.TransactCtx(ctx, func(txCtx context.Context, session sqlx.Session) error {
+		res, err := m.InsertWithTx(txCtx, session, data)
+		if err != nil {
+			return err
+		}
+		insertedId, _ = res.LastInsertId()
+		return nil
+	})
+	require.NoError(t, err, "InsertWithTx should include tokenVersion in SQL parameters")
+	defer testutil.CleanTable(ctx, conn, m.TableName(), insertedId)
+
+	got, err := m.FindOne(ctx, insertedId)
+	require.NoError(t, err)
+	assert.Equal(t, int64(0), got.TokenVersion)
+}
+
+// TC-0296: Update 正常更新含TokenVersion
+func TestSysUserModel_Update_WithTokenVersion(t *testing.T) {
+	ctx := context.Background()
+	m, conn := newModel(t)
+	username := "tv_update_" + testutil.UniqueId()
+	data := newTestSysUser(username, 0)
+
+	res, err := m.Insert(ctx, data)
+	require.NoError(t, err)
+	id, _ := res.LastInsertId()
+	defer testutil.CleanTable(ctx, conn, m.TableName(), id)
+
+	got, err := m.FindOne(ctx, id)
+	require.NoError(t, err)
+
+	got.TokenVersion = 5
+	got.Nickname = "updated_nick"
+	err = m.Update(ctx, got)
+	require.NoError(t, err, "Update should include tokenVersion in SQL parameters")
+
+	updated, err := m.FindOne(ctx, id)
+	require.NoError(t, err)
+	assert.Equal(t, int64(5), updated.TokenVersion)
+	assert.Equal(t, "updated_nick", updated.Nickname)
+}
+
+// TC-0308: BatchInsert 批量插入含TokenVersion
+func TestSysUserModel_BatchInsert_WithTokenVersion(t *testing.T) {
+	ctx := context.Background()
+	m, conn := newModel(t)
+
+	dataList := make([]*user.SysUser, 3)
+	for i := range dataList {
+		dataList[i] = newTestSysUser("tv_batch_"+testutil.UniqueId(), 0)
+	}
+
+	err := m.BatchInsert(ctx, dataList)
+	require.NoError(t, err, "BatchInsert should include tokenVersion in SQL parameters")
+
+	for _, d := range dataList {
+		got, err := m.FindOneByUsername(ctx, d.Username)
+		require.NoError(t, err)
+		assert.Equal(t, int64(0), got.TokenVersion)
+		t.Cleanup(func() { testutil.CleanTable(ctx, conn, m.TableName(), got.Id) })
+	}
+}
+
+// TC-0317: BatchUpdate 批量更新不污染数据
+func TestSysUserModel_BatchUpdate_NoDataCorruption(t *testing.T) {
+	ctx := context.Background()
+	m, conn := newModel(t)
+
+	now := time.Now().Unix()
+	dataList := make([]*user.SysUser, 2)
+	var ids []int64
+	for i := range dataList {
+		dataList[i] = newTestSysUser("tv_bupd_"+testutil.UniqueId(), 0)
+		res, err := m.Insert(ctx, dataList[i])
+		require.NoError(t, err)
+		id, _ := res.LastInsertId()
+		ids = append(ids, id)
+		dataList[i].Id = id
+	}
+	t.Cleanup(func() { testutil.CleanTable(ctx, conn, m.TableName(), ids...) })
+
+	dataList[0].TokenVersion = 10
+	dataList[0].Nickname = "batch_updated_0"
+	dataList[0].UpdateTime = now + 100
+	dataList[1].TokenVersion = 20
+	dataList[1].Nickname = "batch_updated_1"
+	dataList[1].UpdateTime = now + 200
+
+	err := m.BatchUpdate(ctx, dataList)
+	require.NoError(t, err, "BatchUpdate should correctly assign values without offset")
+
+	for i, id := range ids {
+		got, err := m.FindOne(ctx, id)
+		require.NoError(t, err)
+		assert.Equal(t, dataList[i].TokenVersion, got.TokenVersion,
+			"tokenVersion must not be corrupted (should not contain createTime value)")
+		assert.Equal(t, dataList[i].Nickname, got.Nickname)
+		assert.NotEqual(t, got.Id, got.UpdateTime,
+			"updateTime must not be corrupted (should not contain Id value)")
+	}
+}

+ 41 - 41
internal/model/userperm/sysUserPermModel_test.go

@@ -54,7 +54,7 @@ func insertTestPerm(t *testing.T, ctx context.Context, conn sqlx.SqlConn, produc
 	return id
 }
 
-// TC-0267: 正常插入
+// TC-0281: 正常插入
 func TestSysUserPermModel_CRUD(t *testing.T) {
 	ctx := context.Background()
 	conn := testutil.GetTestSqlConn()
@@ -119,7 +119,7 @@ func TestSysUserPermModel_CRUD(t *testing.T) {
 	}
 }
 
-// TC-0407: 正常查询
+// TC-0432: 正常查询
 func TestSysUserPermModel_FindByUserId(t *testing.T) {
 	ctx := context.Background()
 	conn := testutil.GetTestSqlConn()
@@ -153,7 +153,7 @@ func TestSysUserPermModel_FindByUserId(t *testing.T) {
 	}
 }
 
-// TC-0409: ALLOW
+// TC-0434: ALLOW
 func TestSysUserPermModel_FindPermIdsByUserIdAndEffect(t *testing.T) {
 	ctx := context.Background()
 	conn := testutil.GetTestSqlConn()
@@ -193,7 +193,7 @@ func TestSysUserPermModel_FindPermIdsByUserIdAndEffect(t *testing.T) {
 	}
 }
 
-// TC-0412: 正常删除
+// TC-0437: 正常删除
 func TestSysUserPermModel_DeleteByUserId(t *testing.T) {
 	ctx := context.Background()
 	conn := testutil.GetTestSqlConn()
@@ -228,7 +228,7 @@ func TestSysUserPermModel_DeleteByUserId(t *testing.T) {
 	}
 }
 
-// TC-0414: 正常删除
+// TC-0439: 正常删除
 func TestSysUserPermModel_DeleteByUserIdForProduct(t *testing.T) {
 	ctx := context.Background()
 	conn := testutil.GetTestSqlConn()
@@ -273,7 +273,7 @@ func TestSysUserPermModel_DeleteByUserIdForProduct(t *testing.T) {
 	}
 }
 
-// TC-0274: 记录不存在
+// TC-0290: 记录不存在
 func TestSysUserPermModel_FindOne_NotFound(t *testing.T) {
 	conn := testutil.GetTestSqlConn()
 	m := NewSysUserPermModel(conn, testutil.GetTestCacheConf(), testutil.GetTestCachePrefix())
@@ -283,7 +283,7 @@ func TestSysUserPermModel_FindOne_NotFound(t *testing.T) {
 	}
 }
 
-// TC-0336: FindOneByUserIdPermId
+// TC-0355: FindOneByUserIdPermId
 func TestSysUserPermModel_FindOneByUserIdPermId_NotFound(t *testing.T) {
 	conn := testutil.GetTestSqlConn()
 	m := NewSysUserPermModel(conn, testutil.GetTestCacheConf(), testutil.GetTestCachePrefix())
@@ -293,7 +293,7 @@ func TestSysUserPermModel_FindOneByUserIdPermId_NotFound(t *testing.T) {
 	}
 }
 
-// TC-0408: 无记录
+// TC-0433: 无记录
 func TestSysUserPermModel_FindByUserId_Empty(t *testing.T) {
 	conn := testutil.GetTestSqlConn()
 	m := NewSysUserPermModel(conn, testutil.GetTestCacheConf(), testutil.GetTestCachePrefix())
@@ -306,7 +306,7 @@ func TestSysUserPermModel_FindByUserId_Empty(t *testing.T) {
 	}
 }
 
-// TC-0411: 无记录
+// TC-0436: 无记录
 func TestSysUserPermModel_FindPermIdsByUserIdAndEffect_Empty(t *testing.T) {
 	conn := testutil.GetTestSqlConn()
 	m := NewSysUserPermModel(conn, testutil.GetTestCacheConf(), testutil.GetTestCachePrefix())
@@ -319,7 +319,7 @@ func TestSysUserPermModel_FindPermIdsByUserIdAndEffect_Empty(t *testing.T) {
 	}
 }
 
-// TC-0413: 事务内删除
+// TC-0438: 事务内删除
 func TestSysUserPermModel_DeleteByUserIdTx(t *testing.T) {
 	ctx := context.Background()
 	conn := testutil.GetTestSqlConn()
@@ -346,7 +346,7 @@ func TestSysUserPermModel_DeleteByUserIdTx(t *testing.T) {
 	}
 }
 
-// TC-0416: 事务内跨产品删除
+// TC-0441: 事务内跨产品删除
 func TestSysUserPermModel_DeleteByUserIdForProductTx(t *testing.T) {
 	ctx := context.Background()
 	conn := testutil.GetTestSqlConn()
@@ -390,7 +390,7 @@ func TestSysUserPermModel_DeleteByUserIdForProductTx(t *testing.T) {
 	}
 }
 
-// TC-0288: 空列表
+// TC-0305: 空列表
 func TestSysUserPermModel_BatchInsert_Empty(t *testing.T) {
 	conn := testutil.GetTestSqlConn()
 	m := NewSysUserPermModel(conn, testutil.GetTestCacheConf(), testutil.GetTestCachePrefix())
@@ -402,7 +402,7 @@ func TestSysUserPermModel_BatchInsert_Empty(t *testing.T) {
 	}
 }
 
-// TC-0305: 空ids
+// TC-0324: 空ids
 func TestSysUserPermModel_BatchDelete_Empty(t *testing.T) {
 	conn := testutil.GetTestSqlConn()
 	m := NewSysUserPermModel(conn, testutil.GetTestCacheConf(), testutil.GetTestCachePrefix())
@@ -414,7 +414,7 @@ func TestSysUserPermModel_BatchDelete_Empty(t *testing.T) {
 	}
 }
 
-// TC-0268: 唯一索引冲突
+// TC-0283: 唯一索引冲突
 func TestSysUserPermModel_Insert_UniqueConflict(t *testing.T) {
 	ctx := context.Background()
 	conn := testutil.GetTestSqlConn()
@@ -441,7 +441,7 @@ func TestSysUserPermModel_Insert_UniqueConflict(t *testing.T) {
 	}
 }
 
-// TC-0270: 事务内插入
+// TC-0285: 事务内插入
 func TestSysUserPermModel_InsertWithTx_Normal(t *testing.T) {
 	ctx := context.Background()
 	conn := testutil.GetTestSqlConn()
@@ -474,7 +474,7 @@ func TestSysUserPermModel_InsertWithTx_Normal(t *testing.T) {
 	}
 }
 
-// TC-0271: 事务回滚后无数据
+// TC-0287: 事务回滚后无数据
 func TestSysUserPermModel_InsertWithTx_Rollback(t *testing.T) {
 	ctx := context.Background()
 	conn := testutil.GetTestSqlConn()
@@ -500,7 +500,7 @@ func TestSysUserPermModel_InsertWithTx_Rollback(t *testing.T) {
 	}
 }
 
-// TC-0280: 记录不存在
+// TC-0297: 记录不存在
 func TestSysUserPermModel_Update_NotFound(t *testing.T) {
 	conn := testutil.GetTestSqlConn()
 	m := NewSysUserPermModel(conn, testutil.GetTestCacheConf(), testutil.GetTestCachePrefix())
@@ -514,7 +514,7 @@ func TestSysUserPermModel_Update_NotFound(t *testing.T) {
 	}
 }
 
-// TC-0281: 事务内更新
+// TC-0298: 事务内更新
 func TestSysUserPermModel_UpdateWithTx(t *testing.T) {
 	ctx := context.Background()
 	conn := testutil.GetTestSqlConn()
@@ -549,7 +549,7 @@ func TestSysUserPermModel_UpdateWithTx(t *testing.T) {
 	}
 }
 
-// TC-0283: 记录不存在
+// TC-0300: 记录不存在
 func TestSysUserPermModel_Delete_NotFound(t *testing.T) {
 	conn := testutil.GetTestSqlConn()
 	m := NewSysUserPermModel(conn, testutil.GetTestCacheConf(), testutil.GetTestCachePrefix())
@@ -559,7 +559,7 @@ func TestSysUserPermModel_Delete_NotFound(t *testing.T) {
 	}
 }
 
-// TC-0284: 事务内删除
+// TC-0301: 事务内删除
 func TestSysUserPermModel_DeleteWithTx(t *testing.T) {
 	ctx := context.Background()
 	conn := testutil.GetTestSqlConn()
@@ -587,7 +587,7 @@ func TestSysUserPermModel_DeleteWithTx(t *testing.T) {
 	}
 }
 
-// TC-0286: fn返回错误
+// TC-0303: fn返回错误
 func TestSysUserPermModel_TransactCtx_Rollback(t *testing.T) {
 	ctx := context.Background()
 	conn := testutil.GetTestSqlConn()
@@ -622,7 +622,7 @@ func TestSysUserPermModel_TransactCtx_Rollback(t *testing.T) {
 	}
 }
 
-// TC-0287: 获取表名
+// TC-0304: 获取表名
 func TestSysUserPermModel_TableName(t *testing.T) {
 	conn := testutil.GetTestSqlConn()
 	m := NewSysUserPermModel(conn, testutil.GetTestCacheConf(), testutil.GetTestCachePrefix())
@@ -631,7 +631,7 @@ func TestSysUserPermModel_TableName(t *testing.T) {
 	}
 }
 
-// TC-0289: 单条记录
+// TC-0306: 单条记录
 func TestSysUserPermModel_BatchInsert_Single(t *testing.T) {
 	ctx := context.Background()
 	conn := testutil.GetTestSqlConn()
@@ -656,7 +656,7 @@ func TestSysUserPermModel_BatchInsert_Single(t *testing.T) {
 	}
 }
 
-// TC-0290: 多条记录(3条)
+// TC-0307: 多条记录(3条)
 func TestSysUserPermModel_BatchInsert_Multi(t *testing.T) {
 	ctx := context.Background()
 	conn := testutil.GetTestSqlConn()
@@ -691,7 +691,7 @@ func TestSysUserPermModel_BatchInsert_Multi(t *testing.T) {
 	}
 }
 
-// TC-0291: 唯一索引冲突
+// TC-0309: 唯一索引冲突
 func TestSysUserPermModel_BatchInsert_UniqueConflict(t *testing.T) {
 	ctx := context.Background()
 	conn := testutil.GetTestSqlConn()
@@ -714,7 +714,7 @@ func TestSysUserPermModel_BatchInsert_UniqueConflict(t *testing.T) {
 	}
 }
 
-// TC-0296: 空列表
+// TC-0314: 空列表
 func TestSysUserPermModel_BatchUpdate_Empty(t *testing.T) {
 	conn := testutil.GetTestSqlConn()
 	m := NewSysUserPermModel(conn, testutil.GetTestCacheConf(), testutil.GetTestCachePrefix())
@@ -726,7 +726,7 @@ func TestSysUserPermModel_BatchUpdate_Empty(t *testing.T) {
 	}
 }
 
-// TC-0298: 多条记录(3条)
+// TC-0316: 多条记录(3条)
 func TestSysUserPermModel_BatchUpdate_Multi(t *testing.T) {
 	ctx := context.Background()
 	conn := testutil.GetTestSqlConn()
@@ -772,7 +772,7 @@ func TestSysUserPermModel_BatchUpdate_Multi(t *testing.T) {
 	}
 }
 
-// TC-0307: 多个id(3个)
+// TC-0326: 多个id(3个)
 func TestSysUserPermModel_BatchDelete_Multi(t *testing.T) {
 	ctx := context.Background()
 	conn := testutil.GetTestSqlConn()
@@ -806,7 +806,7 @@ func TestSysUserPermModel_BatchDelete_Multi(t *testing.T) {
 	}
 }
 
-// TC-0306: 单个id
+// TC-0325: 单个id
 func TestSysUserPermModel_BatchDelete_Single(t *testing.T) {
 	ctx := context.Background()
 	conn := testutil.GetTestSqlConn()
@@ -831,7 +831,7 @@ func TestSysUserPermModel_BatchDelete_Single(t *testing.T) {
 	}
 }
 
-// TC-0308: 包含不存在id
+// TC-0327: 包含不存在id
 func TestSysUserPermModel_BatchDelete_ContainsNonExist(t *testing.T) {
 	ctx := context.Background()
 	conn := testutil.GetTestSqlConn()
@@ -856,7 +856,7 @@ func TestSysUserPermModel_BatchDelete_ContainsNonExist(t *testing.T) {
 	}
 }
 
-// TC-0294: 正常多条
+// TC-0312: 正常多条
 func TestSysUserPermModel_BatchInsertWithTx_Normal(t *testing.T) {
 	ctx := context.Background()
 	conn := testutil.GetTestSqlConn()
@@ -886,7 +886,7 @@ func TestSysUserPermModel_BatchInsertWithTx_Normal(t *testing.T) {
 	defer testutil.CleanTable(ctx, conn, "sys_user_perm", got1.Id, got2.Id)
 }
 
-// TC-0293: 空列表
+// TC-0311: 空列表
 func TestSysUserPermModel_BatchInsertWithTx_Empty(t *testing.T) {
 	ctx := context.Background()
 	conn := testutil.GetTestSqlConn()
@@ -903,7 +903,7 @@ func TestSysUserPermModel_BatchInsertWithTx_Empty(t *testing.T) {
 	}
 }
 
-// TC-0295: 事务回滚
+// TC-0313: 事务回滚
 func TestSysUserPermModel_BatchInsertWithTx_Rollback(t *testing.T) {
 	ctx := context.Background()
 	conn := testutil.GetTestSqlConn()
@@ -930,7 +930,7 @@ func TestSysUserPermModel_BatchInsertWithTx_Rollback(t *testing.T) {
 	}
 }
 
-// TC-0301: 正常多条
+// TC-0320: 正常多条
 func TestSysUserPermModel_BatchUpdateWithTx_Normal(t *testing.T) {
 	ctx := context.Background()
 	conn := testutil.GetTestSqlConn()
@@ -971,7 +971,7 @@ func TestSysUserPermModel_BatchUpdateWithTx_Normal(t *testing.T) {
 	}
 }
 
-// TC-0300: 空列表
+// TC-0319: 空列表
 func TestSysUserPermModel_BatchUpdateWithTx_Empty(t *testing.T) {
 	ctx := context.Background()
 	conn := testutil.GetTestSqlConn()
@@ -988,7 +988,7 @@ func TestSysUserPermModel_BatchUpdateWithTx_Empty(t *testing.T) {
 	}
 }
 
-// TC-0310: 正常多条
+// TC-0329: 正常多条
 func TestSysUserPermModel_BatchDeleteWithTx_Normal(t *testing.T) {
 	ctx := context.Background()
 	conn := testutil.GetTestSqlConn()
@@ -1025,7 +1025,7 @@ func TestSysUserPermModel_BatchDeleteWithTx_Normal(t *testing.T) {
 	}
 }
 
-// TC-0309: 空ids
+// TC-0328: 空ids
 func TestSysUserPermModel_BatchDeleteWithTx_Empty(t *testing.T) {
 	ctx := context.Background()
 	conn := testutil.GetTestSqlConn()
@@ -1042,7 +1042,7 @@ func TestSysUserPermModel_BatchDeleteWithTx_Empty(t *testing.T) {
 	}
 }
 
-// TC-0278: 事务内可见性
+// TC-0294: 事务内可见性
 func TestSysUserPermModel_FindOneWithTx_InsertThenFind(t *testing.T) {
 	ctx := context.Background()
 	conn := testutil.GetTestSqlConn()
@@ -1074,7 +1074,7 @@ func TestSysUserPermModel_FindOneWithTx_InsertThenFind(t *testing.T) {
 	defer testutil.CleanTable(ctx, conn, "sys_user_perm", insertedId)
 }
 
-// TC-0277: 事务内记录不存在
+// TC-0293: 事务内记录不存在
 func TestSysUserPermModel_FindOneWithTx_NotFound(t *testing.T) {
 	ctx := context.Background()
 	m := NewSysUserPermModel(testutil.GetTestSqlConn(), testutil.GetTestCacheConf(), testutil.GetTestCachePrefix())
@@ -1087,7 +1087,7 @@ func TestSysUserPermModel_FindOneWithTx_NotFound(t *testing.T) {
 	require.NoError(t, err)
 }
 
-// TC-0337: FindOneByUserIdPermIdWithTx
+// TC-0356: FindOneByUserIdPermIdWithTx
 func TestSysUserPermModel_FindOneByUserIdPermIdWithTx_InsertThenFind(t *testing.T) {
 	ctx := context.Background()
 	conn := testutil.GetTestSqlConn()
@@ -1118,7 +1118,7 @@ func TestSysUserPermModel_FindOneByUserIdPermIdWithTx_InsertThenFind(t *testing.
 	defer testutil.CleanTable(ctx, conn, "sys_user_perm", insertedId)
 }
 
-// TC-0338: FindOneByUserIdPermIdWithTx
+// TC-0357: FindOneByUserIdPermIdWithTx
 func TestSysUserPermModel_FindOneByUserIdPermIdWithTx_NotFound(t *testing.T) {
 	ctx := context.Background()
 	m := NewSysUserPermModel(testutil.GetTestSqlConn(), testutil.GetTestCacheConf(), testutil.GetTestCachePrefix())

+ 43 - 43
internal/model/userrole/sysUserRoleModel_test.go

@@ -54,7 +54,7 @@ func insertTestRole(t *testing.T, ctx context.Context, conn sqlx.SqlConn, produc
 	return id
 }
 
-// TC-0267: 正常插入
+// TC-0281: 正常插入
 func TestSysUserRoleModel_CRUD(t *testing.T) {
 	ctx := context.Background()
 	conn := testutil.GetTestSqlConn()
@@ -117,7 +117,7 @@ func TestSysUserRoleModel_CRUD(t *testing.T) {
 	}
 }
 
-// TC-0417: 正常查询
+// TC-0442: 正常查询
 func TestSysUserRoleModel_FindRoleIdsByUserId(t *testing.T) {
 	ctx := context.Background()
 	conn := testutil.GetTestSqlConn()
@@ -151,7 +151,7 @@ func TestSysUserRoleModel_FindRoleIdsByUserId(t *testing.T) {
 	}
 }
 
-// TC-0419: 正常查询
+// TC-0444: 正常查询
 func TestSysUserRoleModel_FindByUserId(t *testing.T) {
 	ctx := context.Background()
 	conn := testutil.GetTestSqlConn()
@@ -177,7 +177,7 @@ func TestSysUserRoleModel_FindByUserId(t *testing.T) {
 	}
 }
 
-// TC-0420: 正常删除
+// TC-0445: 正常删除
 func TestSysUserRoleModel_DeleteByUserId(t *testing.T) {
 	ctx := context.Background()
 	conn := testutil.GetTestSqlConn()
@@ -212,7 +212,7 @@ func TestSysUserRoleModel_DeleteByUserId(t *testing.T) {
 	}
 }
 
-// TC-0422: 正常删除
+// TC-0447: 正常删除
 func TestSysUserRoleModel_DeleteByRoleIdTx(t *testing.T) {
 	ctx := context.Background()
 	conn := testutil.GetTestSqlConn()
@@ -240,7 +240,7 @@ func TestSysUserRoleModel_DeleteByRoleIdTx(t *testing.T) {
 	}
 }
 
-// TC-0423: 正常删除
+// TC-0448: 正常删除
 func TestSysUserRoleModel_DeleteByUserIdForProduct(t *testing.T) {
 	ctx := context.Background()
 	conn := testutil.GetTestSqlConn()
@@ -285,7 +285,7 @@ func TestSysUserRoleModel_DeleteByUserIdForProduct(t *testing.T) {
 	}
 }
 
-// TC-0274: 记录不存在
+// TC-0290: 记录不存在
 func TestSysUserRoleModel_FindOne_NotFound(t *testing.T) {
 	conn := testutil.GetTestSqlConn()
 	m := NewSysUserRoleModel(conn, testutil.GetTestCacheConf(), testutil.GetTestCachePrefix())
@@ -295,7 +295,7 @@ func TestSysUserRoleModel_FindOne_NotFound(t *testing.T) {
 	}
 }
 
-// TC-0340: FindOneByUserIdRoleId
+// TC-0359: FindOneByUserIdRoleId
 func TestSysUserRoleModel_FindOneByUserIdRoleId_NotFound(t *testing.T) {
 	conn := testutil.GetTestSqlConn()
 	m := NewSysUserRoleModel(conn, testutil.GetTestCacheConf(), testutil.GetTestCachePrefix())
@@ -305,7 +305,7 @@ func TestSysUserRoleModel_FindOneByUserIdRoleId_NotFound(t *testing.T) {
 	}
 }
 
-// TC-0418: 无绑定
+// TC-0443: 无绑定
 func TestSysUserRoleModel_FindRoleIdsByUserId_Empty(t *testing.T) {
 	conn := testutil.GetTestSqlConn()
 	m := NewSysUserRoleModel(conn, testutil.GetTestCacheConf(), testutil.GetTestCachePrefix())
@@ -318,7 +318,7 @@ func TestSysUserRoleModel_FindRoleIdsByUserId_Empty(t *testing.T) {
 	}
 }
 
-// TC-0421: 事务内删除
+// TC-0446: 事务内删除
 func TestSysUserRoleModel_DeleteByUserIdTx(t *testing.T) {
 	ctx := context.Background()
 	conn := testutil.GetTestSqlConn()
@@ -345,7 +345,7 @@ func TestSysUserRoleModel_DeleteByUserIdTx(t *testing.T) {
 	}
 }
 
-// TC-0425: 事务内跨产品删除
+// TC-0450: 事务内跨产品删除
 func TestSysUserRoleModel_DeleteByUserIdForProductTx(t *testing.T) {
 	ctx := context.Background()
 	conn := testutil.GetTestSqlConn()
@@ -389,7 +389,7 @@ func TestSysUserRoleModel_DeleteByUserIdForProductTx(t *testing.T) {
 	}
 }
 
-// TC-0288: 空列表
+// TC-0305: 空列表
 func TestSysUserRoleModel_BatchInsert_Empty(t *testing.T) {
 	conn := testutil.GetTestSqlConn()
 	m := NewSysUserRoleModel(conn, testutil.GetTestCacheConf(), testutil.GetTestCachePrefix())
@@ -401,7 +401,7 @@ func TestSysUserRoleModel_BatchInsert_Empty(t *testing.T) {
 	}
 }
 
-// TC-0305: 空ids
+// TC-0324: 空ids
 func TestSysUserRoleModel_BatchDelete_Empty(t *testing.T) {
 	conn := testutil.GetTestSqlConn()
 	m := NewSysUserRoleModel(conn, testutil.GetTestCacheConf(), testutil.GetTestCachePrefix())
@@ -413,7 +413,7 @@ func TestSysUserRoleModel_BatchDelete_Empty(t *testing.T) {
 	}
 }
 
-// TC-0268: 唯一索引冲突
+// TC-0283: 唯一索引冲突
 func TestSysUserRoleModel_Insert_UniqueConflict(t *testing.T) {
 	ctx := context.Background()
 	conn := testutil.GetTestSqlConn()
@@ -440,7 +440,7 @@ func TestSysUserRoleModel_Insert_UniqueConflict(t *testing.T) {
 	}
 }
 
-// TC-0270: 事务内插入
+// TC-0285: 事务内插入
 func TestSysUserRoleModel_InsertWithTx_Normal(t *testing.T) {
 	ctx := context.Background()
 	conn := testutil.GetTestSqlConn()
@@ -473,7 +473,7 @@ func TestSysUserRoleModel_InsertWithTx_Normal(t *testing.T) {
 	}
 }
 
-// TC-0271: 事务回滚后无数据
+// TC-0287: 事务回滚后无数据
 func TestSysUserRoleModel_InsertWithTx_Rollback(t *testing.T) {
 	ctx := context.Background()
 	conn := testutil.GetTestSqlConn()
@@ -499,7 +499,7 @@ func TestSysUserRoleModel_InsertWithTx_Rollback(t *testing.T) {
 	}
 }
 
-// TC-0280: 记录不存在
+// TC-0297: 记录不存在
 func TestSysUserRoleModel_Update_NotFound(t *testing.T) {
 	conn := testutil.GetTestSqlConn()
 	m := NewSysUserRoleModel(conn, testutil.GetTestCacheConf(), testutil.GetTestCachePrefix())
@@ -513,7 +513,7 @@ func TestSysUserRoleModel_Update_NotFound(t *testing.T) {
 	}
 }
 
-// TC-0281: 事务内更新
+// TC-0298: 事务内更新
 func TestSysUserRoleModel_UpdateWithTx(t *testing.T) {
 	ctx := context.Background()
 	conn := testutil.GetTestSqlConn()
@@ -548,7 +548,7 @@ func TestSysUserRoleModel_UpdateWithTx(t *testing.T) {
 	}
 }
 
-// TC-0283: 记录不存在
+// TC-0300: 记录不存在
 func TestSysUserRoleModel_Delete_NotFound(t *testing.T) {
 	conn := testutil.GetTestSqlConn()
 	m := NewSysUserRoleModel(conn, testutil.GetTestCacheConf(), testutil.GetTestCachePrefix())
@@ -558,7 +558,7 @@ func TestSysUserRoleModel_Delete_NotFound(t *testing.T) {
 	}
 }
 
-// TC-0284: 事务内删除
+// TC-0301: 事务内删除
 func TestSysUserRoleModel_DeleteWithTx(t *testing.T) {
 	ctx := context.Background()
 	conn := testutil.GetTestSqlConn()
@@ -586,7 +586,7 @@ func TestSysUserRoleModel_DeleteWithTx(t *testing.T) {
 	}
 }
 
-// TC-0286: fn返回错误
+// TC-0303: fn返回错误
 func TestSysUserRoleModel_TransactCtx_Rollback(t *testing.T) {
 	ctx := context.Background()
 	conn := testutil.GetTestSqlConn()
@@ -621,7 +621,7 @@ func TestSysUserRoleModel_TransactCtx_Rollback(t *testing.T) {
 	}
 }
 
-// TC-0287: 获取表名
+// TC-0304: 获取表名
 func TestSysUserRoleModel_TableName(t *testing.T) {
 	conn := testutil.GetTestSqlConn()
 	m := NewSysUserRoleModel(conn, testutil.GetTestCacheConf(), testutil.GetTestCachePrefix())
@@ -630,7 +630,7 @@ func TestSysUserRoleModel_TableName(t *testing.T) {
 	}
 }
 
-// TC-0289: 单条记录
+// TC-0306: 单条记录
 func TestSysUserRoleModel_BatchInsert_Single(t *testing.T) {
 	ctx := context.Background()
 	conn := testutil.GetTestSqlConn()
@@ -655,7 +655,7 @@ func TestSysUserRoleModel_BatchInsert_Single(t *testing.T) {
 	}
 }
 
-// TC-0290: 多条记录(3条)
+// TC-0307: 多条记录(3条)
 func TestSysUserRoleModel_BatchInsert_Multi(t *testing.T) {
 	ctx := context.Background()
 	conn := testutil.GetTestSqlConn()
@@ -690,7 +690,7 @@ func TestSysUserRoleModel_BatchInsert_Multi(t *testing.T) {
 	}
 }
 
-// TC-0291: 唯一索引冲突
+// TC-0309: 唯一索引冲突
 func TestSysUserRoleModel_BatchInsert_UniqueConflict(t *testing.T) {
 	ctx := context.Background()
 	conn := testutil.GetTestSqlConn()
@@ -713,7 +713,7 @@ func TestSysUserRoleModel_BatchInsert_UniqueConflict(t *testing.T) {
 	}
 }
 
-// TC-0296: 空列表
+// TC-0314: 空列表
 func TestSysUserRoleModel_BatchUpdate_Empty(t *testing.T) {
 	conn := testutil.GetTestSqlConn()
 	m := NewSysUserRoleModel(conn, testutil.GetTestCacheConf(), testutil.GetTestCachePrefix())
@@ -725,7 +725,7 @@ func TestSysUserRoleModel_BatchUpdate_Empty(t *testing.T) {
 	}
 }
 
-// TC-0298: 多条记录(3条)
+// TC-0316: 多条记录(3条)
 func TestSysUserRoleModel_BatchUpdate_Multi(t *testing.T) {
 	ctx := context.Background()
 	conn := testutil.GetTestSqlConn()
@@ -772,7 +772,7 @@ func TestSysUserRoleModel_BatchUpdate_Multi(t *testing.T) {
 	}
 }
 
-// TC-0307: 多个id(3个)
+// TC-0326: 多个id(3个)
 func TestSysUserRoleModel_BatchDelete_Multi(t *testing.T) {
 	ctx := context.Background()
 	conn := testutil.GetTestSqlConn()
@@ -806,7 +806,7 @@ func TestSysUserRoleModel_BatchDelete_Multi(t *testing.T) {
 	}
 }
 
-// TC-0306: 单个id
+// TC-0325: 单个id
 func TestSysUserRoleModel_BatchDelete_Single(t *testing.T) {
 	ctx := context.Background()
 	conn := testutil.GetTestSqlConn()
@@ -831,7 +831,7 @@ func TestSysUserRoleModel_BatchDelete_Single(t *testing.T) {
 	}
 }
 
-// TC-0308: 包含不存在id
+// TC-0327: 包含不存在id
 func TestSysUserRoleModel_BatchDelete_ContainsNonExist(t *testing.T) {
 	ctx := context.Background()
 	conn := testutil.GetTestSqlConn()
@@ -856,7 +856,7 @@ func TestSysUserRoleModel_BatchDelete_ContainsNonExist(t *testing.T) {
 	}
 }
 
-// TC-0294: 正常多条
+// TC-0312: 正常多条
 func TestSysUserRoleModel_BatchInsertWithTx_Normal(t *testing.T) {
 	ctx := context.Background()
 	conn := testutil.GetTestSqlConn()
@@ -886,7 +886,7 @@ func TestSysUserRoleModel_BatchInsertWithTx_Normal(t *testing.T) {
 	defer testutil.CleanTable(ctx, conn, "sys_user_role", got1.Id, got2.Id)
 }
 
-// TC-0293: 空列表
+// TC-0311: 空列表
 func TestSysUserRoleModel_BatchInsertWithTx_Empty(t *testing.T) {
 	ctx := context.Background()
 	conn := testutil.GetTestSqlConn()
@@ -903,7 +903,7 @@ func TestSysUserRoleModel_BatchInsertWithTx_Empty(t *testing.T) {
 	}
 }
 
-// TC-0295: 事务回滚
+// TC-0313: 事务回滚
 func TestSysUserRoleModel_BatchInsertWithTx_Rollback(t *testing.T) {
 	ctx := context.Background()
 	conn := testutil.GetTestSqlConn()
@@ -930,7 +930,7 @@ func TestSysUserRoleModel_BatchInsertWithTx_Rollback(t *testing.T) {
 	}
 }
 
-// TC-0301: 正常多条
+// TC-0320: 正常多条
 func TestSysUserRoleModel_BatchUpdateWithTx_Normal(t *testing.T) {
 	ctx := context.Background()
 	conn := testutil.GetTestSqlConn()
@@ -971,7 +971,7 @@ func TestSysUserRoleModel_BatchUpdateWithTx_Normal(t *testing.T) {
 	}
 }
 
-// TC-0300: 空列表
+// TC-0319: 空列表
 func TestSysUserRoleModel_BatchUpdateWithTx_Empty(t *testing.T) {
 	ctx := context.Background()
 	conn := testutil.GetTestSqlConn()
@@ -988,7 +988,7 @@ func TestSysUserRoleModel_BatchUpdateWithTx_Empty(t *testing.T) {
 	}
 }
 
-// TC-0310: 正常多条
+// TC-0329: 正常多条
 func TestSysUserRoleModel_BatchDeleteWithTx_Normal(t *testing.T) {
 	ctx := context.Background()
 	conn := testutil.GetTestSqlConn()
@@ -1025,7 +1025,7 @@ func TestSysUserRoleModel_BatchDeleteWithTx_Normal(t *testing.T) {
 	}
 }
 
-// TC-0309: 空ids
+// TC-0328: 空ids
 func TestSysUserRoleModel_BatchDeleteWithTx_Empty(t *testing.T) {
 	ctx := context.Background()
 	conn := testutil.GetTestSqlConn()
@@ -1042,7 +1042,7 @@ func TestSysUserRoleModel_BatchDeleteWithTx_Empty(t *testing.T) {
 	}
 }
 
-// TC-0278: 事务内可见性
+// TC-0294: 事务内可见性
 func TestSysUserRoleModel_FindOneWithTx_InsertThenFind(t *testing.T) {
 	ctx := context.Background()
 	conn := testutil.GetTestSqlConn()
@@ -1073,7 +1073,7 @@ func TestSysUserRoleModel_FindOneWithTx_InsertThenFind(t *testing.T) {
 	defer testutil.CleanTable(ctx, conn, "sys_user_role", insertedId)
 }
 
-// TC-0277: 事务内记录不存在
+// TC-0293: 事务内记录不存在
 func TestSysUserRoleModel_FindOneWithTx_NotFound(t *testing.T) {
 	ctx := context.Background()
 	m := NewSysUserRoleModel(testutil.GetTestSqlConn(), testutil.GetTestCacheConf(), testutil.GetTestCachePrefix())
@@ -1086,7 +1086,7 @@ func TestSysUserRoleModel_FindOneWithTx_NotFound(t *testing.T) {
 	require.NoError(t, err)
 }
 
-// TC-0341: FindOneByUserIdRoleIdWithTx
+// TC-0360: FindOneByUserIdRoleIdWithTx
 func TestSysUserRoleModel_FindOneByUserIdRoleIdWithTx_InsertThenFind(t *testing.T) {
 	ctx := context.Background()
 	conn := testutil.GetTestSqlConn()
@@ -1117,7 +1117,7 @@ func TestSysUserRoleModel_FindOneByUserIdRoleIdWithTx_InsertThenFind(t *testing.
 	defer testutil.CleanTable(ctx, conn, "sys_user_role", insertedId)
 }
 
-// TC-0342: FindOneByUserIdRoleIdWithTx
+// TC-0361: FindOneByUserIdRoleIdWithTx
 func TestSysUserRoleModel_FindOneByUserIdRoleIdWithTx_NotFound(t *testing.T) {
 	ctx := context.Background()
 	m := NewSysUserRoleModel(testutil.GetTestSqlConn(), testutil.GetTestCacheConf(), testutil.GetTestCachePrefix())
@@ -1130,7 +1130,7 @@ func TestSysUserRoleModel_FindOneByUserIdRoleIdWithTx_NotFound(t *testing.T) {
 	require.NoError(t, err)
 }
 
-// TC-0498: FindUserIdsByRoleId 正常返回角色下用户ID列表
+// TC-0451: FindUserIdsByRoleId 正常返回角色下用户ID列表
 func TestSysUserRoleModel_FindUserIdsByRoleId_Normal(t *testing.T) {
 	ctx := context.Background()
 	conn := testutil.GetTestSqlConn()
@@ -1155,7 +1155,7 @@ func TestSysUserRoleModel_FindUserIdsByRoleId_Normal(t *testing.T) {
 	assert.True(t, int64SliceEqualIgnoreOrder(got, []int64{u1, u2}))
 }
 
-// TC-0499: FindUserIdsByRoleId 无绑定返回空
+// TC-0452: FindUserIdsByRoleId 无绑定返回空
 func TestSysUserRoleModel_FindUserIdsByRoleId_Empty(t *testing.T) {
 	conn := testutil.GetTestSqlConn()
 	m := NewSysUserRoleModel(conn, testutil.GetTestCacheConf(), testutil.GetTestCachePrefix())

+ 8 - 8
internal/response/response_test.go

@@ -12,14 +12,14 @@ import (
 	"github.com/zeromicro/go-zero/rest/httpx"
 )
 
-// TC-0190: 触发404等
+// TC-0230: 触发404等
 func TestNewCodeError(t *testing.T) {
 	err := NewCodeError(400, "bad request")
 	assert.Equal(t, 400, err.Code())
 	assert.Equal(t, "bad request", err.Error())
 }
 
-// TC-0190: 触发404等
+// TC-0230: 触发404等
 func TestCodeErrorHelpers(t *testing.T) {
 	tests := []struct {
 		name   string
@@ -43,7 +43,7 @@ func TestCodeErrorHelpers(t *testing.T) {
 	}
 }
 
-// TC-0190: 触发404等
+// TC-0230: 触发404等
 func TestCodeError_AsInterface(t *testing.T) {
 	var err error = NewCodeError(404, "not found")
 
@@ -54,7 +54,7 @@ func TestCodeError_AsInterface(t *testing.T) {
 	assert.Equal(t, "not found", ce.Error())
 }
 
-// TC-0190: 触发404等
+// TC-0230: 触发404等
 func TestCodeError_NonCodeError(t *testing.T) {
 	err := errors.New("internal error")
 
@@ -63,7 +63,7 @@ func TestCodeError_NonCodeError(t *testing.T) {
 	assert.False(t, ok)
 }
 
-// TC-0190: 触发404等
+// TC-0230: 触发404等
 func TestSetup_ErrorHandler_CodeError(t *testing.T) {
 	Setup()
 
@@ -85,7 +85,7 @@ func TestSetup_ErrorHandler_CodeError(t *testing.T) {
 	assert.Nil(t, body.Data)
 }
 
-// TC-0191: DB异常
+// TC-0231: DB异常
 func TestSetup_ErrorHandler_InternalError(t *testing.T) {
 	Setup()
 
@@ -107,7 +107,7 @@ func TestSetup_ErrorHandler_InternalError(t *testing.T) {
 	assert.Nil(t, body.Data)
 }
 
-// TC-0192: 正常请求
+// TC-0232: 正常请求
 func TestSetup_OkHandler_WithData(t *testing.T) {
 	Setup()
 
@@ -132,7 +132,7 @@ func TestSetup_OkHandler_WithData(t *testing.T) {
 	assert.Equal(t, "value", dataMap["key"])
 }
 
-// TC-0193: 返回nil
+// TC-0233: 返回nil
 func TestSetup_OkHandler_NilData(t *testing.T) {
 	Setup()
 

+ 13 - 2
internal/server/permserver.go

@@ -4,6 +4,7 @@ import (
 	"context"
 	"crypto/subtle"
 	"fmt"
+	"net"
 	"time"
 
 	"perms-system-server/internal/consts"
@@ -116,7 +117,10 @@ func (s *PermServer) Login(ctx context.Context, req *pb.LoginReq) (*pb.LoginResp
 	if s.svcCtx.GrpcLoginLimiter != nil {
 		p, ok := peer.FromContext(ctx)
 		if ok {
-			ip := p.Addr.String()
+			ip, _, _ := net.SplitHostPort(p.Addr.String())
+			if ip == "" {
+				ip = p.Addr.String()
+			}
 			code, _ := s.svcCtx.GrpcLoginLimiter.Take(fmt.Sprintf("grpc:login:%s", ip))
 			if code == limit.OverQuota {
 				return nil, status.Error(codes.ResourceExhausted, "请求过于频繁,请稍后再试")
@@ -176,9 +180,13 @@ func (s *PermServer) RefreshToken(ctx context.Context, req *pb.RefreshTokenReq)
 		return nil, status.Error(codes.PermissionDenied, "您已不是该产品的成员")
 	}
 
+	if claims.TokenVersion != ud.TokenVersion {
+		return nil, status.Error(codes.Unauthenticated, "登录状态已失效,请重新登录")
+	}
+
 	accessToken, err := authHelper.GenerateAccessToken(
 		s.svcCtx.Config.Auth.AccessSecret, s.svcCtx.Config.Auth.AccessExpire,
-		ud.UserId, ud.Username, ud.ProductCode, ud.MemberType, ud.Perms,
+		ud.UserId, ud.Username, ud.ProductCode, ud.MemberType, ud.Perms, ud.TokenVersion,
 	)
 	if err != nil {
 		return nil, status.Error(codes.Internal, "生成token失败")
@@ -211,6 +219,9 @@ func (s *PermServer) VerifyToken(ctx context.Context, req *pb.VerifyTokenReq) (*
 	if claims.ProductCode != "" && !ud.IsSuperAdmin && ud.MemberType == "" {
 		return &pb.VerifyTokenResp{Valid: false}, nil
 	}
+	if claims.TokenVersion != ud.TokenVersion {
+		return &pb.VerifyTokenResp{Valid: false}, nil
+	}
 
 	return &pb.VerifyTokenResp{
 		Valid:      true,

+ 37 - 37
internal/server/permserver_test.go

@@ -29,7 +29,7 @@ import (
 
 // ---------- SyncPermissions ----------
 
-// TC-0161: 正常同步
+// TC-0195: 正常同步
 func TestSyncPermissions_Normal(t *testing.T) {
 	ctx := context.Background()
 	svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
@@ -76,7 +76,7 @@ func TestSyncPermissions_Normal(t *testing.T) {
 	assert.Equal(t, int64(1), resp2.Disabled)
 }
 
-// TC-0162: appKey无效
+// TC-0196: appKey无效
 func TestSyncPermissions_InvalidAppKey(t *testing.T) {
 	ctx := context.Background()
 	svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
@@ -92,7 +92,7 @@ func TestSyncPermissions_InvalidAppKey(t *testing.T) {
 	assert.Equal(t, "无效的appKey", status.Convert(err).Message())
 }
 
-// TC-0163: appSecret错误
+// TC-0197: appSecret错误
 func TestSyncPermissions_WrongAppSecret(t *testing.T) {
 	ctx := context.Background()
 	svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
@@ -122,7 +122,7 @@ func TestSyncPermissions_WrongAppSecret(t *testing.T) {
 	assert.Equal(t, "appSecret验证失败", status.Convert(err).Message())
 }
 
-// TC-0164: 产品已禁用
+// TC-0198: 产品已禁用
 func TestSyncPermissions_ProductDisabled(t *testing.T) {
 	ctx := context.Background()
 	svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
@@ -154,7 +154,7 @@ func TestSyncPermissions_ProductDisabled(t *testing.T) {
 
 // ---------- Login ----------
 
-// TC-0166: 正常登录(普通用户+productCode)
+// TC-0200: 正常登录(普通用户+productCode)
 func TestLogin_Normal(t *testing.T) {
 	ctx := context.Background()
 	svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
@@ -204,7 +204,7 @@ func TestLogin_Normal(t *testing.T) {
 	assert.Equal(t, uid, resp.Username)
 }
 
-// TC-0167: 用户不存在
+// TC-0201: 用户不存在
 func TestLogin_UserNotFound(t *testing.T) {
 	ctx := context.Background()
 	svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
@@ -220,7 +220,7 @@ func TestLogin_UserNotFound(t *testing.T) {
 	assert.Equal(t, "用户名或密码错误", status.Convert(err).Message())
 }
 
-// TC-0168: 密码错误
+// TC-0202: 密码错误
 func TestLogin_WrongPassword(t *testing.T) {
 	ctx := context.Background()
 	svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
@@ -251,7 +251,7 @@ func TestLogin_WrongPassword(t *testing.T) {
 	assert.Equal(t, "用户名或密码错误", status.Convert(err).Message())
 }
 
-// TC-0169: 账号冻结
+// TC-0203: 账号冻结
 func TestLogin_AccountFrozen(t *testing.T) {
 	ctx := context.Background()
 	svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
@@ -282,7 +282,7 @@ func TestLogin_AccountFrozen(t *testing.T) {
 	assert.Equal(t, "账号已被冻结", status.Convert(err).Message())
 }
 
-// TC-0170: 超管被拒绝
+// TC-0204: 超管被拒绝
 func TestLogin_SuperAdminRejected(t *testing.T) {
 	ctx := context.Background()
 	svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
@@ -313,7 +313,7 @@ func TestLogin_SuperAdminRejected(t *testing.T) {
 	assert.Equal(t, "超级管理员不允许通过产品端登录,请使用管理后台", status.Convert(err).Message())
 }
 
-// TC-0171: 普通用户+productCode
+// TC-0205: 普通用户+productCode
 func TestLogin_NormalUserWithProductCode(t *testing.T) {
 	ctx := context.Background()
 	svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
@@ -392,7 +392,7 @@ func TestLogin_NormalUserWithProductCode(t *testing.T) {
 	assert.NotEmpty(t, resp.RefreshToken)
 }
 
-// TC-0510: productCode为空
+// TC-0207: productCode为空
 func TestLogin_EmptyProductCode(t *testing.T) {
 	ctx := context.Background()
 	svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
@@ -410,7 +410,7 @@ func TestLogin_EmptyProductCode(t *testing.T) {
 
 // ---------- RefreshToken ----------
 
-// TC-0172: 正常刷新(refreshToken原样返回,不重新生成)
+// TC-0208: 正常刷新(refreshToken原样返回,不重新生成)
 func TestRefreshToken_Normal(t *testing.T) {
 	ctx := context.Background()
 	svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
@@ -431,7 +431,7 @@ func TestRefreshToken_Normal(t *testing.T) {
 	})
 
 	cfg := testutil.GetTestConfig()
-	refreshToken, err := authHelper.GenerateRefreshToken(cfg.Auth.RefreshSecret, cfg.Auth.RefreshExpire, uId, "")
+	refreshToken, err := authHelper.GenerateRefreshToken(cfg.Auth.RefreshSecret, cfg.Auth.RefreshExpire, uId, "", 0)
 	require.NoError(t, err)
 
 	srv := NewPermServer(svcCtx)
@@ -444,7 +444,7 @@ func TestRefreshToken_Normal(t *testing.T) {
 	assert.True(t, resp.Expires > time.Now().Unix(), "expires应为未来的unix时间戳")
 }
 
-// TC-0173: token无效
+// TC-0209: token无效
 func TestRefreshToken_InvalidToken(t *testing.T) {
 	ctx := context.Background()
 	svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
@@ -458,7 +458,7 @@ func TestRefreshToken_InvalidToken(t *testing.T) {
 	assert.Equal(t, "refreshToken无效或已过期", status.Convert(err).Message())
 }
 
-// TC-0174: 账号冻结
+// TC-0210: 账号冻结
 func TestRefreshToken_AccountFrozen(t *testing.T) {
 	ctx := context.Background()
 	svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
@@ -479,7 +479,7 @@ func TestRefreshToken_AccountFrozen(t *testing.T) {
 	})
 
 	cfg := testutil.GetTestConfig()
-	refreshToken, err := authHelper.GenerateRefreshToken(cfg.Auth.RefreshSecret, cfg.Auth.RefreshExpire, uId, "")
+	refreshToken, err := authHelper.GenerateRefreshToken(cfg.Auth.RefreshSecret, cfg.Auth.RefreshExpire, uId, "", 0)
 	require.NoError(t, err)
 
 	srv := NewPermServer(svcCtx)
@@ -491,7 +491,7 @@ func TestRefreshToken_AccountFrozen(t *testing.T) {
 	assert.Equal(t, "账号已被冻结", status.Convert(err).Message())
 }
 
-// TC-0175: productCode回退到claims
+// TC-0211: productCode回退到claims
 func TestRefreshToken_FallbackToClaimsProductCode(t *testing.T) {
 	ctx := context.Background()
 	svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
@@ -528,7 +528,7 @@ func TestRefreshToken_FallbackToClaimsProductCode(t *testing.T) {
 	})
 
 	cfg := testutil.GetTestConfig()
-	refreshToken, err := authHelper.GenerateRefreshToken(cfg.Auth.RefreshSecret, cfg.Auth.RefreshExpire, uId, uid)
+	refreshToken, err := authHelper.GenerateRefreshToken(cfg.Auth.RefreshSecret, cfg.Auth.RefreshExpire, uId, uid, 0)
 	require.NoError(t, err)
 
 	srv := NewPermServer(svcCtx)
@@ -541,7 +541,7 @@ func TestRefreshToken_FallbackToClaimsProductCode(t *testing.T) {
 	assert.Equal(t, refreshToken, resp.RefreshToken, "refreshToken应原样返回")
 }
 
-// TC-0176: 超管+productCode
+// TC-0212: 超管+productCode
 func TestRefreshToken_SuperAdminWithProductCode(t *testing.T) {
 	ctx := context.Background()
 	svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
@@ -578,7 +578,7 @@ func TestRefreshToken_SuperAdminWithProductCode(t *testing.T) {
 	})
 
 	cfg := testutil.GetTestConfig()
-	refreshToken, err := authHelper.GenerateRefreshToken(cfg.Auth.RefreshSecret, cfg.Auth.RefreshExpire, uId, uid)
+	refreshToken, err := authHelper.GenerateRefreshToken(cfg.Auth.RefreshSecret, cfg.Auth.RefreshExpire, uId, uid, 0)
 	require.NoError(t, err)
 
 	srv := NewPermServer(svcCtx)
@@ -591,7 +591,7 @@ func TestRefreshToken_SuperAdminWithProductCode(t *testing.T) {
 	assert.Equal(t, refreshToken, resp.RefreshToken, "refreshToken应原样返回")
 }
 
-// TC-0177: 普通用户+productCode
+// TC-0213: 普通用户+productCode
 func TestRefreshToken_NormalUserWithProductCode(t *testing.T) {
 	ctx := context.Background()
 	svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
@@ -658,7 +658,7 @@ func TestRefreshToken_NormalUserWithProductCode(t *testing.T) {
 	})
 
 	cfg := testutil.GetTestConfig()
-	refreshToken, err := authHelper.GenerateRefreshToken(cfg.Auth.RefreshSecret, cfg.Auth.RefreshExpire, uId, uid)
+	refreshToken, err := authHelper.GenerateRefreshToken(cfg.Auth.RefreshSecret, cfg.Auth.RefreshExpire, uId, uid, 0)
 	require.NoError(t, err)
 
 	srv := NewPermServer(svcCtx)
@@ -673,7 +673,7 @@ func TestRefreshToken_NormalUserWithProductCode(t *testing.T) {
 
 // ---------- VerifyToken ----------
 
-// TC-0178: 有效token(VerifyToken 现在实时查询DB,需要真实数据)
+// TC-0214: 有效token(VerifyToken 现在实时查询DB,需要真实数据)
 func TestVerifyToken_Valid(t *testing.T) {
 	ctx := context.Background()
 	svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
@@ -730,7 +730,7 @@ func TestVerifyToken_Valid(t *testing.T) {
 
 	accessToken, err := authHelper.GenerateAccessToken(
 		cfg.Auth.AccessSecret, cfg.Auth.AccessExpire,
-		uId, uid, uid, "ADMIN", []string{"perm_a", "perm_b"},
+		uId, uid, uid, "ADMIN", []string{"perm_a", "perm_b"}, 0,
 	)
 	require.NoError(t, err)
 
@@ -744,7 +744,7 @@ func TestVerifyToken_Valid(t *testing.T) {
 	assert.ElementsMatch(t, []string{"perm_a", "perm_b"}, resp.Perms)
 }
 
-// TC-0179: 无效token
+// TC-0215: 无效token
 func TestVerifyToken_Invalid(t *testing.T) {
 	ctx := context.Background()
 	svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
@@ -755,7 +755,7 @@ func TestVerifyToken_Invalid(t *testing.T) {
 	assert.False(t, resp.Valid)
 }
 
-// TC-0180: 缺少userId
+// TC-0216: 缺少userId
 func TestVerifyToken_MissingUserId(t *testing.T) {
 	ctx := context.Background()
 	svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
@@ -772,7 +772,7 @@ func TestVerifyToken_MissingUserId(t *testing.T) {
 
 // ---------- GetUserPerms ----------
 
-// TC-0181: 用户不存在
+// TC-0220: 用户不存在
 func TestGetUserPerms_UserNotFound(t *testing.T) {
 	ctx := context.Background()
 	svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
@@ -787,7 +787,7 @@ func TestGetUserPerms_UserNotFound(t *testing.T) {
 	assert.Equal(t, "用户不存在", status.Convert(err).Message())
 }
 
-// TC-0182: 超管
+// TC-0221: 超管
 func TestGetUserPerms_SuperAdmin(t *testing.T) {
 	ctx := context.Background()
 	svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
@@ -841,7 +841,7 @@ func TestGetUserPerms_SuperAdmin(t *testing.T) {
 	assert.Contains(t, resp.Perms, uid+"_c1")
 }
 
-// TC-0165: 验证disabled计数
+// TC-0199: 验证disabled计数
 func TestSyncPermissions_VerifyDisabledCount(t *testing.T) {
 	ctx := context.Background()
 	svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
@@ -885,7 +885,7 @@ func TestSyncPermissions_VerifyDisabledCount(t *testing.T) {
 	assert.Equal(t, int64(3), resp.Disabled)
 }
 
-// TC-0183: MEMBER-DENY覆盖
+// TC-0222: MEMBER-DENY覆盖
 func TestGetUserPerms_MemberDENYOverride(t *testing.T) {
 	ctx := context.Background()
 	svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
@@ -983,7 +983,7 @@ func TestGetUserPerms_MemberDENYOverride(t *testing.T) {
 	assert.NotContains(t, resp.Perms, uid+"_pA")
 }
 
-// TC-0545: gRPC VerifyToken 用户已冻结返回valid=false(H-4修复验证)
+// TC-0217: gRPC VerifyToken 用户已冻结返回valid=false(H-4修复验证)
 func TestVerifyToken_FrozenUserReturnsInvalid(t *testing.T) {
 	ctx := context.Background()
 	svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
@@ -1003,7 +1003,7 @@ func TestVerifyToken_FrozenUserReturnsInvalid(t *testing.T) {
 
 	accessToken, err := authHelper.GenerateAccessToken(
 		cfg.Auth.AccessSecret, cfg.Auth.AccessExpire,
-		uId, uid, "", "MEMBER", []string{"some_perm"},
+		uId, uid, "", "MEMBER", []string{"some_perm"}, 0,
 	)
 	require.NoError(t, err)
 
@@ -1013,7 +1013,7 @@ func TestVerifyToken_FrozenUserReturnsInvalid(t *testing.T) {
 	assert.False(t, resp.Valid, "frozen user token should be invalid")
 }
 
-// TC-0546: gRPC VerifyToken 非产品成员返回valid=false(H-4修复验证)
+// TC-0218: gRPC VerifyToken 非产品成员返回valid=false(H-4修复验证)
 func TestVerifyToken_NonMemberReturnsInvalid(t *testing.T) {
 	ctx := context.Background()
 	svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
@@ -1045,7 +1045,7 @@ func TestVerifyToken_NonMemberReturnsInvalid(t *testing.T) {
 
 	accessToken, err := authHelper.GenerateAccessToken(
 		cfg.Auth.AccessSecret, cfg.Auth.AccessExpire,
-		uId, uid, pc, "MEMBER", []string{"perm_a"},
+		uId, uid, pc, "MEMBER", []string{"perm_a"}, 0,
 	)
 	require.NoError(t, err)
 
@@ -1055,7 +1055,7 @@ func TestVerifyToken_NonMemberReturnsInvalid(t *testing.T) {
 	assert.False(t, resp.Valid, "non-member user with productCode should be invalid")
 }
 
-// TC-0547: gRPC VerifyToken 返回实时权限和成员类型(H-4修复验证)
+// TC-0219: gRPC VerifyToken 返回实时权限和成员类型(H-4修复验证)
 func TestVerifyToken_ReturnsRealtimeData(t *testing.T) {
 	ctx := context.Background()
 	svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
@@ -1102,7 +1102,7 @@ func TestVerifyToken_ReturnsRealtimeData(t *testing.T) {
 
 	accessToken, err := authHelper.GenerateAccessToken(
 		cfg.Auth.AccessSecret, cfg.Auth.AccessExpire,
-		uId, uid, uid, "MEMBER", []string{"old_perm"},
+		uId, uid, uid, "MEMBER", []string{"old_perm"}, 0,
 	)
 	require.NoError(t, err)
 
@@ -1116,7 +1116,7 @@ func TestVerifyToken_ReturnsRealtimeData(t *testing.T) {
 	assert.Contains(t, resp.Perms, uid+"_rt", "should return realtime perms")
 }
 
-// TC-0548: gRPC Login 产品成员被禁用时拒绝(H-3修复验证)
+// TC-0206: gRPC Login 产品成员被禁用时拒绝(H-3修复验证)
 func TestLogin_DisabledMemberRejected(t *testing.T) {
 	ctx := context.Background()
 	svcCtx := svc.NewServiceContext(testutil.GetTestConfig())

+ 3 - 0
internal/svc/servicecontext.go

@@ -16,6 +16,7 @@ type ServiceContext struct {
 	Config            config.Config
 	JwtAuth           rest.Middleware
 	LoginRateLimit    rest.Middleware
+	SyncRateLimit     rest.Middleware
 	GrpcLoginLimiter  *limit.PeriodLimit
 	UserDetailsLoader *loaders.UserDetailsLoader
 	*model.Models
@@ -27,12 +28,14 @@ func NewServiceContext(c config.Config) *ServiceContext {
 	models := model.NewModels(conn, c.CacheRedis.Nodes, c.CacheRedis.KeyPrefix)
 	udLoader := loaders.NewUserDetailsLoader(rds, c.CacheRedis.KeyPrefix, models)
 	rlMiddleware := middleware.NewRateLimitMiddleware(rds, 60, 20, c.CacheRedis.KeyPrefix+":rl:login")
+	syncRlMiddleware := middleware.NewRateLimitMiddleware(rds, 60, 10, c.CacheRedis.KeyPrefix+":rl:sync")
 	grpcLimiter := limit.NewPeriodLimit(60, 20, rds, c.CacheRedis.KeyPrefix+":rl:grpc:login")
 
 	return &ServiceContext{
 		Config:            c,
 		JwtAuth:           middleware.NewJwtAuthMiddleware(c.Auth.AccessSecret, udLoader).Handle,
 		LoginRateLimit:    rlMiddleware.Handle,
+		SyncRateLimit:     syncRlMiddleware.Handle,
 		GrpcLoginLimiter:  grpcLimiter,
 		UserDetailsLoader: udLoader,
 		Models:            models,

+ 3 - 3
internal/util/validate_test.go

@@ -6,7 +6,7 @@ import (
 	"github.com/stretchr/testify/assert"
 )
 
-// TC-0194: page=2, pageSize=10
+// TC-0234: page=2, pageSize=10
 func TestNormalizePage(t *testing.T) {
 	tests := []struct {
 		name             string
@@ -34,7 +34,7 @@ func TestNormalizePage(t *testing.T) {
 	}
 }
 
-// TC-0202: `[email protected]`
+// TC-0242: `[email protected]`
 func TestIsValidEmail(t *testing.T) {
 	tests := []struct {
 		name  string
@@ -62,7 +62,7 @@ func TestIsValidEmail(t *testing.T) {
 	}
 }
 
-// TC-0209: `13800138000`
+// TC-0249: `13800138000`
 func TestIsValidPhone(t *testing.T) {
 	tests := []struct {
 		name  string

+ 24 - 20
perm.sql

@@ -1,5 +1,7 @@
 SET NAMES utf8mb4;
-SET FOREIGN_KEY_CHECKS = 0;
+
+SET
+  FOREIGN_KEY_CHECKS = 0;
 
 -- ----------------------------
 -- Table structure for sys_product
@@ -18,7 +20,7 @@ CREATE TABLE IF NOT EXISTS `sys_product` (
   PRIMARY KEY (`id`),
   UNIQUE KEY `uk_code` (`code`) USING BTREE,
   UNIQUE KEY `uk_app_key` (`appKey`) USING BTREE
-) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
+) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci;
 
 -- ----------------------------
 -- Table structure for sys_dept
@@ -37,7 +39,7 @@ CREATE TABLE IF NOT EXISTS `sys_dept` (
   `updateTime` int NOT NULL DEFAULT '0' COMMENT '修改时间',
   PRIMARY KEY (`id`),
   KEY `idx_parent` (`parentId`) USING BTREE
-) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
+) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci;
 
 -- ----------------------------
 -- Table structure for sys_perm
@@ -53,8 +55,8 @@ CREATE TABLE IF NOT EXISTS `sys_perm` (
   `createTime` int NOT NULL DEFAULT '0' COMMENT '创建时间',
   `updateTime` int NOT NULL DEFAULT '0' COMMENT '修改时间',
   PRIMARY KEY (`id`),
-  UNIQUE KEY `uk_product_code` (`productCode`,`code`) USING BTREE
-) ENGINE=InnoDB AUTO_INCREMENT=14955 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
+  UNIQUE KEY `uk_product_code` (`productCode`, `code`) USING BTREE
+) ENGINE = InnoDB AUTO_INCREMENT = 14955 DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci;
 
 -- ----------------------------
 -- Table structure for sys_role
@@ -70,8 +72,8 @@ CREATE TABLE IF NOT EXISTS `sys_role` (
   `createTime` int NOT NULL DEFAULT '0' COMMENT '创建时间',
   `updateTime` int NOT NULL DEFAULT '0' COMMENT '修改时间',
   PRIMARY KEY (`id`),
-  UNIQUE KEY `uk_product_name` (`productCode`,`name`) USING BTREE
-) ENGINE=InnoDB AUTO_INCREMENT=12 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
+  UNIQUE KEY `uk_product_name` (`productCode`, `name`) USING BTREE
+) ENGINE = InnoDB AUTO_INCREMENT = 12 DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci;
 
 -- ----------------------------
 -- Table structure for sys_role_perm
@@ -84,8 +86,8 @@ CREATE TABLE IF NOT EXISTS `sys_role_perm` (
   `createTime` int NOT NULL DEFAULT '0' COMMENT '创建时间',
   `updateTime` int NOT NULL DEFAULT '0' COMMENT '修改时间',
   PRIMARY KEY (`id`),
-  UNIQUE KEY `uk_role_perm` (`roleId`,`permId`) USING BTREE
-) ENGINE=InnoDB AUTO_INCREMENT=3346 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
+  UNIQUE KEY `uk_role_perm` (`roleId`, `permId`) USING BTREE
+) ENGINE = InnoDB AUTO_INCREMENT = 3346 DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci;
 
 -- ----------------------------
 -- Table structure for sys_user
@@ -104,12 +106,13 @@ CREATE TABLE IF NOT EXISTS `sys_user` (
   `isSuperAdmin` tinyint(1) NOT NULL DEFAULT '2' COMMENT '是否为超级管理员 1是 2否',
   `mustChangePassword` tinyint(1) NOT NULL DEFAULT '2' COMMENT '是否需要强制修改密码 1是 2否',
   `status` tinyint(1) NOT NULL DEFAULT '1' COMMENT '状态 1正常 2冻结',
+  `tokenVersion` bigint NOT NULL DEFAULT 0 COMMENT 'Token版本号,修改密码/冻结时递增',
   `createTime` int NOT NULL DEFAULT '0' COMMENT '创建时间',
   `updateTime` int NOT NULL DEFAULT '0' COMMENT '修改时间',
   PRIMARY KEY (`id`) USING BTREE,
   UNIQUE KEY `uk_username` (`username`) USING BTREE,
   KEY `idx_dept` (`deptId`) USING BTREE
-) ENGINE=InnoDB AUTO_INCREMENT=10 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
+) ENGINE = InnoDB AUTO_INCREMENT = 10 DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci;
 
 -- ----------------------------
 -- Table structure for sys_product_member
@@ -124,14 +127,14 @@ CREATE TABLE IF NOT EXISTS `sys_product_member` (
   `id` bigint NOT NULL AUTO_INCREMENT COMMENT 'ID',
   `productCode` varchar(64) NOT NULL DEFAULT '' COMMENT '产品编码',
   `userId` bigint NOT NULL COMMENT '用户ID',
-  `memberType` enum('DEVELOPER','ADMIN','MEMBER') NOT NULL DEFAULT 'MEMBER' COMMENT '成员类型',
+  `memberType` enum('DEVELOPER', 'ADMIN', 'MEMBER') NOT NULL DEFAULT 'MEMBER' COMMENT '成员类型',
   `status` tinyint(1) NOT NULL DEFAULT '1' COMMENT '状态 1启用 2禁用',
   `createTime` int NOT NULL DEFAULT '0' COMMENT '创建时间',
   `updateTime` int NOT NULL DEFAULT '0' COMMENT '修改时间',
   PRIMARY KEY (`id`),
-  UNIQUE KEY `uk_product_user` (`productCode`,`userId`) USING BTREE,
+  UNIQUE KEY `uk_product_user` (`productCode`, `userId`) USING BTREE,
   KEY `idx_user` (`userId`) USING BTREE
-) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
+) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci;
 
 -- ----------------------------
 -- Table structure for sys_user_perm
@@ -141,13 +144,13 @@ CREATE TABLE IF NOT EXISTS `sys_user_perm` (
   `id` bigint NOT NULL AUTO_INCREMENT COMMENT 'ID',
   `userId` bigint NOT NULL COMMENT '用户ID',
   `permId` bigint NOT NULL COMMENT '权限ID',
-  `effect` enum('ALLOW','DENY') NOT NULL DEFAULT 'ALLOW' COMMENT '权限控制策略 ALLOW允许 DENY拒绝',
+  `effect` enum('ALLOW', 'DENY') NOT NULL DEFAULT 'ALLOW' COMMENT '权限控制策略 ALLOW允许 DENY拒绝',
   `createTime` int NOT NULL DEFAULT '0' COMMENT '创建时间',
   `updateTime` int NOT NULL DEFAULT '0' COMMENT '修改时间',
   PRIMARY KEY (`id`) USING BTREE,
-  UNIQUE KEY `uk_user_perm` (`userId`,`permId`) USING BTREE,
-  KEY `idx_user_effect` (`userId`,`effect`)
-) ENGINE=InnoDB AUTO_INCREMENT=58 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
+  UNIQUE KEY `uk_user_perm` (`userId`, `permId`) USING BTREE,
+  KEY `idx_user_effect` (`userId`, `effect`)
+) ENGINE = InnoDB AUTO_INCREMENT = 58 DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci;
 
 -- ----------------------------
 -- Table structure for sys_user_role
@@ -160,7 +163,8 @@ CREATE TABLE IF NOT EXISTS `sys_user_role` (
   `createTime` int NOT NULL DEFAULT '0' COMMENT '创建时间',
   `updateTime` int NOT NULL DEFAULT '0' COMMENT '修改时间',
   PRIMARY KEY (`id`),
-  UNIQUE KEY `uk_user_role` (`userId`,`roleId`) USING BTREE
-) ENGINE=InnoDB AUTO_INCREMENT=7 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
+  UNIQUE KEY `uk_user_role` (`userId`, `roleId`) USING BTREE
+) ENGINE = InnoDB AUTO_INCREMENT = 7 DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci;
 
-SET FOREIGN_KEY_CHECKS = 1;
+SET
+  FOREIGN_KEY_CHECKS = 1;

تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 449 - 427
test-design.md


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 586 - 591
test-report.md


برخی فایل ها در این مقایسه diff نمایش داده نمی شوند زیرا تعداد فایل ها بسیار زیاد است