Browse Source

feat: 权限系统代码首次提交

BaiLuoYan 1 month ago
parent
commit
c52b2cbd3f
100 changed files with 5120 additions and 0 deletions
  1. 38 0
      .gitignore
  2. 6 0
      .gitmodules
  3. 1319 0
      README.md
  4. 12 0
      cli/goctl/api/config.tpl
  5. 20 0
      cli/goctl/api/context.tpl
  6. 3 0
      cli/goctl/api/etc.tpl
  7. 31 0
      cli/goctl/api/handler.tpl
  8. 84 0
      cli/goctl/api/handler_test.tpl
  9. 116 0
      cli/goctl/api/integration_test.tpl
  10. 29 0
      cli/goctl/api/logic.tpl
  11. 72 0
      cli/goctl/api/logic_test.tpl
  12. 29 0
      cli/goctl/api/main.tpl
  13. 22 0
      cli/goctl/api/middleware.tpl
  14. 4 0
      cli/goctl/api/route-addition.tpl
  15. 15 0
      cli/goctl/api/routes.tpl
  16. 68 0
      cli/goctl/api/sse_handler.tpl
  17. 29 0
      cli/goctl/api/sse_logic.tpl
  18. 60 0
      cli/goctl/api/svc_test.tpl
  19. 24 0
      cli/goctl/api/template.tpl
  20. 8 0
      cli/goctl/api/types.tpl
  21. 36 0
      cli/goctl/docker/docker.tpl
  22. 18 0
      cli/goctl/gateway/etc.tpl
  23. 20 0
      cli/goctl/gateway/main.tpl
  24. 117 0
      cli/goctl/kube/deployment.tpl
  25. 37 0
      cli/goctl/kube/job.tpl
  26. 25 0
      cli/goctl/model/customized.tpl
  27. 100 0
      cli/goctl/model/delete.tpl
  28. 5 0
      cli/goctl/model/err.tpl
  29. 1 0
      cli/goctl/model/field.tpl
  30. 8 0
      cli/goctl/model/find-one-by-field-extra-method.tpl
  31. 44 0
      cli/goctl/model/find-one-by-field.tpl
  32. 40 0
      cli/goctl/model/find-one.tpl
  33. 14 0
      cli/goctl/model/import-no-cache.tpl
  34. 16 0
      cli/goctl/model/import.tpl
  35. 67 0
      cli/goctl/model/insert.tpl
  36. 5 0
      cli/goctl/model/interface-delete.tpl
  37. 2 0
      cli/goctl/model/interface-find-one-by-field.tpl
  38. 2 0
      cli/goctl/model/interface-find-one.tpl
  39. 5 0
      cli/goctl/model/interface-insert.tpl
  40. 4 0
      cli/goctl/model/interface-update.tpl
  41. 16 0
      cli/goctl/model/model-gen.tpl
  42. 12 0
      cli/goctl/model/model-new.tpl
  43. 38 0
      cli/goctl/model/model.tpl
  44. 3 0
      cli/goctl/model/table-name.tpl
  45. 1 0
      cli/goctl/model/tag.tpl
  46. 14 0
      cli/goctl/model/types.tpl
  47. 127 0
      cli/goctl/model/update.tpl
  48. 8 0
      cli/goctl/model/var.tpl
  49. 12 0
      cli/goctl/mongo/err.tpl
  50. 79 0
      cli/goctl/mongo/model.tpl
  51. 38 0
      cli/goctl/mongo/model_custom.tpl
  52. 14 0
      cli/goctl/mongo/model_types.tpl
  53. 14 0
      cli/goctl/newapi/newtemplate.tpl
  54. 33 0
      cli/goctl/rpc/call.tpl
  55. 7 0
      cli/goctl/rpc/config.tpl
  56. 6 0
      cli/goctl/rpc/etc.tpl
  57. 6 0
      cli/goctl/rpc/logic-func.tpl
  58. 24 0
      cli/goctl/rpc/logic.tpl
  59. 36 0
      cli/goctl/rpc/main.tpl
  60. 6 0
      cli/goctl/rpc/server-func.tpl
  61. 22 0
      cli/goctl/rpc/server.tpl
  62. 13 0
      cli/goctl/rpc/svc.tpl
  63. 16 0
      cli/goctl/rpc/template.tpl
  64. 22 0
      etc/perm-api-dev.yaml
  65. 22 0
      etc/perm-api-prod.yaml
  66. 22 0
      etc/perm-api-test.yaml
  67. 51 0
      gen-api.sh
  68. 244 0
      gen-model.sh
  69. 106 0
      go.mod
  70. 379 0
      go.sum
  71. 30 0
      internal/config/config.go
  72. 45 0
      internal/consts/consts.go
  73. 32 0
      internal/handler/auth/changePasswordHandler.go
  74. 24 0
      internal/handler/auth/userInfoHandler.go
  75. 32 0
      internal/handler/dept/createDeptHandler.go
  76. 32 0
      internal/handler/dept/deleteDeptHandler.go
  77. 24 0
      internal/handler/dept/deptTreeHandler.go
  78. 32 0
      internal/handler/dept/updateDeptHandler.go
  79. 32 0
      internal/handler/member/addMemberHandler.go
  80. 32 0
      internal/handler/member/memberListHandler.go
  81. 32 0
      internal/handler/member/removeMemberHandler.go
  82. 32 0
      internal/handler/member/updateMemberHandler.go
  83. 32 0
      internal/handler/perm/permListHandler.go
  84. 32 0
      internal/handler/product/createProductHandler.go
  85. 32 0
      internal/handler/product/productDetailHandler.go
  86. 32 0
      internal/handler/product/productListHandler.go
  87. 32 0
      internal/handler/product/updateProductHandler.go
  88. 32 0
      internal/handler/pub/loginHandler.go
  89. 37 0
      internal/handler/pub/loginHandler_test.go
  90. 32 0
      internal/handler/pub/refreshTokenHandler.go
  91. 32 0
      internal/handler/pub/syncPermsHandler.go
  92. 32 0
      internal/handler/role/bindRolePermsHandler.go
  93. 32 0
      internal/handler/role/createRoleHandler.go
  94. 32 0
      internal/handler/role/deleteRoleHandler.go
  95. 32 0
      internal/handler/role/roleDetailHandler.go
  96. 32 0
      internal/handler/role/roleListHandler.go
  97. 32 0
      internal/handler/role/updateRoleHandler.go
  98. 246 0
      internal/handler/routes.go
  99. 32 0
      internal/handler/user/bindRolesHandler.go
  100. 32 0
      internal/handler/user/createUserHandler.go

+ 38 - 0
.gitignore

@@ -0,0 +1,38 @@
+# Go build output
+*.exe
+*.exe~
+*.dll
+*.so
+*.dylib
+
+# Go test binary & output
+*.test
+*.out
+*.prof
+
+# Go module cache (local vendor cache)
+.gomodcache/
+
+# IDE
+.ai-context/
+.cursorrules/
+.idea/
+.vscode/
+*.swp
+*.swo
+*~
+
+# macOS
+.DS_Store
+.AppleDouble
+.LSOverride
+
+# Config (may contain secrets)
+**/etc/**/*.*
+!**/etc/**/*-prod.yaml
+!**/etc/**/*-test.yaml
+!**/etc/**/*-dev.yaml
+
+# Temp / logs
+*.log
+tmp/

+ 6 - 0
.gitmodules

@@ -0,0 +1,6 @@
+[submodule ".cursorrules"]
+	path = .cursorrules
+	url = https://github.com/zeromicro/ai-context.git
+[submodule ".ai-context/zero-skills"]
+	path = .ai-context/zero-skills
+	url = https://github.com/zeromicro/zero-skills.git

+ 1319 - 0
README.md

@@ -0,0 +1,1319 @@
+# 统一权限管理系统(Permission System Server)
+
+集中式多产品权限管理平台后端服务。为多个产品提供统一的用户认证、权限管理、角色管理能力,产品通过 HTTP API 或 gRPC 接入。
+
+## 核心特性
+
+- **多产品隔离** — 权限、角色、成员按产品(`productCode`)隔离,一个账号可跨产品通用
+- **自动权限同步** — 产品启动时通过 API 自动上报权限列表,系统自动新增/更新/禁用
+- **灵活的权限模型** — 角色权限 + 用户级 ALLOW/DENY 覆盖,细粒度控制
+- **研发部门自动授权** — 研发部门(`deptType=DEV`)的成员加入产品后自动拥有全部权限
+- **多维度操作权限管控** — 集中式访问控制,覆盖超管/产品管理员/成员类型/部门层级/权限级别五个维度
+- **用户信息缓存** — `UserDetailsLoader` 一次性加载用户完整信息,Redis 缓存 + TTL + 主动失效
+- **双协议** — 同时提供 HTTP REST API(管理 UI)和 gRPC(产品后端高性能调用)
+- **JWT 本地验证** — 登录获取 JWT,产品后端可本地验证,无需每次请求回调权限系统
+- **Token 类型安全** — access/refresh token 通过 `tokenType` 字段严格区分,防止 token 混用
+- **四层安全纵深** — JWT 中间件 → 用户状态实时检查 → 冻结账号拦截 → Logic 层操作权限控制
+
+## 系统架构
+
+```mermaid
+graph TB
+    subgraph 权限系统
+        HTTP[HTTP API :10001]
+        GRPC[gRPC Server :10002]
+        DB[(MySQL)]
+        CACHE[(Redis Cache)]
+        HTTP --> DB
+        HTTP --> CACHE
+        GRPC --> DB
+        GRPC --> CACHE
+    end
+
+    UI[管理后台 UI] -->|REST API| HTTP
+    ProductA[产品 A 后端] -->|gRPC| GRPC
+    ProductB[产品 B 后端] -->|gRPC| GRPC
+    ProductC[产品 C 后端] -->|HTTP| HTTP
+```
+
+---
+
+## 核心概念与实体关系
+
+### 实体关系总览
+
+```mermaid
+erDiagram
+    sys_product ||--o{ sys_perm : "productCode → 产品拥有权限"
+    sys_product ||--o{ sys_role : "productCode → 产品拥有角色"
+    sys_product ||--o{ sys_product_member : "productCode → 产品拥有成员"
+    sys_user ||--o{ sys_product_member : "userId → 用户加入产品"
+    sys_user ||--o{ sys_user_role : "userId → 用户绑定角色"
+    sys_user ||--o{ sys_user_perm : "userId → 用户自定义权限"
+    sys_user }o--o| sys_dept : "deptId → 用户所属部门"
+    sys_dept }o--o| sys_dept : "parentId → 父子部门"
+    sys_role ||--o{ sys_role_perm : "roleId → 角色绑定权限"
+    sys_role ||--o{ sys_user_role : "roleId → 角色分配给用户"
+    sys_perm ||--o{ sys_role_perm : "permId → 权限绑定到角色"
+    sys_perm ||--o{ sys_user_perm : "permId → 权限直接授予用户"
+
+    sys_product {
+        bigint id PK
+        varchar code UK "产品编码"
+        varchar name "产品名称"
+        varchar appKey UK "接入密钥"
+        varchar appSecret "签名密钥"
+        tinyint status "1启用 2禁用"
+    }
+    sys_user {
+        bigint id PK
+        varchar username UK "登录名"
+        varchar password "bcrypt密码"
+        varchar nickname "昵称"
+        bigint deptId FK "部门ID"
+        tinyint isSuperAdmin "1是 2否"
+        tinyint mustChangePassword "1是 2否"
+        tinyint status "1正常 2冻结"
+    }
+    sys_dept {
+        bigint id PK
+        bigint parentId "父部门ID"
+        varchar name "部门名称"
+        varchar path "层级路径"
+        varchar deptType "NORMAL普通 DEV研发"
+        int sort "排序值"
+        tinyint status "1启用 2禁用"
+    }
+    sys_perm {
+        bigint id PK
+        varchar productCode "所属产品"
+        varchar name "权限名"
+        varchar code UK "权限code"
+        tinyint status "1启用 2禁用"
+    }
+    sys_role {
+        bigint id PK
+        varchar productCode "所属产品"
+        varchar name UK "角色名"
+        int permsLevel "权限等级"
+        tinyint status "1启用 2禁用"
+    }
+    sys_product_member {
+        bigint id PK
+        varchar productCode "产品编码"
+        bigint userId FK "用户ID"
+        varchar memberType "成员类型"
+        tinyint status "1启用 2禁用"
+    }
+    sys_role_perm {
+        bigint id PK
+        bigint roleId FK
+        bigint permId FK
+    }
+    sys_user_role {
+        bigint id PK
+        bigint userId FK
+        bigint roleId FK
+    }
+    sys_user_perm {
+        bigint id PK
+        bigint userId FK
+        bigint permId FK
+        varchar effect "ALLOW或DENY"
+    }
+```
+
+### 五大核心实体
+
+| 实体 | 说明 | 隔离范围 |
+|------|------|----------|
+| **产品 (Product)** | 接入权限系统的业务系统(如 CRM、OA),每个产品有独立的 `appKey`/`appSecret` | 全局 |
+| **部门 (Dept)** | 组织架构树形结构,支持无限层级嵌套。`deptType=DEV` 的研发部门有特殊权限逻辑 | 全局 |
+| **用户 (User)** | 全局账号,通过 `deptId` 归属部门,通过成员关系加入不同产品 | 全局 |
+| **角色 (Role)** | 产品级别的权限集合,同一角色名在不同产品中互不干扰 | 产品级 |
+| **权限 (Perm)** | 产品级别的最小权限单元,由产品后端通过 `SyncPerms` 自动上报 | 产品级 |
+
+### 实体间关系详解
+
+```
+全局组织架构                   产品权限体系(按产品隔离)
+┌─────────────────┐           ┌────────────────────────────────────┐
+│  部门 (Dept)     │           │  产品 (Product)                    │
+│  ├── 研发部(DEV) │           │  ┌─────────┐  ┌──────────┐        │
+│  ├── 市场部      │           │  │ 权限列表  │  │ 角色列表  │        │
+│  └── 财务部      │           │  │ Perm     │  │ Role     │        │
+└───────┬─────────┘           │  └────┬────┘  └────┬─────┘        │
+        │ deptId              │       │            │              │
+┌───────┴──────┐              │  ┌────┴────────────┴────┐         │
+│  用户 (User)  │─────────────│  │ 角色-权限绑定 RolePerm │         │
+│              │   成员关系    │  └─────────────────────┘         │
+│              │──────────────│                                   │
+│              │              │  ┌───────────────────────┐        │
+│              │──────────────│  │ 用户-角色 UserRole      │        │
+│              │              │  └───────────────────────┘        │
+│              │──────────────│  ┌───────────────────────┐        │
+│              │              │  │ 用户-权限覆盖 UserPerm  │        │
+└──────────────┘              │  │ ALLOW / DENY           │        │
+                              │  └───────────────────────┘        │
+                              └────────────────────────────────────┘
+```
+
+**关键关系**:
+
+1. **用户 → 产品**:通过 `sys_product_member` 建立,一个用户可以是多个产品的成员
+2. **用户 → 部门**:通过 `sys_user.deptId` 关联,一个用户只属于一个部门
+3. **角色 → 权限**:通过 `sys_role_perm` 绑定,一个角色可绑定多个权限
+4. **用户 → 角色**:通过 `sys_user_role` 分配,一个用户可拥有多个角色
+5. **用户 → 权限覆盖**:通过 `sys_user_perm` 直接授予/拒绝,优先级高于角色权限
+
+### 成员类型与权限层级
+
+| 优先级 | 类型 | 权限范围 | 来源 |
+|--------|------|----------|------|
+| 1 | `SUPER_ADMIN` | **所有产品**的全部权限 | `sys_user.isSuperAdmin = 1` |
+| 2 | `DEVELOPER` | **该产品**全部权限 | `sys_product_member.memberType` |
+| 3 | `ADMIN` | **该产品**全部权限 | `sys_product_member.memberType` |
+| 4 | 研发部门成员 | **该产品**全部权限 | `sys_dept.deptType = DEV` + 是产品成员 |
+| 5 | `MEMBER` | 角色权限 ∪ ALLOW - DENY | 角色绑定 + 用户级覆盖计算 |
+
+### 权限计算流程
+
+```mermaid
+flowchart TD
+    START[获取用户权限] --> IS_SUPER{是超级管理员?}
+    IS_SUPER -->|是| ALL_PERMS[返回该产品全部权限码]
+    IS_SUPER -->|否| FIND_MEMBER[查询 sys_product_member]
+    FIND_MEMBER --> HAS_MEMBER{是产品成员?}
+    HAS_MEMBER -->|否| EMPTY[返回空权限]
+    HAS_MEMBER -->|是| CHECK_TYPE{成员类型?}
+    CHECK_TYPE -->|DEVELOPER / ADMIN| ALL_PERMS
+    CHECK_TYPE -->|MEMBER| CHECK_DEPT{所属部门是研发部门?}
+    CHECK_DEPT -->|是| ALL_PERMS
+    CHECK_DEPT -->|否| CALC[计算权限]
+    CALC --> ROLE_PERMS[查询用户角色绑定的权限 ID]
+    ROLE_PERMS --> USER_ALLOW[查询用户级 ALLOW 权限 ID]
+    USER_ALLOW --> USER_DENY[查询用户级 DENY 权限 ID]
+    USER_DENY --> MERGE["合并: (角色权限 ∪ ALLOW) - DENY"]
+    MERGE --> FILTER[过滤已禁用的权限]
+    FILTER --> CODES[转换为权限 code 列表返回]
+```
+
+---
+
+## 实际业务配置指南
+
+以下以一个典型的中小公司为例,说明如何配置和使用权限系统。
+
+### 场景假设
+
+公司有**研发部**(30 人)、**市场部**(10 人)、**运营部**(15 人),负责维护 3 个产品:**CRM 系统**、**OA 系统**、**电商后台**。研发人员需要在产品间流动。
+
+### 第一步:搭建组织架构
+
+```mermaid
+graph TD
+    ROOT[公司] --> DEV[研发部 deptType=DEV]
+    ROOT --> MKT[市场部 deptType=NORMAL]
+    ROOT --> OPS[运营部 deptType=NORMAL]
+    DEV --> FE[前端组]
+    DEV --> BE[后端组]
+    DEV --> QA[测试组]
+```
+
+```bash
+# 创建顶级部门
+POST /api/dept/create  {"parentId": 0, "name": "研发部", "deptType": "DEV", "sort": 1}
+# 返回 id=1
+
+POST /api/dept/create  {"parentId": 0, "name": "市场部", "sort": 2}
+# deptType 不传默认为 NORMAL,返回 id=2
+
+POST /api/dept/create  {"parentId": 0, "name": "运营部", "sort": 3}
+# 返回 id=3
+
+# 创建研发部子部门
+POST /api/dept/create  {"parentId": 1, "name": "前端组", "deptType": "DEV", "sort": 1}
+POST /api/dept/create  {"parentId": 1, "name": "后端组", "deptType": "DEV", "sort": 2}
+POST /api/dept/create  {"parentId": 1, "name": "测试组", "deptType": "DEV", "sort": 3}
+```
+
+> **研发部门的特殊能力**:`deptType=DEV` 的部门中的成员,只要被添加到某个产品下,就自动拥有该产品的全部权限。当从产品成员中移除时,权限立即收回。
+
+### 第二步:注册产品
+
+```bash
+POST /api/product/create  {"code": "crm", "name": "CRM 系统"}
+# 返回 appKey, appSecret, adminUser, adminPassword
+
+POST /api/product/create  {"code": "oa", "name": "OA 系统"}
+POST /api/product/create  {"code": "mall", "name": "电商后台"}
+```
+
+每个产品创建后会自动生成一个初始管理员账号(如 `admin_crm`),该账号是该产品的 `SUPER_ADMIN` 级别成员。
+
+### 第三步:产品上报权限
+
+各产品后端在启动时自动调用 `POST /api/perm/sync` 上报权限列表。以 CRM 为例:
+
+```json
+{
+  "appKey": "...", "appSecret": "...",
+  "perms": [
+    {"code": "customer:list",   "name": "查看客户列表"},
+    {"code": "customer:create", "name": "创建客户"},
+    {"code": "customer:update", "name": "编辑客户"},
+    {"code": "customer:delete", "name": "删除客户"},
+    {"code": "order:list",      "name": "查看订单"},
+    {"code": "order:create",    "name": "创建订单"},
+    {"code": "report:export",   "name": "导出报表"}
+  ]
+}
+```
+
+权限由产品代码自动管理,管理员无需手动创建。
+
+### 第四步:创建角色并绑定权限
+
+```bash
+# CRM 产品下创建角色
+POST /api/role/create  {"productCode": "crm", "name": "销售经理", "permsLevel": 10}
+POST /api/role/create  {"productCode": "crm", "name": "普通销售", "permsLevel": 20}
+POST /api/role/create  {"productCode": "crm", "name": "客服",     "permsLevel": 30}
+
+# 为角色绑定权限
+POST /api/role/bindPerms  {"roleId": 1, "permIds": [1,2,3,4,5,6,7]}  # 销售经理:全部
+POST /api/role/bindPerms  {"roleId": 2, "permIds": [1,2,5,6]}        # 普通销售:查看+创建
+POST /api/role/bindPerms  {"roleId": 3, "permIds": [1,5]}            # 客服:仅查看
+```
+
+### 第五步:创建用户并配置权限
+
+**场景 A:研发人员张三** — 属于研发部,需要参与 CRM 和 OA 的开发
+
+```bash
+# 创建用户,归属研发部
+POST /api/user/create  {"username": "zhangsan", "password": "123456", "nickname": "张三", "deptId": 1}
+# 返回 userId=10
+
+# 将张三添加为 CRM 和 OA 的成员(MEMBER 类型即可)
+POST /api/member/add  {"productCode": "crm", "userId": 10, "memberType": "MEMBER"}
+POST /api/member/add  {"productCode": "oa",  "userId": 10, "memberType": "MEMBER"}
+
+# 因为张三属于研发部(DEV),他登录 CRM 或 OA 时自动拥有全部权限
+# 无需为他分配角色!
+```
+
+**场景 B:一段时间后张三不再负责 OA** — 只需移除成员关系
+
+```bash
+POST /api/member/remove  {"id": <张三在OA中的成员记录ID>}
+# OA 的权限立即收回,CRM 的权限不受影响
+```
+
+**场景 C:市场部李四** — 需要使用 CRM,分配销售角色
+
+```bash
+POST /api/user/create  {"username": "lisi", "password": "123456", "nickname": "李四", "deptId": 2}
+# 返回 userId=11
+
+POST /api/member/add    {"productCode": "crm", "userId": 11, "memberType": "MEMBER"}
+POST /api/user/bindRoles  {"userId": 11, "roleIds": [2]}  # 分配"普通销售"角色
+```
+
+李四登录 CRM 后拥有的权限 = 普通销售角色的权限 = `[customer:list, customer:create, order:list, order:create]`。
+
+**场景 D:给李四额外权限** — 在角色基础上微调
+
+```bash
+# 额外给李四"导出报表"权限,但禁止他"创建订单"
+POST /api/user/setPerms  {
+  "userId": 11,
+  "perms": [
+    {"permId": 7, "effect": "ALLOW"},
+    {"permId": 6, "effect": "DENY"}
+  ]
+}
+```
+
+最终李四的权限 = 角色 `[1,2,5,6]` ∪ ALLOW `[7]` - DENY `[6]` = `[customer:list, customer:create, order:list, report:export]`
+
+**场景 E:外部临时协作人员** — 不属于任何部门
+
+```bash
+POST /api/user/create   {"username": "temp_wang", "password": "123456", "nickname": "外包王五"}
+# deptId 不传,不归属任何部门
+
+POST /api/member/add    {"productCode": "crm", "userId": 12, "memberType": "MEMBER"}
+POST /api/user/bindRoles  {"userId": 12, "roleIds": [3]}  # 分配"客服"角色(只读)
+```
+
+### 权限配置决策树
+
+```mermaid
+flowchart TD
+    Q1{该用户是研发人员?}
+    Q1 -->|是| A1[归属研发部门 deptType=DEV]
+    A1 --> A2["添加为产品成员(MEMBER)即可<br>自动拥有全部权限"]
+    Q1 -->|否| Q2{该用户是产品管理员?}
+    Q2 -->|是| A3["添加为产品成员(ADMIN)<br>自动拥有全部权限"]
+    Q2 -->|否| Q3{需要精细权限控制?}
+    Q3 -->|是| A4["添加为产品成员(MEMBER)"]
+    A4 --> A5[分配角色 + 可选 ALLOW/DENY 覆盖]
+    Q3 -->|否| A6[不添加为成员 = 无权限]
+```
+
+---
+
+## 操作权限控制(Access Control)
+
+系统内置一套集中式操作权限管控机制,对所有管理类接口实施多维度的访问控制,防止越权操作。
+
+### 安全架构:四层纵深防护
+
+```mermaid
+flowchart LR
+    REQ[HTTP 请求] --> L1["① JWT 中间件<br>解析 token / 验证签名"]
+    L1 --> L2["② UserDetailsLoader<br>加载用户实时状态"]
+    L2 --> L3["③ 冻结检查<br>Status != 1 → 403"]
+    L3 --> L4["④ Logic 层<br>操作权限控制"]
+    L4 --> BIZ[业务逻辑]
+```
+
+| 层级 | 组件 | 职责 |
+|------|------|------|
+| 第一层 | JWT 中间件 | 解析 access token、验证签名、校验 `tokenType` |
+| 第二层 | UserDetailsLoader | 从 Redis 缓存或 DB 加载用户完整信息(含部门、角色、权限) |
+| 第三层 | 用户状态检查 | 冻结账号(`status ≠ 1`)直接返回 403 |
+| 第四层 | access.go | 按接口类型检查超管/产品管理员/部门层级/权限级别 |
+
+### 接口操作权限矩阵
+
+| 接口 | 权限要求 | 额外检查 |
+|------|----------|----------|
+| **产品管理** |||
+| 创建产品 | 仅超级管理员 | — |
+| 更新产品 | 仅超级管理员 | — |
+| **部门管理** |||
+| 创建部门 | 仅超级管理员 | — |
+| 更新部门 | 仅超级管理员 | — |
+| 删除部门 | 仅超级管理员 | 有子部门时拒绝 |
+| **角色管理** |||
+| 创建角色 | 超管 或 产品管理员 | — |
+| 更新角色 | 超管 或 产品管理员 | — |
+| 删除角色 | 超管 或 产品管理员 | 级联删除关联数据 |
+| 绑定角色权限 | 超管 或 产品管理员 | — |
+| **用户管理** |||
+| 创建用户 | 超管 或 产品管理员 | — |
+| 更新用户信息 | 仅本人 或 超管 | — |
+| 冻结/解冻用户 | 通过 `CheckManageAccess` | 不可冻结自己和超管 |
+| 绑定角色 | 通过 `CheckManageAccess` | — |
+| 设置权限覆盖 | 通过 `CheckManageAccess` | — |
+| **成员管理** |||
+| 添加成员 | 通过 `CheckManageAccess` + `CheckMemberTypeAssignment` | 不可分配同级或更高类型 |
+| 更新成员 | 通过 `CheckManageAccess` + `CheckMemberTypeAssignment` | — |
+| 移除成员 | 通过 `CheckManageAccess` | 级联清理角色/权限绑定 |
+| **查询类接口** |||
+| 产品/部门/角色/用户/成员列表与详情 | 已登录即可 | — |
+| 用户信息 (userInfo) | 已登录即可 | 返回当前登录用户自己的信息 |
+| **公开接口** |||
+| 登录 / 刷新令牌 / 同步权限 | 无需鉴权 | 同步权限通过 appKey/appSecret 认证 |
+
+### CheckManageAccess — 用户管理权限检查
+
+`CheckManageAccess` 是管理类操作的核心权限检查函数,执行多维度判定:
+
+```mermaid
+flowchart TD
+    START[CheckManageAccess] --> SA{是超级管理员?}
+    SA -->|是| PASS[✅ 通过]
+    SA -->|否| SELF{操作自己?}
+    SELF -->|是| PASS
+    SELF -->|否| DEPT[部门层级检查]
+    DEPT --> IS_ADMIN{操作者是 ADMIN?}
+    IS_ADMIN -->|是| LEVEL[权限级别检查]
+    IS_ADMIN -->|否| HAS_DEPT{操作者有部门?}
+    HAS_DEPT -->|是| TARGET_DEPT{目标在操作者<br>本部门或子部门?}
+    HAS_DEPT -->|否| DENY1[❌ 未归属部门]
+    TARGET_DEPT -->|是| LEVEL
+    TARGET_DEPT -->|否| DENY2[❌ 跨部门拒绝]
+    LEVEL --> CMP_TYPE{比较 memberType 优先级}
+    CMP_TYPE -->|操作者更高| PASS
+    CMP_TYPE -->|目标更高| DENY3[❌ 权限不足]
+    CMP_TYPE -->|同级| CMP_PERM{比较 permsLevel}
+    CMP_PERM -->|操作者更小| PASS
+    CMP_PERM -->|相等或更大| DENY4[❌ 同级或更低]
+```
+
+**检查维度说明**:
+
+1. **超管豁免**:`SUPER_ADMIN` 不受任何限制
+2. **自我豁免**:操作自己的记录总是允许
+3. **部门层级**:操作者只能管理本部门及下级子部门的用户(`ADMIN` 和超管豁免此检查)
+4. **权限级别**:先比 `memberType` 优先级,同级再比 `permsLevel` 数值
+   - `memberType` 优先级:`SUPER_ADMIN(0) > ADMIN(1) > DEVELOPER(2) > MEMBER(3)`
+   - `permsLevel`:数值越小权限越高(如 10 > 20),无角色默认为 `MaxInt64`(最低)
+
+### UserDetailsLoader — 用户信息缓存
+
+`UserDetailsLoader` 是一个集中式的用户信息加载与缓存组件,为中间件、登录、用户信息查询等多个场景提供统一的数据来源。
+
+**加载的完整数据**:
+
+| 数据来源 | 加载的字段 |
+|----------|-----------|
+| `sys_user` | userId, username, nickname, avatar, email, phone, remark, isSuperAdmin, mustChangePassword, status |
+| `sys_dept` | deptId, deptName, deptPath, deptType |
+| `sys_product` | productCode, productName |
+| `sys_product_member` | memberType |
+| `sys_role` (当前产品) | roles[], minPermsLevel |
+| 计算后的权限 | perms[] (权限 code 集合) |
+
+**缓存策略**:
+
+- **存储**:Redis JSON,key 格式 `{prefix}:ud:{userId}:{productCode}`
+- **TTL**:300 秒(5 分钟)自然过期
+- **主动失效**:所有写操作均触发对应的缓存清除
+
+| 操作 | 失效方法 | 失效范围 |
+|------|----------|----------|
+| 更新用户信息 / 冻结解冻 / 修改密码 | `Clean(userId)` | 该用户所有产品缓存 |
+| 设置用户权限覆盖 / 添加成员 / 更新成员 | `Del(userId, productCode)` | 该用户在指定产品的缓存 |
+| 更新角色 / 删除角色 / 绑定角色权限 | `BatchDel(userIds, productCode)` | 受影响用户在指定产品的缓存 |
+| 更新产品 / 同步权限 | `CleanByProduct(productCode)` | 该产品下所有用户的缓存 |
+| 更新部门 | `Clean(uid)` × N | 该部门下所有用户的缓存 |
+
+---
+
+## 快速开始
+
+### 环境要求
+
+- Go 1.21+
+- MySQL 8.0+
+- Redis 6.0+
+- goctl 1.10+
+
+### 部署步骤
+
+```bash
+# 1. 创建数据库并导入表结构
+mysql -u root -p -e "CREATE DATABASE perms_system DEFAULT CHARACTER SET utf8mb4;"
+mysql -u root -p perms_system < perm.sql
+
+# 2. 修改配置(数据库、Redis、JWT 密钥等)
+vim etc/perm-api.yaml
+
+# 3. 安装依赖并启动
+go mod tidy
+go run perm.go
+
+# 服务启动后:
+# - HTTP API: http://localhost:10001
+# - gRPC:     localhost:10002
+```
+
+### 初始化超级管理员
+
+首次部署后,需手动在数据库中插入超级管理员账号:
+
+```sql
+INSERT INTO sys_user (username, password, nickname, isSuperAdmin, status, createTime, updateTime)
+VALUES ('superadmin', '$2a$10$这里替换为bcrypt加密后的密码', '超级管理员', 1, 1, UNIX_TIMESTAMP(), UNIX_TIMESTAMP());
+```
+
+可使用任意 bcrypt 工具生成密码哈希,或在 Go 中执行:
+
+```go
+hash, _ := bcrypt.GenerateFromPassword([]byte("your-password"), bcrypt.DefaultCost)
+fmt.Println(string(hash))
+```
+
+---
+
+## 产品接入指南
+
+本节以 "CRM 系统"(编码 `crm`)为例,完整演示接入流程。
+
+### 接入全景图
+
+```mermaid
+sequenceDiagram
+    participant SA as 超级管理员
+    participant PS as 权限系统
+    participant CRM as CRM 后端
+    participant U as 终端用户
+
+    rect rgb(230, 245, 255)
+    Note over SA, PS: 阶段一:注册产品
+    SA ->> PS: POST /api/product/create
+    PS -->> SA: appKey + appSecret + 初始管理员
+    end
+
+    rect rgb(230, 255, 230)
+    Note over CRM, PS: 阶段二:CRM 启动 → 同步权限
+    CRM ->> PS: POST /api/perm/sync (appKey + appSecret + 权限列表)
+    PS -->> CRM: {added, updated, disabled}
+    end
+
+    rect rgb(255, 245, 230)
+    Note over SA, PS: 阶段三:管理员配置角色与用户
+    SA ->> PS: 创建角色、绑定权限、创建用户、分配角色
+    end
+
+    rect rgb(245, 230, 255)
+    Note over U, CRM: 阶段四:用户登录与鉴权
+    U ->> CRM: 提交用户名密码
+    CRM ->> PS: POST /api/auth/login 或 gRPC Login
+    PS -->> CRM: accessToken + refreshToken + perms[]
+    CRM -->> U: 返回 token
+    U ->> CRM: 业务请求 + Bearer token
+    CRM ->> CRM: 本地 JWT 解析,检查 perms[]
+    CRM -->> U: 返回业务数据
+    end
+```
+
+### 阶段一:注册产品
+
+超级管理员登录后创建产品:
+
+```bash
+POST /api/product/create
+{"code": "crm", "name": "CRM 系统", "remark": "客户关系管理"}
+```
+
+响应中包含 `appKey`、`appSecret`、`adminUser`、`adminPassword`,**请立即保存,后续不再展示**。
+
+### 阶段二:产品启动时同步权限
+
+CRM 后端在启动阶段上报全部权限列表。**此接口无需 JWT,通过 appKey + appSecret 认证**。
+
+```bash
+POST /api/perm/sync
+{
+  "appKey": "a1b2c3d4e5f6...",
+  "appSecret": "x9y8z7w6v5u4...",
+  "perms": [
+    {"code": "customer:list",   "name": "查看客户列表"},
+    {"code": "customer:create", "name": "创建客户"},
+    {"code": "customer:update", "name": "编辑客户"},
+    {"code": "customer:delete", "name": "删除客户"},
+    {"code": "order:list",      "name": "查看订单列表"},
+    {"code": "order:create",    "name": "创建订单"},
+    {"code": "report:export",   "name": "导出报表"}
+  ]
+}
+```
+
+**Go 代码示例(放在 main 启动流程中):**
+
+```go
+var permsList = []map[string]string{
+    {"code": "customer:list",   "name": "查看客户列表"},
+    {"code": "customer:create", "name": "创建客户"},
+    // ...
+}
+
+func syncPermsOnStartup(permSystemURL, appKey, appSecret string) {
+    body, _ := json.Marshal(map[string]interface{}{
+        "appKey": appKey, "appSecret": appSecret, "perms": permsList,
+    })
+    resp, err := http.Post(permSystemURL+"/api/perm/sync", "application/json", bytes.NewReader(body))
+    if err != nil {
+        log.Fatalf("同步权限失败: %v", err)
+    }
+    defer resp.Body.Close()
+    log.Println("权限同步完成")
+}
+```
+
+### 阶段三:管理员配置角色与用户
+
+详见上文「实际业务配置指南」。
+
+### 阶段四:用户登录与鉴权
+
+#### 1. 用户登录
+
+**方式 A:HTTP API**
+
+```bash
+POST /api/auth/login
+{"username": "zhangsan", "password": "123456", "productCode": "crm"}
+```
+
+**方式 B:gRPC SDK(推荐 Go 项目)**
+
+```go
+import "perms-system-server/permclient"
+
+client, err := permclient.NewPermClient("perm-system:10002")
+resp, err := client.Login(ctx, "crm", "zhangsan", "123456")
+// resp.AccessToken, resp.RefreshToken, resp.Perms
+```
+
+#### 2. 本地 JWT 验证(推荐)
+
+产品后端配置与权限系统相同的 `AccessSecret`,本地解析 JWT 即可获取用户信息和权限列表,无需每次回调。
+
+```go
+type Claims struct {
+    TokenType   string   `json:"tokenType"`
+    UserId      int64    `json:"userId"`
+    Username    string   `json:"username"`
+    ProductCode string   `json:"productCode"`
+    MemberType  string   `json:"memberType"`
+    Perms       []string `json:"perms"`
+    jwt.RegisteredClaims
+}
+```
+
+> `tokenType` 字段区分 `"access"` 和 `"refresh"`,验证时应检查 `tokenType == "access"`。
+
+#### 3. 业务接口中检查权限
+
+```go
+func CreateOrderHandler(w http.ResponseWriter, r *http.Request) {
+    claims := middleware.GetClaims(r.Context())
+    if !hasPermission(claims.Perms, "order:create") {
+        // 返回 403
+        return
+    }
+    // 执行业务逻辑...
+}
+```
+
+#### 4. Token 续期
+
+```bash
+POST /api/auth/refreshToken
+Authorization: Bearer eyJhbGciOi...   # refreshToken 放在 header 中
+Content-Type: application/json
+
+{"productCode": "crm"}
+```
+
+> `refreshToken` 有效期默认 7 天,刷新时返回原始 refreshToken(不重新签发),过期后必须重新登录。这确保了 token 有固定的生命周期。
+
+### 接入检查清单
+
+- [ ] 已获取产品的 `appKey` 和 `appSecret`
+- [ ] 产品启动时调用 `/api/perm/sync` 同步了全部权限
+- [ ] 后端配置了与权限系统相同的 `AccessSecret`
+- [ ] 登录接口正确调用权限系统并返回 token
+- [ ] JWT 鉴权中间件已加入受保护路由,并验证 `tokenType == "access"`
+- [ ] 业务接口中根据 `claims.Perms` 做了权限校验
+- [ ] 前端在 `accessToken` 过期时能自动调用 `refreshToken` 续期
+
+---
+
+## HTTP API 接口文档
+
+所有接口基础路径为 `/api`,请求方式统一为 **POST**,参数通过 **JSON Body** 传递。需鉴权接口须携带 `Authorization: Bearer {accessToken}`。
+
+### 统一响应格式
+
+所有 HTTP 接口均返回统一的 JSON 结构:
+
+```json
+{
+  "code": 0,
+  "msg": "ok",
+  "data": { ... }
+}
+```
+
+| 字段 | 类型 | 说明 |
+|------|------|------|
+| `code` | int | 业务状态码,`0` 表示成功,非零表示失败 |
+| `msg` | string | 状态描述 |
+| `data` | object/null | 业务数据,失败时无此字段 |
+
+### 错误码一览
+
+| Code | 语义 | 典型场景 |
+|------|------|----------|
+| `0` | 成功 | 所有正常响应 |
+| `400` | 请求不合法 | 参数缺失/格式错误、原密码错误、存在子部门无法删除等 |
+| `401` | 未认证 | 未登录、token 无效/过期/类型错误、用户名密码错误 |
+| `403` | 无权限 | 账号已冻结、产品已禁用、非超管操作产品/部门、非管理员操作角色/用户、跨部门/越级管理 |
+| `404` | 资源不存在 | 用户/产品/角色/部门/成员不存在 |
+| `409` | 资源冲突 | 用户名已存在、产品编码已存在、角色名重复、成员重复添加 |
+| `500` | 系统错误 | 数据库异常等未预期的内部错误(不暴露具体信息) |
+
+### 公开接口(无需鉴权)
+
+#### POST /api/auth/login — 用户登录
+
+| 字段 | 类型 | 必填 | 说明 |
+|------|------|------|------|
+| username | string | 是 | 登录名 |
+| password | string | 是 | 密码 |
+| productCode | string | 否 | 产品编码,传入则返回该产品的权限列表 |
+
+**响应 data:**
+
+| 字段 | 类型 | 说明 |
+|------|------|------|
+| accessToken | string | 访问令牌 |
+| refreshToken | string | 刷新令牌 |
+| expires | int64 | accessToken 过期时间(Unix 时间戳,秒) |
+| userInfo | object | 用户信息(含 perms 权限码数组) |
+
+#### POST /api/auth/refreshToken — 刷新令牌
+
+通过 `Authorization: Bearer {refreshToken}` 请求头传入 refresh token。
+
+| 字段 | 类型 | 必填 | 说明 |
+|------|------|------|------|
+| Authorization | header | 是 | `Bearer {refreshToken}` |
+| productCode | string | 否 | 切换产品上下文时传入(Body) |
+
+**响应 data:** 与登录接口相同。注意返回的 `refreshToken` 是原始值(不重新签发),refresh token 有固定有效期,过期后需重新登录。
+
+#### POST /api/perm/sync — 同步产品权限
+
+通过 appKey/appSecret 认证,产品后端上报全量权限列表。
+
+| 字段 | 类型 | 必填 | 说明 |
+|------|------|------|------|
+| appKey | string | 是 | 产品接入密钥 |
+| appSecret | string | 是 | 产品签名密钥 |
+| perms | array | 是 | 权限列表 |
+| perms[].code | string | 是 | 权限码(如 `user:create`) |
+| perms[].name | string | 是 | 权限名 |
+| perms[].remark | string | 否 | 备注 |
+
+**响应 data:** `{"added": 3, "updated": 1, "disabled": 0}`
+
+### 认证接口(需鉴权)
+
+#### POST /api/auth/userInfo — 获取当前用户信息
+
+无请求参数。**响应 data:** `UserInfo` 对象。
+
+#### POST /api/auth/changePassword — 修改密码
+
+| 字段 | 类型 | 必填 | 说明 |
+|------|------|------|------|
+| oldPassword | string | 是 | 原密码 |
+| newPassword | string | 是 | 新密码(6-72 字符,不能与旧密码相同) |
+
+### 产品管理(仅超级管理员)
+
+#### POST /api/product/create — 创建产品
+
+| 字段 | 类型 | 必填 | 说明 |
+|------|------|------|------|
+| code | string | 是 | 产品编码(全局唯一) |
+| name | string | 是 | 产品名称 |
+| remark | string | 否 | 备注 |
+
+**响应 data:** `{"id", "code", "appKey", "appSecret", "adminUser", "adminPassword"}`
+
+#### POST /api/product/update — 更新产品
+
+| 字段 | 类型 | 必填 | 说明 |
+|------|------|------|------|
+| id | int64 | 是 | 产品 ID |
+| name | string | 是 | 产品名称 |
+| remark | string | 否 | 备注 |
+| status | int64 | 否 | 1=启用 2=禁用 |
+
+#### POST /api/product/list — 产品列表
+
+| 字段 | 类型 | 必填 | 说明 |
+|------|------|------|------|
+| page | int64 | 否 | 页码,默认 1 |
+| pageSize | int64 | 否 | 每页条数,默认 20,上限 100 |
+
+**响应 data:** `{"total": N, "list": [ProductItem...]}`
+
+#### POST /api/product/detail — 产品详情
+
+| 字段 | 类型 | 必填 | 说明 |
+|------|------|------|------|
+| id | int64 | 是 | 产品 ID |
+
+### 部门管理(仅超级管理员)
+
+#### POST /api/dept/create — 创建部门
+
+| 字段 | 类型 | 必填 | 说明 |
+|------|------|------|------|
+| parentId | int64 | 是 | 父部门 ID,0 表示顶级部门 |
+| name | string | 是 | 部门名称 |
+| sort | int64 | 否 | 排序值 |
+| deptType | string | 否 | `NORMAL`(默认)或 `DEV`(研发部门) |
+| remark | string | 否 | 备注 |
+
+**响应 data:** `{"id": 1}`
+
+#### POST /api/dept/update — 更新部门
+
+| 字段 | 类型 | 必填 | 说明 |
+|------|------|------|------|
+| id | int64 | 是 | 部门 ID |
+| name | string | 是 | 名称 |
+| sort | int64 | 否 | 排序值 |
+| deptType | string | 否 | `NORMAL` 或 `DEV` |
+| remark | string | 否 | 备注 |
+| status | int64 | 否 | 状态 |
+
+#### POST /api/dept/delete — 删除部门
+
+存在子部门时无法删除。
+
+| 字段 | 类型 | 必填 | 说明 |
+|------|------|------|------|
+| id | int64 | 是 | 部门 ID |
+
+#### POST /api/dept/tree — 部门树
+
+无请求参数。返回完整的部门树形结构(含 `children` 嵌套和 `deptType`)。
+
+### 权限管理
+
+#### POST /api/perm/list — 权限列表
+
+| 字段 | 类型 | 必填 | 说明 |
+|------|------|------|------|
+| productCode | string | 是 | 产品编码 |
+| page | int64 | 否 | 页码 |
+| pageSize | int64 | 否 | 每页条数 |
+
+**响应 data:** `{"total": N, "list": [PermItem...]}`
+
+### 角色管理(超级管理员 或 产品管理员)
+
+#### POST /api/role/create — 创建角色
+
+| 字段 | 类型 | 必填 | 说明 |
+|------|------|------|------|
+| productCode | string | 是 | 所属产品编码 |
+| name | string | 是 | 角色名(产品内唯一) |
+| remark | string | 否 | 备注 |
+| permsLevel | int64 | 是 | 权限等级 |
+
+**响应 data:** `{"id": 1}`
+
+#### POST /api/role/update — 更新角色
+
+| 字段 | 类型 | 必填 | 说明 |
+|------|------|------|------|
+| id | int64 | 是 | 角色 ID |
+| name | string | 是 | 角色名 |
+| remark | string | 否 | 备注 |
+| permsLevel | int64 | 是 | 权限等级 |
+| status | int64 | 否 | 状态 |
+
+#### POST /api/role/delete — 删除角色
+
+级联删除角色关联的权限绑定和用户绑定。
+
+| 字段 | 类型 | 必填 | 说明 |
+|------|------|------|------|
+| id | int64 | 是 | 角色 ID |
+
+#### POST /api/role/list — 角色列表
+
+| 字段 | 类型 | 必填 | 说明 |
+|------|------|------|------|
+| productCode | string | 是 | 产品编码 |
+| page | int64 | 否 | 页码 |
+| pageSize | int64 | 否 | 每页条数 |
+
+#### POST /api/role/detail — 角色详情
+
+返回角色信息及绑定的权限 ID 列表(`permIds`)。
+
+| 字段 | 类型 | 必填 | 说明 |
+|------|------|------|------|
+| id | int64 | 是 | 角色 ID |
+
+#### POST /api/role/bindPerms — 绑定角色权限
+
+全量替换该角色的权限。
+
+| 字段 | 类型 | 必填 | 说明 |
+|------|------|------|------|
+| roleId | int64 | 是 | 角色 ID |
+| permIds | []int64 | 是 | 权限 ID 列表(空数组清空绑定) |
+
+### 用户管理
+
+#### POST /api/user/create — 创建用户(超级管理员 或 产品管理员)
+
+| 字段 | 类型 | 必填 | 说明 |
+|------|------|------|------|
+| username | string | 是 | 登录名(唯一) |
+| password | string | 是 | 密码(6-72 字符) |
+| nickname | string | 否 | 昵称 |
+| email | string | 否 | 邮箱(需合法格式) |
+| phone | string | 否 | 手机号(7-15 位数字,可含 `+` 前缀) |
+| remark | string | 否 | 备注 |
+| deptId | int64 | 否 | 部门 ID |
+
+**响应 data:** `{"id": 1}`
+
+#### POST /api/user/update — 更新用户(仅本人 或 超级管理员)
+
+支持字段清空:传 `""` 清空字符串字段,传 `0` 清空 deptId,不传字段则不更新。
+
+| 字段 | 类型 | 必填 | 说明 |
+|------|------|------|------|
+| id | int64 | 是 | 用户 ID |
+| nickname | *string | 否 | 昵称 |
+| email | *string | 否 | 邮箱 |
+| phone | *string | 否 | 手机号 |
+| remark | *string | 否 | 备注 |
+| deptId | *int64 | 否 | 部门 ID(传 0 取消部门) |
+| status | int64 | 否 | 状态 |
+
+#### POST /api/user/list — 用户列表
+
+| 字段 | 类型 | 必填 | 说明 |
+|------|------|------|------|
+| productCode | string | 否 | 产品编码(传入则附带成员类型) |
+| page | int64 | 否 | 页码 |
+| pageSize | int64 | 否 | 每页条数 |
+
+#### POST /api/user/detail — 用户详情
+
+返回用户信息及绑定的角色 ID 列表(`roleIds`)。
+
+| 字段 | 类型 | 必填 | 说明 |
+|------|------|------|------|
+| id | int64 | 是 | 用户 ID |
+
+#### POST /api/user/bindRoles — 绑定用户角色(需管理权限)
+
+需通过 `CheckManageAccess` 权限检查。全量替换该用户的角色。
+
+| 字段 | 类型 | 必填 | 说明 |
+|------|------|------|------|
+| userId | int64 | 是 | 用户 ID |
+| roleIds | []int64 | 是 | 角色 ID 列表 |
+
+#### POST /api/user/setPerms — 设置用户权限覆盖(需管理权限)
+
+需通过 `CheckManageAccess` 权限检查。全量替换用户级别的 ALLOW/DENY 权限覆盖。
+
+| 字段 | 类型 | 必填 | 说明 |
+|------|------|------|------|
+| userId | int64 | 是 | 用户 ID |
+| perms | array | 是 | 权限覆盖列表 |
+| perms[].permId | int64 | 是 | 权限 ID |
+| perms[].effect | string | 是 | `ALLOW` 或 `DENY` |
+
+#### POST /api/user/updateStatus — 更新用户状态(需管理权限)
+
+需通过 `CheckManageAccess` 权限检查。不允许冻结自己和超级管理员。
+
+| 字段 | 类型 | 必填 | 说明 |
+|------|------|------|------|
+| id | int64 | 是 | 用户 ID |
+| status | int64 | 是 | 1=正常 2=冻结 |
+
+### 产品成员管理(需管理权限)
+
+#### POST /api/member/add — 添加产品成员
+
+需通过 `CheckManageAccess` + `CheckMemberTypeAssignment` 权限检查。不可分配与自己同级或更高级的成员类型。
+
+| 字段 | 类型 | 必填 | 说明 |
+|------|------|------|------|
+| productCode | string | 是 | 产品编码 |
+| userId | int64 | 是 | 用户 ID |
+| memberType | string | 是 | `ADMIN` / `DEVELOPER` / `MEMBER` |
+
+**响应 data:** `{"id": 1}`
+
+#### POST /api/member/update — 更新成员
+
+需通过 `CheckManageAccess` + `CheckMemberTypeAssignment` 权限检查。
+
+| 字段 | 类型 | 必填 | 说明 |
+|------|------|------|------|
+| id | int64 | 是 | 成员记录 ID |
+| memberType | string | 是 | 成员类型 |
+| status | int64 | 否 | 状态 |
+
+#### POST /api/member/remove — 移除成员
+
+需通过 `CheckManageAccess` 权限检查。级联清理该成员在该产品下的角色绑定和权限覆盖。
+
+| 字段 | 类型 | 必填 | 说明 |
+|------|------|------|------|
+| id | int64 | 是 | 成员记录 ID |
+
+#### POST /api/member/list — 成员列表
+
+| 字段 | 类型 | 必填 | 说明 |
+|------|------|------|------|
+| productCode | string | 是 | 产品编码 |
+| page | int64 | 否 | 页码 |
+| pageSize | int64 | 否 | 每页条数 |
+
+**响应 data:** `{"total": N, "list": [MemberItem...]}`
+
+---
+
+## gRPC 接口文档
+
+gRPC 服务定义见 `pb/perm.proto`,默认监听 `:10002`。
+
+| 方法 | 说明 | 使用场景 |
+|------|------|----------|
+| `SyncPermissions` | 同步产品权限列表 | 产品启动时调用 |
+| `Login` | 用户登录 | 产品后端代理用户登录 |
+| `RefreshToken` | 刷新令牌 | accessToken 过期续期 |
+| `VerifyToken` | 验证令牌 | 产品后端验证用户 token(可选,推荐本地 JWT 验证) |
+| `GetUserPerms` | 获取用户权限 | 实时查询用户最新权限 |
+
+所有 gRPC 错误使用标准 `status.Error(codes.Xxx, msg)` 格式。
+
+Go 项目可直接引用 `permclient` 包:
+
+```go
+import "perms-system-server/permclient"
+
+client, err := permclient.NewPermClient("perm-system:10002")
+
+resp, err := client.SyncPermissions(ctx, &pb.SyncPermissionsReq{...})
+resp, err := client.Login(ctx, &pb.LoginReq{...})
+resp, err := client.VerifyToken(ctx, &pb.VerifyTokenReq{AccessToken: token})
+```
+
+---
+
+## 项目结构
+
+```
+server/
+├── perm.go                           # 入口(同时启动 HTTP + gRPC)
+├── perm.api                          # HTTP API 定义(go-zero 标准)
+├── perm.sql                          # 数据库 DDL
+├── gen-api.sh                        # API 代码生成脚本
+├── gen-model.sh                      # Model 代码生成脚本
+├── run-test.sh                       # 测试运行脚本
+├── test-design.md                    # 测试用例设计文档(433 TC)
+├── test-report.md                    # 测试报告
+├── etc/
+│   └── perm-api.yaml                 # 服务配置
+├── pb/
+│   ├── perm.proto                    # gRPC 接口定义
+│   ├── perm.pb.go                    # protoc 生成
+│   └── perm_grpc.pb.go              # protoc 生成
+├── permclient/
+│   └── permclient.go                 # gRPC 客户端 SDK
+├── cli/goctl/                        # 自定义 goctl 模板
+│   ├── api/handler.tpl               # Handler 模板(含参数校验 → 400 处理)
+│   └── model/                        # Model 模板(含 *WithTx 事务查询方法)
+└── internal/
+    ├── config/config.go              # 配置结构体
+    ├── consts/consts.go              # 全局常量(Status/MemberType/DeptType/TokenType 等)
+    ├── response/response.go          # 统一响应 + 错误码
+    ├── util/                         # 工具函数(分页、校验、JWT)
+    ├── loaders/
+    │   └── userDetailsLoader.go      # 用户详情加载器(Redis 缓存 + DB 回源)
+    ├── middleware/
+    │   └── jwtauthMiddleware.go      # JWT 鉴权中间件(token 验证 + 状态检查 + UserDetails 注入)
+    ├── svc/serviceContext.go         # 依赖注入容器
+    ├── server/permserver.go          # gRPC 服务实现
+    ├── types/types.go                # 请求/响应结构体(goctl 生成)
+    ├── handler/                      # HTTP handler(按模块分组,goctl 生成)
+    │   ├── routes.go
+    │   ├── pub/                      # 公开接口
+    │   ├── auth/                     # 认证接口
+    │   ├── product/                  # 产品管理
+    │   ├── dept/                     # 部门管理
+    │   ├── perm/                     # 权限管理
+    │   ├── role/                     # 角色管理
+    │   ├── user/                     # 用户管理
+    │   └── member/                   # 产品成员
+    ├── logic/                        # 业务逻辑层(按模块分组)
+    │   ├── auth/
+    │   │   ├── jwt.go                # JWT 生成/解析(含 tokenType 区分)
+    │   │   ├── perms.go              # 权限计算核心逻辑
+    │   │   └── access.go             # 集中式操作权限控制(超管/管理员/部门/级别检查)
+    │   ├── pub/                      # 登录、刷新、同步
+    │   ├── product/                  # 产品 CRUD
+    │   ├── dept/                     # 部门 CRUD + 树
+    │   ├── perm/                     # 权限列表
+    │   ├── role/                     # 角色 CRUD + 绑定权限 + 级联删除
+    │   ├── user/                     # 用户 CRUD + 绑定角色/权限
+    │   └── member/                   # 成员管理 + 级联清理
+    └── model/                        # 数据模型层(每张表独立目录)
+        ├── models.go                 # Models 聚合结构体(统一初始化所有 Model 实例)
+        ├── product/                  # sys_product
+        ├── dept/                     # sys_dept
+        ├── perm/                     # sys_perm
+        ├── role/                     # sys_role
+        ├── roleperm/                 # sys_role_perm
+        ├── user/                     # sys_user
+        ├── userperm/                 # sys_user_perm
+        ├── userrole/                 # sys_user_role
+        └── productmember/            # sys_product_member
+```
+
+---
+
+## 配置说明
+
+配置文件位于 `etc/perm-api.yaml`:
+
+| 配置项 | 说明 | 默认值 |
+|--------|------|--------|
+| `Host` / `Port` | HTTP 监听地址 | `0.0.0.0:10001` |
+| `RpcServerConf.ListenOn` | gRPC 监听地址 | `0.0.0.0:10002` |
+| `MySQL.DataSource` | MySQL 连接串 | — |
+| `CacheRedis[].Host` | Redis 地址 | `127.0.0.1:6379` |
+| `Auth.AccessSecret` | JWT accessToken 签名密钥 | — |
+| `Auth.AccessExpire` | accessToken 有效期(秒) | `7200`(2h) |
+| `Auth.RefreshSecret` | JWT refreshToken 签名密钥 | — |
+| `Auth.RefreshExpire` | refreshToken 有效期(秒) | `604800`(7d) |
+
+> **生产环境部署前,务必修改 `AccessSecret` 和 `RefreshSecret` 为安全的随机字符串,并确保产品后端的本地验证密钥与 `AccessSecret` 一致。**
+
+---
+
+## 测试
+
+### 测试概览
+
+| 指标 | 数值 |
+|------|------|
+| 测试用例总数 | 499 |
+| 已覆盖 TC | 498(99.8%) |
+| 测试函数 | 662 |
+| 测试子用例 | 744 |
+| 测试包 | 23 |
+| 通过率 | 99.7% |
+
+测试覆盖范围:
+
+- **REST API 测试** — 全部 HTTP 接口的正常路径、参数边界、错误路径
+- **gRPC 接口测试** — 全部 5 个 RPC 方法的正常/异常场景
+- **中间件测试** — JWT 鉴权、冻结账号拦截、UserDetails 注入、统一响应
+- **访问控制测试** — access.go 全部函数的正面/负面测试
+- **UserDetailsLoader 测试** — 缓存命中/miss、Clean/Del/BatchDel/CleanByProduct、权限计算
+- **Logic 层访问控制负面测试** — 非超管/非管理员操作被拒绝
+- **Model 层测试** — 9 个 Model 的 CRUD、事务、批量操作、唯一索引方法、自定义方法
+- **Logic 层测试** — 业务逻辑单元测试(含 mock 测试)
+- **工具函数测试** — 分页、邮箱/手机校验、JWT 生成解析
+
+### 测试文档
+
+| 文件 | 说明 |
+|------|------|
+| `test-design.md` | 测试用例设计文档(499 个 TC,含测试场景、输入数据、预期结果) |
+| `test-report.md` | 测试执行报告(含各包耗时、TC 明细、源码审计结果) |
+
+### 运行测试
+
+项目提供 `run-test.sh` 脚本,自动完成以下流程:
+
+1. **连通性检查** — 验证 MySQL 和 Redis 是否可达,不可达则提前终止
+2. **编译检查** — 先执行 `go build ./...`,编译失败则不跑测试
+3. **执行测试** — 运行 `go test`,结束后汇总通过/失败的包数和耗时
+
+```bash
+# 运行全部测试
+./run-test.sh
+
+# 运行指定包的测试
+./run-test.sh ./internal/logic/auth/...
+./run-test.sh ./internal/model/...
+
+# 查看帮助
+./run-test.sh -h
+```
+
+**环境变量**:
+
+| 变量 | 默认值 | 说明 |
+|------|--------|------|
+| `TEST_VERBOSE` | 空(关闭) | 设为 `1` 开启详细输出(`-v`) |
+| `TEST_RUN` | 空(全部) | 只运行匹配的测试函数名 |
+| `TEST_TIMEOUT` | `600s` | 单个包的超时时间 |
+| `TEST_COUNT` | `1` | 每个测试运行次数 |
+
+```bash
+# 组合使用示例
+TEST_VERBOSE=1 TEST_RUN="TestLogin" ./run-test.sh ./internal/logic/pub/...
+```
+
+### 测试文件组织
+
+测试文件遵循 Go 标准命名,与被测文件同目录:
+
+```
+internal/logic/auth/
+├── jwt.go                    # 源码
+├── jwt_test.go               # 测试
+├── perms.go                  # 源码
+├── perms_test.go             # 测试
+└── perms_mock_test.go        # Mock 测试
+
+internal/model/product/
+├── sysProductModel.go        # 自定义 Model
+├── sysProductModel_gen.go    # 生成的 Model
+└── sysProductModel_test.go   # 测试
+```
+
+---
+
+## 开发指南
+
+本项目严格遵循 go-zero 标准开发流程:
+
+```mermaid
+flowchart LR
+    A[编辑 perm.api] --> B["./gen-api.sh"]
+    B --> C[生成 handler + types + routes]
+    C --> D[在 logic 中填充业务逻辑]
+    D --> E[go build 验证]
+
+    F[编辑 perm.sql] --> G["./gen-model.sh"]
+    G --> H[生成 model 到对应子目录]
+    H --> I[在 sysXxxModel.go 中扩展自定义查询]
+```
+
+### 新增 HTTP 接口
+
+```bash
+# 1. 在 perm.api 中添加类型和路由定义
+# 2. 重新生成(已有 logic 文件不会被覆盖)
+./gen-api.sh
+# 3. 在生成的 logic 文件中填写业务逻辑
+```
+
+### 新增数据表 Model
+
+```bash
+# 1. 在 perm.sql 中添加建表语句
+# 2. 生成 model(使用自定义模板,包含 *WithTx 事务查询方法)
+./gen-model.sh
+# 3. 在生成的 sysXxxModel.go 中添加自定义查询方法
+```
+
+### 自定义 goctl 模板
+
+项目使用自定义模板(`cli/goctl/`),主要增强:
+
+- **Handler 模板** — 参数解析错误自动包装为 `400 Bad Request`(而非 500)
+- **Model 模板** — 所有 `FindOne` 和 `FindOneByXxx` 方法均提供 `*WithTx` 事务变体,确保事务内查询使用 session 连接
+
+### 代码规范
+
+- 所有魔数已替换为 `internal/consts/consts.go` 中的命名常量
+- 自定义 SQL 中的 MySQL 关键字统一使用大写
+- 多步写入操作均在 `TransactCtx` 中,事务内查询使用 `*WithTx` 方法
+- 删除角色/移除成员时有完整的级联清理
+- 列表接口统一 `NormalizePage`,`pageSize` 上限 100
+- 输入校验涵盖邮箱/手机格式、密码长度、状态值合法性、实体存在性
+- JWT token 通过 `tokenType` 字段区分 `access`/`refresh`,防止混用
+- 管理类接口统一通过 `access.go` 进行操作权限控制
+- 用户完整信息通过 `UserDetailsLoader` 集中加载,Redis 缓存 + 主动失效
+- 所有写操作均触发对应的缓存失效(`Clean`/`Del`/`BatchDel`/`CleanByProduct`)
+- `refreshToken` 不重新签发,具有固定有效期

+ 12 - 0
cli/goctl/api/config.tpl

@@ -0,0 +1,12 @@
+// Code scaffolded by goctl. Safe to edit.
+// goctl {{.version}}
+
+package config
+
+import {{.authImport}}
+
+type Config struct {
+	rest.RestConf
+	{{.auth}}
+	{{.jwtTrans}}
+}

+ 20 - 0
cli/goctl/api/context.tpl

@@ -0,0 +1,20 @@
+// Code scaffolded by goctl. Safe to edit.
+// goctl {{.version}}
+
+package svc
+
+import (
+	{{.configImport}}
+)
+
+type ServiceContext struct {
+	Config {{.config}}
+	{{.middleware}}
+}
+
+func NewServiceContext(c {{.config}}) *ServiceContext {
+	return &ServiceContext{
+		Config: c,
+		{{.middlewareAssignment}}
+	}
+}

+ 3 - 0
cli/goctl/api/etc.tpl

@@ -0,0 +1,3 @@
+Name: {{.serviceName}}
+Host: {{.host}}
+Port: {{.port}}

+ 31 - 0
cli/goctl/api/handler.tpl

@@ -0,0 +1,31 @@
+// Code scaffolded by goctl. Safe to edit.
+// goctl {{.version}}
+
+package {{.PkgName}}
+
+import (
+	"net/http"
+
+	"github.com/zeromicro/go-zero/rest/httpx"
+	{{if .HasRequest}}"perms-system-server/internal/response"
+	{{end}}{{.ImportPackages}}
+)
+
+{{if .HasDoc}}{{.Doc}}{{end}}
+func {{.HandlerName}}(svcCtx *svc.ServiceContext) http.HandlerFunc {
+	return func(w http.ResponseWriter, r *http.Request) {
+		{{if .HasRequest}}var req types.{{.RequestType}}
+		if err := httpx.Parse(r, &req); err != nil {
+			httpx.ErrorCtx(r.Context(), w, response.ErrBadRequest(err.Error()))
+			return
+		}
+
+		{{end}}l := {{.LogicName}}.New{{.LogicType}}(r.Context(), svcCtx)
+		{{if .HasResp}}resp, {{end}}err := l.{{.Call}}({{if .HasRequest}}&req{{end}})
+		if err != nil {
+			httpx.ErrorCtx(r.Context(), w, err)
+		} else {
+			{{if .HasResp}}httpx.OkJsonCtx(r.Context(), w, resp){{else}}httpx.Ok(w){{end}}
+		}
+	}
+}

+ 84 - 0
cli/goctl/api/handler_test.tpl

@@ -0,0 +1,84 @@
+// Code scaffolded by goctl. Safe to edit.
+// goctl {{.version}}
+
+package {{.PkgName}}
+
+import (
+	"bytes"
+	{{if .HasRequest}}"encoding/json"{{end}}
+	"net/http"
+	"net/http/httptest"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+	{{.ImportPackages}}
+)
+
+{{if .HasDoc}}{{.Doc}}{{end}}
+func Test{{.HandlerName}}(t *testing.T) {
+	// new service context
+	c := config.Config{}
+	svcCtx := svc.NewServiceContext(c)
+    // init mock service context here
+
+	tests := []struct {
+		name       string
+		reqBody    interface{}
+		wantStatus int
+		wantResp   string
+		setupMocks func()
+	}{
+		{
+			name:    "invalid request body",
+			reqBody: "invalid",
+			wantStatus: http.StatusBadRequest,
+			wantResp:   "unsupported type", // Adjust based on actual error response
+			setupMocks: func() {
+				// No setup needed for this test case
+			},
+		},
+		{
+			name: "handler error",
+			{{if .HasRequest}}reqBody: types.{{.RequestType}}{
+				//TODO: add fields here
+			},
+			{{end}}wantStatus: http.StatusBadRequest,
+			wantResp:  "error", // Adjust based on actual error response
+			setupMocks: func() {
+				// Mock login logic to return an error
+			},
+		},
+		{
+			name: "handler successful",
+			{{if .HasRequest}}reqBody: types.{{.RequestType}}{
+				//TODO: add fields here
+			},
+			{{end}}wantStatus: http.StatusOK,
+			wantResp:   `{"code":0,"msg":"success","data":{}}`, // Adjust based on actual success response
+			setupMocks: func() {
+				// Mock login logic to return success
+			},
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			tt.setupMocks()
+			var reqBody []byte
+			{{if .HasRequest}}var err error
+			reqBody, err = json.Marshal(tt.reqBody)
+			require.NoError(t, err){{end}}
+			req, err := http.NewRequest("POST", "/ut", bytes.NewBuffer(reqBody))
+			require.NoError(t, err)
+			req.Header.Set("Content-Type", "application/json")
+
+			rr := httptest.NewRecorder()
+			handler := {{.HandlerName}}(svcCtx)
+			handler.ServeHTTP(rr, req)
+			t.Log(rr.Body.String())
+			assert.Equal(t, tt.wantStatus, rr.Code)
+			assert.Contains(t, rr.Body.String(), tt.wantResp)
+		})
+	}
+}

+ 116 - 0
cli/goctl/api/integration_test.tpl

@@ -0,0 +1,116 @@
+// Code scaffolded by goctl. Safe to edit.
+// goctl {{.version}}
+
+package main
+
+import (
+	"net/http"
+	"net/http/httptest"
+	"testing"
+	"time"
+
+	"{{.projectPkg}}/internal/config"
+	"{{.projectPkg}}/internal/handler"
+	"{{.projectPkg}}/internal/svc"
+
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+	"github.com/zeromicro/go-zero/rest"
+)
+
+func TestMain(m *testing.M) {
+	// TODO: Add setup/teardown logic here if needed
+	m.Run()
+}
+
+func TestServerIntegration(t *testing.T) {
+	// Create test server
+	c := config.Config{
+		RestConf: rest.RestConf{
+			Host: "127.0.0.1",
+			Port: 0, // Use random available port
+		},
+	}
+
+	server := rest.MustNewServer(c.RestConf)
+	defer server.Stop()
+
+	ctx := svc.NewServiceContext(c)
+	handler.RegisterHandlers(server, ctx)
+
+	// Create serverless wrapper for testing
+	serverless, err := rest.NewServerless(server)
+	require.NoError(t, err)
+
+	tests := []struct {
+		name           string
+		method         string
+		path           string
+		body           string
+		expectedStatus int
+		setup          func()
+	}{
+		{
+			name:           "health check",
+			method:         http.MethodGet,
+			path:           "/health",
+			expectedStatus: http.StatusNotFound, // Adjust based on actual routes
+			setup:          func() {},
+		},
+		{{if .hasRoutes}}{{range .routes}}{
+			name:           "{{.Method}} {{.Path}}",
+			method:         "{{.Method}}",
+			path:           "{{.Path}}",
+			expectedStatus: http.StatusOK, // TODO: Adjust expected status
+			setup:          func() {
+				// TODO: Add setup logic for this endpoint
+			},
+		},
+		{{end}}{{end}}{
+			name:           "not found route",
+			method:         http.MethodGet,
+			path:           "/nonexistent",
+			expectedStatus: http.StatusNotFound,
+			setup:          func() {},
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			tt.setup()
+
+			req, err := http.NewRequest(tt.method, tt.path, nil)
+			require.NoError(t, err)
+
+			rr := httptest.NewRecorder()
+			serverless.Serve(rr, req)
+
+			assert.Equal(t, tt.expectedStatus, rr.Code)
+
+			// TODO: Add response body assertions
+			t.Logf("Response: %s", rr.Body.String())
+		})
+	}
+}
+
+func TestServerLifecycle(t *testing.T) {
+	c := config.Config{
+		RestConf: rest.RestConf{
+			Host: "127.0.0.1",
+			Port: 0,
+		},
+	}
+
+	server := rest.MustNewServer(c.RestConf)
+
+	// Test server can start and stop without errors
+	ctx := svc.NewServiceContext(c)
+	handler.RegisterHandlers(server, ctx)
+
+	// In a real integration test, you might start the server in a goroutine
+	// and test actual HTTP requests, but for scaffolding we keep it simple
+	server.Stop()
+
+	// TODO: Add more lifecycle tests as needed
+	assert.True(t, true, "Server lifecycle test passed")
+}

+ 29 - 0
cli/goctl/api/logic.tpl

@@ -0,0 +1,29 @@
+// Code scaffolded by goctl. Safe to edit.
+// goctl {{.version}}
+
+package {{.pkgName}}
+
+import (
+	{{.imports}}
+)
+
+type {{.logic}} struct {
+	logx.Logger
+	ctx    context.Context
+	svcCtx *svc.ServiceContext
+}
+
+{{if .hasDoc}}{{.doc}}{{end}}
+func New{{.logic}}(ctx context.Context, svcCtx *svc.ServiceContext) *{{.logic}} {
+	return &{{.logic}}{
+		Logger: logx.WithContext(ctx),
+		ctx:    ctx,
+		svcCtx: svcCtx,
+	}
+}
+
+func (l *{{.logic}}) {{.function}}({{.request}}) {{.responseType}} {
+	// todo: add your logic here and delete this line
+
+	{{.returnString}}
+}

+ 72 - 0
cli/goctl/api/logic_test.tpl

@@ -0,0 +1,72 @@
+// Code scaffolded by goctl. Safe to edit.
+// goctl {{.version}}
+
+package {{.pkgName}}
+
+import (
+    "context"
+    "testing"
+
+    {{.imports}}
+    "github.com/stretchr/testify/assert"
+    "github.com/stretchr/testify/require"
+)
+
+func Test{{.logic}}_{{.function}}(t *testing.T) {
+    c := config.Config{}
+    mockSvcCtx := svc.NewServiceContext(c)
+    // init mock service context here
+
+    tests := []struct {
+        name       string
+        ctx        context.Context
+        setupMocks func()
+        {{if .hasRequest}}req        *{{.requestType}}{{end}}
+        wantErr    bool
+        checkResp  func{{if .hasResponse}}{{.responseType}}{{else}}(err error){{end}}
+    }{
+        {
+            name: "response error",
+            ctx:  context.Background(),
+            setupMocks: func() {
+                // mock data for this test case
+            },
+            {{if .hasRequest}}req:  &{{.requestType}}{
+                // TODO: init your request here
+            },{{end}}
+            wantErr: true,
+            checkResp: func{{if .hasResponse}}{{.responseType}}{{else}}(err error){{end}} {
+                // TODO: Add your check logic here
+            },
+        },
+        {
+            name: "successful",
+            ctx:  context.Background(),
+            setupMocks: func() {
+                // Mock data for this test case
+            },
+            {{if .hasRequest}}req:  &{{.requestType}}{
+                // TODO: init your request here
+            },{{end}}
+            wantErr: false,
+            checkResp: func{{if .hasResponse}}{{.responseType}}{{else}}(err error){{end}} {
+                // TODO: Add your check logic here
+            },
+        },
+    }
+
+    for _, tt := range tests {
+        t.Run(tt.name, func(t *testing.T) {
+            tt.setupMocks()
+            l := New{{.logic}}(tt.ctx, mockSvcCtx)
+            {{if .hasResponse}}resp, {{end}}err := l.{{.function}}({{if .hasRequest}}tt.req{{end}})
+            if tt.wantErr {
+                assert.Error(t, err)
+            } else {
+                require.NoError(t, err)
+                {{if .hasResponse}}assert.NotNil(t, resp){{end}}
+            }
+            tt.checkResp({{if .hasResponse}}resp, {{end}}err)
+        })
+    }
+}

+ 29 - 0
cli/goctl/api/main.tpl

@@ -0,0 +1,29 @@
+// Code scaffolded by goctl. Safe to edit.
+// goctl {{.version}}
+
+package main
+
+import (
+	"flag"
+	"fmt"
+
+	{{.importPackages}}
+)
+
+var configFile = flag.String("f", "etc/{{.serviceName}}.yaml", "the config file")
+
+func main() {
+	flag.Parse()
+
+	var c config.Config
+	conf.MustLoad(*configFile, &c)
+
+	server := rest.MustNewServer(c.RestConf)
+	defer server.Stop()
+
+	ctx := svc.NewServiceContext(c)
+	handler.RegisterHandlers(server, ctx)
+
+	fmt.Printf("Starting server at %s:%d...\n", c.Host, c.Port)
+	server.Start()
+}

+ 22 - 0
cli/goctl/api/middleware.tpl

@@ -0,0 +1,22 @@
+// Code scaffolded by goctl. Safe to edit.
+// goctl {{.version}}
+
+package middleware
+
+import "net/http"
+
+type {{.name}} struct {
+}
+
+func New{{.name}}() *{{.name}} {
+	return &{{.name}}{}
+}
+
+func (m *{{.name}})Handle(next http.HandlerFunc) http.HandlerFunc {
+	return func(w http.ResponseWriter, r *http.Request) {
+		// TODO generate middleware implement function, delete after code implementation
+
+		// Passthrough to next handler if need
+		next(w, r)
+	}
+}

+ 4 - 0
cli/goctl/api/route-addition.tpl

@@ -0,0 +1,4 @@
+
+	server.AddRoutes(
+		{{.routes}} {{.jwt}}{{.signature}} {{.prefix}} {{.timeout}} {{.maxBytes}} {{.sse}}
+	)

+ 15 - 0
cli/goctl/api/routes.tpl

@@ -0,0 +1,15 @@
+// Code generated by goctl. DO NOT EDIT.
+// goctl {{.version}}
+
+package handler
+
+import (
+	"net/http"{{if .hasTimeout}}
+	"time"{{end}}
+
+	{{.importPackages}}
+)
+
+func RegisterHandlers(server *rest.Server, serverCtx *svc.ServiceContext) {
+	{{.routesAdditions}}
+}

+ 68 - 0
cli/goctl/api/sse_handler.tpl

@@ -0,0 +1,68 @@
+// Code scaffolded by goctl. Safe to edit.
+// goctl {{.version}}
+
+package {{.PkgName}}
+
+import (
+    "encoding/json"
+    "fmt"
+	"net/http"
+
+    "github.com/zeromicro/go-zero/core/logc"
+    "github.com/zeromicro/go-zero/core/threading"
+	{{if .HasRequest}}"github.com/zeromicro/go-zero/rest/httpx"{{end}}
+	{{.ImportPackages}}
+)
+
+{{if .HasDoc}}{{.Doc}}{{end}}
+func {{.HandlerName}}(svcCtx *svc.ServiceContext) http.HandlerFunc {
+	return func(w http.ResponseWriter, r *http.Request) {
+		{{if .HasRequest}}var req types.{{.RequestType}}
+		if err := httpx.Parse(r, &req); err != nil {
+			httpx.ErrorCtx(r.Context(), w, err)
+			return
+		}
+
+		{{end}}// Buffer size of 16 is chosen as a reasonable default to balance throughput and memory usage.
+		// You can change this based on your application's needs.
+		// if your go-zero version less than 1.8.1, you need to add 3 lines below.
+        // w.Header().Set("Content-Type", "text/event-stream")
+        // w.Header().Set("Cache-Control", "no-cache")
+        // w.Header().Set("Connection", "keep-alive")
+		client := make(chan {{.ResponseType}}, 16)
+
+        l := {{.LogicName}}.New{{.LogicType}}(r.Context(), svcCtx)
+        threading.GoSafeCtx(r.Context(), func() {
+            defer close(client)
+            err := l.{{.Call}}({{if .HasRequest}}&req, {{end}}client)
+            if err != nil {
+                logc.Errorw(r.Context(), "{{.HandlerName}}", logc.Field("error", err))
+                return
+            }
+        })
+
+        for {
+            select {
+            case data, ok := <-client:
+                if !ok {
+                    return
+                }
+                output, err := json.Marshal(data)
+                if err != nil {
+                    logc.Errorw(r.Context(), "{{.HandlerName}}", logc.Field("error", err))
+                    continue
+                }
+
+                if _, err := fmt.Fprintf(w, "data: %s\n\n", string(output)); err != nil {
+                    logc.Errorw(r.Context(), "{{.HandlerName}}", logc.Field("error", err))
+                    return
+                }
+               if flusher, ok := w.(http.Flusher); ok {
+                   flusher.Flush()
+               }
+            case <-r.Context().Done():
+                return
+            }
+        }
+	}
+}

+ 29 - 0
cli/goctl/api/sse_logic.tpl

@@ -0,0 +1,29 @@
+// Code scaffolded by goctl. Safe to edit.
+// goctl {{.version}}
+
+package {{.pkgName}}
+
+import (
+	{{.imports}}
+)
+
+type {{.logic}} struct {
+	logx.Logger
+	ctx    context.Context
+	svcCtx *svc.ServiceContext
+}
+
+{{if .hasDoc}}{{.doc}}{{end}}
+func New{{.logic}}(ctx context.Context, svcCtx *svc.ServiceContext) *{{.logic}} {
+	return &{{.logic}}{
+		Logger: logx.WithContext(ctx),
+		ctx:    ctx,
+		svcCtx: svcCtx,
+	}
+}
+
+func (l *{{.logic}}) {{.function}}({{.request}}) {{.responseType}} {
+    // todo: add your logic here and delete this line
+
+	{{.returnString}}
+}

+ 60 - 0
cli/goctl/api/svc_test.tpl

@@ -0,0 +1,60 @@
+// Code scaffolded by goctl. Safe to edit.
+// goctl {{.version}}
+
+package svc
+
+import (
+	"testing"
+
+	"{{.projectPkg}}/internal/config"
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+)
+
+func TestNewServiceContext(t *testing.T) {
+	tests := []struct {
+		name   string
+		config config.Config
+		setup  func() config.Config
+	}{
+		{
+			name: "default config",
+			setup: func() config.Config {
+				return config.Config{}
+			},
+		},
+		{
+			name: "valid config", 
+			setup: func() config.Config {
+				return config.Config{
+					// TODO: Add valid config values here
+				}
+			},
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			c := tt.setup()
+			svcCtx := NewServiceContext(c)
+
+			// Basic assertions
+			require.NotNil(t, svcCtx)
+			assert.Equal(t, c, svcCtx.Config)
+
+			// TODO: Add additional assertions for middleware and dependencies
+		})
+	}
+}
+
+func TestServiceContext_Initialization(t *testing.T) {
+	c := config.Config{}
+	svcCtx := NewServiceContext(c)
+
+	// Verify service context is properly initialized
+	assert.NotNil(t, svcCtx)
+	assert.Equal(t, c, svcCtx.Config)
+
+	// TODO: Add tests for middleware initialization if any
+	// TODO: Add tests for external dependencies if any
+}

+ 24 - 0
cli/goctl/api/template.tpl

@@ -0,0 +1,24 @@
+syntax = "v1"
+
+info (
+	title: // TODO: add title
+	desc: // TODO: add description
+	author: "{{.gitUser}}"
+	email: "{{.gitEmail}}"
+)
+
+type request {
+	// TODO: add members here and delete this comment
+}
+
+type response {
+	// TODO: add members here and delete this comment
+}
+
+service {{.serviceName}} {
+	@handler GetUser // TODO: set handler name and delete this comment
+	get /users/id/:userId(request) returns(response)
+
+	@handler CreateUser // TODO: set handler name and delete this comment
+	post /users/create(request)
+}

+ 8 - 0
cli/goctl/api/types.tpl

@@ -0,0 +1,8 @@
+// Code generated by goctl. DO NOT EDIT.
+// goctl {{.version}}
+
+package types{{if .containsTime}}
+import (
+	"time"
+){{end}}
+{{.types}}

+ 36 - 0
cli/goctl/docker/docker.tpl

@@ -0,0 +1,36 @@
+FROM golang:{{.Version}}alpine AS builder
+
+LABEL stage=gobuilder
+
+ENV CGO_ENABLED 0
+
+{{if .HasTimezone}}
+RUN apk update --no-cache && apk add --no-cache tzdata
+{{- end}}
+
+WORKDIR /build
+
+ADD go.mod .
+ADD go.sum .
+RUN go mod download
+COPY . .
+
+RUN go build -ldflags="-s -w" -o /app/{{.ExeFile}} {{.GoMainFrom}}
+
+
+FROM {{.BaseImage}}
+
+COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt
+{{if .HasTimezone -}}
+COPY --from=builder /usr/share/zoneinfo/{{.Timezone}} /usr/share/zoneinfo/{{.Timezone}}
+ENV TZ {{.Timezone}}
+{{end}}
+WORKDIR /app
+COPY --from=builder /app/{{.ExeFile}} /app/{{.ExeFile}}
+{{if .Argument -}}
+COPY {{.GoRelPath}}/etc /app/etc
+{{- end}}
+{{if .HasPort}}
+EXPOSE {{.Port}}
+{{end}}
+CMD ["./{{.ExeFile}}"{{.Argument}}]

+ 18 - 0
cli/goctl/gateway/etc.tpl

@@ -0,0 +1,18 @@
+Name: gateway-example # gateway name
+Host: localhost # gateway host
+Port: 8888 # gateway port
+Upstreams: # upstreams
+  - Grpc: # grpc upstream
+      Target: 0.0.0.0:8080 # grpc target,the direct grpc server address,for only one node
+#      Endpoints: [0.0.0.0:8080,192.168.120.1:8080] # grpc endpoints, the grpc server address list, for multiple nodes
+#      Etcd: # etcd config, if you want to use etcd to discover the grpc server address
+#        Hosts: [127.0.0.1:2378,127.0.0.1:2379] # etcd hosts
+#        Key: greet.grpc # the discovery key
+    # protoset mode
+    ProtoSets:
+      - hello.pb
+    # Mappings can also be written in proto options
+#    Mappings: # routes mapping
+#      - Method: get
+#        Path: /ping
+#        RpcPath: hello.Hello/Ping

+ 20 - 0
cli/goctl/gateway/main.tpl

@@ -0,0 +1,20 @@
+package main
+
+import (
+	"flag"
+
+	"github.com/zeromicro/go-zero/core/conf"
+	"github.com/zeromicro/go-zero/gateway"
+)
+
+var configFile = flag.String("f", "etc/gateway.yaml", "config file")
+
+func main() {
+	flag.Parse()
+
+	var c gateway.GatewayConf
+	conf.MustLoad(*configFile, &c)
+	gw := gateway.MustNewServer(c)
+	defer gw.Stop()
+	gw.Start()
+}

+ 117 - 0
cli/goctl/kube/deployment.tpl

@@ -0,0 +1,117 @@
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+  name: {{.Name}}
+  namespace: {{.Namespace}}
+  labels:
+    app: {{.Name}}
+spec:
+  replicas: {{.Replicas}}
+  revisionHistoryLimit: {{.Revisions}}
+  selector:
+    matchLabels:
+      app: {{.Name}}
+  template:
+    metadata:
+      labels:
+        app: {{.Name}}
+    spec:{{if .ServiceAccount}}
+      serviceAccountName: {{.ServiceAccount}}{{end}}
+      containers:
+      - name: {{.Name}}
+        image: {{.Image}}
+        {{if .ImagePullPolicy}}imagePullPolicy: {{.ImagePullPolicy}}
+        {{end}}ports:
+        - containerPort: {{.Port}}
+        readinessProbe:
+          tcpSocket:
+            port: {{.Port}}
+          initialDelaySeconds: 5
+          periodSeconds: 10
+        livenessProbe:
+          tcpSocket:
+            port: {{.Port}}
+          initialDelaySeconds: 15
+          periodSeconds: 20
+        resources:
+          requests:
+            cpu: {{.RequestCpu}}m
+            memory: {{.RequestMem}}Mi
+          limits:
+            cpu: {{.LimitCpu}}m
+            memory: {{.LimitMem}}Mi
+        volumeMounts:
+        - name: timezone
+          mountPath: /etc/localtime
+      {{if .Secret}}imagePullSecrets:
+      - name: {{.Secret}}
+      {{end}}volumes:
+        - name: timezone
+          hostPath:
+            path: /usr/share/zoneinfo/Asia/Shanghai
+
+---
+
+apiVersion: v1
+kind: Service
+metadata:
+  name: {{.Name}}-svc
+  namespace: {{.Namespace}}
+spec:
+  ports:
+  {{if .UseNodePort}}- nodePort: {{.NodePort}}
+    port: {{.Port}}
+    protocol: TCP
+    targetPort: {{.TargetPort}}
+  type: NodePort{{else}}- port: {{.Port}}
+    targetPort: {{.TargetPort}}{{end}}
+  selector:
+    app: {{.Name}}
+
+---
+
+apiVersion: autoscaling/v2
+kind: HorizontalPodAutoscaler
+metadata:
+  name: {{.Name}}-hpa-c
+  namespace: {{.Namespace}}
+  labels:
+    app: {{.Name}}-hpa-c
+spec:
+  scaleTargetRef:
+    apiVersion: apps/v1
+    kind: Deployment
+    name: {{.Name}}
+  minReplicas: {{.MinReplicas}}
+  maxReplicas: {{.MaxReplicas}}
+  metrics:
+  - type: Resource
+    resource:
+      name: cpu
+      target:
+        type: Utilization
+        averageUtilization: 80
+
+---
+
+apiVersion: autoscaling/v2beta2
+kind: HorizontalPodAutoscaler
+metadata:
+  name: {{.Name}}-hpa-m
+  namespace: {{.Namespace}}
+  labels:
+    app: {{.Name}}-hpa-m
+spec:
+  scaleTargetRef:
+    apiVersion: apps/v1
+    kind: Deployment
+    name: {{.Name}}
+  minReplicas: {{.MinReplicas}}
+  maxReplicas: {{.MaxReplicas}}
+  metrics:
+  - type: Resource
+    resource:
+      name: memory
+      target:
+        type: Utilization
+        averageUtilization: 80

+ 37 - 0
cli/goctl/kube/job.tpl

@@ -0,0 +1,37 @@
+apiVersion: batch/v1
+kind: CronJob
+metadata:
+  name: {{.Name}}
+  namespace: {{.Namespace}}
+spec:
+  successfulJobsHistoryLimit: {{.SuccessfulJobsHistoryLimit}}
+  schedule: "{{.Schedule}}"
+  jobTemplate:
+    spec:
+      template:
+        spec:{{if .ServiceAccount}}
+          serviceAccountName: {{.ServiceAccount}}{{end}}
+	      {{end}}containers:
+          - name: {{.Name}}
+            image: # todo image url
+            resources:
+              requests:
+                cpu: {{.RequestCpu}}m
+                memory: {{.RequestMem}}Mi
+              limits:
+                cpu: {{.LimitCpu}}m
+                memory: {{.LimitMem}}Mi
+            command:
+            - ./{{.ServiceName}}
+            - -f
+            - ./{{.Name}}.yaml
+            volumeMounts:
+            - name: timezone
+              mountPath: /etc/localtime
+          imagePullSecrets:
+          - name: # registry secret, if no, remove this
+          restartPolicy: OnFailure
+          volumes:
+          - name: timezone
+            hostPath:
+              path: /usr/share/zoneinfo/Asia/Shanghai

+ 25 - 0
cli/goctl/model/customized.tpl

@@ -0,0 +1,25 @@
+func (m *default{{.upperStartCamelObject}}Model) findListByPrimaryKeys(ctx context.Context, {{.lowerStartCamelPrimaryKey}}s []interface{}) ([]*{{.upperStartCamelObject}}, error) {
+	if len({{.lowerStartCamelPrimaryKey}}s) == 0 {
+		return []*{{.upperStartCamelObject}}{}, nil
+	}
+
+	placeholders := make([]string, len({{.lowerStartCamelPrimaryKey}}s))
+	args := make([]interface{}, len({{.lowerStartCamelPrimaryKey}}s))
+	for i, {{.lowerStartCamelPrimaryKey}} := range {{.lowerStartCamelPrimaryKey}}s {
+		{{if .postgreSql}}placeholders[i] = fmt.Sprintf("$%d", i+1){{else}}placeholders[i] = "?"{{end}}
+		args[i] = {{.lowerStartCamelPrimaryKey}}
+	}
+
+	var resp []*{{.upperStartCamelObject}}
+	query := fmt.Sprintf("SELECT %s FROM %s WHERE {{.originalPrimaryKey}} IN (%s)", {{.lowerStartCamelObject}}Rows, m.table, strings.Join(placeholders, ","))
+	err := {{if .withCache}}m.QueryRowsNoCacheCtx{{else}}m.conn.QueryRowsCtx{{end}}(ctx, &resp, query, args...)
+	if err != nil {
+		return nil, err
+	}
+
+	return resp, nil
+}
+
+func (m *default{{.upperStartCamelObject}}Model) getPrimaryKeyValue(data *{{.upperStartCamelObject}}) interface{} {
+	return data.{{.upperStartCamelPrimaryKey}}
+}

+ 100 - 0
cli/goctl/model/delete.tpl

@@ -0,0 +1,100 @@
+func (m *default{{.upperStartCamelObject}}Model) Delete(ctx context.Context, {{.lowerStartCamelPrimaryKey}} {{.dataType}}) error {
+	{{if .withCache}}{{if .containsIndexCache}}data, err:=m.FindOne(ctx, {{.lowerStartCamelPrimaryKey}})
+	if err!=nil{
+		return err
+	}
+
+{{end}}	{{.keys}}
+    _, err {{if .containsIndexCache}}={{else}}:={{end}} m.ExecCtx(ctx, func(ctx context.Context, conn sqlx.SqlConn) (result sql.Result, err error) {
+		query := fmt.Sprintf("DELETE FROM %s WHERE {{.originalPrimaryKey}} = {{if .postgreSql}}$1{{else}}?{{end}}", m.table)
+		return conn.ExecCtx(ctx, query, {{.lowerStartCamelPrimaryKey}})
+	}, {{.keyValues}}){{else}}query := fmt.Sprintf("DELETE FROM %s WHERE {{.originalPrimaryKey}} = {{if .postgreSql}}$1{{else}}?{{end}}", m.table)
+		_,err:=m.conn.ExecCtx(ctx, query, {{.lowerStartCamelPrimaryKey}}){{end}}
+	return err
+}
+
+func (m *default{{.upperStartCamelObject}}Model) DeleteWithTx(ctx context.Context, session sqlx.Session, {{.lowerStartCamelPrimaryKey}} {{.dataType}}) error {
+	{{if .withCache}}{{if .containsIndexCache}}data, err:=m.FindOne(ctx, {{.lowerStartCamelPrimaryKey}})
+	if err!=nil{
+		return err
+	}
+
+{{end}}	{{.keys}}
+    _, err {{if .containsIndexCache}}={{else}}:={{end}} m.ExecCtx(ctx, func(ctx context.Context, conn sqlx.SqlConn) (result sql.Result, err error) {
+		query := fmt.Sprintf("DELETE FROM %s WHERE {{.originalPrimaryKey}} = {{if .postgreSql}}$1{{else}}?{{end}}", m.table)
+		return session.ExecCtx(ctx, query, {{.lowerStartCamelPrimaryKey}})
+	}, {{.keyValues}}){{else}}query := fmt.Sprintf("DELETE FROM %s WHERE {{.originalPrimaryKey}} = {{if .postgreSql}}$1{{else}}?{{end}}", m.table)
+		_,err:=session.ExecCtx(ctx, query, {{.lowerStartCamelPrimaryKey}}){{end}}
+	return err
+}
+
+func (m *default{{.upperStartCamelObject}}Model) BatchDelete(ctx context.Context, {{.lowerStartCamelPrimaryKey}}s []{{.dataType}}) error {
+	if len({{.lowerStartCamelPrimaryKey}}s) == 0 {
+		return nil
+	}
+
+	{{if .withCache}}{{if .containsIndexCache}}primaryKeys := make([]interface{}, 0, len({{.lowerStartCamelPrimaryKey}}s))
+	for _, key := range {{.lowerStartCamelPrimaryKey}}s {
+		primaryKeys = append(primaryKeys, key)
+	}
+	oldDataList, err := m.findListByPrimaryKeys(ctx, primaryKeys)
+	if err != nil {
+		return err
+	}{{end}}
+
+	keys := make([]string, 0)
+	for _, {{if .containsIndexCache}}data{{else}}{{.lowerStartCamelPrimaryKey}}{{end}} := range {{if .containsIndexCache}}oldDataList{{else}}{{.lowerStartCamelPrimaryKey}}s{{end}} { {{if .containsIndexCache}}
+		{{.lowerStartCamelPrimaryKey}} := m.getPrimaryKeyValue(data){{end}}
+		{{.keys}}
+		keys = append(keys, {{.keyValues}})
+	}{{end}}
+
+	placeholders := make([]string, 0, len({{.lowerStartCamelPrimaryKey}}s))
+	args := make([]interface{}, 0, len({{.lowerStartCamelPrimaryKey}}s))
+	for {{if .postgreSql}}i, {{else}}_, {{end}}{{.lowerStartCamelPrimaryKey}} := range {{.lowerStartCamelPrimaryKey}}s {
+		{{if .postgreSql}}placeholders = append(placeholders, fmt.Sprintf("$%d", i+1)){{else}}placeholders = append(placeholders, "?"){{end}}
+		args = append(args, {{.lowerStartCamelPrimaryKey}})
+	}
+
+	{{if .withCache}}_, err {{if .containsIndexCache}}={{else}}:={{end}} m.ExecCtx(ctx, func(ctx context.Context, conn sqlx.SqlConn) (result sql.Result, err error) {
+		query := fmt.Sprintf("DELETE FROM %s WHERE {{.originalPrimaryKey}} IN (%s)", m.table, strings.Join(placeholders, ","))
+		return conn.ExecCtx(ctx, query, args...)
+	}, keys...){{else}}query := fmt.Sprintf("DELETE FROM %s WHERE {{.originalPrimaryKey}} IN (%s)", m.table, strings.Join(placeholders, ","))
+	_, err := m.conn.ExecCtx(ctx, query, args...){{end}}
+	return err
+}
+
+func (m *default{{.upperStartCamelObject}}Model) BatchDeleteWithTx(ctx context.Context, session sqlx.Session, {{.lowerStartCamelPrimaryKey}}s []{{.dataType}}) error {
+	if len({{.lowerStartCamelPrimaryKey}}s) == 0 {
+		return nil
+	}
+
+	{{if .withCache}}{{if .containsIndexCache}}primaryKeys := make([]interface{}, 0, len({{.lowerStartCamelPrimaryKey}}s))
+	for _, key := range {{.lowerStartCamelPrimaryKey}}s {
+		primaryKeys = append(primaryKeys, key)
+	}
+	oldDataList, err := m.findListByPrimaryKeys(ctx, primaryKeys)
+	if err != nil {
+		return err
+	}{{end}}
+
+	keys := make([]string, 0)
+	for _, {{if .containsIndexCache}}data{{else}}{{.lowerStartCamelPrimaryKey}}{{end}} := range {{if .containsIndexCache}}oldDataList{{else}}{{.lowerStartCamelPrimaryKey}}s{{end}} { {{if .containsIndexCache}}
+		{{.lowerStartCamelPrimaryKey}} := m.getPrimaryKeyValue(data){{end}}
+		{{.keys}}
+		keys = append(keys, {{.keyValues}})
+	}{{end}}
+
+	placeholders := make([]string, 0, len({{.lowerStartCamelPrimaryKey}}s))
+	args := make([]interface{}, 0, len({{.lowerStartCamelPrimaryKey}}s))
+	for {{if .postgreSql}}i, {{else}}_, {{end}}{{.lowerStartCamelPrimaryKey}} := range {{.lowerStartCamelPrimaryKey}}s {
+		{{if .postgreSql}}placeholders = append(placeholders, fmt.Sprintf("$%d", i+1)){{else}}placeholders = append(placeholders, "?"){{end}}
+		args = append(args, {{.lowerStartCamelPrimaryKey}})
+	}
+
+	query := fmt.Sprintf("DELETE FROM %s WHERE {{.originalPrimaryKey}} IN (%s)", m.table, strings.Join(placeholders, ","))
+	{{if .withCache}}_, err {{if .containsIndexCache}}={{else}}:={{end}} m.ExecCtx(ctx, func(ctx context.Context, conn sqlx.SqlConn) (result sql.Result, err error) {
+		return session.ExecCtx(ctx, query, args...)
+	}, keys...){{else}}_, err := session.ExecCtx(ctx, query, args...){{end}}
+	return err
+}

+ 5 - 0
cli/goctl/model/err.tpl

@@ -0,0 +1,5 @@
+package {{.pkg}}
+
+import "github.com/zeromicro/go-zero/core/stores/sqlx"
+
+var ErrNotFound = sqlx.ErrNotFound

+ 1 - 0
cli/goctl/model/field.tpl

@@ -0,0 +1 @@
+{{.name}} {{.type}} {{.tag}} {{if .hasComment}}// {{.comment}}{{end}}

+ 8 - 0
cli/goctl/model/find-one-by-field-extra-method.tpl

@@ -0,0 +1,8 @@
+func (m *default{{.upperStartCamelObject}}Model) formatPrimary(primary any) string {
+	return fmt.Sprintf("%s%v", {{.primaryKeyLeft}}, primary)
+}
+
+func (m *default{{.upperStartCamelObject}}Model) queryPrimary(ctx context.Context, conn sqlx.SqlConn, v, primary any) error {
+	query := fmt.Sprintf("SELECT %s FROM %s WHERE {{.originalPrimaryField}} = {{if .postgreSql}}$1{{else}}?{{end}} LIMIT 1", {{.lowerStartCamelObject}}Rows, m.table )
+	return conn.QueryRowCtx(ctx, v, query, primary)
+}

+ 44 - 0
cli/goctl/model/find-one-by-field.tpl

@@ -0,0 +1,44 @@
+func (m *default{{.upperStartCamelObject}}Model) FindOneBy{{.upperField}}(ctx context.Context, {{.in}}) (*{{.upperStartCamelObject}}, error) {
+	{{if .withCache}}{{.cacheKey}}
+	var resp {{.upperStartCamelObject}}
+	err := m.QueryRowIndexCtx(ctx, &resp, {{.cacheKeyVariable}}, m.formatPrimary, func(ctx context.Context, conn sqlx.SqlConn, v any) (i any, e error) {
+		query := fmt.Sprintf("SELECT %s FROM %s WHERE {{.originalField}} LIMIT 1", {{.lowerStartCamelObject}}Rows, m.table)
+		if err := conn.QueryRowCtx(ctx, &resp, query, {{.lowerStartCamelField}}); err != nil {
+			return nil, err
+		}
+		return resp.{{.upperStartCamelPrimaryKey}}, nil
+	}, m.queryPrimary)
+	switch err {
+	case nil:
+		return &resp, nil
+	case sqlc.ErrNotFound:
+		return nil, ErrNotFound
+	default:
+		return nil, err
+	}
+}{{else}}var resp {{.upperStartCamelObject}}
+	query := fmt.Sprintf("SELECT %s FROM %s WHERE {{.originalField}} LIMIT 1", {{.lowerStartCamelObject}}Rows, m.table )
+	err := m.conn.QueryRowCtx(ctx, &resp, query, {{.lowerStartCamelField}})
+	switch err {
+	case nil:
+		return &resp, nil
+	case sqlx.ErrNotFound:
+		return nil, ErrNotFound
+	default:
+		return nil, err
+	}
+}{{end}}
+
+func (m *default{{.upperStartCamelObject}}Model) FindOneBy{{.upperField}}WithTx(ctx context.Context, session sqlx.Session, {{.in}}) (*{{.upperStartCamelObject}}, error) {
+	var resp {{.upperStartCamelObject}}
+	query := fmt.Sprintf("SELECT %s FROM %s WHERE {{.originalField}} LIMIT 1", {{.lowerStartCamelObject}}Rows, m.table)
+	err := session.QueryRowCtx(ctx, &resp, query, {{.lowerStartCamelField}})
+	switch err {
+	case nil:
+		return &resp, nil
+	case sqlx.ErrNotFound:
+		return nil, ErrNotFound
+	default:
+		return nil, err
+	}
+}

+ 40 - 0
cli/goctl/model/find-one.tpl

@@ -0,0 +1,40 @@
+func (m *default{{.upperStartCamelObject}}Model) FindOne(ctx context.Context, {{.lowerStartCamelPrimaryKey}} {{.dataType}}) (*{{.upperStartCamelObject}}, error) {
+	{{if .withCache}}{{.cacheKey}}
+	var resp {{.upperStartCamelObject}}
+	err := m.QueryRowCtx(ctx, &resp, {{.cacheKeyVariable}}, func(ctx context.Context, conn sqlx.SqlConn, v any) error {
+		query :=  fmt.Sprintf("SELECT %s FROM %s WHERE {{.originalPrimaryKey}} = {{if .postgreSql}}$1{{else}}?{{end}} LIMIT 1", {{.lowerStartCamelObject}}Rows, m.table)
+		return conn.QueryRowCtx(ctx, v, query, {{.lowerStartCamelPrimaryKey}})
+	})
+	switch err {
+	case nil:
+		return &resp, nil
+	case sqlc.ErrNotFound:
+		return nil, ErrNotFound
+	default:
+		return nil, err
+	}{{else}}query := fmt.Sprintf("SELECT %s FROM %s WHERE {{.originalPrimaryKey}} = {{if .postgreSql}}$1{{else}}?{{end}} LIMIT 1", {{.lowerStartCamelObject}}Rows, m.table)
+	var resp {{.upperStartCamelObject}}
+	err := m.conn.QueryRowCtx(ctx, &resp, query, {{.lowerStartCamelPrimaryKey}})
+	switch err {
+	case nil:
+		return &resp, nil
+	case sqlx.ErrNotFound:
+		return nil, ErrNotFound
+	default:
+		return nil, err
+	}{{end}}
+}
+
+func (m *default{{.upperStartCamelObject}}Model) FindOneWithTx(ctx context.Context, session sqlx.Session, {{.lowerStartCamelPrimaryKey}} {{.dataType}}) (*{{.upperStartCamelObject}}, error) {
+	var resp {{.upperStartCamelObject}}
+	query := fmt.Sprintf("SELECT %s FROM %s WHERE {{.originalPrimaryKey}} = {{if .postgreSql}}$1{{else}}?{{end}} LIMIT 1", {{.lowerStartCamelObject}}Rows, m.table)
+	err := session.QueryRowCtx(ctx, &resp, query, {{.lowerStartCamelPrimaryKey}})
+	switch err {
+	case nil:
+		return &resp, nil
+	case sqlx.ErrNotFound:
+		return nil, ErrNotFound
+	default:
+		return nil, err
+	}
+}

+ 14 - 0
cli/goctl/model/import-no-cache.tpl

@@ -0,0 +1,14 @@
+import (
+	"context"
+	"database/sql"
+	"fmt"
+	"strings"
+	{{if .time}}"time"{{end}}
+
+    {{if .containsPQ}}"github.com/lib/pq"{{end}}
+	"github.com/zeromicro/go-zero/core/stores/builder"
+	"github.com/zeromicro/go-zero/core/stores/sqlx"
+	"github.com/zeromicro/go-zero/core/stringx"
+
+	{{.third}}
+)

+ 16 - 0
cli/goctl/model/import.tpl

@@ -0,0 +1,16 @@
+import (
+	"context"
+	"database/sql"
+	"fmt"
+	"strings"
+	{{if .time}}"time"{{end}}
+
+	{{if .containsPQ}}"github.com/lib/pq"{{end}}
+	"github.com/zeromicro/go-zero/core/stores/builder"
+	"github.com/zeromicro/go-zero/core/stores/cache"
+	"github.com/zeromicro/go-zero/core/stores/sqlc"
+	"github.com/zeromicro/go-zero/core/stores/sqlx"
+	"github.com/zeromicro/go-zero/core/stringx"
+
+	{{.third}}
+)

+ 67 - 0
cli/goctl/model/insert.tpl

@@ -0,0 +1,67 @@
+func (m *default{{.upperStartCamelObject}}Model) Insert(ctx context.Context, data *{{.upperStartCamelObject}}) (sql.Result,error) {
+	{{if .withCache}}{{.keys}}
+    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 ({{.expression}})", m.table, {{.lowerStartCamelObject}}RowsExpectAutoSet)
+		return conn.ExecCtx(ctx, query, {{.expressionValues}})
+	}, {{.keyValues}}){{else}}query := fmt.Sprintf("INSERT INTO %s (%s) VALUES ({{.expression}})", m.table, {{.lowerStartCamelObject}}RowsExpectAutoSet)
+    ret,err:=m.conn.ExecCtx(ctx, query, {{.expressionValues}}){{end}}
+	return ret,err
+}
+
+func (m *default{{.upperStartCamelObject}}Model) InsertWithTx(ctx context.Context, session sqlx.Session, data *{{.upperStartCamelObject}}) (sql.Result,error) {
+	{{if .withCache}}{{.keys}}
+    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 ({{.expression}})", m.table, {{.lowerStartCamelObject}}RowsExpectAutoSet)
+		return session.ExecCtx(ctx, query, {{.expressionValues}})
+	}, {{.keyValues}}){{else}}query := fmt.Sprintf("INSERT INTO %s (%s) VALUES ({{.expression}})", m.table, {{.lowerStartCamelObject}}RowsExpectAutoSet)
+    ret,err:=session.ExecCtx(ctx, query, {{.expressionValues}}){{end}}
+	return ret,err
+}
+
+func (m *default{{.upperStartCamelObject}}Model) BatchInsert(ctx context.Context, dataList []*{{.upperStartCamelObject}}) error {
+	if len(dataList) == 0 {
+		return nil
+	}
+	{{if .withCache}}keys := make([]string, 0, len(dataList))
+	{{end}}valueSets := make([]string, 0, len(dataList))
+	args := make([]interface{}, 0)
+	for _, data := range dataList {
+		valueSets = append(valueSets, "({{.expression}})")
+		args = append(args, {{.expressionValues}})
+		{{if .withCache}}{{.keys}}
+		keys = append(keys, {{.keyValues}})
+		{{end}}
+	}
+	query := fmt.Sprintf("INSERT INTO %s (%s) VALUES %s", m.table, {{.lowerStartCamelObject}}RowsExpectAutoSet, strings.Join(valueSets, ","))
+	{{if .withCache}}_, err := m.ExecCtx(ctx, func(ctx context.Context, conn sqlx.SqlConn) (result sql.Result, err error) {
+		return conn.ExecCtx(ctx, query, args...)
+	}, keys...){{else}}_, err := m.conn.ExecCtx(ctx, query, args...){{end}}
+	return err
+}
+
+func (m *default{{.upperStartCamelObject}}Model) BatchInsertWithTx(ctx context.Context, session sqlx.Session, dataList []*{{.upperStartCamelObject}}) error {
+	if len(dataList) == 0 {
+		return nil
+	}
+	{{if .withCache}}keys := make([]string, 0, len(dataList))
+	{{end}}valueSets := make([]string, 0, len(dataList))
+	args := make([]interface{}, 0)
+	for _, data := range dataList {
+		valueSets = append(valueSets, "({{.expression}})")
+		args = append(args, {{.expressionValues}})
+		{{if .withCache}}{{.keys}}
+		keys = append(keys, {{.keyValues}})
+		{{end}}
+	}
+	query := fmt.Sprintf("INSERT INTO %s (%s) VALUES %s", m.table, {{.lowerStartCamelObject}}RowsExpectAutoSet, strings.Join(valueSets, ","))
+	{{if .withCache}}_, err := m.ExecCtx(ctx, func(ctx context.Context, conn sqlx.SqlConn) (result sql.Result, err error) {
+		return session.ExecCtx(ctx, query, args...)
+	}, keys...){{else}}_, err := session.ExecCtx(ctx, query, args...){{end}}
+	return err
+}
+
+{{if not .withCache}}
+func (m *default{{.upperStartCamelObject}}Model) TransactCtx(ctx context.Context, fn func(context.Context, sqlx.Session) error) error {
+	return m.conn.TransactCtx(ctx, fn)
+}
+{{end}}

+ 5 - 0
cli/goctl/model/interface-delete.tpl

@@ -0,0 +1,5 @@
+Delete(ctx context.Context, {{.lowerStartCamelPrimaryKey}} {{.dataType}}) error
+DeleteWithTx(ctx context.Context, session sqlx.Session, {{.lowerStartCamelPrimaryKey}} {{.dataType}}) error
+BatchDelete(ctx context.Context, {{.lowerStartCamelPrimaryKey}}s []{{.dataType}}) error
+BatchDeleteWithTx(ctx context.Context, session sqlx.Session, {{.lowerStartCamelPrimaryKey}}s []{{.dataType}}) error
+TableName() string

+ 2 - 0
cli/goctl/model/interface-find-one-by-field.tpl

@@ -0,0 +1,2 @@
+FindOneBy{{.upperField}}(ctx context.Context, {{.in}}) (*{{.upperStartCamelObject}}, error)
+		FindOneBy{{.upperField}}WithTx(ctx context.Context, session sqlx.Session, {{.in}}) (*{{.upperStartCamelObject}}, error)

+ 2 - 0
cli/goctl/model/interface-find-one.tpl

@@ -0,0 +1,2 @@
+FindOne(ctx context.Context, {{.lowerStartCamelPrimaryKey}} {{.dataType}}) (*{{.upperStartCamelObject}}, error)
+		FindOneWithTx(ctx context.Context, session sqlx.Session, {{.lowerStartCamelPrimaryKey}} {{.dataType}}) (*{{.upperStartCamelObject}}, error)

+ 5 - 0
cli/goctl/model/interface-insert.tpl

@@ -0,0 +1,5 @@
+Insert(ctx context.Context, data *{{.upperStartCamelObject}}) (sql.Result,error)
+InsertWithTx(ctx context.Context, session sqlx.Session, data *{{.upperStartCamelObject}}) (sql.Result,error)
+BatchInsert(ctx context.Context, dataList []*{{.upperStartCamelObject}}) error
+BatchInsertWithTx(ctx context.Context, session sqlx.Session, dataList []*{{.upperStartCamelObject}}) error
+TransactCtx(ctx context.Context, fn func(context.Context, sqlx.Session) error) error

+ 4 - 0
cli/goctl/model/interface-update.tpl

@@ -0,0 +1,4 @@
+Update(ctx context.Context, {{if .containsIndexCache}}newData{{else}}data{{end}} *{{.upperStartCamelObject}}) error
+UpdateWithTx(ctx context.Context, session sqlx.Session, data *{{.upperStartCamelObject}}) error
+BatchUpdate(ctx context.Context, dataList []*{{.upperStartCamelObject}}) error
+BatchUpdateWithTx(ctx context.Context, session sqlx.Session, dataList []*{{.upperStartCamelObject}}) error

+ 16 - 0
cli/goctl/model/model-gen.tpl

@@ -0,0 +1,16 @@
+// Code generated by goctl. DO NOT EDIT.
+// versions:
+//  goctl version: 1.10.0
+
+package {{.pkg}}
+{{.imports}}
+{{.vars}}
+{{.types}}
+{{.new}}
+{{.delete}}
+{{.find}}
+{{.insert}}
+{{.update}}
+{{.extraMethod}}
+{{.tableName}}
+{{.customized}}

+ 12 - 0
cli/goctl/model/model-new.tpl

@@ -0,0 +1,12 @@
+func new{{.upperStartCamelObject}}Model(conn sqlx.SqlConn{{if .withCache}}, c cache.CacheConf, cachePrefix string, opts ...cache.Option{{end}}) *default{{.upperStartCamelObject}}Model {
+	{{if .withCache}}if cachePrefix != "" {
+		{{.data.PrimaryCacheKey.VarLeft}} = cachePrefix + ":" + {{.data.PrimaryCacheKey.VarRight}}
+		{{range .data.UniqueCacheKey}}{{.VarLeft}} = cachePrefix + ":" + {{.VarRight}}
+		{{end}}
+	}
+	{{end}}return &default{{.upperStartCamelObject}}Model{
+		{{if .withCache}}CachedConn: sqlc.NewConn(conn, c, opts...){{else}}conn:conn{{end}},
+		table:      {{.table}},
+	}
+}
+

+ 38 - 0
cli/goctl/model/model.tpl

@@ -0,0 +1,38 @@
+package {{.pkg}}
+{{if .withCache}}
+import (
+	"github.com/zeromicro/go-zero/core/stores/cache"
+	"github.com/zeromicro/go-zero/core/stores/sqlx"
+)
+{{else}}
+
+import "github.com/zeromicro/go-zero/core/stores/sqlx"
+{{end}}
+var _ {{.upperStartCamelObject}}Model = (*custom{{.upperStartCamelObject}}Model)(nil)
+
+type (
+	// {{.upperStartCamelObject}}Model is an interface to be customized, add more methods here,
+	// and implement the added methods in custom{{.upperStartCamelObject}}Model.
+	{{.upperStartCamelObject}}Model interface {
+		{{.lowerStartCamelObject}}Model
+		{{if not .withCache}}withSession(session sqlx.Session) {{.upperStartCamelObject}}Model{{end}}
+	}
+
+	custom{{.upperStartCamelObject}}Model struct {
+		*default{{.upperStartCamelObject}}Model
+	}
+)
+
+// New{{.upperStartCamelObject}}Model returns a model for the database table.
+func New{{.upperStartCamelObject}}Model(conn sqlx.SqlConn{{if .withCache}}, c cache.CacheConf, cachePrefix string, opts ...cache.Option{{end}}) {{.upperStartCamelObject}}Model {
+	return &custom{{.upperStartCamelObject}}Model{
+		default{{.upperStartCamelObject}}Model: new{{.upperStartCamelObject}}Model(conn{{if .withCache}}, c, cachePrefix, opts...{{end}}),
+	}
+}
+
+{{if not .withCache}}
+func (m *custom{{.upperStartCamelObject}}Model) withSession(session sqlx.Session) {{.upperStartCamelObject}}Model {
+    return New{{.upperStartCamelObject}}Model(sqlx.NewSqlConnFromSession(session))
+}
+{{end}}
+

+ 3 - 0
cli/goctl/model/table-name.tpl

@@ -0,0 +1,3 @@
+func (m *default{{.upperStartCamelObject}}Model) TableName() string {
+	return m.table
+}

+ 1 - 0
cli/goctl/model/tag.tpl

@@ -0,0 +1 @@
+`db:"{{.field}}"`

+ 14 - 0
cli/goctl/model/types.tpl

@@ -0,0 +1,14 @@
+type (
+	{{.lowerStartCamelObject}}Model interface{
+		{{.method}}
+	}
+
+	default{{.upperStartCamelObject}}Model struct {
+		{{if .withCache}}sqlc.CachedConn{{else}}conn sqlx.SqlConn{{end}}
+		table string
+	}
+
+	{{.upperStartCamelObject}} struct {
+		{{.fields}}
+	}
+)

+ 127 - 0
cli/goctl/model/update.tpl

@@ -0,0 +1,127 @@
+func (m *default{{.upperStartCamelObject}}Model) Update(ctx context.Context, {{if .containsIndexCache}}newData{{else}}data{{end}} *{{.upperStartCamelObject}}) error {
+	{{if .withCache}}{{if .containsIndexCache}}data, err:=m.FindOne(ctx, newData.{{.upperStartCamelPrimaryKey}})
+	if err!=nil{
+		return err
+	}
+
+{{end}}	{{.keys}}
+    _, {{if .containsIndexCache}}err{{else}}err:{{end}}= m.ExecCtx(ctx, func(ctx context.Context, conn sqlx.SqlConn) (result sql.Result, err error) {
+		query := fmt.Sprintf("UPDATE %s SET %s WHERE {{.originalPrimaryKey}} = {{if .postgreSql}}$1{{else}}?{{end}}", m.table, {{.lowerStartCamelObject}}RowsWithPlaceHolder)
+		return conn.ExecCtx(ctx, query, {{.expressionValues}})
+	}, {{.keyValues}}){{else}}query := fmt.Sprintf("UPDATE %s SET %s WHERE {{.originalPrimaryKey}} = {{if .postgreSql}}$1{{else}}?{{end}}", m.table, {{.lowerStartCamelObject}}RowsWithPlaceHolder)
+    _,err:=m.conn.ExecCtx(ctx, query, {{.expressionValues}}){{end}}
+	return err
+}
+
+func (m *default{{.upperStartCamelObject}}Model) UpdateWithTx(ctx context.Context, session sqlx.Session, {{if .containsIndexCache}}newData{{else}}data{{end}} *{{.upperStartCamelObject}}) error {
+	{{if .withCache}}{{if .containsIndexCache}}data, err:=m.FindOne(ctx, newData.{{.upperStartCamelPrimaryKey}})
+	if err!=nil{
+		return err
+	}
+
+{{end}}	{{.keys}}
+    _, {{if .containsIndexCache}}err{{else}}err:{{end}}= m.ExecCtx(ctx, func(ctx context.Context, conn sqlx.SqlConn) (result sql.Result, err error) {
+		query := fmt.Sprintf("UPDATE %s SET %s WHERE {{.originalPrimaryKey}} = {{if .postgreSql}}$1{{else}}?{{end}}", m.table, {{.lowerStartCamelObject}}RowsWithPlaceHolder)
+		return session.ExecCtx(ctx, query, {{.expressionValues}})
+	}, {{.keyValues}}){{else}}query := fmt.Sprintf("UPDATE %s SET %s WHERE {{.originalPrimaryKey}} = {{if .postgreSql}}$1{{else}}?{{end}}", m.table, {{.lowerStartCamelObject}}RowsWithPlaceHolder)
+    _,err:=session.ExecCtx(ctx, query, {{.expressionValues}}){{end}}
+	return err
+}
+
+func (m *default{{.upperStartCamelObject}}Model) BatchUpdate(ctx context.Context, dataList []*{{.upperStartCamelObject}}) error {
+	if len(dataList) == 0 {
+		return nil
+	}
+
+	{{if .withCache}}keys := make([]string, 0)
+	{{if .containsIndexCache}}primaryKeys := make([]interface{}, 0, len(dataList))
+	for _, item := range dataList {
+		primaryKeys = append(primaryKeys, item.{{.upperStartCamelPrimaryKey}})
+	}
+	oldList, err := m.findListByPrimaryKeys(ctx, primaryKeys)
+	if err != nil {
+		return err
+	}{{end}}
+
+	for _, data := range {{if .containsIndexCache}}oldList{{else}}dataList{{end}} {
+		{{.keys}}
+		keys = append(keys, {{.keyValues}})
+	}
+
+	query, vals := m.buildBatchUpdateQuery(dataList)
+	_, err {{if .containsIndexCache}}={{else}}:={{end}} m.ExecCtx(ctx, func(ctx context.Context, conn sqlx.SqlConn) (result sql.Result, err error) {
+		return conn.ExecCtx(ctx, query, vals...)
+	}, keys...)
+	{{else}}query, vals := m.buildBatchUpdateQuery(dataList)
+	_, err := m.conn.ExecCtx(ctx, query, vals...)
+	{{end}}
+	return err
+}
+
+func (m *default{{.upperStartCamelObject}}Model) BatchUpdateWithTx(ctx context.Context, session sqlx.Session, dataList []*{{.upperStartCamelObject}}) error {
+	if len(dataList) == 0 {
+		return nil
+	}
+
+	{{if .withCache}}keys := make([]string, 0)
+	{{if .containsIndexCache}}primaryKeys := make([]interface{}, 0, len(dataList))
+	for _, item := range dataList {
+		primaryKeys = append(primaryKeys, item.{{.upperStartCamelPrimaryKey}})
+	}
+	oldList, err := m.findListByPrimaryKeys(ctx, primaryKeys)
+	if err != nil {
+		return err
+	}{{end}}
+
+	for _, data := range {{if .containsIndexCache}}oldList{{else}}dataList{{end}} {
+		{{.keys}}
+		keys = append(keys, {{.keyValues}})
+	}
+
+	query, vals := m.buildBatchUpdateQuery(dataList)
+	_, err {{if .containsIndexCache}}={{else}}:={{end}} m.ExecCtx(ctx, func(ctx context.Context, conn sqlx.SqlConn) (result sql.Result, err error) {
+		return session.ExecCtx(ctx, query, vals...)
+	}, keys...)
+	{{else}}query, vals := m.buildBatchUpdateQuery(dataList)
+	_, err := session.ExecCtx(ctx, query, vals...)
+	{{end}}
+	return err
+}
+
+func (m *default{{.upperStartCamelObject}}Model) buildBatchUpdateQuery(dataList []*{{.upperStartCamelObject}}) (string, []interface{}) {
+	{{if .postgreSql}}rawParts := strings.Split({{.lowerStartCamelObject}}RowsWithPlaceHolder, ", ")
+	fields := make([]string, len(rawParts))
+	for i, part := range rawParts {
+		fields[i] = part[:strings.Index(part, " =")]
+	}{{else}}fields := strings.Split(strings.ReplaceAll({{.lowerStartCamelObject}}RowsWithPlaceHolder, "=?", ""), ","){{end}}
+
+	listValues := make([][]interface{}, 0, len(dataList))
+	for _, {{if .containsIndexCache}}newData{{else}}data{{end}} := range dataList {
+		values := make([]interface{}, 0, len(fields)+1)
+		values = append(values, {{.expressionValues}})
+		listValues = append(listValues, values)
+	}
+
+	vals := make([]interface{}, 0, len(dataList)*(len(fields)*2+1))
+	setClauses := make([]string, len(fields))
+	{{if .postgreSql}}placeholderIdx := 1{{end}}
+	for i, field := range fields {
+		caseClauses := make([]string, len(dataList))
+		for j, item := range dataList {
+			{{if .postgreSql}}caseClauses[j] = fmt.Sprintf("WHEN {{.originalPrimaryKey}} = $%d THEN $%d", placeholderIdx, placeholderIdx+1)
+			placeholderIdx += 2{{else}}caseClauses[j] = "WHEN {{.originalPrimaryKey}} = ? THEN ?"{{end}}
+			vals = append(vals, item.{{.upperStartCamelPrimaryKey}}, listValues[j][i])
+		}
+		setClauses[i] = fmt.Sprintf("%s = CASE %s ELSE %s END", field, strings.Join(caseClauses, " "), field)
+	}
+
+	wherePlaceholders := make([]string, len(dataList))
+	for i, item := range dataList {
+		{{if .postgreSql}}wherePlaceholders[i] = fmt.Sprintf("$%d", placeholderIdx)
+		placeholderIdx++{{else}}wherePlaceholders[i] = "?"{{end}}
+		vals = append(vals, item.{{.upperStartCamelPrimaryKey}})
+	}
+
+	query := fmt.Sprintf("UPDATE %s SET %s WHERE {{.originalPrimaryKey}} IN (%s)", m.table, strings.Join(setClauses, ", "), strings.Join(wherePlaceholders, ","))
+	return query, vals
+}

+ 8 - 0
cli/goctl/model/var.tpl

@@ -0,0 +1,8 @@
+var (
+{{.lowerStartCamelObject}}FieldNames = builder.RawFieldNames(&{{.upperStartCamelObject}}{}{{if .postgreSql}}, true{{end}})
+{{.lowerStartCamelObject}}Rows = strings.Join({{.lowerStartCamelObject}}FieldNames, ",")
+{{.lowerStartCamelObject}}RowsExpectAutoSet = {{if .postgreSql}}strings.Join(stringx.Remove({{.lowerStartCamelObject}}FieldNames, {{if .autoIncrement}}"{{.originalPrimaryKey}}", {{end}} {{.ignoreColumns}}), ","){{else}}strings.Join(stringx.Remove({{.lowerStartCamelObject}}FieldNames, {{if .autoIncrement}}"{{.originalPrimaryKey}}", {{end}} {{.ignoreColumns}}), ","){{end}}
+{{.lowerStartCamelObject}}RowsWithPlaceHolder = {{if .postgreSql}}builder.PostgreSqlJoin(stringx.Remove({{.lowerStartCamelObject}}FieldNames, "{{.originalPrimaryKey}}", {{.ignoreColumns}})){{else}}strings.Join(stringx.Remove({{.lowerStartCamelObject}}FieldNames, "{{.originalPrimaryKey}}", {{.ignoreColumns}}), "=?,") + "=?"{{end}}
+
+{{if .withCache}}{{.cacheKeys}}{{end}}
+)

+ 12 - 0
cli/goctl/mongo/err.tpl

@@ -0,0 +1,12 @@
+package model
+
+import (
+	"errors"
+
+	"github.com/zeromicro/go-zero/core/stores/mon"
+)
+
+var (
+	ErrNotFound        = mon.ErrNotFound
+	ErrInvalidObjectId = errors.New("invalid objectId")
+)

+ 79 - 0
cli/goctl/mongo/model.tpl

@@ -0,0 +1,79 @@
+// Code generated by goctl. DO NOT EDIT.
+// goctl {{.version}}
+
+package model
+
+import (
+    "context"
+    "time"
+
+    {{if .Cache}}"github.com/zeromicro/go-zero/core/stores/monc"{{else}}"github.com/zeromicro/go-zero/core/stores/mon"{{end}}
+    "go.mongodb.org/mongo-driver/v2/bson"
+    "go.mongodb.org/mongo-driver/v2/mongo"
+)
+
+{{if .Cache}}var prefix{{.Type}}CacheKey = "{{if .Prefix}}{{.Prefix}}:{{end}}cache:{{.lowerType}}:"{{end}}
+
+type {{.lowerType}}Model interface{
+    Insert(ctx context.Context,data *{{.Type}}) error
+    FindOne(ctx context.Context,id string) (*{{.Type}}, error)
+    Update(ctx context.Context,data *{{.Type}}) (*mongo.UpdateResult, error)
+    Delete(ctx context.Context,id string) (int64, error)
+}
+
+type default{{.Type}}Model struct {
+    conn {{if .Cache}}*monc.Model{{else}}*mon.Model{{end}}
+}
+
+func newDefault{{.Type}}Model(conn {{if .Cache}}*monc.Model{{else}}*mon.Model{{end}}) *default{{.Type}}Model {
+    return &default{{.Type}}Model{conn: conn}
+}
+
+
+func (m *default{{.Type}}Model) Insert(ctx context.Context, data *{{.Type}}) error {
+    if data.ID.IsZero() {
+        data.ID = bson.NewObjectID()
+        data.CreateAt = time.Now()
+        data.UpdateAt = time.Now()
+    }
+
+    {{if .Cache}}key := prefix{{.Type}}CacheKey + data.ID.Hex(){{end}}
+    _, err := m.conn.InsertOne(ctx, {{if .Cache}}key, {{end}} data)
+    return err
+}
+
+func (m *default{{.Type}}Model) FindOne(ctx context.Context, id string) (*{{.Type}}, error) {
+    oid, err := bson.ObjectIDFromHex(id)
+    if err != nil {
+        return nil, ErrInvalidObjectId
+    }
+
+    var data {{.Type}}
+    {{if .Cache}}key := prefix{{.Type}}CacheKey + id{{end}}
+    err = m.conn.FindOne(ctx, {{if .Cache}}key, {{end}}&data, bson.M{"_id": oid})
+    switch err {
+    case nil:
+        return &data, nil
+    case {{if .Cache}}monc{{else}}mon{{end}}.ErrNotFound:
+        return nil, ErrNotFound
+    default:
+        return nil, err
+    }
+}
+
+func (m *default{{.Type}}Model) Update(ctx context.Context, data *{{.Type}}) (*mongo.UpdateResult, error) {
+    data.UpdateAt = time.Now()
+    {{if .Cache}}key := prefix{{.Type}}CacheKey + data.ID.Hex(){{end}}
+    res, err := m.conn.UpdateOne(ctx, {{if .Cache}}key, {{end}}bson.M{"_id": data.ID}, bson.M{"$set": data})
+    return res, err
+}
+
+func (m *default{{.Type}}Model) Delete(ctx context.Context, id string) (int64, error) {
+    oid, err := bson.ObjectIDFromHex(id)
+    if err != nil {
+        return 0, ErrInvalidObjectId
+    }
+	{{if .Cache}}key := prefix{{.Type}}CacheKey +id{{end}}
+    res, err := m.conn.DeleteOne(ctx, {{if .Cache}}key, {{end}}bson.M{"_id": oid})
+	return res, err
+}

+ 38 - 0
cli/goctl/mongo/model_custom.tpl

@@ -0,0 +1,38 @@
+package model
+
+{{if .Cache}}import (
+    "github.com/zeromicro/go-zero/core/stores/cache"
+    "github.com/zeromicro/go-zero/core/stores/monc"
+){{else}}import "github.com/zeromicro/go-zero/core/stores/mon"{{end}}
+
+{{if .Easy}}
+const {{.Type}}CollectionName = "{{.snakeType}}"
+{{end}}
+
+var _ {{.Type}}Model = (*custom{{.Type}}Model)(nil)
+
+type (
+    // {{.Type}}Model is an interface to be customized, add more methods here,
+    // and implement the added methods in custom{{.Type}}Model.
+    {{.Type}}Model interface {
+        {{.lowerType}}Model
+    }
+
+    custom{{.Type}}Model struct {
+        *default{{.Type}}Model
+    }
+)
+
+
+// New{{.Type}}Model returns a model for the mongo.
+{{if .Easy}}func New{{.Type}}Model(url, db string{{if .Cache}}, c cache.CacheConf{{end}}) {{.Type}}Model {
+    conn := {{if .Cache}}monc{{else}}mon{{end}}.MustNewModel(url, db, {{.Type}}CollectionName{{if .Cache}}, c{{end}})
+    return &custom{{.Type}}Model{
+        default{{.Type}}Model: newDefault{{.Type}}Model(conn),
+    }
+}{{else}}func New{{.Type}}Model(url, db, collection string{{if .Cache}}, c cache.CacheConf{{end}}) {{.Type}}Model {
+    conn := {{if .Cache}}monc{{else}}mon{{end}}.MustNewModel(url, db, collection{{if .Cache}}, c{{end}})
+    return &custom{{.Type}}Model{
+        default{{.Type}}Model: newDefault{{.Type}}Model(conn),
+    }
+}{{end}}

+ 14 - 0
cli/goctl/mongo/model_types.tpl

@@ -0,0 +1,14 @@
+package model
+
+import (
+	"time"
+
+	"go.mongodb.org/mongo-driver/v2/bson"
+)
+
+type {{.Type}} struct {
+	ID bson.ObjectID `bson:"_id,omitempty" json:"id,omitempty"`
+	// TODO: Fill your own fields
+	UpdateAt time.Time `bson:"updateAt,omitempty" json:"updateAt,omitempty"`
+	CreateAt time.Time `bson:"createAt,omitempty" json:"createAt,omitempty"`
+}

+ 14 - 0
cli/goctl/newapi/newtemplate.tpl

@@ -0,0 +1,14 @@
+syntax = "v1"
+
+type Request {
+  Name string `path:"name,options=you|me"`
+}
+
+type Response {
+  Message string `json:"message"`
+}
+
+service {{.name}}-api {
+  @handler {{.handler}}Handler
+  get /from/:name(Request) returns (Response)
+}

+ 33 - 0
cli/goctl/rpc/call.tpl

@@ -0,0 +1,33 @@
+{{.head}}
+
+package {{.filePackage}}
+
+import (
+	"context"
+
+	{{.pbPackage}}
+	{{if ne .pbPackage .protoGoPackage}}{{.protoGoPackage}}{{end}}
+
+	"github.com/zeromicro/go-zero/zrpc"
+	"google.golang.org/grpc"
+)
+
+type (
+	{{.alias}}
+
+	{{.serviceName}} interface {
+		{{.interface}}
+	}
+
+	default{{.serviceName}} struct {
+		cli zrpc.Client
+	}
+)
+
+func New{{.serviceName}}(cli zrpc.Client) {{.serviceName}} {
+	return &default{{.serviceName}}{
+		cli: cli,
+	}
+}
+
+{{.functions}}

+ 7 - 0
cli/goctl/rpc/config.tpl

@@ -0,0 +1,7 @@
+package config
+
+import "github.com/zeromicro/go-zero/zrpc"
+
+type Config struct {
+	zrpc.RpcServerConf
+}

+ 6 - 0
cli/goctl/rpc/etc.tpl

@@ -0,0 +1,6 @@
+Name: {{.serviceName}}.rpc
+ListenOn: 0.0.0.0:8080
+Etcd:
+  Hosts:
+  - 127.0.0.1:2379
+  Key: {{.serviceName}}.rpc

+ 6 - 0
cli/goctl/rpc/logic-func.tpl

@@ -0,0 +1,6 @@
+{{if .hasComment}}{{.comment}}{{end}}
+func (l *{{.logicName}}) {{.method}} ({{if .hasReq}}in {{.request}}{{if .stream}},stream {{.streamBody}}{{end}}{{else}}stream {{.streamBody}}{{end}}) ({{if .hasReply}}{{.response}},{{end}} error) {
+	// todo: add your logic here and delete this line
+	
+	return {{if .hasReply}}&{{.responseType}}{},{{end}} nil
+}

+ 24 - 0
cli/goctl/rpc/logic.tpl

@@ -0,0 +1,24 @@
+package {{.packageName}}
+
+import (
+	"context"
+
+	{{.imports}}
+
+	"github.com/zeromicro/go-zero/core/logx"
+)
+
+type {{.logicName}} struct {
+	ctx    context.Context
+	svcCtx *svc.ServiceContext
+	logx.Logger
+}
+
+func New{{.logicName}}(ctx context.Context,svcCtx *svc.ServiceContext) *{{.logicName}} {
+	return &{{.logicName}}{
+		ctx:    ctx,
+		svcCtx: svcCtx,
+		Logger: logx.WithContext(ctx),
+	}
+}
+{{.functions}}

+ 36 - 0
cli/goctl/rpc/main.tpl

@@ -0,0 +1,36 @@
+package main
+
+import (
+	"flag"
+	"fmt"
+
+	{{.imports}}
+
+	"github.com/zeromicro/go-zero/core/conf"
+	"github.com/zeromicro/go-zero/core/service"
+	"github.com/zeromicro/go-zero/zrpc"
+	"google.golang.org/grpc"
+	"google.golang.org/grpc/reflection"
+)
+
+var configFile = flag.String("f", "etc/{{.serviceName}}.yaml", "the config file")
+
+func main() {
+	flag.Parse()
+
+	var c config.Config
+	conf.MustLoad(*configFile, &c)
+	ctx := svc.NewServiceContext(c)
+
+	s := zrpc.MustNewServer(c.RpcServerConf, func(grpcServer *grpc.Server) {
+{{range .serviceNames}}       {{.Pkg}}.Register{{.GRPCService}}Server(grpcServer, {{.ServerPkg}}.New{{.Service}}Server(ctx))
+{{end}}
+		if c.Mode == service.DevMode || c.Mode == service.TestMode {
+			reflection.Register(grpcServer)
+		}
+	})
+	defer s.Stop()
+
+	fmt.Printf("Starting rpc server at %s...\n", c.ListenOn)
+	s.Start()
+}

+ 6 - 0
cli/goctl/rpc/server-func.tpl

@@ -0,0 +1,6 @@
+
+{{if .hasComment}}{{.comment}}{{end}}
+func (s *{{.server}}Server) {{.method}} ({{if .notStream}}ctx context.Context,{{if .hasReq}} in {{.request}}{{end}}{{else}}{{if .hasReq}} in {{.request}},{{end}}stream {{.streamBody}}{{end}}) ({{if .notStream}}{{.response}},{{end}}error) {
+	l := {{.logicPkg}}.New{{.logicName}}({{if .notStream}}ctx,{{else}}stream.Context(),{{end}}s.svcCtx)
+	return l.{{.method}}({{if .hasReq}}in{{if .stream}} ,stream{{end}}{{else}}{{if .stream}}stream{{end}}{{end}})
+}

+ 22 - 0
cli/goctl/rpc/server.tpl

@@ -0,0 +1,22 @@
+{{.head}}
+
+package server
+
+import (
+	{{if .notStream}}"context"{{end}}
+
+	{{.imports}}
+)
+
+type {{.server}}Server struct {
+	svcCtx *svc.ServiceContext
+	{{.unimplementedServer}}
+}
+
+func New{{.server}}Server(svcCtx *svc.ServiceContext) *{{.server}}Server {
+	return &{{.server}}Server{
+		svcCtx: svcCtx,
+	}
+}
+
+{{.funcs}}

+ 13 - 0
cli/goctl/rpc/svc.tpl

@@ -0,0 +1,13 @@
+package svc
+
+import {{.imports}}
+
+type ServiceContext struct {
+	Config config.Config
+}
+
+func NewServiceContext(c config.Config) *ServiceContext {
+	return &ServiceContext{
+		Config:c,
+	}
+}

+ 16 - 0
cli/goctl/rpc/template.tpl

@@ -0,0 +1,16 @@
+syntax = "proto3";
+
+package {{.package}};
+option go_package="./{{.package}}";
+
+message Request {
+  string ping = 1;
+}
+
+message Response {
+  string pong = 1;
+}
+
+service {{.serviceName}} {
+  rpc Ping(Request) returns(Response);
+}

+ 22 - 0
etc/perm-api-dev.yaml

@@ -0,0 +1,22 @@
+Name: perm-api
+Host: 0.0.0.0
+Port: 10001
+
+RpcServerConf:
+  ListenOn: 0.0.0.0:10002
+
+MySQL:
+  DataSource: "root:NsDmWyM@312@tcp(127.0.0.1:3306)/perms_system?charset=utf8mb4&parseTime=true&loc=Asia%2FShanghai"
+
+CacheRedis:
+  Nodes:
+    - Host: 127.0.0.1:6379
+      Pass: "NsDmWyM@312"
+      Type: node
+  KeyPrefix: "PermsSystem"
+
+Auth:
+  AccessSecret: "a64b9b07f7bb82f85efa44c47a10c50b"
+  AccessExpire: 7200
+  RefreshSecret: "f3a234543b30bfbc2e14225743830b62"
+  RefreshExpire: 604800

+ 22 - 0
etc/perm-api-prod.yaml

@@ -0,0 +1,22 @@
+Name: perm-api
+Host: 0.0.0.0
+Port: 10001
+
+RpcServerConf:
+  ListenOn: 0.0.0.0:10002
+
+MySQL:
+  DataSource: "root:NsDmWyM@312@tcp(127.0.0.1:3306)/perms_system?charset=utf8mb4&parseTime=true&loc=Asia%2FShanghai"
+
+CacheRedis:
+  Nodes:
+    - Host: 127.0.0.1:6379
+      Pass: "NsDmWyM@312"
+      Type: node
+  KeyPrefix: "PermsSystem"
+
+Auth:
+  AccessSecret: "234225d7f000ef18d17ae5b61dcec0ce"
+  AccessExpire: 7200
+  RefreshSecret: "dfe03caffcfc73be4a941a862dc59ae4"
+  RefreshExpire: 604800

+ 22 - 0
etc/perm-api-test.yaml

@@ -0,0 +1,22 @@
+Name: perm-api
+Host: 0.0.0.0
+Port: 10001
+
+RpcServerConf:
+  ListenOn: 0.0.0.0:10002
+
+MySQL:
+  DataSource: "root:NsDmWyM@312@tcp(127.0.0.1:3306)/perms_system?charset=utf8mb4&parseTime=true&loc=Asia%2FShanghai"
+
+CacheRedis:
+  Nodes:
+    - Host: 127.0.0.1:6379
+      Pass: "NsDmWyM@312"
+      Type: node
+  KeyPrefix: "PermsSystem"
+
+Auth:
+  AccessSecret: "1b037598ed19541cc375a67727f4dc1d"
+  AccessExpire: 7200
+  RefreshSecret: "cbf9b9cfc6516a50e737f580d8e51310"
+  RefreshExpire: 604800

+ 51 - 0
gen-api.sh

@@ -0,0 +1,51 @@
+#!/bin/bash
+#
+# gen-api.sh — 根据 .api 文件生成 HTTP 接口代码 (handler / types / routes)
+#
+# 用法:
+#   ./gen-api.sh [api文件]
+#
+# 示例:
+#   ./gen-api.sh              # 默认使用 perm.api
+#   ./gen-api.sh perm.api     # 指定 api 文件
+#
+# 生成内容:
+#   internal/handler/routes.go         — 路由注册(覆盖)
+#   internal/handler/<group>/*.go      — handler 文件(覆盖)
+#   internal/types/types.go            — 请求/响应结构体(覆盖)
+#   internal/logic/<group>/*Logic.go   — 业务逻辑(仅新增,不覆盖已有文件)
+#
+# 注意: goctl 默认不会覆盖已存在的 logic 文件,可安全重复执行。
+
+set -euo pipefail
+
+SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
+TEMPLATE_HOME="$SCRIPT_DIR/cli/goctl"
+STYLE="goZero"
+
+API_FILE="${1:-perm.api}"
+
+# 支持相对路径
+if [[ "$API_FILE" != /* ]]; then
+    API_FILE="$SCRIPT_DIR/$API_FILE"
+fi
+
+if [[ ! -f "$API_FILE" ]]; then
+    echo "错误: API 文件不存在: $API_FILE"
+    exit 1
+fi
+
+echo "API 文件:   $API_FILE"
+echo "模板目录:   $TEMPLATE_HOME"
+echo "输出目录:   $SCRIPT_DIR"
+echo "文件风格:   $STYLE"
+echo ""
+
+goctl api go \
+    -api "$API_FILE" \
+    -dir "$SCRIPT_DIR" \
+    --home "$TEMPLATE_HOME" \
+    --style "$STYLE"
+
+echo ""
+echo "完成!已根据 $(basename "$API_FILE") 生成接口代码。"

+ 244 - 0
gen-model.sh

@@ -0,0 +1,244 @@
+#!/bin/bash
+#
+# gen-model.sh — 基于自定义 goctl 模板生成 model 代码
+#
+# 用法:
+#   ./gen-model.sh ddl        -src <sql文件> [-table <表1,表2,...>]
+#   ./gen-model.sh datasource -url <DSN>     -table <表1,表2,...>
+#
+# 示例:
+#   ./gen-model.sh ddl -src nomo.sql                              # 从 DDL 生成所有表
+#   ./gen-model.sh ddl -src nomo.sql -table sys_user,sys_role     # 从 DDL 生成指定表
+#   ./gen-model.sh datasource \
+#       -url 'root:123456@tcp(127.0.0.1:3306)/mydb' \
+#       -table sys_user,sys_role                                  # 从数据库生成指定表
+#
+# 表名到目录的映射规则:
+#   去掉 sys_ 前缀,再去掉下划线。如 sys_role_perm -> roleperm
+#   生成目录: internal/model/<pkg>/
+#   文件风格: 小驼峰 (--style goZero)
+
+set -euo pipefail
+
+SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
+TEMPLATE_HOME="$SCRIPT_DIR/cli/goctl"
+MODEL_BASE_DIR="$SCRIPT_DIR/internal/model"
+STYLE="goZero"
+
+# ---- 工具函数 ----
+
+# 表名 -> 包名/目录名
+# sys_user -> user, sys_role_perm -> roleperm, sys_product_member -> productmember
+table_to_pkg() {
+    local table="$1"
+    local name="${table#sys_}"
+    echo "${name//_/}"
+}
+
+# 从 SQL 文件中拆分出每张表的独立 DDL
+# 输出: 临时目录路径(包含 <表名>.sql 文件)
+split_ddl() {
+    local sql_file="$1"
+    local tmp_dir
+    tmp_dir=$(mktemp -d)
+
+    local current_table=""
+    local outfile=""
+    local printing=0
+
+    while IFS= read -r line; do
+        if echo "$line" | grep -q "DROP TABLE IF EXISTS"; 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
+                printing=0
+            fi
+        fi
+    done < "$sql_file"
+
+    echo "$tmp_dir"
+}
+
+# 从 DDL 文件生成单张表的 model
+gen_from_ddl() {
+    local ddl_file="$1"
+    local table_name="$2"
+    local pkg
+    pkg=$(table_to_pkg "$table_name")
+    local out_dir="$MODEL_BASE_DIR/$pkg"
+
+    mkdir -p "$out_dir"
+    echo "  [$table_name] -> $out_dir/"
+    goctl model mysql ddl \
+        -src "$ddl_file" \
+        -dir "$out_dir" \
+        -c \
+        --home "$TEMPLATE_HOME" \
+        --style "$STYLE"
+}
+
+# 从数据库连接生成单张表的 model
+gen_from_datasource() {
+    local url="$1"
+    local table_name="$2"
+    local pkg
+    pkg=$(table_to_pkg "$table_name")
+    local out_dir="$MODEL_BASE_DIR/$pkg"
+
+    mkdir -p "$out_dir"
+    echo "  [$table_name] -> $out_dir/"
+    goctl model mysql datasource \
+        -url "$url" \
+        -table "$table_name" \
+        -dir "$out_dir" \
+        -c \
+        --home "$TEMPLATE_HOME" \
+        --style "$STYLE"
+}
+
+# ---- 主流程 ----
+
+usage() {
+    cat <<'EOF'
+用法:
+  ./gen-model.sh ddl        -src <sql文件> [-table <表1,表2,...>]
+  ./gen-model.sh datasource -url <DSN>     -table <表1,表2,...>
+
+选项:
+  ddl 模式:
+    -src    SQL DDL 文件路径(必填)
+    -table  要生成的表名,逗号分隔(可选,默认生成 SQL 中所有表)
+
+  datasource 模式:
+    -url    MySQL DSN 连接串(必填),如 'root:pass@tcp(127.0.0.1:3306)/dbname'
+    -table  要生成的表名,逗号分隔(必填)
+
+示例:
+  ./gen-model.sh ddl -src perm.sql
+  ./gen-model.sh ddl -src perm.sql -table sys_user,sys_role
+  ./gen-model.sh datasource -url 'root:123456@tcp(127.0.0.1:3306)/mydb' -table sys_user
+EOF
+    exit 1
+}
+
+if [[ $# -lt 1 ]]; then
+    usage
+fi
+
+MODE="$1"
+shift
+
+case "$MODE" in
+ddl)
+    SRC=""
+    TABLES=""
+    while [[ $# -gt 0 ]]; do
+        case "$1" in
+            -src)   SRC="$2";    shift 2 ;;
+            -table) TABLES="$2"; shift 2 ;;
+            *)      echo "未知选项: $1"; usage ;;
+        esac
+    done
+
+    if [[ -z "$SRC" ]]; then
+        echo "错误: ddl 模式必须指定 -src"
+        usage
+    fi
+
+    # 支持相对路径
+    if [[ "$SRC" != /* ]]; then
+        SRC="$SCRIPT_DIR/$SRC"
+    fi
+
+    if [[ ! -f "$SRC" ]]; then
+        echo "错误: SQL 文件不存在: $SRC"
+        exit 1
+    fi
+
+    echo "拆分 DDL 文件: $SRC"
+    TMP_DIR=$(split_ddl "$SRC")
+    trap "rm -rf '$TMP_DIR'" EXIT
+
+    # 获取拆分出的所有表名
+    ALL_TABLES=()
+    for f in "$TMP_DIR"/*.sql; do
+        [[ -f "$f" ]] || continue
+        ALL_TABLES+=("$(basename "$f" .sql)")
+    done
+
+    # 如果指定了 -table 则过滤
+    if [[ -n "$TABLES" ]]; then
+        IFS=',' read -ra FILTER <<< "$TABLES"
+        TARGET_TABLES=()
+        for t in "${FILTER[@]}"; do
+            t="${t## }"; t="${t%% }"
+            if [[ -f "$TMP_DIR/${t}.sql" ]]; then
+                TARGET_TABLES+=("$t")
+            else
+                echo "警告: 表 '$t' 未在 SQL 文件中找到,跳过"
+            fi
+        done
+    else
+        TARGET_TABLES=("${ALL_TABLES[@]}")
+    fi
+
+    if [[ ${#TARGET_TABLES[@]} -eq 0 ]]; then
+        echo "错误: 没有可生成的表"
+        exit 1
+    fi
+
+    echo "即将生成 ${#TARGET_TABLES[@]} 张表: ${TARGET_TABLES[*]}"
+    echo ""
+
+    for t in "${TARGET_TABLES[@]}"; do
+        gen_from_ddl "$TMP_DIR/${t}.sql" "$t"
+    done
+
+    echo ""
+    echo "完成!共生成 ${#TARGET_TABLES[@]} 个 model。"
+    ;;
+
+datasource)
+    URL=""
+    TABLES=""
+    while [[ $# -gt 0 ]]; do
+        case "$1" in
+            -url)   URL="$2";    shift 2 ;;
+            -table) TABLES="$2"; shift 2 ;;
+            *)      echo "未知选项: $1"; usage ;;
+        esac
+    done
+
+    if [[ -z "$URL" ]]; then
+        echo "错误: datasource 模式必须指定 -url"
+        usage
+    fi
+    if [[ -z "$TABLES" ]]; then
+        echo "错误: datasource 模式必须指定 -table"
+        usage
+    fi
+
+    IFS=',' read -ra TARGET_TABLES <<< "$TABLES"
+
+    echo "连接数据库..."
+    echo "即将生成 ${#TARGET_TABLES[@]} 张表: ${TARGET_TABLES[*]}"
+    echo ""
+
+    for t in "${TARGET_TABLES[@]}"; do
+        t="${t## }"; t="${t%% }"
+        gen_from_datasource "$URL" "$t"
+    done
+
+    echo ""
+    echo "完成!共生成 ${#TARGET_TABLES[@]} 个 model。"
+    ;;
+
+*)
+    echo "未知模式: $MODE"
+    usage
+    ;;
+esac

+ 106 - 0
go.mod

@@ -0,0 +1,106 @@
+module perms-system-server
+
+go 1.25.0
+
+require (
+	github.com/go-sql-driver/mysql v1.9.3
+	github.com/golang-jwt/jwt/v4 v4.5.2
+	github.com/stretchr/testify v1.11.1
+	github.com/zeromicro/go-zero v1.10.1
+	go.uber.org/mock v0.6.0
+	golang.org/x/crypto v0.48.0
+	google.golang.org/grpc v1.79.3
+	google.golang.org/protobuf v1.36.11
+)
+
+require (
+	filippo.io/edwards25519 v1.1.0 // indirect
+	github.com/alicebob/miniredis/v2 v2.37.0 // indirect
+	github.com/beorn7/perks v1.0.1 // indirect
+	github.com/cenkalti/backoff/v5 v5.0.3 // indirect
+	github.com/cespare/xxhash/v2 v2.3.0 // indirect
+	github.com/coreos/go-semver v0.3.1 // indirect
+	github.com/coreos/go-systemd/v22 v22.5.0 // indirect
+	github.com/davecgh/go-spew v1.1.1 // indirect
+	github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
+	github.com/emicklei/go-restful/v3 v3.12.2 // indirect
+	github.com/fatih/color v1.18.0 // indirect
+	github.com/fxamacker/cbor/v2 v2.9.0 // indirect
+	github.com/go-logr/logr v1.4.3 // indirect
+	github.com/go-logr/stdr v1.2.2 // indirect
+	github.com/go-openapi/jsonpointer v0.21.0 // indirect
+	github.com/go-openapi/jsonreference v0.20.2 // indirect
+	github.com/go-openapi/swag v0.23.0 // indirect
+	github.com/gogo/protobuf v1.3.2 // indirect
+	github.com/golang/protobuf v1.5.4 // indirect
+	github.com/google/gnostic-models v0.7.0 // indirect
+	github.com/google/go-cmp v0.7.0 // indirect
+	github.com/google/uuid v1.6.0 // indirect
+	github.com/grafana/pyroscope-go v1.2.8 // indirect
+	github.com/grafana/pyroscope-go/godeltaprof v0.1.9 // indirect
+	github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.7 // indirect
+	github.com/josharian/intern v1.0.0 // indirect
+	github.com/json-iterator/go v1.1.12 // indirect
+	github.com/klauspost/compress v1.18.0 // indirect
+	github.com/mailru/easyjson v0.7.7 // indirect
+	github.com/mattn/go-colorable v0.1.13 // indirect
+	github.com/mattn/go-isatty v0.0.20 // indirect
+	github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
+	github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect
+	github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
+	github.com/openzipkin/zipkin-go v0.4.3 // indirect
+	github.com/pelletier/go-toml/v2 v2.3.0 // indirect
+	github.com/pkg/errors v0.9.1 // indirect
+	github.com/pmezard/go-difflib v1.0.0 // indirect
+	github.com/prometheus/client_golang v1.23.2 // indirect
+	github.com/prometheus/client_model v0.6.2 // indirect
+	github.com/prometheus/common v0.66.1 // indirect
+	github.com/prometheus/procfs v0.16.1 // indirect
+	github.com/redis/go-redis/v9 v9.18.0 // indirect
+	github.com/spaolacci/murmur3 v1.1.0 // indirect
+	github.com/titanous/json5 v1.0.0 // indirect
+	github.com/x448/float16 v0.8.4 // indirect
+	github.com/yuin/gopher-lua v1.1.1 // indirect
+	go.etcd.io/etcd/api/v3 v3.5.21 // indirect
+	go.etcd.io/etcd/client/pkg/v3 v3.5.21 // indirect
+	go.etcd.io/etcd/client/v3 v3.5.21 // indirect
+	go.opentelemetry.io/auto/sdk v1.2.1 // indirect
+	go.opentelemetry.io/otel v1.40.0 // indirect
+	go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.40.0 // indirect
+	go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.40.0 // indirect
+	go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.40.0 // indirect
+	go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.40.0 // indirect
+	go.opentelemetry.io/otel/exporters/zipkin v1.40.0 // indirect
+	go.opentelemetry.io/otel/metric v1.40.0 // indirect
+	go.opentelemetry.io/otel/sdk v1.40.0 // indirect
+	go.opentelemetry.io/otel/trace v1.40.0 // indirect
+	go.opentelemetry.io/proto/otlp v1.9.0 // indirect
+	go.uber.org/atomic v1.11.0 // indirect
+	go.uber.org/automaxprocs v1.6.0 // indirect
+	go.uber.org/multierr v1.9.0 // indirect
+	go.uber.org/zap v1.24.0 // indirect
+	go.yaml.in/yaml/v2 v2.4.2 // indirect
+	go.yaml.in/yaml/v3 v3.0.4 // indirect
+	golang.org/x/net v0.50.0 // indirect
+	golang.org/x/oauth2 v0.34.0 // indirect
+	golang.org/x/sys v0.41.0 // indirect
+	golang.org/x/term v0.40.0 // indirect
+	golang.org/x/text v0.34.0 // indirect
+	golang.org/x/time v0.14.0 // indirect
+	google.golang.org/genproto/googleapis/api v0.0.0-20260128011058-8636f8732409 // indirect
+	google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409 // indirect
+	gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect
+	gopkg.in/inf.v0 v0.9.1 // indirect
+	gopkg.in/yaml.v2 v2.4.0 // indirect
+	gopkg.in/yaml.v3 v3.0.1 // indirect
+	k8s.io/api v0.34.3 // indirect
+	k8s.io/apimachinery v0.34.3 // indirect
+	k8s.io/client-go v0.34.3 // indirect
+	k8s.io/klog/v2 v2.130.1 // indirect
+	k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b // indirect
+	k8s.io/utils v0.0.0-20260319190234-28399d86e0b5 // indirect
+	sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect
+	sigs.k8s.io/randfill v1.0.0 // indirect
+	sigs.k8s.io/structured-merge-diff/v6 v6.3.0 // indirect
+	sigs.k8s.io/yaml v1.6.0 // indirect
+)

+ 379 - 0
go.sum

@@ -0,0 +1,379 @@
+cel.dev/expr v0.25.1/go.mod h1:hrXvqGP6G6gyx8UAHSHJ5RGk//1Oj5nXQ2NI02Nrsg4=
+cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10=
+filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
+filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
+github.com/DATA-DOG/go-sqlmock v1.5.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7OputlJIzU=
+github.com/DATA-DOG/go-sqlmock v1.5.2/go.mod h1:88MAG/4G7SMwSE3CeA0ZKzrT5CiOU3OJ+JlNzwDqpNU=
+github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.30.0/go.mod h1:P4WPRUkOhJC13W//jWpyfJNDAIpvRbAUIYLX/4jtlE0=
+github.com/IBM/sarama v1.43.1/go.mod h1:GG5q1RURtDNPz8xxJs3mgX6Ytak8Z9eLhAkJPObe2xE=
+github.com/NYTimes/gziphandler v1.1.1/go.mod h1:n/CVRwUEOgIxrgPvAQhUUr9oeUtvrhMomdKFjzJNB0c=
+github.com/alecthomas/kingpin/v2 v2.4.0/go.mod h1:0gyi0zQnjuFk8xrkNKamJoyUo382HRL7ATRpFZCw6tE=
+github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137/go.mod h1:OMCwj8VM1Kc9e19TLln2VL61YJF0x1XFtfdL4JdbSyE=
+github.com/alicebob/miniredis/v2 v2.37.0 h1:RheObYW32G1aiJIj81XVt78ZHJpHonHLHW7OLIshq68=
+github.com/alicebob/miniredis/v2 v2.37.0/go.mod h1:TcL7YfarKPGDAthEtl5NBeHZfeUQj6OXMm/+iu5cLMM=
+github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
+github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
+github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8=
+github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
+github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
+github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
+github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
+github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
+github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
+github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
+github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM=
+github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
+github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
+github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
+github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
+github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
+github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
+github.com/cncf/xds/go v0.0.0-20251210132809-ee656c7534f5/go.mod h1:KdCmV+x/BuvyMxRnYBlmVaq4OLiKW6iRQfvC62cvdkI=
+github.com/coreos/go-semver v0.3.1 h1:yi21YpKnrx1gt5R+la8n5WgS0kCrsPp33dmEyHReZr4=
+github.com/coreos/go-semver v0.3.1/go.mod h1:irMmmIw/7yzSRPWryHsK7EYSg09caPQL03VsM8rvUec=
+github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs=
+github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
+github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
+github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
+github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
+github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
+github.com/eapache/go-resiliency v1.6.0/go.mod h1:5yPzW0MIvSe0JDsv0v+DvcjEv2FyD6iZYSs1ZI+iQho=
+github.com/eapache/go-xerial-snappy v0.0.0-20230731223053-c322873962e3/go.mod h1:YvSRo5mw33fLEx1+DlK6L2VV43tJt5Eyel9n9XBcR+0=
+github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I=
+github.com/emicklei/go-restful/v3 v3.12.2 h1:DhwDP0vY3k8ZzE0RunuJy8GhNpPL6zqLkDf9B/a0/xU=
+github.com/emicklei/go-restful/v3 v3.12.2/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
+github.com/envoyproxy/go-control-plane v0.14.0/go.mod h1:NcS5X47pLl/hfqxU70yPwL9ZMkUlwlKxtAohpi2wBEU=
+github.com/envoyproxy/go-control-plane/envoy v1.36.0/go.mod h1:ty89S1YCCVruQAm9OtKeEkQLTb+Lkz0k8v9W0Oxsv98=
+github.com/envoyproxy/go-control-plane/ratelimit v0.1.0/go.mod h1:Wk+tMFAFbCXaJPzVVHnPgRKdUdwW/KdbRt94AzgRee4=
+github.com/envoyproxy/protoc-gen-validate v1.3.0/go.mod h1:HvYl7zwPa5mffgyeTUHA9zHIH36nmrm7oCbo4YKoSWA=
+github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
+github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
+github.com/fullstorydev/grpcurl v1.9.3/go.mod h1:/b4Wxe8bG6ndAjlfSUjwseQReUDUvBJiFEB7UllOlUE=
+github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM=
+github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
+github.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=
+github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
+github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
+github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
+github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
+github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
+github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs=
+github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ=
+github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY=
+github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE=
+github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k=
+github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14=
+github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE=
+github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ=
+github.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo=
+github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU=
+github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI=
+github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls=
+github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
+github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
+github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
+github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
+github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
+github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI=
+github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
+github.com/golang/glog v1.2.5/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwmO+w=
+github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
+github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
+github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
+github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4=
+github.com/google/gnostic-models v0.7.0 h1:qwTtogB15McXDaNqTZdzPJRHvaVJlAl+HVQnLmJEJxo=
+github.com/google/gnostic-models v0.7.0/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ=
+github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
+github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
+github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
+github.com/google/jsonschema-go v0.4.2/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE=
+github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db h1:097atOisP2aRj7vFgYQBbFN4U4JNXUNYpxael3UzMyo=
+github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144=
+github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
+github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674/go.mod h1:r4w70xmWCQKmi1ONH4KIaBptdivuRPyosB9RmPlGEwA=
+github.com/grafana/pyroscope-go v1.2.8 h1:UvCwIhlx9DeV7F6TW/z8q1Mi4PIm3vuUJ2ZlCEvmA4M=
+github.com/grafana/pyroscope-go v1.2.8/go.mod h1:SSi59eQ1/zmKoY/BKwa5rSFsJaq+242Bcrr4wPix1g8=
+github.com/grafana/pyroscope-go/godeltaprof v0.1.9 h1:c1Us8i6eSmkW+Ez05d3co8kasnuOY813tbMN8i/a3Og=
+github.com/grafana/pyroscope-go/godeltaprof v0.1.9/go.mod h1:2+l7K7twW49Ct4wFluZD3tZ6e0SjanjcUUBPVD/UuGU=
+github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA=
+github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
+github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
+github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.7 h1:X+2YciYSxvMQK0UZ7sg45ZVabVZBeBuvMkmuI2V3Fak=
+github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.7/go.mod h1:lW34nIZuQ8UDPdkon5fmfp2l3+ZkQ2me/+oecHYLOII=
+github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 h1:2VTzZjLZBgl62/EtslCrtky5vbi9dd7HrQPQIx6wqiw=
+github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542/go.mod h1:Ow0tF8D4Kplbc8s8sSb3V2oUCygFHVp8gC3Dn6U4MNI=
+github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
+github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
+github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
+github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
+github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
+github.com/jackc/pgx/v5 v5.8.0/go.mod h1:QVeDInX2m9VyzvNeiCJVjCkNFqzsNb43204HshNSZKw=
+github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
+github.com/jcmturner/aescts/v2 v2.0.0/go.mod h1:AiaICIRyfYg35RUkr8yESTqvSy7csK90qZ5xfvvsoNs=
+github.com/jcmturner/dnsutils/v2 v2.0.0/go.mod h1:b0TnjGOvI/n42bZa+hmXL+kFJZsFT7G4t3HTlQ184QM=
+github.com/jcmturner/gofork v1.7.6/go.mod h1:1622LH6i/EZqLloHfE7IeZ0uEJwMSUyQ/nDd82IeqRo=
+github.com/jcmturner/gokrb5/v8 v8.4.4/go.mod h1:1btQEpgT6k+unzCwX1KdWMEwPPkkgBtP+F6aCACiMrs=
+github.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc=
+github.com/jhump/protoreflect v1.18.0/go.mod h1:ezWcltJIVF4zYdIFM+D/sHV4Oh5LNU08ORzCGfwvTz8=
+github.com/jhump/protoreflect/v2 v2.0.0-beta.1/go.mod h1:D9LBEowZyv8/iSu97FU2zmXG3JxVTmNw21mu63niFzU=
+github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
+github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
+github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=
+github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
+github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
+github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
+github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
+github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
+github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
+github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
+github.com/klauspost/cpuid/v2 v2.0.9 h1:lgaqFMSdTdQYdZ04uHyN2d/eKdOMyi2YLSvlQIBFYa4=
+github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
+github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
+github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
+github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
+github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
+github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
+github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
+github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
+github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
+github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
+github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
+github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
+github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
+github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
+github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
+github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
+github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
+github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
+github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
+github.com/moby/spdystream v0.5.0/go.mod h1:xBAYlnt/ay+11ShkdFKNAG7LsyK/tmNBVvVOwrfMgdI=
+github.com/modelcontextprotocol/go-sdk v1.4.0/go.mod h1:Nxc2n+n/GdCebUaqCOhTetptS17SXXNu9IfNTaLDi1E=
+github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
+github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
+github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
+github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
+github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee h1:W5t00kpgFdJifH4BDsTlE89Zl93FEloxaWZfGcifgq8=
+github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
+github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
+github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
+github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
+github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw=
+github.com/onsi/ginkgo/v2 v2.21.0 h1:7rg/4f3rB88pb5obDgNZrNHrQ4e6WpjonchcpuBRnZM=
+github.com/onsi/ginkgo/v2 v2.21.0/go.mod h1:7Du3c42kxCUegi0IImZ1wUQzMBVecgIHjR1C+NkhLQo=
+github.com/onsi/gomega v1.35.1 h1:Cwbd75ZBPxFSuZ6T+rN/WCb/gOc6YgFBXLlZLhC7Ds4=
+github.com/onsi/gomega v1.35.1/go.mod h1:PvZbdDc8J6XJEpDK4HCuRBm8a6Fzp9/DmhC9C7yFlog=
+github.com/openzipkin/zipkin-go v0.4.3 h1:9EGwpqkgnwdEIJ+Od7QVSEIH+ocmm5nPat0G7sjsSdg=
+github.com/openzipkin/zipkin-go v0.4.3/go.mod h1:M9wCJZFWCo2RiY+o1eBCEMe0Dp2S5LDHcMZmk3RmK7c=
+github.com/pelletier/go-toml/v2 v2.3.0 h1:k59bC/lIZREW0/iVaQR8nDHxVq8OVlIzYCOJf421CaM=
+github.com/pelletier/go-toml/v2 v2.3.0/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
+github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU=
+github.com/petermattis/goid v0.0.0-20260113132338-7c7de50cc741/go.mod h1:pxMtw7cyUw6B2bRH0ZBANSPg+AoSud1I1iyJHI69jH4=
+github.com/pierrec/lz4/v4 v4.1.21/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
+github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
+github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8=
+github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g=
+github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U=
+github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o=
+github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg=
+github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
+github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
+github.com/prometheus/common v0.66.1 h1:h5E0h5/Y8niHc5DlaLlWLArTQI7tMrsfQjHV+d9ZoGs=
+github.com/prometheus/common v0.66.1/go.mod h1:gcaUsgf3KfRSwHY4dIMXLPV0K/Wg1oZ8+SbZk/HH/dA=
+github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg=
+github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is=
+github.com/rabbitmq/amqp091-go v1.9.0/go.mod h1:+jPrT9iY2eLjRaMSRHUhc3z14E/l85kv/f+6luSD3pc=
+github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
+github.com/redis/go-redis/v9 v9.18.0 h1:pMkxYPkEbMPwRdenAzUNyFNrDgHx9U+DrBabWNfSRQs=
+github.com/redis/go-redis/v9 v9.18.0/go.mod h1:k3ufPphLU5YXwNTUcCRXGxUoF1fqxnhFQmscfkCoDA0=
+github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
+github.com/robertkrimen/otto v0.2.1 h1:FVP0PJ0AHIjC+N4pKCG9yCDz6LHNPCwi/GKID5pGGF0=
+github.com/robertkrimen/otto v0.2.1/go.mod h1:UPwtJ1Xu7JrLcZjNWN8orJaM5n5YEtqL//farB5FlRY=
+github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
+github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
+github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
+github.com/segmentio/asm v1.1.3/go.mod h1:Ld3L4ZXGNcSLRg4JBsZ3//1+f/TjYl0Mzen/DQy1EJg=
+github.com/segmentio/encoding v0.5.3/go.mod h1:HS1ZKa3kSN32ZHVZ7ZLPLXWvOVIiZtyJnO1gPH1sKt0=
+github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI=
+github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
+github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
+github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
+github.com/spiffe/go-spiffe/v2 v2.6.0/go.mod h1:gm2SeUoMZEtpnzPNs2Csc0D/gX33k1xIx7lEzqblHEs=
+github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
+github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
+github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
+github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
+github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
+github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
+github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
+github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
+github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
+github.com/titanous/json5 v1.0.0 h1:hJf8Su1d9NuI/ffpxgxQfxh/UiBFZX7bMPid0rIL/7s=
+github.com/titanous/json5 v1.0.0/go.mod h1:7JH1M8/LHKc6cyP5o5g3CSaRj+mBrIimTxzpvmckH8c=
+github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
+github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
+github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
+github.com/xdg-go/scram v1.2.0/go.mod h1:3dlrS0iBaWKYVt2ZfA4cj48umJZ+cAEbR6/SjLA88I8=
+github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM=
+github.com/xhit/go-str2duration/v2 v2.1.0/go.mod h1:ohY8p+0f07DiV6Em5LKB0s2YpLtXVyJfNt1+BlmyAsU=
+github.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4=
+github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfSfmXjznFBSZNN13rSJjlIOI1fUNAtF7rmI=
+github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
+github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
+github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
+github.com/yuin/gopher-lua v1.1.1 h1:kYKnWBjvbNP4XLT3+bPEwAXJx262OhaHDWDVOPjL46M=
+github.com/yuin/gopher-lua v1.1.1/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw=
+github.com/zeebo/xxh3 v1.0.2 h1:xZmwmqxHZA8AI603jOQ0tMqmBr9lPeFwGg6d+xy9DC0=
+github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA=
+github.com/zeromicro/go-zero v1.10.1 h1:1nM3ilvYx97GUqyaNH2IQPtfNyK7tp5JvN63c7m6QKU=
+github.com/zeromicro/go-zero v1.10.1/go.mod h1:z41DXmO6gx/Se7Ow5UIwPxcUmpVj3ebhoNCcZ1gfp5k=
+go.etcd.io/etcd/api/v3 v3.5.21 h1:A6O2/JDb3tvHhiIz3xf9nJ7REHvtEFJJ3veW3FbCnS8=
+go.etcd.io/etcd/api/v3 v3.5.21/go.mod h1:c3aH5wcvXv/9dqIw2Y810LDXJfhSYdHQ0vxmP3CCHVY=
+go.etcd.io/etcd/client/pkg/v3 v3.5.21 h1:lPBu71Y7osQmzlflM9OfeIV2JlmpBjqBNlLtcoBqUTc=
+go.etcd.io/etcd/client/pkg/v3 v3.5.21/go.mod h1:BgqT/IXPjK9NkeSDjbzwsHySX3yIle2+ndz28nVsjUs=
+go.etcd.io/etcd/client/v3 v3.5.21 h1:T6b1Ow6fNjOLOtM0xSoKNQt1ASPCLWrF9XMHcH9pEyY=
+go.etcd.io/etcd/client/v3 v3.5.21/go.mod h1:mFYy67IOqmbRf/kRUvsHixzo3iG+1OF2W2+jVIQRAnU=
+go.mongodb.org/mongo-driver/v2 v2.5.0/go.mod h1:yOI9kBsufol30iFsl1slpdq1I0eHPzybRWdyYUs8K/0=
+go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
+go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
+go.opentelemetry.io/contrib/detectors/gcp v1.39.0/go.mod h1:t/OGqzHBa5v6RHZwrDBJ2OirWc+4q/w2fTbLZwAKjTk=
+go.opentelemetry.io/otel v1.40.0 h1:oA5YeOcpRTXq6NN7frwmwFR0Cn3RhTVZvXsP4duvCms=
+go.opentelemetry.io/otel v1.40.0/go.mod h1:IMb+uXZUKkMXdPddhwAHm6UfOwJyh4ct1ybIlV14J0g=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.40.0 h1:QKdN8ly8zEMrByybbQgv8cWBcdAarwmIPZ6FThrWXJs=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.40.0/go.mod h1:bTdK1nhqF76qiPoCCdyFIV+N/sRHYXYCTQc+3VCi3MI=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.40.0 h1:DvJDOPmSWQHWywQS6lKL+pb8s3gBLOZUtw4N+mavW1I=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.40.0/go.mod h1:EtekO9DEJb4/jRyN4v4Qjc2yA7AtfCBuz2FynRUWTXs=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.40.0 h1:wVZXIWjQSeSmMoxF74LzAnpVQOAFDo3pPji9Y4SOFKc=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.40.0/go.mod h1:khvBS2IggMFNwZK/6lEeHg/W57h/IX6J4URh57fuI40=
+go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.40.0 h1:MzfofMZN8ulNqobCmCAVbqVL5syHw+eB2qPRkCMA/fQ=
+go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.40.0/go.mod h1:E73G9UFtKRXrxhBsHtG00TB5WxX57lpsQzogDkqBTz8=
+go.opentelemetry.io/otel/exporters/zipkin v1.40.0 h1:zu+I4j+FdO6xIxBVPeuncQVbjxUM4LiMgv6GwGe9REE=
+go.opentelemetry.io/otel/exporters/zipkin v1.40.0/go.mod h1:zS6cC4nFBYXbu18e7aLfMzubBjOiN7ZcROu477qtMf8=
+go.opentelemetry.io/otel/metric v1.40.0 h1:rcZe317KPftE2rstWIBitCdVp89A2HqjkxR3c11+p9g=
+go.opentelemetry.io/otel/metric v1.40.0/go.mod h1:ib/crwQH7N3r5kfiBZQbwrTge743UDc7DTFVZrrXnqc=
+go.opentelemetry.io/otel/sdk v1.40.0 h1:KHW/jUzgo6wsPh9At46+h4upjtccTmuZCFAc9OJ71f8=
+go.opentelemetry.io/otel/sdk v1.40.0/go.mod h1:Ph7EFdYvxq72Y8Li9q8KebuYUr2KoeyHx0DRMKrYBUE=
+go.opentelemetry.io/otel/sdk/metric v1.40.0 h1:mtmdVqgQkeRxHgRv4qhyJduP3fYJRMX4AtAlbuWdCYw=
+go.opentelemetry.io/otel/sdk/metric v1.40.0/go.mod h1:4Z2bGMf0KSK3uRjlczMOeMhKU2rhUqdWNoKcYrtcBPg=
+go.opentelemetry.io/otel/trace v1.40.0 h1:WA4etStDttCSYuhwvEa8OP8I5EWu24lkOzp+ZYblVjw=
+go.opentelemetry.io/otel/trace v1.40.0/go.mod h1:zeAhriXecNGP/s2SEG3+Y8X9ujcJOTqQ5RgdEJcawiA=
+go.opentelemetry.io/proto/otlp v1.9.0 h1:l706jCMITVouPOqEnii2fIAuO3IVGBRPV5ICjceRb/A=
+go.opentelemetry.io/proto/otlp v1.9.0/go.mod h1:xE+Cx5E/eEHw+ISFkwPLwCZefwVjY+pqKg1qcK03+/4=
+go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
+go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
+go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs=
+go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8=
+go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
+go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
+go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
+go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
+go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI=
+go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ=
+go.uber.org/zap v1.24.0 h1:FiJd5l1UOLj0wCgbSE0rwwXHzEdAZS6hiiSnxJN/D60=
+go.uber.org/zap v1.24.0/go.mod h1:2kMP+WWQ8aoFoedH3T2sq6iJ2yDWpHbP0f6MQbS9Gkg=
+go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI=
+go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU=
+go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
+go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
+golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
+golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
+golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
+golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
+golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
+golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
+golang.org/x/mod v0.32.0/go.mod h1:SgipZ/3h2Ci89DlEtEXWUk/HteuRin+HHhN+WbNhguU=
+golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
+golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60=
+golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM=
+golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw=
+golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
+golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
+golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
+golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
+golang.org/x/term v0.40.0 h1:36e4zGLqU4yhjlmxEaagx2KuYbJq3EwY8K943ZsHcvg=
+golang.org/x/term v0.40.0/go.mod h1:w2P8uVp06p2iyKKuvXIm7N/y0UCRt3UfJTfZ7oOpglM=
+golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
+golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
+golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
+golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
+golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
+golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
+golang.org/x/tools v0.41.0 h1:a9b8iMweWG+S0OBnlU36rzLp20z1Rp10w+IY2czHTQc=
+golang.org/x/tools v0.41.0/go.mod h1:XSY6eDqxVNiYgezAVqqCeihT4j1U2CCsqvH3WhQpnlg=
+golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
+gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
+google.golang.org/genproto v0.0.0-20230822172742-b8732ec3820d/go.mod h1:yZTlhN0tQnXo3h00fuXNCxJdLdIdnVFVBaRJ5LWBbw4=
+google.golang.org/genproto/googleapis/api v0.0.0-20260128011058-8636f8732409 h1:merA0rdPeUV3YIIfHHcH4qBkiQAc1nfCKSI7lB4cV2M=
+google.golang.org/genproto/googleapis/api v0.0.0-20260128011058-8636f8732409/go.mod h1:fl8J1IvUjCilwZzQowmw2b7HQB2eAuYBabMXzWurF+I=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409 h1:H86B94AW+VfJWDqFeEbBPhEtHzJwJfTbgE2lZa54ZAQ=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
+google.golang.org/grpc v1.79.3 h1:sybAEdRIEtvcD68Gx7dmnwjZKlyfuc61Dyo9pGXXkKE=
+google.golang.org/grpc v1.79.3/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ=
+google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
+google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
+gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
+gopkg.in/cheggaaa/pb.v1 v1.0.28/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw=
+gopkg.in/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSPG+6V4=
+gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M=
+gopkg.in/h2non/gock.v1 v1.1.2 h1:jBbHXgGBK/AoPVfJh5x4r/WxIrElvbLel8TCZkkZJoY=
+gopkg.in/h2non/gock.v1 v1.1.2/go.mod h1:n7UGz/ckNChHiK05rDoiC4MYSunEC/lyaUm2WWaDva0=
+gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
+gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
+gopkg.in/sourcemap.v1 v1.0.5 h1:inv58fC9f9J3TK2Y2R1NPntXEn3/wjWHkonhIUODNTI=
+gopkg.in/sourcemap.v1 v1.0.5/go.mod h1:2RlvNNSMglmRrcvhfuzp4hQHwOtjxlbjX7UPY/GXb78=
+gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
+gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
+gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+k8s.io/api v0.34.3 h1:D12sTP257/jSH2vHV2EDYrb16bS7ULlHpdNdNhEw2S4=
+k8s.io/api v0.34.3/go.mod h1:PyVQBF886Q5RSQZOim7DybQjAbVs8g7gwJNhGtY5MBk=
+k8s.io/apimachinery v0.34.3 h1:/TB+SFEiQvN9HPldtlWOTp0hWbJ+fjU+wkxysf/aQnE=
+k8s.io/apimachinery v0.34.3/go.mod h1:/GwIlEcWuTX9zKIg2mbw0LRFIsXwrfoVxn+ef0X13lw=
+k8s.io/client-go v0.34.3 h1:wtYtpzy/OPNYf7WyNBTj3iUA0XaBHVqhv4Iv3tbrF5A=
+k8s.io/client-go v0.34.3/go.mod h1:OxxeYagaP9Kdf78UrKLa3YZixMCfP6bgPwPwNBQBzpM=
+k8s.io/gengo/v2 v2.0.0-20250604051438-85fd79dbfd9f/go.mod h1:EJykeLsmFC60UQbYJezXkEsG2FLrt0GPNkU5iK5GWxU=
+k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk=
+k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE=
+k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b h1:MloQ9/bdJyIu9lb1PzujOPolHyvO06MXG5TUIj2mNAA=
+k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b/go.mod h1:UZ2yyWbFTpuhSbFhv24aGNOdoRdJZgsIObGBUaYVsts=
+k8s.io/utils v0.0.0-20260319190234-28399d86e0b5 h1:kBawHLSnx/mYHmRnNUf9d4CpjREbeZuxoSGOX/J+aYM=
+k8s.io/utils v0.0.0-20260319190234-28399d86e0b5/go.mod h1:xDxuJ0whA3d0I4mf/C4ppKHxXynQ+fxnkmQH0vTHnuk=
+sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 h1:gBQPwqORJ8d8/YNZWEjoZs7npUVDpVXUUOFfW6CgAqE=
+sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg=
+sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU=
+sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY=
+sigs.k8s.io/structured-merge-diff/v6 v6.3.0 h1:jTijUJbW353oVOd9oTlifJqOGEkUw2jB/fXCbTiQEco=
+sigs.k8s.io/structured-merge-diff/v6 v6.3.0/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE=
+sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs=
+sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4=

+ 30 - 0
internal/config/config.go

@@ -0,0 +1,30 @@
+package config
+
+import (
+	"github.com/zeromicro/go-zero/core/stores/cache"
+	"github.com/zeromicro/go-zero/rest"
+	"github.com/zeromicro/go-zero/zrpc"
+)
+
+type CacheRedisConf struct {
+	Nodes     cache.CacheConf
+	KeyPrefix string `json:",optional"`
+}
+
+type Config struct {
+	rest.RestConf
+	RpcServerConf zrpc.RpcServerConf
+
+	MySQL struct {
+		DataSource string
+	}
+
+	CacheRedis CacheRedisConf
+
+	Auth struct {
+		AccessSecret  string
+		AccessExpire  int64
+		RefreshSecret string
+		RefreshExpire int64
+	}
+}

+ 45 - 0
internal/consts/consts.go

@@ -0,0 +1,45 @@
+package consts
+
+// Status 通用状态 (用于 user, product, perm, role, dept, member)
+const (
+	StatusEnabled  = 1 // 启用
+	StatusDisabled = 2 // 禁用/冻结
+)
+
+// IsSuperAdmin 是否超级管理员
+const (
+	IsSuperAdminYes = 1 // 是超级管理员
+	IsSuperAdminNo  = 2 // 非超级管理员
+)
+
+// MustChangePassword 是否需要修改密码
+const (
+	MustChangePasswordYes = 1 // 需要修改密码
+	MustChangePasswordNo  = 2 // 不需要修改密码
+)
+
+// MemberType 成员类型
+const (
+	MemberTypeSuperAdmin = "SUPER_ADMIN"
+	MemberTypeAdmin      = "ADMIN"
+	MemberTypeDeveloper  = "DEVELOPER"
+	MemberTypeMember     = "MEMBER"
+)
+
+// DeptType 部门类型
+const (
+	DeptTypeNormal = "NORMAL" // 普通部门
+	DeptTypeDev    = "DEV"    // 研发部门(成员加入产品后自动拥有全权限)
+)
+
+// PermEffect 权限效果
+const (
+	PermEffectAllow = "ALLOW"
+	PermEffectDeny  = "DENY"
+)
+
+// TokenType JWT token 类型
+const (
+	TokenTypeAccess  = "access"
+	TokenTypeRefresh = "refresh"
+)

+ 32 - 0
internal/handler/auth/changePasswordHandler.go

@@ -0,0 +1,32 @@
+// Code scaffolded by goctl. Safe to edit.
+// goctl 1.10.0
+
+package auth
+
+import (
+	"net/http"
+
+	"github.com/zeromicro/go-zero/rest/httpx"
+	"perms-system-server/internal/logic/auth"
+	"perms-system-server/internal/response"
+	"perms-system-server/internal/svc"
+	"perms-system-server/internal/types"
+)
+
+func ChangePasswordHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
+	return func(w http.ResponseWriter, r *http.Request) {
+		var req types.ChangePasswordReq
+		if err := httpx.Parse(r, &req); err != nil {
+			httpx.ErrorCtx(r.Context(), w, response.ErrBadRequest(err.Error()))
+			return
+		}
+
+		l := auth.NewChangePasswordLogic(r.Context(), svcCtx)
+		err := l.ChangePassword(&req)
+		if err != nil {
+			httpx.ErrorCtx(r.Context(), w, err)
+		} else {
+			httpx.Ok(w)
+		}
+	}
+}

+ 24 - 0
internal/handler/auth/userInfoHandler.go

@@ -0,0 +1,24 @@
+// Code scaffolded by goctl. Safe to edit.
+// goctl 1.10.0
+
+package auth
+
+import (
+	"net/http"
+
+	"github.com/zeromicro/go-zero/rest/httpx"
+	"perms-system-server/internal/logic/auth"
+	"perms-system-server/internal/svc"
+)
+
+func UserInfoHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
+	return func(w http.ResponseWriter, r *http.Request) {
+		l := auth.NewUserInfoLogic(r.Context(), svcCtx)
+		resp, err := l.UserInfo()
+		if err != nil {
+			httpx.ErrorCtx(r.Context(), w, err)
+		} else {
+			httpx.OkJsonCtx(r.Context(), w, resp)
+		}
+	}
+}

+ 32 - 0
internal/handler/dept/createDeptHandler.go

@@ -0,0 +1,32 @@
+// Code scaffolded by goctl. Safe to edit.
+// goctl 1.10.0
+
+package dept
+
+import (
+	"net/http"
+
+	"github.com/zeromicro/go-zero/rest/httpx"
+	"perms-system-server/internal/logic/dept"
+	"perms-system-server/internal/response"
+	"perms-system-server/internal/svc"
+	"perms-system-server/internal/types"
+)
+
+func CreateDeptHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
+	return func(w http.ResponseWriter, r *http.Request) {
+		var req types.CreateDeptReq
+		if err := httpx.Parse(r, &req); err != nil {
+			httpx.ErrorCtx(r.Context(), w, response.ErrBadRequest(err.Error()))
+			return
+		}
+
+		l := dept.NewCreateDeptLogic(r.Context(), svcCtx)
+		resp, err := l.CreateDept(&req)
+		if err != nil {
+			httpx.ErrorCtx(r.Context(), w, err)
+		} else {
+			httpx.OkJsonCtx(r.Context(), w, resp)
+		}
+	}
+}

+ 32 - 0
internal/handler/dept/deleteDeptHandler.go

@@ -0,0 +1,32 @@
+// Code scaffolded by goctl. Safe to edit.
+// goctl 1.10.0
+
+package dept
+
+import (
+	"net/http"
+
+	"github.com/zeromicro/go-zero/rest/httpx"
+	"perms-system-server/internal/logic/dept"
+	"perms-system-server/internal/response"
+	"perms-system-server/internal/svc"
+	"perms-system-server/internal/types"
+)
+
+func DeleteDeptHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
+	return func(w http.ResponseWriter, r *http.Request) {
+		var req types.DeleteDeptReq
+		if err := httpx.Parse(r, &req); err != nil {
+			httpx.ErrorCtx(r.Context(), w, response.ErrBadRequest(err.Error()))
+			return
+		}
+
+		l := dept.NewDeleteDeptLogic(r.Context(), svcCtx)
+		err := l.DeleteDept(&req)
+		if err != nil {
+			httpx.ErrorCtx(r.Context(), w, err)
+		} else {
+			httpx.Ok(w)
+		}
+	}
+}

+ 24 - 0
internal/handler/dept/deptTreeHandler.go

@@ -0,0 +1,24 @@
+// Code scaffolded by goctl. Safe to edit.
+// goctl 1.10.0
+
+package dept
+
+import (
+	"net/http"
+
+	"github.com/zeromicro/go-zero/rest/httpx"
+	"perms-system-server/internal/logic/dept"
+	"perms-system-server/internal/svc"
+)
+
+func DeptTreeHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
+	return func(w http.ResponseWriter, r *http.Request) {
+		l := dept.NewDeptTreeLogic(r.Context(), svcCtx)
+		resp, err := l.DeptTree()
+		if err != nil {
+			httpx.ErrorCtx(r.Context(), w, err)
+		} else {
+			httpx.OkJsonCtx(r.Context(), w, resp)
+		}
+	}
+}

+ 32 - 0
internal/handler/dept/updateDeptHandler.go

@@ -0,0 +1,32 @@
+// Code scaffolded by goctl. Safe to edit.
+// goctl 1.10.0
+
+package dept
+
+import (
+	"net/http"
+
+	"github.com/zeromicro/go-zero/rest/httpx"
+	"perms-system-server/internal/logic/dept"
+	"perms-system-server/internal/response"
+	"perms-system-server/internal/svc"
+	"perms-system-server/internal/types"
+)
+
+func UpdateDeptHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
+	return func(w http.ResponseWriter, r *http.Request) {
+		var req types.UpdateDeptReq
+		if err := httpx.Parse(r, &req); err != nil {
+			httpx.ErrorCtx(r.Context(), w, response.ErrBadRequest(err.Error()))
+			return
+		}
+
+		l := dept.NewUpdateDeptLogic(r.Context(), svcCtx)
+		err := l.UpdateDept(&req)
+		if err != nil {
+			httpx.ErrorCtx(r.Context(), w, err)
+		} else {
+			httpx.Ok(w)
+		}
+	}
+}

+ 32 - 0
internal/handler/member/addMemberHandler.go

@@ -0,0 +1,32 @@
+// Code scaffolded by goctl. Safe to edit.
+// goctl 1.10.0
+
+package member
+
+import (
+	"net/http"
+
+	"github.com/zeromicro/go-zero/rest/httpx"
+	"perms-system-server/internal/logic/member"
+	"perms-system-server/internal/response"
+	"perms-system-server/internal/svc"
+	"perms-system-server/internal/types"
+)
+
+func AddMemberHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
+	return func(w http.ResponseWriter, r *http.Request) {
+		var req types.AddMemberReq
+		if err := httpx.Parse(r, &req); err != nil {
+			httpx.ErrorCtx(r.Context(), w, response.ErrBadRequest(err.Error()))
+			return
+		}
+
+		l := member.NewAddMemberLogic(r.Context(), svcCtx)
+		resp, err := l.AddMember(&req)
+		if err != nil {
+			httpx.ErrorCtx(r.Context(), w, err)
+		} else {
+			httpx.OkJsonCtx(r.Context(), w, resp)
+		}
+	}
+}

+ 32 - 0
internal/handler/member/memberListHandler.go

@@ -0,0 +1,32 @@
+// Code scaffolded by goctl. Safe to edit.
+// goctl 1.10.0
+
+package member
+
+import (
+	"net/http"
+
+	"github.com/zeromicro/go-zero/rest/httpx"
+	"perms-system-server/internal/logic/member"
+	"perms-system-server/internal/response"
+	"perms-system-server/internal/svc"
+	"perms-system-server/internal/types"
+)
+
+func MemberListHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
+	return func(w http.ResponseWriter, r *http.Request) {
+		var req types.MemberListReq
+		if err := httpx.Parse(r, &req); err != nil {
+			httpx.ErrorCtx(r.Context(), w, response.ErrBadRequest(err.Error()))
+			return
+		}
+
+		l := member.NewMemberListLogic(r.Context(), svcCtx)
+		resp, err := l.MemberList(&req)
+		if err != nil {
+			httpx.ErrorCtx(r.Context(), w, err)
+		} else {
+			httpx.OkJsonCtx(r.Context(), w, resp)
+		}
+	}
+}

+ 32 - 0
internal/handler/member/removeMemberHandler.go

@@ -0,0 +1,32 @@
+// Code scaffolded by goctl. Safe to edit.
+// goctl 1.10.0
+
+package member
+
+import (
+	"net/http"
+
+	"github.com/zeromicro/go-zero/rest/httpx"
+	"perms-system-server/internal/logic/member"
+	"perms-system-server/internal/response"
+	"perms-system-server/internal/svc"
+	"perms-system-server/internal/types"
+)
+
+func RemoveMemberHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
+	return func(w http.ResponseWriter, r *http.Request) {
+		var req types.RemoveMemberReq
+		if err := httpx.Parse(r, &req); err != nil {
+			httpx.ErrorCtx(r.Context(), w, response.ErrBadRequest(err.Error()))
+			return
+		}
+
+		l := member.NewRemoveMemberLogic(r.Context(), svcCtx)
+		err := l.RemoveMember(&req)
+		if err != nil {
+			httpx.ErrorCtx(r.Context(), w, err)
+		} else {
+			httpx.Ok(w)
+		}
+	}
+}

+ 32 - 0
internal/handler/member/updateMemberHandler.go

@@ -0,0 +1,32 @@
+// Code scaffolded by goctl. Safe to edit.
+// goctl 1.10.0
+
+package member
+
+import (
+	"net/http"
+
+	"github.com/zeromicro/go-zero/rest/httpx"
+	"perms-system-server/internal/logic/member"
+	"perms-system-server/internal/response"
+	"perms-system-server/internal/svc"
+	"perms-system-server/internal/types"
+)
+
+func UpdateMemberHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
+	return func(w http.ResponseWriter, r *http.Request) {
+		var req types.UpdateMemberReq
+		if err := httpx.Parse(r, &req); err != nil {
+			httpx.ErrorCtx(r.Context(), w, response.ErrBadRequest(err.Error()))
+			return
+		}
+
+		l := member.NewUpdateMemberLogic(r.Context(), svcCtx)
+		err := l.UpdateMember(&req)
+		if err != nil {
+			httpx.ErrorCtx(r.Context(), w, err)
+		} else {
+			httpx.Ok(w)
+		}
+	}
+}

+ 32 - 0
internal/handler/perm/permListHandler.go

@@ -0,0 +1,32 @@
+// Code scaffolded by goctl. Safe to edit.
+// goctl 1.10.0
+
+package perm
+
+import (
+	"net/http"
+
+	"github.com/zeromicro/go-zero/rest/httpx"
+	"perms-system-server/internal/logic/perm"
+	"perms-system-server/internal/response"
+	"perms-system-server/internal/svc"
+	"perms-system-server/internal/types"
+)
+
+func PermListHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
+	return func(w http.ResponseWriter, r *http.Request) {
+		var req types.PermListReq
+		if err := httpx.Parse(r, &req); err != nil {
+			httpx.ErrorCtx(r.Context(), w, response.ErrBadRequest(err.Error()))
+			return
+		}
+
+		l := perm.NewPermListLogic(r.Context(), svcCtx)
+		resp, err := l.PermList(&req)
+		if err != nil {
+			httpx.ErrorCtx(r.Context(), w, err)
+		} else {
+			httpx.OkJsonCtx(r.Context(), w, resp)
+		}
+	}
+}

+ 32 - 0
internal/handler/product/createProductHandler.go

@@ -0,0 +1,32 @@
+// Code scaffolded by goctl. Safe to edit.
+// goctl 1.10.0
+
+package product
+
+import (
+	"net/http"
+
+	"github.com/zeromicro/go-zero/rest/httpx"
+	"perms-system-server/internal/logic/product"
+	"perms-system-server/internal/response"
+	"perms-system-server/internal/svc"
+	"perms-system-server/internal/types"
+)
+
+func CreateProductHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
+	return func(w http.ResponseWriter, r *http.Request) {
+		var req types.CreateProductReq
+		if err := httpx.Parse(r, &req); err != nil {
+			httpx.ErrorCtx(r.Context(), w, response.ErrBadRequest(err.Error()))
+			return
+		}
+
+		l := product.NewCreateProductLogic(r.Context(), svcCtx)
+		resp, err := l.CreateProduct(&req)
+		if err != nil {
+			httpx.ErrorCtx(r.Context(), w, err)
+		} else {
+			httpx.OkJsonCtx(r.Context(), w, resp)
+		}
+	}
+}

+ 32 - 0
internal/handler/product/productDetailHandler.go

@@ -0,0 +1,32 @@
+// Code scaffolded by goctl. Safe to edit.
+// goctl 1.10.0
+
+package product
+
+import (
+	"net/http"
+
+	"github.com/zeromicro/go-zero/rest/httpx"
+	"perms-system-server/internal/logic/product"
+	"perms-system-server/internal/response"
+	"perms-system-server/internal/svc"
+	"perms-system-server/internal/types"
+)
+
+func ProductDetailHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
+	return func(w http.ResponseWriter, r *http.Request) {
+		var req types.ProductDetailReq
+		if err := httpx.Parse(r, &req); err != nil {
+			httpx.ErrorCtx(r.Context(), w, response.ErrBadRequest(err.Error()))
+			return
+		}
+
+		l := product.NewProductDetailLogic(r.Context(), svcCtx)
+		resp, err := l.ProductDetail(&req)
+		if err != nil {
+			httpx.ErrorCtx(r.Context(), w, err)
+		} else {
+			httpx.OkJsonCtx(r.Context(), w, resp)
+		}
+	}
+}

+ 32 - 0
internal/handler/product/productListHandler.go

@@ -0,0 +1,32 @@
+// Code scaffolded by goctl. Safe to edit.
+// goctl 1.10.0
+
+package product
+
+import (
+	"net/http"
+
+	"github.com/zeromicro/go-zero/rest/httpx"
+	"perms-system-server/internal/logic/product"
+	"perms-system-server/internal/response"
+	"perms-system-server/internal/svc"
+	"perms-system-server/internal/types"
+)
+
+func ProductListHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
+	return func(w http.ResponseWriter, r *http.Request) {
+		var req types.ProductListReq
+		if err := httpx.Parse(r, &req); err != nil {
+			httpx.ErrorCtx(r.Context(), w, response.ErrBadRequest(err.Error()))
+			return
+		}
+
+		l := product.NewProductListLogic(r.Context(), svcCtx)
+		resp, err := l.ProductList(&req)
+		if err != nil {
+			httpx.ErrorCtx(r.Context(), w, err)
+		} else {
+			httpx.OkJsonCtx(r.Context(), w, resp)
+		}
+	}
+}

+ 32 - 0
internal/handler/product/updateProductHandler.go

@@ -0,0 +1,32 @@
+// Code scaffolded by goctl. Safe to edit.
+// goctl 1.10.0
+
+package product
+
+import (
+	"net/http"
+
+	"github.com/zeromicro/go-zero/rest/httpx"
+	"perms-system-server/internal/logic/product"
+	"perms-system-server/internal/response"
+	"perms-system-server/internal/svc"
+	"perms-system-server/internal/types"
+)
+
+func UpdateProductHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
+	return func(w http.ResponseWriter, r *http.Request) {
+		var req types.UpdateProductReq
+		if err := httpx.Parse(r, &req); err != nil {
+			httpx.ErrorCtx(r.Context(), w, response.ErrBadRequest(err.Error()))
+			return
+		}
+
+		l := product.NewUpdateProductLogic(r.Context(), svcCtx)
+		err := l.UpdateProduct(&req)
+		if err != nil {
+			httpx.ErrorCtx(r.Context(), w, err)
+		} else {
+			httpx.Ok(w)
+		}
+	}
+}

+ 32 - 0
internal/handler/pub/loginHandler.go

@@ -0,0 +1,32 @@
+// Code scaffolded by goctl. Safe to edit.
+// goctl 1.10.0
+
+package pub
+
+import (
+	"net/http"
+
+	"github.com/zeromicro/go-zero/rest/httpx"
+	"perms-system-server/internal/logic/pub"
+	"perms-system-server/internal/response"
+	"perms-system-server/internal/svc"
+	"perms-system-server/internal/types"
+)
+
+func LoginHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
+	return func(w http.ResponseWriter, r *http.Request) {
+		var req types.LoginReq
+		if err := httpx.Parse(r, &req); err != nil {
+			httpx.ErrorCtx(r.Context(), w, response.ErrBadRequest(err.Error()))
+			return
+		}
+
+		l := pub.NewLoginLogic(r.Context(), svcCtx)
+		resp, err := l.Login(&req)
+		if err != nil {
+			httpx.ErrorCtx(r.Context(), w, err)
+		} else {
+			httpx.OkJsonCtx(r.Context(), w, resp)
+		}
+	}
+}

+ 37 - 0
internal/handler/pub/loginHandler_test.go

@@ -0,0 +1,37 @@
+package pub
+
+import (
+	"encoding/json"
+	"net/http"
+	"net/http/httptest"
+	"strings"
+	"testing"
+
+	"perms-system-server/internal/response"
+	"perms-system-server/internal/svc"
+	"perms-system-server/internal/testutil"
+
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+)
+
+func init() {
+	response.Setup()
+}
+
+// TC-0012: 缺少必填字段
+func TestLoginHandler_MissingFields(t *testing.T) {
+	svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
+	handler := LoginHandler(svcCtx)
+
+	req := httptest.NewRequest(http.MethodPost, "/api/auth/login", strings.NewReader("{}"))
+	req.Header.Set("Content-Type", "application/json")
+	rr := httptest.NewRecorder()
+	handler.ServeHTTP(rr, req)
+
+	var body response.Body
+	err := json.Unmarshal(rr.Body.Bytes(), &body)
+	require.NoError(t, err)
+	assert.Equal(t, 400, body.Code)
+	assert.Contains(t, body.Msg, "username")
+}

+ 32 - 0
internal/handler/pub/refreshTokenHandler.go

@@ -0,0 +1,32 @@
+// Code scaffolded by goctl. Safe to edit.
+// goctl 1.10.0
+
+package pub
+
+import (
+	"net/http"
+
+	"github.com/zeromicro/go-zero/rest/httpx"
+	"perms-system-server/internal/logic/pub"
+	"perms-system-server/internal/response"
+	"perms-system-server/internal/svc"
+	"perms-system-server/internal/types"
+)
+
+func RefreshTokenHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
+	return func(w http.ResponseWriter, r *http.Request) {
+		var req types.RefreshTokenReq
+		if err := httpx.Parse(r, &req); err != nil {
+			httpx.ErrorCtx(r.Context(), w, response.ErrBadRequest(err.Error()))
+			return
+		}
+
+		l := pub.NewRefreshTokenLogic(r.Context(), svcCtx)
+		resp, err := l.RefreshToken(&req)
+		if err != nil {
+			httpx.ErrorCtx(r.Context(), w, err)
+		} else {
+			httpx.OkJsonCtx(r.Context(), w, resp)
+		}
+	}
+}

+ 32 - 0
internal/handler/pub/syncPermsHandler.go

@@ -0,0 +1,32 @@
+// Code scaffolded by goctl. Safe to edit.
+// goctl 1.10.0
+
+package pub
+
+import (
+	"net/http"
+
+	"github.com/zeromicro/go-zero/rest/httpx"
+	"perms-system-server/internal/logic/pub"
+	"perms-system-server/internal/response"
+	"perms-system-server/internal/svc"
+	"perms-system-server/internal/types"
+)
+
+func SyncPermsHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
+	return func(w http.ResponseWriter, r *http.Request) {
+		var req types.SyncPermsReq
+		if err := httpx.Parse(r, &req); err != nil {
+			httpx.ErrorCtx(r.Context(), w, response.ErrBadRequest(err.Error()))
+			return
+		}
+
+		l := pub.NewSyncPermsLogic(r.Context(), svcCtx)
+		resp, err := l.SyncPerms(&req)
+		if err != nil {
+			httpx.ErrorCtx(r.Context(), w, err)
+		} else {
+			httpx.OkJsonCtx(r.Context(), w, resp)
+		}
+	}
+}

+ 32 - 0
internal/handler/role/bindRolePermsHandler.go

@@ -0,0 +1,32 @@
+// Code scaffolded by goctl. Safe to edit.
+// goctl 1.10.0
+
+package role
+
+import (
+	"net/http"
+
+	"github.com/zeromicro/go-zero/rest/httpx"
+	"perms-system-server/internal/logic/role"
+	"perms-system-server/internal/response"
+	"perms-system-server/internal/svc"
+	"perms-system-server/internal/types"
+)
+
+func BindRolePermsHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
+	return func(w http.ResponseWriter, r *http.Request) {
+		var req types.BindPermsReq
+		if err := httpx.Parse(r, &req); err != nil {
+			httpx.ErrorCtx(r.Context(), w, response.ErrBadRequest(err.Error()))
+			return
+		}
+
+		l := role.NewBindRolePermsLogic(r.Context(), svcCtx)
+		err := l.BindRolePerms(&req)
+		if err != nil {
+			httpx.ErrorCtx(r.Context(), w, err)
+		} else {
+			httpx.Ok(w)
+		}
+	}
+}

+ 32 - 0
internal/handler/role/createRoleHandler.go

@@ -0,0 +1,32 @@
+// Code scaffolded by goctl. Safe to edit.
+// goctl 1.10.0
+
+package role
+
+import (
+	"net/http"
+
+	"github.com/zeromicro/go-zero/rest/httpx"
+	"perms-system-server/internal/logic/role"
+	"perms-system-server/internal/response"
+	"perms-system-server/internal/svc"
+	"perms-system-server/internal/types"
+)
+
+func CreateRoleHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
+	return func(w http.ResponseWriter, r *http.Request) {
+		var req types.CreateRoleReq
+		if err := httpx.Parse(r, &req); err != nil {
+			httpx.ErrorCtx(r.Context(), w, response.ErrBadRequest(err.Error()))
+			return
+		}
+
+		l := role.NewCreateRoleLogic(r.Context(), svcCtx)
+		resp, err := l.CreateRole(&req)
+		if err != nil {
+			httpx.ErrorCtx(r.Context(), w, err)
+		} else {
+			httpx.OkJsonCtx(r.Context(), w, resp)
+		}
+	}
+}

+ 32 - 0
internal/handler/role/deleteRoleHandler.go

@@ -0,0 +1,32 @@
+// Code scaffolded by goctl. Safe to edit.
+// goctl 1.10.0
+
+package role
+
+import (
+	"net/http"
+
+	"github.com/zeromicro/go-zero/rest/httpx"
+	"perms-system-server/internal/logic/role"
+	"perms-system-server/internal/response"
+	"perms-system-server/internal/svc"
+	"perms-system-server/internal/types"
+)
+
+func DeleteRoleHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
+	return func(w http.ResponseWriter, r *http.Request) {
+		var req types.DeleteRoleReq
+		if err := httpx.Parse(r, &req); err != nil {
+			httpx.ErrorCtx(r.Context(), w, response.ErrBadRequest(err.Error()))
+			return
+		}
+
+		l := role.NewDeleteRoleLogic(r.Context(), svcCtx)
+		err := l.DeleteRole(&req)
+		if err != nil {
+			httpx.ErrorCtx(r.Context(), w, err)
+		} else {
+			httpx.Ok(w)
+		}
+	}
+}

+ 32 - 0
internal/handler/role/roleDetailHandler.go

@@ -0,0 +1,32 @@
+// Code scaffolded by goctl. Safe to edit.
+// goctl 1.10.0
+
+package role
+
+import (
+	"net/http"
+
+	"github.com/zeromicro/go-zero/rest/httpx"
+	"perms-system-server/internal/logic/role"
+	"perms-system-server/internal/response"
+	"perms-system-server/internal/svc"
+	"perms-system-server/internal/types"
+)
+
+func RoleDetailHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
+	return func(w http.ResponseWriter, r *http.Request) {
+		var req types.RoleDetailReq
+		if err := httpx.Parse(r, &req); err != nil {
+			httpx.ErrorCtx(r.Context(), w, response.ErrBadRequest(err.Error()))
+			return
+		}
+
+		l := role.NewRoleDetailLogic(r.Context(), svcCtx)
+		resp, err := l.RoleDetail(&req)
+		if err != nil {
+			httpx.ErrorCtx(r.Context(), w, err)
+		} else {
+			httpx.OkJsonCtx(r.Context(), w, resp)
+		}
+	}
+}

+ 32 - 0
internal/handler/role/roleListHandler.go

@@ -0,0 +1,32 @@
+// Code scaffolded by goctl. Safe to edit.
+// goctl 1.10.0
+
+package role
+
+import (
+	"net/http"
+
+	"github.com/zeromicro/go-zero/rest/httpx"
+	"perms-system-server/internal/logic/role"
+	"perms-system-server/internal/response"
+	"perms-system-server/internal/svc"
+	"perms-system-server/internal/types"
+)
+
+func RoleListHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
+	return func(w http.ResponseWriter, r *http.Request) {
+		var req types.RoleListReq
+		if err := httpx.Parse(r, &req); err != nil {
+			httpx.ErrorCtx(r.Context(), w, response.ErrBadRequest(err.Error()))
+			return
+		}
+
+		l := role.NewRoleListLogic(r.Context(), svcCtx)
+		resp, err := l.RoleList(&req)
+		if err != nil {
+			httpx.ErrorCtx(r.Context(), w, err)
+		} else {
+			httpx.OkJsonCtx(r.Context(), w, resp)
+		}
+	}
+}

+ 32 - 0
internal/handler/role/updateRoleHandler.go

@@ -0,0 +1,32 @@
+// Code scaffolded by goctl. Safe to edit.
+// goctl 1.10.0
+
+package role
+
+import (
+	"net/http"
+
+	"github.com/zeromicro/go-zero/rest/httpx"
+	"perms-system-server/internal/logic/role"
+	"perms-system-server/internal/response"
+	"perms-system-server/internal/svc"
+	"perms-system-server/internal/types"
+)
+
+func UpdateRoleHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
+	return func(w http.ResponseWriter, r *http.Request) {
+		var req types.UpdateRoleReq
+		if err := httpx.Parse(r, &req); err != nil {
+			httpx.ErrorCtx(r.Context(), w, response.ErrBadRequest(err.Error()))
+			return
+		}
+
+		l := role.NewUpdateRoleLogic(r.Context(), svcCtx)
+		err := l.UpdateRole(&req)
+		if err != nil {
+			httpx.ErrorCtx(r.Context(), w, err)
+		} else {
+			httpx.Ok(w)
+		}
+	}
+}

+ 246 - 0
internal/handler/routes.go

@@ -0,0 +1,246 @@
+// Code generated by goctl. DO NOT EDIT.
+// goctl 1.10.0
+
+package handler
+
+import (
+	"net/http"
+
+	auth "perms-system-server/internal/handler/auth"
+	dept "perms-system-server/internal/handler/dept"
+	member "perms-system-server/internal/handler/member"
+	perm "perms-system-server/internal/handler/perm"
+	product "perms-system-server/internal/handler/product"
+	pub "perms-system-server/internal/handler/pub"
+	role "perms-system-server/internal/handler/role"
+	user "perms-system-server/internal/handler/user"
+	"perms-system-server/internal/svc"
+
+	"github.com/zeromicro/go-zero/rest"
+)
+
+func RegisterHandlers(server *rest.Server, serverCtx *svc.ServiceContext) {
+	server.AddRoutes(
+		rest.WithMiddlewares(
+			[]rest.Middleware{serverCtx.JwtAuth},
+			[]rest.Route{
+				{
+					Method:  http.MethodPost,
+					Path:    "/auth/changePassword",
+					Handler: auth.ChangePasswordHandler(serverCtx),
+				},
+				{
+					Method:  http.MethodPost,
+					Path:    "/auth/userInfo",
+					Handler: auth.UserInfoHandler(serverCtx),
+				},
+			}...,
+		),
+		rest.WithPrefix("/api"),
+	)
+
+	server.AddRoutes(
+		rest.WithMiddlewares(
+			[]rest.Middleware{serverCtx.JwtAuth},
+			[]rest.Route{
+				{
+					Method:  http.MethodPost,
+					Path:    "/create",
+					Handler: dept.CreateDeptHandler(serverCtx),
+				},
+				{
+					Method:  http.MethodPost,
+					Path:    "/delete",
+					Handler: dept.DeleteDeptHandler(serverCtx),
+				},
+				{
+					Method:  http.MethodPost,
+					Path:    "/tree",
+					Handler: dept.DeptTreeHandler(serverCtx),
+				},
+				{
+					Method:  http.MethodPost,
+					Path:    "/update",
+					Handler: dept.UpdateDeptHandler(serverCtx),
+				},
+			}...,
+		),
+		rest.WithPrefix("/api/dept"),
+	)
+
+	server.AddRoutes(
+		rest.WithMiddlewares(
+			[]rest.Middleware{serverCtx.JwtAuth},
+			[]rest.Route{
+				{
+					Method:  http.MethodPost,
+					Path:    "/add",
+					Handler: member.AddMemberHandler(serverCtx),
+				},
+				{
+					Method:  http.MethodPost,
+					Path:    "/list",
+					Handler: member.MemberListHandler(serverCtx),
+				},
+				{
+					Method:  http.MethodPost,
+					Path:    "/remove",
+					Handler: member.RemoveMemberHandler(serverCtx),
+				},
+				{
+					Method:  http.MethodPost,
+					Path:    "/update",
+					Handler: member.UpdateMemberHandler(serverCtx),
+				},
+			}...,
+		),
+		rest.WithPrefix("/api/member"),
+	)
+
+	server.AddRoutes(
+		rest.WithMiddlewares(
+			[]rest.Middleware{serverCtx.JwtAuth},
+			[]rest.Route{
+				{
+					Method:  http.MethodPost,
+					Path:    "/list",
+					Handler: perm.PermListHandler(serverCtx),
+				},
+			}...,
+		),
+		rest.WithPrefix("/api/perm"),
+	)
+
+	server.AddRoutes(
+		rest.WithMiddlewares(
+			[]rest.Middleware{serverCtx.JwtAuth},
+			[]rest.Route{
+				{
+					Method:  http.MethodPost,
+					Path:    "/create",
+					Handler: product.CreateProductHandler(serverCtx),
+				},
+				{
+					Method:  http.MethodPost,
+					Path:    "/detail",
+					Handler: product.ProductDetailHandler(serverCtx),
+				},
+				{
+					Method:  http.MethodPost,
+					Path:    "/list",
+					Handler: product.ProductListHandler(serverCtx),
+				},
+				{
+					Method:  http.MethodPost,
+					Path:    "/update",
+					Handler: product.UpdateProductHandler(serverCtx),
+				},
+			}...,
+		),
+		rest.WithPrefix("/api/product"),
+	)
+
+	server.AddRoutes(
+		[]rest.Route{
+			{
+				Method:  http.MethodPost,
+				Path:    "/auth/login",
+				Handler: pub.LoginHandler(serverCtx),
+			},
+			{
+				Method:  http.MethodPost,
+				Path:    "/auth/refreshToken",
+				Handler: pub.RefreshTokenHandler(serverCtx),
+			},
+			{
+				Method:  http.MethodPost,
+				Path:    "/perm/sync",
+				Handler: pub.SyncPermsHandler(serverCtx),
+			},
+		},
+		rest.WithPrefix("/api"),
+	)
+
+	server.AddRoutes(
+		rest.WithMiddlewares(
+			[]rest.Middleware{serverCtx.JwtAuth},
+			[]rest.Route{
+				{
+					Method:  http.MethodPost,
+					Path:    "/bindPerms",
+					Handler: role.BindRolePermsHandler(serverCtx),
+				},
+				{
+					Method:  http.MethodPost,
+					Path:    "/create",
+					Handler: role.CreateRoleHandler(serverCtx),
+				},
+				{
+					Method:  http.MethodPost,
+					Path:    "/delete",
+					Handler: role.DeleteRoleHandler(serverCtx),
+				},
+				{
+					Method:  http.MethodPost,
+					Path:    "/detail",
+					Handler: role.RoleDetailHandler(serverCtx),
+				},
+				{
+					Method:  http.MethodPost,
+					Path:    "/list",
+					Handler: role.RoleListHandler(serverCtx),
+				},
+				{
+					Method:  http.MethodPost,
+					Path:    "/update",
+					Handler: role.UpdateRoleHandler(serverCtx),
+				},
+			}...,
+		),
+		rest.WithPrefix("/api/role"),
+	)
+
+	server.AddRoutes(
+		rest.WithMiddlewares(
+			[]rest.Middleware{serverCtx.JwtAuth},
+			[]rest.Route{
+				{
+					Method:  http.MethodPost,
+					Path:    "/bindRoles",
+					Handler: user.BindRolesHandler(serverCtx),
+				},
+				{
+					Method:  http.MethodPost,
+					Path:    "/create",
+					Handler: user.CreateUserHandler(serverCtx),
+				},
+				{
+					Method:  http.MethodPost,
+					Path:    "/detail",
+					Handler: user.UserDetailHandler(serverCtx),
+				},
+				{
+					Method:  http.MethodPost,
+					Path:    "/list",
+					Handler: user.UserListHandler(serverCtx),
+				},
+				{
+					Method:  http.MethodPost,
+					Path:    "/setPerms",
+					Handler: user.SetUserPermsHandler(serverCtx),
+				},
+				{
+					Method:  http.MethodPost,
+					Path:    "/update",
+					Handler: user.UpdateUserHandler(serverCtx),
+				},
+				{
+					Method:  http.MethodPost,
+					Path:    "/updateStatus",
+					Handler: user.UpdateUserStatusHandler(serverCtx),
+				},
+			}...,
+		),
+		rest.WithPrefix("/api/user"),
+	)
+}

+ 32 - 0
internal/handler/user/bindRolesHandler.go

@@ -0,0 +1,32 @@
+// Code scaffolded by goctl. Safe to edit.
+// goctl 1.10.0
+
+package user
+
+import (
+	"net/http"
+
+	"github.com/zeromicro/go-zero/rest/httpx"
+	"perms-system-server/internal/logic/user"
+	"perms-system-server/internal/response"
+	"perms-system-server/internal/svc"
+	"perms-system-server/internal/types"
+)
+
+func BindRolesHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
+	return func(w http.ResponseWriter, r *http.Request) {
+		var req types.BindRolesReq
+		if err := httpx.Parse(r, &req); err != nil {
+			httpx.ErrorCtx(r.Context(), w, response.ErrBadRequest(err.Error()))
+			return
+		}
+
+		l := user.NewBindRolesLogic(r.Context(), svcCtx)
+		err := l.BindRoles(&req)
+		if err != nil {
+			httpx.ErrorCtx(r.Context(), w, err)
+		} else {
+			httpx.Ok(w)
+		}
+	}
+}

+ 32 - 0
internal/handler/user/createUserHandler.go

@@ -0,0 +1,32 @@
+// Code scaffolded by goctl. Safe to edit.
+// goctl 1.10.0
+
+package user
+
+import (
+	"net/http"
+
+	"github.com/zeromicro/go-zero/rest/httpx"
+	"perms-system-server/internal/logic/user"
+	"perms-system-server/internal/response"
+	"perms-system-server/internal/svc"
+	"perms-system-server/internal/types"
+)
+
+func CreateUserHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
+	return func(w http.ResponseWriter, r *http.Request) {
+		var req types.CreateUserReq
+		if err := httpx.Parse(r, &req); err != nil {
+			httpx.ErrorCtx(r.Context(), w, response.ErrBadRequest(err.Error()))
+			return
+		}
+
+		l := user.NewCreateUserLogic(r.Context(), svcCtx)
+		resp, err := l.CreateUser(&req)
+		if err != nil {
+			httpx.ErrorCtx(r.Context(), w, err)
+		} else {
+			httpx.OkJsonCtx(r.Context(), w, resp)
+		}
+	}
+}

Some files were not shown because too many files changed in this diff