From a02ac144818bba1cd833c14b1b1a46c6dc3dfe64 Mon Sep 17 00:00:00 2001 From: zeaslity Date: Mon, 2 Feb 2026 15:06:28 +0800 Subject: [PATCH] =?UTF-8?q?=E6=9B=B4=E6=96=B0RMDC=E7=B3=BB=E7=BB=9F?= =?UTF-8?q?=E7=9A=84=E6=A8=A1=E5=9D=97SKILL?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .idea/data_source_mapping.xml | 6 + .idea/go.imports.xml | 11 + .idea/sqldialects.xml | 7 + 0-pandoc-失败/epub-失败/mermaid-filter.err | 0 0-pandoc-失败/epub-失败/output.epub | Bin 0 -> 35748 bytes 0-pandoc-失败/output.pdf | Bin 0 -> 78502 bytes 0-pandoc-失败/pdf/mermaid-filter.err | 0 1-AgentSkills/coding-go-gin-gorm/SKILL.md | 21 +- .../reference/api-design-spec.md | 2 + 1-AgentSkills/coding-vue3-vuetify/SKILL.md | 168 +-- 1-AgentSkills/designing-contracts/SKILL.md | 105 ++ .../reference/api-versioning-policy.md | 46 + .../reference/breaking-change-checklist.md | 35 + .../reference/event-schema-rules.md | 44 + .../scripts/verify-api-compatibility.sh | 57 ++ .../developing-project-management/SKILL.md | 124 ++- .../module-dependencies.md | 54 + .../lifecycle-states.md | 58 ++ .../workflow-state-mapping.md | 156 ++- .../03-permission-model/acl-permission.md | 82 ++ .../04-version-control/version-design.md | 164 +++ .../05-database-schema/data-structures.md | 172 ++++ .../05-database-schema/database-schema.md | 195 ++++ .../reference/06-api-design/api-endpoints.md | 141 +++ .../06-api-design/business-workflows.md | 98 ++ .../component-specifications.md | 151 +++ .../interaction-sequences.md | 128 +++ .../lifecycle-workflow-display.md | 212 ++++ .../07-frontend-design/module-design-specs.md | 111 ++ .../07-frontend-design/page-architecture.md | 102 ++ .../user-admin-difference.md | 183 ++++ .../07-frontend-design/view-edit-states.md | 128 +++ .../07-frontend-design/visual-design-specs.md | 183 ++++ .../reference/acl-permission-model.md | 158 --- .../reference/api-endpoints.md | 151 --- .../reference/data-structures.md | 428 -------- .../reference/database-schema.md | 239 ----- .../reference/frontend-design.md | 496 --------- .../reference/lifecycle-state-machine.md | 90 -- .../reference/version-control-design.md | 448 -------- .../scripts/verify-project-module.sh | 323 +++--- 1-AgentSkills/developing-rmdc/SKILL.md | 109 ++ .../reference/module-dependencies.md | 32 + .../developing-rmdc/reference/terminology.md | 18 + .../reference/version-compatibility.md | 36 + .../scripts/verify-module-deps.sh | 56 + 1-AgentSkills/developing-user-auth/SKILL.md | 271 +++++ .../examples/auth-handler-skeleton.go | 145 +++ .../examples/permission-check-skeleton.go | 137 +++ .../examples/workflow-callback-skeleton.go | 185 ++++ .../reference/01-overview/module-overview.md | 28 + .../02-architecture/interface-injection.md | 53 + .../02-architecture/module-dependencies.md | 42 + .../reference/02-architecture/tech-stack.md | 41 + .../reference/03-authentication/jwt-claims.md | 68 ++ .../03-authentication/login-design.md | 68 ++ .../04-user-lifecycle/user-lifecycle.md | 85 ++ .../reference/05-rbac/rbac-roles.md | 86 ++ .../registration-workflow.md | 80 ++ .../management-workflow.md | 105 ++ .../business-info-registry.md | 81 ++ .../08-permission-model/jenkins-acls.md | 115 +++ .../permission-architecture.md | 105 ++ .../08-permission-model/project-acls.md | 94 ++ .../09-data-model/permission-tables-schema.md | 120 +++ .../09-data-model/user-table-schema.md | 91 ++ .../reference/10-api-design/api-endpoints.md | 116 +++ .../11-security/security-compliance.md | 71 ++ .../scripts/verify-user-auth.sh | 315 ++++++ 1-AgentSkills/managing-db-migrations/SKILL.md | 111 ++ .../examples/migration-template.sql | 27 + .../reference/field-evolution-rules.md | 58 ++ .../reference/migration-naming.md | 49 + .../reference/rollback-policy.md | 43 + .../scripts/verify-migration-rollback.sh | 66 ++ 1-AgentSkills/managing-observability/SKILL.md | 127 +++ .../examples/structured-log-example.go | 85 ++ .../reference/audit-alignment.md | 74 ++ .../reference/log-format.md | 58 ++ .../reference/metrics-naming.md | 52 + .../scripts/verify-observability.sh | 128 +++ 2-需求转换专业设计/2-DDS转AgentSkills.md | 275 +++++ 2-需求转换专业设计/DDS转AgentSkill.md | 10 +- 2-需求转换专业设计/中文表头转换.md | 1 + 2-需求转换专业设计/转换的prompt.md | 7 + .../2-rmdc-project-management-DDS.md | 2 +- .../3-china-province-city copy.md | 361 +++++++ ...{1-user-auth-DDS.md => 1-user-auth-PRD.md} | 0 .../9-rmdc-user-auth/2-user-auth-DDS.md | 954 ++++++++++++++++++ 89 files changed, 8101 insertions(+), 2417 deletions(-) create mode 100644 .idea/data_source_mapping.xml create mode 100644 .idea/go.imports.xml create mode 100644 .idea/sqldialects.xml create mode 100644 0-pandoc-失败/epub-失败/mermaid-filter.err create mode 100644 0-pandoc-失败/epub-失败/output.epub create mode 100644 0-pandoc-失败/output.pdf create mode 100644 0-pandoc-失败/pdf/mermaid-filter.err create mode 100644 1-AgentSkills/designing-contracts/SKILL.md create mode 100644 1-AgentSkills/designing-contracts/reference/api-versioning-policy.md create mode 100644 1-AgentSkills/designing-contracts/reference/breaking-change-checklist.md create mode 100644 1-AgentSkills/designing-contracts/reference/event-schema-rules.md create mode 100644 1-AgentSkills/designing-contracts/scripts/verify-api-compatibility.sh create mode 100644 1-AgentSkills/developing-project-management/reference/01-architecture-overview/module-dependencies.md create mode 100644 1-AgentSkills/developing-project-management/reference/02-lifecycle-state-machine/lifecycle-states.md rename 1-AgentSkills/developing-project-management/reference/{ => 02-lifecycle-state-machine}/workflow-state-mapping.md (50%) create mode 100644 1-AgentSkills/developing-project-management/reference/03-permission-model/acl-permission.md create mode 100644 1-AgentSkills/developing-project-management/reference/04-version-control/version-design.md create mode 100644 1-AgentSkills/developing-project-management/reference/05-database-schema/data-structures.md create mode 100644 1-AgentSkills/developing-project-management/reference/05-database-schema/database-schema.md create mode 100644 1-AgentSkills/developing-project-management/reference/06-api-design/api-endpoints.md create mode 100644 1-AgentSkills/developing-project-management/reference/06-api-design/business-workflows.md create mode 100644 1-AgentSkills/developing-project-management/reference/07-frontend-design/component-specifications.md create mode 100644 1-AgentSkills/developing-project-management/reference/07-frontend-design/interaction-sequences.md create mode 100644 1-AgentSkills/developing-project-management/reference/07-frontend-design/lifecycle-workflow-display.md create mode 100644 1-AgentSkills/developing-project-management/reference/07-frontend-design/module-design-specs.md create mode 100644 1-AgentSkills/developing-project-management/reference/07-frontend-design/page-architecture.md create mode 100644 1-AgentSkills/developing-project-management/reference/07-frontend-design/user-admin-difference.md create mode 100644 1-AgentSkills/developing-project-management/reference/07-frontend-design/view-edit-states.md create mode 100644 1-AgentSkills/developing-project-management/reference/07-frontend-design/visual-design-specs.md delete mode 100644 1-AgentSkills/developing-project-management/reference/acl-permission-model.md delete mode 100644 1-AgentSkills/developing-project-management/reference/api-endpoints.md delete mode 100644 1-AgentSkills/developing-project-management/reference/data-structures.md delete mode 100644 1-AgentSkills/developing-project-management/reference/database-schema.md delete mode 100644 1-AgentSkills/developing-project-management/reference/frontend-design.md delete mode 100644 1-AgentSkills/developing-project-management/reference/lifecycle-state-machine.md delete mode 100644 1-AgentSkills/developing-project-management/reference/version-control-design.md create mode 100644 1-AgentSkills/developing-rmdc/SKILL.md create mode 100644 1-AgentSkills/developing-rmdc/reference/module-dependencies.md create mode 100644 1-AgentSkills/developing-rmdc/reference/terminology.md create mode 100644 1-AgentSkills/developing-rmdc/reference/version-compatibility.md create mode 100644 1-AgentSkills/developing-rmdc/scripts/verify-module-deps.sh create mode 100644 1-AgentSkills/developing-user-auth/SKILL.md create mode 100644 1-AgentSkills/developing-user-auth/examples/auth-handler-skeleton.go create mode 100644 1-AgentSkills/developing-user-auth/examples/permission-check-skeleton.go create mode 100644 1-AgentSkills/developing-user-auth/examples/workflow-callback-skeleton.go create mode 100644 1-AgentSkills/developing-user-auth/reference/01-overview/module-overview.md create mode 100644 1-AgentSkills/developing-user-auth/reference/02-architecture/interface-injection.md create mode 100644 1-AgentSkills/developing-user-auth/reference/02-architecture/module-dependencies.md create mode 100644 1-AgentSkills/developing-user-auth/reference/02-architecture/tech-stack.md create mode 100644 1-AgentSkills/developing-user-auth/reference/03-authentication/jwt-claims.md create mode 100644 1-AgentSkills/developing-user-auth/reference/03-authentication/login-design.md create mode 100644 1-AgentSkills/developing-user-auth/reference/04-user-lifecycle/user-lifecycle.md create mode 100644 1-AgentSkills/developing-user-auth/reference/05-rbac/rbac-roles.md create mode 100644 1-AgentSkills/developing-user-auth/reference/06-registration-workflow/registration-workflow.md create mode 100644 1-AgentSkills/developing-user-auth/reference/07-management-workflow/management-workflow.md create mode 100644 1-AgentSkills/developing-user-auth/reference/08-permission-model/business-info-registry.md create mode 100644 1-AgentSkills/developing-user-auth/reference/08-permission-model/jenkins-acls.md create mode 100644 1-AgentSkills/developing-user-auth/reference/08-permission-model/permission-architecture.md create mode 100644 1-AgentSkills/developing-user-auth/reference/08-permission-model/project-acls.md create mode 100644 1-AgentSkills/developing-user-auth/reference/09-data-model/permission-tables-schema.md create mode 100644 1-AgentSkills/developing-user-auth/reference/09-data-model/user-table-schema.md create mode 100644 1-AgentSkills/developing-user-auth/reference/10-api-design/api-endpoints.md create mode 100644 1-AgentSkills/developing-user-auth/reference/11-security/security-compliance.md create mode 100644 1-AgentSkills/developing-user-auth/scripts/verify-user-auth.sh create mode 100644 1-AgentSkills/managing-db-migrations/SKILL.md create mode 100644 1-AgentSkills/managing-db-migrations/examples/migration-template.sql create mode 100644 1-AgentSkills/managing-db-migrations/reference/field-evolution-rules.md create mode 100644 1-AgentSkills/managing-db-migrations/reference/migration-naming.md create mode 100644 1-AgentSkills/managing-db-migrations/reference/rollback-policy.md create mode 100644 1-AgentSkills/managing-db-migrations/scripts/verify-migration-rollback.sh create mode 100644 1-AgentSkills/managing-observability/SKILL.md create mode 100644 1-AgentSkills/managing-observability/examples/structured-log-example.go create mode 100644 1-AgentSkills/managing-observability/reference/audit-alignment.md create mode 100644 1-AgentSkills/managing-observability/reference/log-format.md create mode 100644 1-AgentSkills/managing-observability/reference/metrics-naming.md create mode 100644 1-AgentSkills/managing-observability/scripts/verify-observability.sh create mode 100644 2-需求转换专业设计/2-DDS转AgentSkills.md create mode 100644 2-需求转换专业设计/中文表头转换.md create mode 100644 8-CMII-RMDC/4-rmdc-project-management/3-china-province-city copy.md rename 8-CMII-RMDC/9-rmdc-user-auth/{1-user-auth-DDS.md => 1-user-auth-PRD.md} (100%) create mode 100644 8-CMII-RMDC/9-rmdc-user-auth/2-user-auth-DDS.md diff --git a/.idea/data_source_mapping.xml b/.idea/data_source_mapping.xml new file mode 100644 index 0000000..7e938da --- /dev/null +++ b/.idea/data_source_mapping.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.idea/go.imports.xml b/.idea/go.imports.xml new file mode 100644 index 0000000..d7202f0 --- /dev/null +++ b/.idea/go.imports.xml @@ -0,0 +1,11 @@ + + + + + + \ No newline at end of file diff --git a/.idea/sqldialects.xml b/.idea/sqldialects.xml new file mode 100644 index 0000000..3052bbd --- /dev/null +++ b/.idea/sqldialects.xml @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/0-pandoc-失败/epub-失败/mermaid-filter.err b/0-pandoc-失败/epub-失败/mermaid-filter.err new file mode 100644 index 0000000..e69de29 diff --git a/0-pandoc-失败/epub-失败/output.epub b/0-pandoc-失败/epub-失败/output.epub new file mode 100644 index 0000000000000000000000000000000000000000..619721798cc9d6c5b466a56e500e4f5ca35f4e4d GIT binary patch literal 35748 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 Qb{NYN=lj23oxyqPyn|=fgYH_@lV4x8 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 literal 0 HcmV?d00001 diff --git a/0-pandoc-失败/pdf/mermaid-filter.err b/0-pandoc-失败/pdf/mermaid-filter.err new file mode 100644 index 0000000..e69de29 diff --git a/1-AgentSkills/coding-go-gin-gorm/SKILL.md b/1-AgentSkills/coding-go-gin-gorm/SKILL.md index aaff56d..f551b5f 100644 --- a/1-AgentSkills/coding-go-gin-gorm/SKILL.md +++ b/1-AgentSkills/coding-go-gin-gorm/SKILL.md @@ -1,7 +1,19 @@ --- -name: developing-go-gin-gorm -description: Generates and reviews Go backend code using GIN and GORM frameworks. Enforces layered architecture (handler→service→dao), unified API response format, POST+RequestBody API design, DTO naming conventions, Chinese comments, Asia/Shanghai timezone, structured logging, and framework best practices. Trigger on Go API development, GIN handler creation, GORM repository implementation, or code review requests. -argument-hint: " " e.g., "create user-handler", "review service/order.go", "scaffold api/v1/product" +name: developing-go-gin-gorm wdd-后端开发 + +description: > + 使用 Gin + GORM 生成、编写、修改、评审 production-ready 的 Go 后端代码(Generate & Review Go backend code with Gin/GORM)。 + 强制分层架构 handler → service → dao/repository(避免业务逻辑堆在 handler;DAO/Repo 只做数据访问与查询组装),并统一 API 响应包装 + consistent response envelope(code/message/data + request_id/trace_id 等可观测字段)。接口风格默认推荐 POST + JSON RequestBody + as default(必要时遵循 REST 语义与幂等约定),规范 DTO/VO/DO 命名与字段映射 conventions(入参 DTO、出参 VO、持久化 DO/Model)。 + 代码注释使用中文(Chinese comments for maintainability),时间处理默认 Asia/Shanghai(time zone aware time handling), + 采用结构化日志 structured logging(携带 request_id/trace_id/user_id/path/latency 等上下文),并遵循 Gin/GORM 工程化最佳实践 + (transactions, context propagation, error wrapping, pagination, soft delete, optimistic locking when needed)。 + 触发场景 Trigger: Go 后端开发 / Gin Handler 创建 / GORM DAO/Repository 实现 / 代码走查与 Review(refactor suggestions, bug fixes, performance tips)。 + +argument-hint: "<动作 action> <目标 target>" 例如/ e.g.: + "create user-handler", "review service/order.go", "scaffold api/v1/product", "add repo for table/users", "optimize gorm query" + allowed-tools: - Read - Write @@ -9,12 +21,13 @@ allowed-tools: - Glob - Grep - Bash + --- # Go GIN/GORM 开发规范 Skill ## 触发条件 -- 用户请求创建/修改 Go 后端代码(GIN handler、GORM dao、service) +- 用户请求创建/修改 Go 后端代码 - 用户请求代码审查 - 用户提及 API 开发、数据库操作、统一响应、日志、时间处理 - 用户请求设计 API 接口、DTO 结构 diff --git a/1-AgentSkills/coding-go-gin-gorm/reference/api-design-spec.md b/1-AgentSkills/coding-go-gin-gorm/reference/api-design-spec.md index 4ee91a2..d06bdab 100644 --- a/1-AgentSkills/coding-go-gin-gorm/reference/api-design-spec.md +++ b/1-AgentSkills/coding-go-gin-gorm/reference/api-design-spec.md @@ -150,6 +150,8 @@ type ListRequest struct { | 项目管理 | `/api/projects/` | | 用户 | `/api/users/` | | 权限 | `/api/permissions/` | +| 权限-Jenkins | `/api/permissions/jenkins/` | +| 权限-项目 | `/api/permissions/projects/` | | 审计 | `/api/audit/` | | Exchange-Hub | `/api/exchange-hub/` | | DCU | `/api/dcu/` | diff --git a/1-AgentSkills/coding-vue3-vuetify/SKILL.md b/1-AgentSkills/coding-vue3-vuetify/SKILL.md index 55a62c0..e1b3606 100644 --- a/1-AgentSkills/coding-vue3-vuetify/SKILL.md +++ b/1-AgentSkills/coding-vue3-vuetify/SKILL.md @@ -1,141 +1,141 @@ --- name: coding-vue3-vuetify -description: Build production-grade Vue 3 + TypeScript + Vuetify 3 interfaces with architectural rigor. Use when creating Vue components, pages, layouts, Pinia stores, or API modules. Enforces strict typing, Composition API patterns, Material Design 3 aesthetics, and bulletproof data handling. +description: Build production-grade Vue 3 + TypeScript + Vuetify 3 interfaces with architectural rigor. 构建生产级 Vue 3 + TypeScript + Vuetify 3 界面。Use when creating Vue components, pages, layouts, Pinia stores, or API modules. 用于创建 Vue 组件、页面、布局、Pinia 状态管理或 API 模块。Enforces strict typing, Composition API patterns, Material Design 3 aesthetics, and bulletproof data handling. --- -This skill crafts Vue 3 + Vuetify 3 code that is architecturally sound, type-safe to the bone, and visually polished. Every component should feel like it belongs in a production codebase that senior engineers would be proud to maintain. +本技能指导构建架构严谨、类型安全、视觉精致的 Vue 3 + Vuetify 3 代码。每个组件都应该达到生产级代码库的标准——让资深工程师也引以为傲。 -The user provides: $ARGUMENTS (component specs, page requirements, feature requests, or architectural questions). +用户输入:$ARGUMENTS(组件规格、页面需求、功能请求或架构问题) -## Architectural Thinking +## 架构思维 -Before writing a single line, establish clarity: +动手写代码之前,先建立清晰认知: -- **Component Identity**: Is this a Page, Layout, Reusable Component, Composable, Store, or API Module? Each has distinct patterns. -- **Data Gravity**: Where does state live? Props flow down, events bubble up. Pinia for cross-component state. `provide/inject` for deep hierarchies. -- **Scroll Strategy**: Which container owns the scroll? Never the body. Always explicit. Always controlled. -- **Failure Modes**: What happens when data is `null`? Empty array? Network timeout? Design for the unhappy path first. +- **组件身份**:这是页面(Page)、布局(Layout)、可复用组件(Component)、组合式函数(Composable)、状态仓库(Store),还是 API 模块?每种都有独特模式。 +- **数据重力**:状态住在哪里?Props 向下流动,Events 向上冒泡。跨组件状态用 Pinia。深层级传递用 `provide/inject`。 +- **滚动策略**:哪个容器拥有滚动权?永远不是 body。必须显式声明。必须可控。 +- **失败模式**:数据为 `null` 时怎么办?空数组?网络超时?先为不幸路径设计。 -**CRITICAL**: Production code anticipates chaos. Type everything. Guard everything. Gracefully degrade everything. +**关键原则**:生产代码预判混乱。为一切加类型。为一切加守卫。让一切优雅降级。 -## Core Dogma +## 核心信条 -### TypeScript Absolutism -- ` +``` + +## SaveConfirmDialog 组件 + +保存前的确认对话框,展示变更差异: + +```html + + + + mdi-check-circle + 确认保存修改 + + + + 以下字段将被修改: + + + + + 字段 + 修改前 + 修改后 + + + + + {{ item.label }} + {{ item.oldValue || '空' }} + {{ item.newValue || '空' }} + + + + + + + 取消 + 确认保存 + + + +``` + +## DiffTextField 组件 + +编辑模式下显示与主线数据差异的输入框: + +```html + + + + + +``` + +## 组件清单 + +| 组件 | 文件路径 | 说明 | +|:---|:---|:---| +| BasicInfoForm | `components/BasicInfoForm.vue` | 基本信息编辑表单 | +| BasicInfoReadonly | `components/BasicInfoReadonly.vue` | 基本信息只读 | +| BusinessInfoReadonly | `components/BusinessInfoReadonly.vue` | 业务信息只读 | +| DeploymentBusinessForm | `components/DeploymentBusinessForm.vue` | 业务信息表单 | +| DeploymentEnvironmentForm | `components/DeploymentEnvironmentForm.vue` | 环境信息表单 | +| EnvironmentInfoReadonly | `components/EnvironmentInfoReadonly.vue` | 环境信息只读 | +| HostsInfoReadonly | `components/HostsInfoReadonly.vue` | 主机信息只读 | +| HostsManagement | `components/HostsManagement.vue` | 主机管理 | +| MiddlewareCardsGrid | `components/MiddlewareCardsGrid.vue` | 中间件卡片网格 | +| MiddlewareInfoReadonly | `components/MiddlewareInfoReadonly.vue` | 中间件只读 | +| AuthorizationManagement | `components/AuthorizationManagement.vue` | 授权管理 | +| VersionHistory | `components/VersionHistory.vue` | 版本历史 | +| SaveConfirmDialog | `components/SaveConfirmDialog.vue` | 保存确认对话框 | +| CopyableField | `components/CopyableField.vue` | 可复制字段 | +| ProjectBasicInfoCard | `components/ProjectBasicInfoCard.vue` | 项目基本信息卡片 | diff --git a/1-AgentSkills/developing-project-management/reference/07-frontend-design/interaction-sequences.md b/1-AgentSkills/developing-project-management/reference/07-frontend-design/interaction-sequences.md new file mode 100644 index 0000000..79c778d --- /dev/null +++ b/1-AgentSkills/developing-project-management/reference/07-frontend-design/interaction-sequences.md @@ -0,0 +1,128 @@ +# 交互时序图 + +> DDS-Section: Frontend DDS - 9. 交互时序图 +> DDS-Lines: L787-L878 + +## 管理员编辑保存流程 + +```mermaid +sequenceDiagram + participant Admin as 超级管理员 + participant Page as ProjectDetail.vue + participant Dialog as SaveConfirmDialog + participant API as 后端API + + Admin->>Page: 点击[编辑]按钮 + Page->>Page: isEditMode = true + Page->>Page: editForm = deepClone(masterData) + + Admin->>Page: 修改字段 + Page->>Page: hasChanges = true + + Admin->>Page: 点击[保存修改] + Page->>Page: 计算 diffItems + Page->>Dialog: 显示确认对话框 + + Admin->>Dialog: 确认保存 + Dialog->>API: updateProject() + API-->>Dialog: 成功 + Dialog->>Page: emit('confirm') + Page->>API: loadProject() + Page->>Page: isEditMode = false + Page-->>Admin: Snackbar: 保存成功 +``` + +## 用户草稿提交流程 + +```mermaid +sequenceDiagram + participant User as 普通用户 + participant Page as UserProjectDetail.vue + participant Dialog as SubmitDialog + participant API as 后端API + participant WF as 工单模块 + + User->>Page: 填写表单 + User->>Page: 点击[保存草稿] + Page->>API: saveDraft() + API-->>Page: 草稿保存成功 + + User->>Page: 点击[提交审核] + Page->>Dialog: 显示确认对话框 + User->>Dialog: 填写备注并确认 + Dialog->>API: saveDraft() (最终版本) + Dialog->>API: submitProjectDetail() + API->>WF: 触发工单状态转换 + WF-->>API: 成功 + API-->>Dialog: 提交成功 + Page-->>User: 跳转至工单详情页 +``` + +## 管理员审批流程 + +```mermaid +sequenceDiagram + participant Admin as 超级管理员 + participant Page as ProjectDetail.vue + participant Dialog as ApproveDialog/RejectDialog + participant API as 后端API + participant WF as 工单模块 + + Note over Page: lifecycle_status = reviewing + + Admin->>Page: 查看待审核内容 + + alt 通过审批 + Admin->>Page: 点击[通过] + Page->>Dialog: 显示审批对话框 + Admin->>Dialog: 填写备注并确认 + Dialog->>API: transitionWorkflow(approve) + API->>WF: 工单状态 → approved + WF-->>API: 回调更新项目状态 + API-->>Dialog: 成功 + Page->>API: updateProjectCertification('official') + Page-->>Admin: Snackbar: 审批通过 + else 打回修改 + Admin->>Page: 点击[打回] + Page->>Dialog: 显示打回对话框 + Admin->>Dialog: 填写打回原因并确认 + Dialog->>API: transitionWorkflow(return) + API->>WF: 工单状态 → returned + WF-->>API: 回调更新项目状态 + API-->>Dialog: 成功 + Page-->>Admin: Snackbar: 已打回 + end +``` + +## 版本冲突处理流程 + +```mermaid +sequenceDiagram + participant User as 普通用户 + participant Page as UserProjectDetail.vue + participant API as 后端API + participant Admin as 超级管理员 + + Note over User,API: 用户 A 基于 v3 版本创建草稿 + + Admin->>API: 直接修改项目 (v3 → v4) + + User->>Page: 点击[提交审核] + Page->>API: submitDraft() (base_version=v3) + API->>API: 检查 base_version != current_version + API-->>Page: 返回 409 Conflict + + Page-->>User: 显示冲突提示对话框 + + alt 选择 Rebase + User->>Page: 点击 [Rebase] + Page->>API: getProject() (最新 v4 版本) + Page->>Page: 自动合并草稿到 v4 + Page-->>User: 显示合并结果,可能需要手动解决冲突 + else 选择 Diff Check + User->>Page: 点击 [查看差异] + Page->>API: getDiff(v3, v4) + Page-->>User: 显示 v3 与 v4 的差异 + User->>User: 决定是否覆盖或调整 + end +``` diff --git a/1-AgentSkills/developing-project-management/reference/07-frontend-design/lifecycle-workflow-display.md b/1-AgentSkills/developing-project-management/reference/07-frontend-design/lifecycle-workflow-display.md new file mode 100644 index 0000000..011aea1 --- /dev/null +++ b/1-AgentSkills/developing-project-management/reference/07-frontend-design/lifecycle-workflow-display.md @@ -0,0 +1,212 @@ +# 生命周期状态与工单关联展示 + +> DDS-Section: Frontend DDS - 5-6. 项目生命周期状态展示 + 工单关联与跳转机制 +> DDS-Lines: L334-L582 + +## 状态标签设计 + +项目详情页 Header 区域展示三类状态标签: + +```html +
+

{{ masterData.project_name }}

+ + + + {{ PROJECT_STATUS[masterData.status] }} + + + + + {{ getLifecycleStatusIcon(masterData.lifecycle_status) }} + {{ LIFECYCLE_STATUS[masterData.lifecycle_status] }} + + + + + {{ PROJECT_CERTIFICATION[masterData.project_certification] }} + + + + + mdi-pencil + 编辑模式 + +
+``` + +## 生命周期状态配置 + +```typescript +// 生命周期状态枚举 +export const LIFECYCLE_STATUS = { + init: '初始化', + drafting: '填写中', + reviewing: '审核中', + released: '已发布', + modifying: '变更中', + archived: '已归档' +} + +// 状态颜色映射 +export const LIFECYCLE_STATUS_COLORS: Record = { + init: 'grey', + drafting: 'info', + reviewing: 'warning', + released: 'success', + modifying: 'primary', + archived: 'grey-darken-1' +} + +// 状态图标 +const getLifecycleStatusIcon = (status: string): string => { + const icons: Record = { + init: 'mdi-clock-outline', + drafting: 'mdi-pencil', + reviewing: 'mdi-eye', + released: 'mdi-check-circle', + modifying: 'mdi-sync', + archived: 'mdi-archive' + } + return icons[status] || 'mdi-help-circle' +} +``` + +## 生命周期提示横幅 (Alert Banner) + +根据当前生命周期状态,在 Header 下方显示上下文提示: + +```typescript +interface LifecycleAlert { + type: 'info' | 'warning' | 'success' | 'error' + message: string + action?: { + text: string + handler: () => void + } +} + +const lifecycleStatusAlert = computed((): LifecycleAlert | null => { + if (!masterData.value) return null + const status = masterData.value.lifecycle_status + + switch (status) { + case 'init': + return { + type: 'info', + message: '项目已创建,等待指定填写人录入详细信息' + } + case 'drafting': + return { + type: 'info', + message: `项目详情正在由 ${masterData.value.detail_filler_name || '填写人'} 填写中`, + action: masterData.value.workflow_id ? { + text: '查看工单', + handler: () => router.push(`/admin/workflows/${masterData.value?.workflow_id}`) + } : undefined + } + case 'reviewing': + return { + type: 'warning', + message: '项目详情已提交,等待审核', + action: masterData.value.workflow_id ? { + text: '查看工单', + handler: () => router.push(`/admin/workflows/${masterData.value?.workflow_id}`) + } : undefined + } + case 'modifying': + return { + type: 'info', + message: '项目存在活跃的变更工单,主线数据不受影响', + action: masterData.value.workflow_id ? { + text: '查看工单', + handler: () => router.push(`/admin/workflows/${masterData.value?.workflow_id}`) + } : undefined + } + case 'archived': + return { + type: 'warning', + message: '项目已归档,仅保留历史数据' + } + default: + return null + } +}) +``` + +## 工单关联与跳转机制 + +### 工单与项目的关系 + +| 工单类型 | 生命周期状态 | 数量关系 | 说明 | +|:---|:---|:---|:---| +| 填写工单 (project_detail) | INIT → DRAFTING | 1:1 | 项目创建时只能有一个 | +| 修改工单 (project_modify) | RELEASED → MODIFYING | 1:N | 已发布项目可有多个 | + +### 工单按钮显示逻辑 + +```typescript +// 是否显示工单按钮 +const showWorkflowButton = computed(() => { + if (!masterData.value?.workflow_id) return false + const status = masterData.value.lifecycle_status + // 在填写中、审核中、变更中状态显示工单按钮 + return ['drafting', 'reviewing', 'modifying'].includes(status) +}) + +// 工单按钮文本 +const workflowButtonText = computed(() => { + if (!masterData.value) return '查看工单' + const status = masterData.value.lifecycle_status + switch (status) { + case 'drafting': + return '查看填写工单' + case 'reviewing': + return '查看审核工单' + case 'modifying': + return '查看修改工单' + default: + return '查看工单' + } +}) +``` + +### 多工单场景处理 + +当项目处于 `MODIFYING` 状态时,可能同时存在多个修改工单: + +```html + + + + + {{ wf.workflow_id }} + + {{ wf.creator_name }} | {{ formatDate(wf.created_at) }} + + + + +``` + +### 从工单页面跳转回项目详情 + +```typescript +// 工单详情页 +const navigateToProject = () => { + if (workflowDetail.value?.business_context?.project_id) { + router.push(`/admin/projects/${workflowDetail.value.business_context.project_id}`) + } +} +``` diff --git a/1-AgentSkills/developing-project-management/reference/07-frontend-design/module-design-specs.md b/1-AgentSkills/developing-project-management/reference/07-frontend-design/module-design-specs.md new file mode 100644 index 0000000..94f27a2 --- /dev/null +++ b/1-AgentSkills/developing-project-management/reference/07-frontend-design/module-design-specs.md @@ -0,0 +1,111 @@ +# 模块详细设计规范 + +> DDS-Section: Frontend DDS - 7. 模块详细设计规范 +> DDS-Lines: L586-L693 + +## 基本信息模块 (Basic Info) + +| 字段 | 类型 | 只读模式 | 编辑模式 | +|:---|:---|:---|:---| +| 项目名称 | String | 文本展示 + 复制 | `v-text-field` | +| 命名空间 | String | 文本展示 + 复制 | `disabled` 不可编辑 | +| 省份 | Enum | 文本展示 | 级联选择器 | +| 城市 | Enum | 文本展示 | 级联选择器(依赖省份) | +| 项目性质 | Enum | 文本展示 | `v-select` | +| 行业组人员 | String | 文本展示 | `v-text-field` | +| 行业组电话 | String | 文本展示 | `v-text-field` | +| 项目描述 | String | 多行文本 | `v-textarea` | + +## 部署业务模块 (Deployment Business) + +| 字段 | 类型 | 只读模式 | 编辑模式 | +|:---|:---|:---|:---| +| 部署人姓名 | String | 文本展示 | `v-text-field` 或用户搜索 | +| 部署人电话 | String | 文本展示 | `v-text-field` | +| 部署系统 | Enum | 文本展示 | `v-select` | +| 系统版本 | String | 文本展示 | `v-text-field` | +| 业务入口 URL | String | 可点击链接 | `v-text-field` | +| 超管账号 | String | 文本展示 + 复制 | `v-text-field` | +| 超管密码 | Password | 脱敏 + 查看按钮 | `v-text-field` 密码输入 | + +## 部署环境模块 (Deployment Environment) + +| 字段 | 类型 | 只读模式 | 编辑模式 | +|:---|:---|:---|:---| +| 网络环境 | Enum | 文本展示 | `v-select` | +| 主公网 IP | String | 文本展示 + 复制 | `v-text-field` IP 校验 | +| 域名 URL | String | 可点击链接 | `v-text-field` | +| 启用 SSL | Boolean | 图标显示 | `v-switch` | +| 主机管理方式 | Enum | 文本展示 | `v-select` | +| 管理控制台 URL | String | 可点击链接 | `v-text-field` | +| 主机数量 | Number | 文本展示 | `v-text-field type=number` | +| CPU 总核数 | Number | 统计卡片 | `v-text-field type=number` | +| 内存总量 | Number | 统计卡片 | `v-text-field type=number` | +| 存储总量 | Number | 统计卡片 | `v-text-field type=number` | + +## 中间件模块 (Middleware) + +采用 **卡片网格 (Card Grid)** 设计。 + +### 数据结构 + +```typescript +interface MiddlewareFormItem { + middleware_type: string + public_ip: string + public_port: number + internal_ip: string + internal_port: number + admin_user: string + admin_password?: string +} +``` + +### 只读模式 +- 每个中间件一张卡片,响应式网格布局 +- 卡片包含:类型图标 + 标题 + IP/Port 信息 +- 图标映射逻辑: + +```typescript +const MIDDLEWARE_ICONS: Record = { + 'mysql': 'mdi-database', + 'redis': 'mdi-database-clock', + 'emqx': 'mdi-broadcast', + 'minio': 'mdi-bucket', + 'influxdb': 'mdi-chart-timeline-variant', + 'nacos': 'mdi-cog-outline', + 'k8s-dashboard': 'mdi-kubernetes' +} +``` + +### 编辑模式 +- 现有卡片右上角显示「编辑」「删除」按钮 +- 列表末尾显示「添加中间件」虚线框卡片 +- 点击添加/编辑弹出对话框: + - 类型选择:`v-combobox` 支持预设 + 自定义 + - 选择预设类型时自动填充默认端口 + +## 主机管理模块 (Hosts Management) + +- 复用 `HostsManagement.vue` 组件 +- 支持表格展示所有主机信息 +- 编辑模式支持添加/删除主机 + +## 授权信息模块 (Authorization) - SuperAdmin Only + +| 功能 | 说明 | +|:---|:---| +| TOTP 密钥展示 | 二维码 + 文本形式 | +| 授权类型切换 | 永久/限时 | +| 授权天数设置 | 数字输入 | +| 下发授权 | 调用 Exchange-Hub 接口 | +| 撤销授权 | 调用 Exchange-Hub 接口 | + +## 版本历史模块 (Version History) - SuperAdmin Only + +| 功能 | 说明 | +|:---|:---| +| 版本列表 | 时间轴形式展示 | +| 版本详情 | 点击查看完整快照 | +| 版本对比 | 选择两个版本进行 Diff | +| 工单关联 | 点击跳转关联工单 | diff --git a/1-AgentSkills/developing-project-management/reference/07-frontend-design/page-architecture.md b/1-AgentSkills/developing-project-management/reference/07-frontend-design/page-architecture.md new file mode 100644 index 0000000..af8e9b2 --- /dev/null +++ b/1-AgentSkills/developing-project-management/reference/07-frontend-design/page-architecture.md @@ -0,0 +1,102 @@ +# 前端页面架构设计 + +> DDS-Section: Frontend DDS - 1-2. 设计概述 + 页面架构设计 +> DDS-Lines: L23-L127 + +## 设计背景 + +项目详情页面是 RMDC 系统的核心交互界面,需要支持复杂的生命周期管理流程,并满足不同角色用户的差异化需求。 + +## 核心设计目标 + +| 目标 | 说明 | +|:---|:---| +| **状态分离** | 明确区分「只读查看模式」与「编辑修改模式」,防止误操作 | +| **角色差异化** | 超级管理员与普通用户看到的内容、操作权限不同,但尽量复用组件 | +| **生命周期可视化** | 清晰展示项目当前状态(INIT/DRAFTING/REVIEWING/RELEASED/MODIFYING/ARCHIVED) | +| **工单关联** | 支持项目详情页与工单详情页的双向跳转,处理多工单场景 | +| **美观专业** | 采用现代 Material Design 风格,注重留白、排版与微动效 | +| **高复用性** | 最大化组件复用,用户侧与管理侧共用核心表单组件 | + +## 页面文件结构 + +``` +frontend/src/modules/admin/ +├── pages/ +│ ├── admin/ +│ │ └── ProjectDetail.vue # 超级管理员端项目详情 +│ └── user/ +│ └── UserProjectDetail.vue # 普通用户端项目详情(填写人视角) +├── components/ +│ ├── BasicInfoForm.vue # 基本信息编辑表单 +│ ├── BasicInfoReadonly.vue # 基本信息只读展示 +│ ├── BusinessInfoReadonly.vue # 业务信息只读展示 +│ ├── DeploymentBusinessForm.vue # 部署业务编辑表单 +│ ├── DeploymentEnvironmentForm.vue # 部署环境编辑表单 +│ ├── EnvironmentInfoReadonly.vue # 环境信息只读展示 +│ ├── HostsInfoReadonly.vue # 主机信息只读展示 +│ ├── HostsManagement.vue # 主机管理组件 +│ ├── MiddlewareCardsGrid.vue # 中间件卡片网格 +│ ├── MiddlewareInfoReadonly.vue # 中间件只读展示 +│ ├── AuthorizationManagement.vue # 授权管理 (SuperAdmin Only) +│ ├── VersionHistory.vue # 版本历史 (SuperAdmin Only) +│ ├── SaveConfirmDialog.vue # 保存确认对话框 +│ ├── ProjectBasicInfoCard.vue # 项目基本信息卡片 +│ ├── CopyableField.vue # 可复制字段组件 +│ └── index.ts # 组件统一导出 +``` + +## 整体布局结构 + +页面采用 **「固定头部 + 固定 Tab 导航 + 可滚动内容区域」** 的三段式布局: + +``` +┌─────────────────────────────────────────────────────────────────────────┐ +│ [固定区域] 生命周期状态提示横幅 (Alert Banner) │ +├─────────────────────────────────────────────────────────────────────────┤ +│ [固定区域] 页面头部 Header │ +│ ┌─────────────────────────────────────┬─────────────────────────────┐ │ +│ │ ← 返回 项目名称 │ [查看工单] [打回] [通过] │ │ +│ │ Namespace | 省份 城市 │ [下载配置] [编辑/保存] │ │ +│ │ 状态标签组 │ │ │ +│ └─────────────────────────────────────┴─────────────────────────────┘ │ +├─────────────────────────────────────────────────────────────────────────┤ +│ [固定区域] Tab 导航栏 │ +│ ┌─────────────────────────────────────────────────────────────────────┐│ +│ │ 基本信息 | 部署业务 | 部署环境 | 主机管理 | 中间件 | 授权 | 版本历史 ││ +│ └─────────────────────────────────────────────────────────────────────┘│ +├─────────────────────────────────────────────────────────────────────────┤ +│ [滚动区域] Tab 内容区域 │ +│ ┌─────────────────────────────────────────────────────────────────────┐│ +│ │ ││ +│ │ 当前 Tab 对应的表单/只读内容 ││ +│ │ ││ +│ └─────────────────────────────────────────────────────────────────────┘│ +└─────────────────────────────────────────────────────────────────────────┘ +``` + +## CSS 布局实现 + +```css +.project-detail-page { + height: 100%; + max-height: 100%; + overflow: hidden; + display: flex; + flex-direction: column; +} + +.header-section { + flex-shrink: 0; + background: rgb(var(--v-theme-surface)); + z-index: 1; +} + +.content-area { + flex: 1 1 auto; + overflow-y: auto; + overflow-x: hidden; + min-height: 0; /* 关键:防止 Flex 子元素撑破父容器 */ + padding-bottom: 24px; +} +``` diff --git a/1-AgentSkills/developing-project-management/reference/07-frontend-design/user-admin-difference.md b/1-AgentSkills/developing-project-management/reference/07-frontend-design/user-admin-difference.md new file mode 100644 index 0000000..90a5ddd --- /dev/null +++ b/1-AgentSkills/developing-project-management/reference/07-frontend-design/user-admin-difference.md @@ -0,0 +1,183 @@ +# 用户侧 vs 管理侧差异化设计 + +> DDS-Section: Frontend DDS - 4. 用户侧 vs 管理侧差异化设计 +> DDS-Lines: L220-L331 + +## 页面对照表 + +| 特性 | 管理员端 (ProjectDetail.vue) | 用户端 (UserProjectDetail.vue) | +|:---|:---|:---| +| **默认模式** | 查看模式 | 根据工单状态决定 | +| **查看权限** | 所有模块 | ACL 授权模块 | +| **授权信息 Tab** | ✅ 可见 | ❌ 不可见 | +| **版本历史 Tab** | ✅ 可见 | ❌ 不可见 | +| **主机管理 Tab** | ✅ 可见 | ❌ 不可见 | +| **基本信息** | 可编辑 | 只读(由管理员填写) | +| **编辑操作** | 直接保存(上帝模式) | 草稿 → 提交审核(工单流程) | +| **审批操作** | ✅ 通过/打回按钮 | ❌ 无 | +| **保存按钮** | 「保存修改」 | 「保存草稿」 | +| **提交按钮** | 无 | 「提交审核」 | + +## Tab 导航差异 + +### 管理员端 Tabs +```html + + 基本信息 + 部署业务 + 部署环境 + 主机管理 + 中间件 + 授权信息 + 版本历史 + +``` + +### 用户端 Tabs +```html + + 基本信息 + 部署业务 + 部署环境 + 中间件 + +``` + +## 组件复用策略 + +```mermaid +graph TB + subgraph 共用组件 + A[BasicInfoForm] + B[DeploymentBusinessForm] + C[DeploymentEnvironmentForm] + D[MiddlewareCardsGrid] + E[BasicInfoReadonly] + F[BusinessInfoReadonly] + G[EnvironmentInfoReadonly] + H[MiddlewareInfoReadonly] + end + + subgraph 管理端专用 + I[AuthorizationManagement] + J[VersionHistory] + K[HostsManagement] + L[SaveConfirmDialog] + end + + subgraph 用户端专用 + M[ProjectBasicInfoCard] + end + + Admin[ProjectDetail.vue] --> A + Admin --> B + Admin --> C + Admin --> D + Admin --> E + Admin --> F + Admin --> G + Admin --> H + Admin --> I + Admin --> J + Admin --> K + Admin --> L + + User[UserProjectDetail.vue] --> B + User --> C + User --> D + User --> M +``` + +## 权限控制逻辑 + +### 管理员端 - 编辑权限判断 + +```typescript +const canEdit = computed(() => { + if (!masterData.value) return false + const status = masterData.value.lifecycle_status + + // 超级管理员在已发布、变更中状态可以编辑 + if (isSuperAdmin.value) { + return status === 'released' || status === 'modifying' + } + return false +}) +``` + +### 用户端 - 编辑权限判断 + +```typescript +const canEdit = computed(() => { + if (!workflowInfo.value) return true // 没有工单信息时默认可编辑 + const status = workflowInfo.value.status + + // 已分配、处理中、已打回状态可编辑 + return ['assigned', 'in_progress', 'returned', 'draft_saved'].includes(status) +}) +``` + +## 操作按钮差异 + +### 管理员端 Header 按钮 + +```html + + + +``` + +### 用户端 Header 按钮 + +```html + + +``` diff --git a/1-AgentSkills/developing-project-management/reference/07-frontend-design/view-edit-states.md b/1-AgentSkills/developing-project-management/reference/07-frontend-design/view-edit-states.md new file mode 100644 index 0000000..a6ad746 --- /dev/null +++ b/1-AgentSkills/developing-project-management/reference/07-frontend-design/view-edit-states.md @@ -0,0 +1,128 @@ +# 查看/编辑状态分离设计 + +> DDS-Section: Frontend DDS - 3. 查看状态 vs 编辑状态 +> DDS-Lines: L130-L217 + +## 状态定义 + +| 状态 | 变量名 | 说明 | +|:---|:---|:---| +| **查看状态** | `isEditMode = false` | 默认状态,展示只读组件 | +| **编辑状态** | `isEditMode = true` | 编辑模式,展示表单组件 | + +## 查看状态 (默认) + +### 展示形式 +- 使用 `*Readonly.vue` 系列组件展示数据 +- 采用 `v-row/v-col` 布局,键值对形式展示 +- 关键字段(IP、URL、密码)支持一键复制 +- 敏感字段(密码)默认脱敏显示 `******` + +### 交互行为 + +| 交互 | 说明 | +|:---|:---| +| **一键复制** | 使用 `CopyableField` 组件,点击复制图标复制到剪贴板 | +| **密码查看** | 点击"小眼睛"图标切换明文/密文 | +| **链接跳转** | URL 字段支持点击新窗口打开 | + +### 组件列表 + +| 组件名称 | 对应模块 | 说明 | +|:---|:---|:---| +| `BasicInfoReadonly.vue` | 基本信息 | 项目名、NS、省市、性质 | +| `BusinessInfoReadonly.vue` | 部署业务 | 部署人、系统版本、入口URL | +| `EnvironmentInfoReadonly.vue` | 部署环境 | 网络、IP、域名、主机统计 | +| `HostsInfoReadonly.vue` | 主机管理 | 主机列表只读表格 | +| `MiddlewareInfoReadonly.vue` | 中间件 | 中间件卡片只读展示 | + +## 编辑状态 + +### 进入条件 +- 点击 Header 的「编辑」按钮 +- 用户必须具备编辑权限(基于角色和生命周期状态) + +### 数据流 + +```mermaid +sequenceDiagram + participant User as 用户 + participant View as 查看模式 + participant Edit as 编辑模式 + participant API as 后端API + + User->>View: 点击[编辑]按钮 + View->>Edit: isEditMode = true + Edit->>Edit: 深拷贝 masterData → editForm + Edit-->>User: 显示表单组件 + + User->>Edit: 修改字段 + Edit->>Edit: computed hasChanges = true + + User->>Edit: 点击[保存] + Edit->>Edit: 弹出 SaveConfirmDialog + User->>Edit: 确认保存 + Edit->>API: 调用更新接口 + API-->>Edit: 返回成功 + Edit->>View: isEditMode = false, 刷新数据 +``` + +### 退出保护 + +```typescript +// 脏数据检测 +const hasChanges = computed(() => { + return JSON.stringify(editForm.value) !== JSON.stringify(masterData.value) +}) + +// 退出确认 +const exitEditMode = () => { + if (hasChanges.value) { + exitConfirmDialog.value = true // 弹出确认对话框 + } else { + isEditMode.value = false + } +} +``` + +### 编辑状态指示器 + +编辑模式下,Header 区域显示明显的「编辑模式」标签: + +```html + + mdi-pencil + 编辑模式 + +``` + +## 编辑模式数据流示例 + +```typescript +// 进入编辑模式 +const enterEditMode = () => { + editForm.value = JSON.parse(JSON.stringify(masterData.value)) // 深拷贝 + isEditMode.value = true +} + +// 保存修改 +const handleSave = async () => { + try { + await updateProject(projectId.value, editForm.value) + await loadProject() // 重新加载数据 + isEditMode.value = false + snackbar.success('保存成功') + } catch (error) { + snackbar.error('保存失败') + } +} + +// 取消编辑 +const handleCancel = () => { + if (hasChanges.value) { + confirmDialog.value = true + } else { + isEditMode.value = false + } +} +``` diff --git a/1-AgentSkills/developing-project-management/reference/07-frontend-design/visual-design-specs.md b/1-AgentSkills/developing-project-management/reference/07-frontend-design/visual-design-specs.md new file mode 100644 index 0000000..bfce1eb --- /dev/null +++ b/1-AgentSkills/developing-project-management/reference/07-frontend-design/visual-design-specs.md @@ -0,0 +1,183 @@ +# 视觉设计与响应式规范 + +> DDS-Section: Frontend DDS - 10-11. 视觉设计规范 + 响应式设计 +> DDS-Lines: L882-L980 + +## 色彩系统 + +| 用途 | 颜色 | Vuetify 类 | +|:---|:---|:---| +| 主色调 | Deep Purple | `color="primary"` | +| 成功状态 | Green | `color="success"` | +| 警告状态 | Orange | `color="warning"` | +| 错误状态 | Red | `color="error"` | +| 信息状态 | Blue | `color="info"` | +| 页面背景 | Light Grey | `bg-grey-lighten-4` | +| 卡片背景 | White | `bg-white` | + +## 卡片设计 + +```html + + + +``` + +设计规范: +- 圆角:`rounded-lg` (8px) +- 阴影:`elevation-2` +- 内边距:`pa-4` (16px) +- Hover 效果:轻微上浮 + 阴影加深 + +## 排版规范 + +| 元素 | 字体样式 | +|:---|:---| +| 页面标题 | `text-h4 font-weight-bold` | +| 卡片标题 | `text-h6` | +| 字段标签 | `text-medium-emphasis text-body-2` | +| 字段值 | `text-high-emphasis` | +| 辅助文字 | `text-caption text-grey` | + +## 间距规范 + +遵循 8px 网格系统: + +| 间距 | Vuetify 类 | 值 | +|:---|:---|:---| +| 紧凑 | `pa-2` / `ma-2` | 8px | +| 标准 | `pa-4` / `ma-4` | 16px | +| 宽松 | `pa-6` / `ma-6` | 24px | +| 组件间距 | `gap-2` | 8px | +| 卡片间距 | `gap-4` | 16px | + +## 动画与过渡 + +| 场景 | 效果 | +|:---|:---| +| Tab 切换 | `v-window` 默认滑动过渡 | +| 模式切换 | `v-expand-transition` | +| 按钮 Hover | 0.2s 缓动 | +| 卡片 Hover | `transform: translateY(-2px)` | +| 加载状态 | `v-skeleton-loader` 骨架屏 | + +## 断点配置 + +遵循 Vuetify 默认断点: + +| 断点 | 宽度范围 | +|:---|:---| +| xs | < 600px | +| sm | 600px - 960px | +| md | 960px - 1280px | +| lg | 1280px - 1920px | +| xl | > 1920px | + +## 响应式布局适配 + +### 中间件卡片网格 + +```html + + + + + +``` + +### 移动端适配要点 + +1. **Tab 导航**:使用 `show-arrows` 支持左右滑动 +2. **操作按钮**:使用 `v-bottom-sheet` 或收起到菜单 +3. **表单布局**:单列堆叠 +4. **表格**:使用 `mobile-breakpoint` 切换卡片视图 + +## TypeScript 类型定义 + +```typescript +// 项目详情 +interface ProjectDetail { + id: number + project_id: string + project_name: string + namespace: string + province: string + city: string + project_nature: string + industry_group_member: string + industry_group_phone: string + description: string + status: string + lifecycle_status: string + project_certification: string + workflow_id: string + detail_filler_id: number + detail_filler_name: string + deployment_business: DeploymentBusiness | null + deployment_environment: DeploymentEnvironment | null + middlewares: Middleware[] + hosts: Host[] + draft_data: Record | null + created_at: string + updated_at: string +} + +// 表单数据类型 +interface BasicFormData { + project_name: string + province: string + city: string + industry_group_member: string + industry_group_phone: string + project_nature: string + description: string +} + +interface BusinessFormData { + deployer_name: string + deployer_phone: string + deploy_system: string + system_version: string + business_entry_url: string + super_admin_user: string + super_admin_password: string +} + +interface EnvironmentFormData { + network_environment: string + main_public_ip: string + domain_url: string + enable_ssl: boolean + host_management_method: string + management_console_url: string + host_count: number + total_cpu: number + total_memory_gb: number + total_storage_gb: number +} + +interface MiddlewareFormItem { + middleware_type: string + public_ip: string + public_port: number + internal_ip: string + internal_port: number + admin_user: string + admin_password?: string +} + +// Diff 项 +interface DiffItem { + field: string + label: string + oldValue: string | number | boolean + newValue: string | number | boolean +} +``` diff --git a/1-AgentSkills/developing-project-management/reference/acl-permission-model.md b/1-AgentSkills/developing-project-management/reference/acl-permission-model.md deleted file mode 100644 index 6f5640e..0000000 --- a/1-AgentSkills/developing-project-management/reference/acl-permission-model.md +++ /dev/null @@ -1,158 +0,0 @@ -# ACL 权限模型 - -## 功能权限 (RBAC) - -| 权限代码 | 说明 | 角色 | -|:---|:---|:---| -| `project:create` | 创建项目 | SuperAdmin | -| `project:delete` | 删除/归档项目 | SuperAdmin | -| `project:edit` | 直接编辑项目 | SuperAdmin | -| `project:edit_workflow` | 通过工单编辑项目 | User (有ACL权限) | -| `project:auth_manage` | 一级/二级授权管理 | SuperAdmin | -| `project:permission_manage` | 项目权限分配 | SuperAdmin | - -## 数据权限 (ACL) - 模块级别 - -### 模块定义 - -| 模块代码 | 模块名称 | 说明 | -|:---|:---|:---| -| `basic_info` | 基本信息模块 | 项目名称、命名空间、省份城市等 | -| `business_info` | 部署业务模块 | 部署人、部署时间、系统版本等 | -| `environment_info` | 部署环境模块 | 主机信息、网络环境、域名等 | -| `middleware_info` | 部署中间件模块 | MySQL、Redis、EMQX等配置 | -| `authorization_info` | 项目授权模块 | TOTP授权信息(仅SuperAdmin) | - -### 权限类型 - -| 权限类型 | 说明 | -|:---|:---| -| `view` | 查看权限(可查看项目信息,可发起修改工单) | -| `export` | 导出权限(可导出项目信息) | - -> **说明**:编辑权限通过工单系统实现,拥有 `view` 权限的用户可以发起修改工单,由 SuperAdmin 审批后生效。 - -## 权限规则 - -1. **SuperAdmin**: 拥有所有项目的所有模块的全部权限,可直接修改 -2. **Admin**: 可以访问自己被授权的项目模块,可以向普通用户转授权限 -3. **Normal User**: 只能访问被授权的项目模块,修改需通过工单 -4. **项目填写人**: 自动获得该项目的查看权限 -5. **授权模块**: 仅 SuperAdmin 可见 - -## ACL 表结构(位于 rmdc-user-auth) - -```go -// ProjectACL 项目权限表 (模块级别) -type ProjectACL struct { - ID int64 `gorm:"primaryKey;autoIncrement" json:"id"` - ProjectID string `gorm:"type:varchar(64);index;not null" json:"project_id"` - UserID int64 `gorm:"index;not null" json:"user_id"` - - // 模块代码: basic_info/business_info/environment_info/middleware_info/authorization_info - ModuleCode string `gorm:"type:varchar(32);not null" json:"module_code"` - - // 权限类型 - CanView bool `gorm:"default:false" json:"can_view"` - CanExport bool `gorm:"default:false" json:"can_export"` - - // 授权信息 - GrantedBy int64 `json:"granted_by"` - GrantedAt time.Time `json:"granted_at"` - - UpdatedAt time.Time `json:"updated_at"` -} -``` - -## 权限检查流程 - -```go -// CheckProjectModulePermission 检查用户对项目模块的权限 -func (s *ACLService) CheckProjectModulePermission( - ctx context.Context, - userID int64, - projectID string, - moduleCode string, - permType string, // "view" or "export" -) (bool, error) { - // 1. 检查是否为 SuperAdmin - if s.IsSuperAdmin(ctx, userID) { - return true, nil - } - - // 2. 授权模块仅 SuperAdmin 可访问 - if moduleCode == "authorization_info" { - return false, nil - } - - // 3. 查询 ACL 表 - var acl ProjectACL - err := s.db.Where("project_id = ? AND user_id = ? AND module_code = ?", - projectID, userID, moduleCode).First(&acl).Error - if err != nil { - return false, nil - } - - // 4. 检查权限类型 - switch permType { - case "view": - return acl.CanView, nil - case "export": - return acl.CanExport, nil - default: - return false, nil - } -} -``` - -## 权限分配接口 - -### 授予权限 - -```json -// POST /api/project/permission/grant -{ - "project_id": "proj_001", - "user_id": 123, - "modules": [ - { - "module_code": "basic_info", - "can_view": true, - "can_export": false - }, - { - "module_code": "business_info", - "can_view": true, - "can_export": true - } - ] -} -``` - -### 批量设置权限 - -```json -// POST /api/project/permission/batch -{ - "project_id": "proj_001", - "permissions": [ - { - "user_id": 123, - "modules": ["basic_info", "business_info"] - }, - { - "user_id": 456, - "modules": ["basic_info"] - } - ] -} -``` - -## 权限继承规则 - -| 场景 | 规则 | -|:---|:---| -| 项目创建 | 填写人自动获得所有模块的 view 权限 | -| 权限转授 | Admin 只能转授自己拥有的权限 | -| 权限撤销 | 不影响已创建的草稿/工单 | -| 项目归档 | 保留权限记录,但无法访问 | diff --git a/1-AgentSkills/developing-project-management/reference/api-endpoints.md b/1-AgentSkills/developing-project-management/reference/api-endpoints.md deleted file mode 100644 index b4fc02e..0000000 --- a/1-AgentSkills/developing-project-management/reference/api-endpoints.md +++ /dev/null @@ -1,151 +0,0 @@ -# API 端点清单 - -## 项目管理 - -| 方法 | 路径 | 描述 | 权限 | -|:---|:---|:---|:---| -| POST | `/api/project/list` | 获取项目列表 (自动过滤ACL) | Login | -| POST | `/api/project/detail` | 获取项目详情 (Master版本) | View ACL | -| POST | `/api/project/create` | 创建项目 | SuperAdmin | -| POST | `/api/project/update` | 直接更新项目 | SuperAdmin | -| POST | `/api/project/delete` | 删除项目 (软删除) | SuperAdmin | -| POST | `/api/project/export` | 导出项目信息 | Export ACL | - -## 版本管理 - -| 方法 | 路径 | 描述 | 权限 | -|:---|:---|:---|:---| -| POST | `/api/project/version/list` | 获取版本历史列表 | View ACL | -| POST | `/api/project/version/detail` | 获取指定版本详情 | View ACL | -| POST | `/api/project/version/diff` | 获取版本差异 | View ACL | -| POST | `/api/project/version/diff-with-current` | 对比指定版本与当前版本差异 | View ACL | - -## 草稿管理 - -| 方法 | 路径 | 描述 | 权限 | -|:---|:---|:---|:---| -| POST | `/api/project/draft/get` | 获取当前用户的草稿 | View ACL | -| POST | `/api/project/draft/save` | 保存草稿 | View ACL | -| POST | `/api/project/draft/submit` | 提交审核 | View ACL | -| POST | `/api/project/draft/discard` | 放弃草稿 | View ACL | - -## 权限管理 (SuperAdmin) - -| 方法 | 路径 | 描述 | 权限 | -|:---|:---|:---|:---| -| POST | `/api/project/permission/list` | 获取项目权限列表 | SuperAdmin | -| POST | `/api/project/permission/grant` | 授予权限 | SuperAdmin | -| POST | `/api/project/permission/revoke` | 撤销权限 | SuperAdmin | -| POST | `/api/project/permission/batch` | 批量设置权限 | SuperAdmin | - -## 授权管理 (SuperAdmin) - -| 方法 | 路径 | 描述 | 权限 | -|:---|:---|:---|:---| -| POST | `/api/project/auth/config` | 获取授权配置 | SuperAdmin | -| POST | `/api/project/auth/update` | 更新授权配置 | SuperAdmin | -| POST | `/api/project/auth/grant` | 下发授权 | SuperAdmin | -| POST | `/api/project/auth/revoke` | 撤销授权 | SuperAdmin | - -## 请求/响应示例 - -### 项目列表 - -**Request:** -```json -{ - "page": 1, - "page_size": 20, - "keyword": "", - "lifecycle_status": "", - "province": "" -} -``` - -**Response:** -```json -{ - "code": 0, - "message": "success", - "data": { - "total": 100, - "list": [ - { - "project_id": "proj_001", - "name": "示例项目", - "namespace": "example-ns", - "lifecycle_status": "RELEASED", - "certification_status": "official", - "province": "北京市", - "city": "北京市" - } - ] - } -} -``` - -### 保存草稿 - -**Request:** -```json -{ - "project_id": "proj_001", - "basic_info": { - "province": "北京市", - "city": "北京市", - "industry_contact": "张三", - "industry_phone": "13800138000" - }, - "deploy_business": { - "deployer_name": "李四", - "system_version": "v2.0.0" - } -} -``` - -**Response:** -```json -{ - "code": 0, - "message": "草稿保存成功", - "data": { - "draft_id": 123, - "updated_at": "2026-01-21T10:00:00Z" - } -} -``` - -### 版本对比 - -**Request:** -```json -{ - "project_id": "proj_001", - "base_version": 2, - "target_version": 3 -} -``` - -**Response:** -```json -{ - "code": 0, - "message": "success", - "data": { - "diffs": [ - { - "module": "部署环境", - "field_diffs": [ - { - "field_path": "deploy_env.host_count", - "field_name": "主机台数", - "old_value": 3, - "new_value": 5, - "change_type": "modify" - } - ] - } - ] - } -} -``` diff --git a/1-AgentSkills/developing-project-management/reference/data-structures.md b/1-AgentSkills/developing-project-management/reference/data-structures.md deleted file mode 100644 index 38e1f98..0000000 --- a/1-AgentSkills/developing-project-management/reference/data-structures.md +++ /dev/null @@ -1,428 +0,0 @@ -# 数据结构定义 - -本文档定义项目管理模块中所有核心数据结构,包括实体、DTO、JSONB 存储结构。 - ---- - -## 1. 项目主表实体 (Project) - -```go -// Project 项目主表 -type Project struct { - ID int64 `gorm:"primaryKey;autoIncrement" json:"id"` - ProjectID string `gorm:"type:varchar(64);uniqueIndex;not null" json:"project_id"` - Name string `gorm:"type:varchar(128);not null" json:"name"` - Namespace string `gorm:"type:varchar(64);uniqueIndex;not null" json:"namespace"` - - // 生命周期状态: INIT/DRAFTING/REVIEWING/RELEASED/MODIFYING/ARCHIVED - LifecycleStatus string `gorm:"type:varchar(32);default:'INIT'" json:"lifecycle_status"` - // 认证状态: draft/pending/official - CertificationStatus string `gorm:"type:varchar(32);default:'draft'" json:"certification_status"` - - // 当前正式版本号 - CurrentVersion int `gorm:"default:0" json:"current_version"` - - // 主版本数据 (使用JSONB存储,便于版本快照) - BasicInfo json.RawMessage `gorm:"type:jsonb" json:"basic_info"` - DeployBusiness json.RawMessage `gorm:"type:jsonb" json:"deploy_business"` - DeployEnv json.RawMessage `gorm:"type:jsonb" json:"deploy_env"` - DeployMiddleware json.RawMessage `gorm:"type:jsonb" json:"deploy_middleware"` - - // 项目填写人 - DetailFillerID int64 `json:"detail_filler_id"` - DetailFillerName string `gorm:"type:varchar(64)" json:"detail_filler_name"` - - // 审计字段 - CreatedBy int64 `json:"created_by"` - CreatedByName string `gorm:"type:varchar(64)" json:"created_by_name"` - - common.BaseModel // CreatedAt, UpdatedAt, DeletedAt -} - -func (Project) TableName() string { - return "projects" -} -``` - ---- - -## 2. 版本表实体 (ProjectVersion) - -```go -// ProjectVersion 项目版本表 (含草稿) -type ProjectVersion struct { - ID int64 `gorm:"primaryKey;autoIncrement" json:"id"` - ProjectID string `gorm:"type:varchar(64);index;not null" json:"project_id"` - - // 版本号 (正式版本递增, 草稿为0) - Version int `gorm:"not null;default:0" json:"version"` - - // 版本类型: official/fill_draft/modify_draft - VersionType string `gorm:"type:varchar(32);not null" json:"version_type"` - - // 基准版本号(草稿基于哪个正式版本创建,用于乐观锁冲突检测) - BaseVersion int `gorm:"default:0" json:"base_version"` - - // 草稿所属用户ID (仅草稿类型有值) - UserID int64 `gorm:"index" json:"user_id"` - UserName string `gorm:"type:varchar(64)" json:"user_name"` - - // 关联工单ID (1:1关系) - WorkflowID string `gorm:"type:varchar(64);index" json:"workflow_id"` - - // 完整快照数据 - SnapshotData json.RawMessage `gorm:"type:jsonb" json:"snapshot_data"` - - // 变更信息 - CommitMessage string `gorm:"type:varchar(255)" json:"commit_message"` - CommitterID int64 `json:"committer_id"` - CommitterName string `gorm:"type:varchar(64)" json:"committer_name"` - - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` -} - -func (ProjectVersion) TableName() string { - return "project_versions" -} -``` - ---- - -## 3. 项目工单关联表 (ProjectWorkflow) - -```go -// ProjectWorkflow 项目与工单关联表 -type ProjectWorkflow struct { - ID int64 `gorm:"primaryKey;autoIncrement" json:"id"` - ProjectID string `gorm:"type:varchar(64);index;not null" json:"project_id"` - WorkflowID string `gorm:"type:varchar(64);uniqueIndex;not null" json:"workflow_id"` - - // 工单类型: fill(填写)/modify(修改) - WorkflowType string `gorm:"type:varchar(32);not null" json:"workflow_type"` - - // 工单状态 (冗余存储,便于查询) - Status string `gorm:"type:varchar(32)" json:"status"` - - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` -} -``` - -### 项目与工单关系说明 - -| 关系类型 | 项目状态 | 约束 | -|:---|:---|:---| -| 项目:填写工单 = 1:1 | INIT/DRAFTING | 项目创建时只能有一个填写工单 | -| 项目:修改工单 = 1:N | RELEASED/MODIFYING | 已发布项目可以有多个修改工单 | -| 用户:修改工单 = 1:1 (per project) | - | 非SuperAdmin用户同一项目只能有一个活跃修改工单 | - ---- - -## 4. 授权配置表 (ProjectAuthConfig) - -```go -// ProjectAuthConfig 项目授权配置 -type ProjectAuthConfig struct { - ID int64 `gorm:"primaryKey;autoIncrement" json:"id"` - ProjectID string `gorm:"type:varchar(64);uniqueIndex;not null" json:"project_id"` - - // 一级授权 (项目管理模块管理) - TierOneSecret string `gorm:"type:varchar(128)" json:"tier_one_secret"` // 加密存储 - TimeOffset int `gorm:"default:30" json:"time_offset"` // 允许时间偏移(秒) - TOTPEnabled bool `gorm:"default:false" json:"totp_enabled"` - - // 二级授权 (来自 Watchdog) - TierTwoSecret string `gorm:"type:varchar(128)" json:"tier_two_secret"` // 加密存储 - - // 授权状态 - AuthType string `gorm:"type:varchar(32)" json:"auth_type"` // permanent/time_limited - AuthDays int `json:"auth_days"` // 授权有效期(天) - AuthorizedAt time.Time `json:"authorized_at"` - RevokedAt time.Time `json:"revoked_at"` - IsOffline bool `gorm:"default:false" json:"is_offline"` // 是否离线授权 - - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` -} -``` - ---- - -## 5. JSONB 结构定义 - -### 5.1 基本信息 (BasicInfo) - -```go -type BasicInfo struct { - Province string `json:"province"` // 省份(枚举,参见省市列表) - City string `json:"city"` // 城市(级联选择) - IndustryContact string `json:"industry_contact"` // 行业组人员姓名 - IndustryPhone string `json:"industry_phone"` // 行业组人员电话 - ProjectNature string `json:"project_nature"` // 项目性质(枚举) -} -``` - -**项目性质枚举**: - -| 值 | 说明 | -|:---|:---| -| `research` | 科研 | -| `test` | 测试 | -| `trial` | 试用 | -| `market` | 市场化 | -| `sub_platform` | 二级平台 | - -### 5.2 部署业务 (DeployBusiness) - -```go -type DeployBusiness struct { - DeployerName string `json:"deployer_name"` // 部署人姓名 - DeployerPhone string `json:"deployer_phone"` // 部署人电话 - DeployStartTime string `json:"deploy_start_time"` // 部署开始时间 (YYYY-MM-DD) - DeployEndTime string `json:"deploy_end_time"` // 部署结束时间 (YYYY-MM-DD) - SystemVersion string `json:"system_version"` // 部署系统版本 - SystemType string `json:"system_type"` // 系统类型(枚举) - MainEntrance string `json:"main_entrance"` // 业务主要入口URL - AdminUsername string `json:"admin_username"` // 系统超管用户名 - AdminPassword string `json:"admin_password"` // 系统超管密码 ⚠️加密存储 -} -``` - -**系统类型枚举**: - -| 值 | 说明 | -|:---|:---| -| `business` | 老行业平台 | -| `fly-control` | 新飞控平台 | -| `supervisor` | 监管平台 | - -### 5.3 部署环境 (DeployEnv) - -```go -type DeployEnv struct { - // 主机信息列表 - Hosts []HostInfo `json:"hosts"` - - // 网络环境 - NetworkType string `json:"network_type"` // 网络类型(枚举) - MainPublicIP string `json:"main_public_ip"` // 主要公网IP - DomainURL string `json:"domain_url"` // 域名URL - SSLEnabled bool `json:"ssl_enabled"` // 是否开启SSL - - // 管理方式 - ManagementType string `json:"management_type"` // 管理类型(枚举) - ManagementURL string `json:"management_url"` // 管理后台URL - ManagementUser string `json:"management_user"` // 管理后台用户名 - ManagementPwd string `json:"management_pwd"` // 管理后台密码 ⚠️加密存储 - - // 统计信息 - HostCount int `json:"host_count"` // 主机台数 - TotalCPU int `json:"total_cpu"` // CPU总核数 - CPUModel string `json:"cpu_model"` // CPU型号 - TotalMemory int `json:"total_memory"` // 内存总大小(GB) - TotalStorage int `json:"total_storage"` // 存储总大小(GB) -} - -type HostInfo struct { - Hostname string `json:"hostname"` // 主机名 - InternalIP string `json:"internal_ip"` // 内网IP - PublicIP string `json:"public_ip"` // 公网IP(可选) - CanAccessPublic bool `json:"can_access_public"` // 能否访问公网 - SSHPort int `json:"ssh_port"` // SSH端口 - SSHUser string `json:"ssh_user"` // SSH用户名 - SSHPwd string `json:"ssh_pwd"` // SSH密码 ⚠️加密存储 - Role string `json:"role"` // 主机角色(枚举) -} -``` - -**网络类型枚举**: - -| 值 | 说明 | -|:---|:---| -| `internal` | 完全内网 | -| `single_public` | 单主机公网 | -| `full_public` | 全访问公网 | - -**管理类型枚举**: - -| 值 | 说明 | -|:---|:---| -| `bastion` | 堡垒机 | -| `whitelist` | 白名单 | -| `vpn` | VPN | - -**主机角色枚举**: - -| 值 | 说明 | -|:---|:---| -| `master` | 主节点 | -| `worker` | 工作节点 | -| `storage` | 存储节点 | - -### 5.4 部署中间件 (DeployMiddleware) - -```go -type DeployMiddleware struct { - MySQL MiddlewareInfo `json:"mysql"` - Redis MiddlewareInfo `json:"redis"` - EMQX MiddlewareInfo `json:"emqx"` - MinIO MiddlewareInfo `json:"minio"` - InfluxDB MiddlewareInfo `json:"influxdb"` - Nacos MiddlewareInfo `json:"nacos"` - K8SDashboard MiddlewareInfo `json:"k8s_dashboard"` -} - -// MiddlewareInfo 通用中间件信息 -type MiddlewareInfo struct { - PublicIP string `json:"public_ip"` // 公网IP - PublicPort int `json:"public_port"` // 公网端口 - InternalIP string `json:"internal_ip"` // 内网IP - InternalPort int `json:"internal_port"` // 内网端口 - K8SAddress string `json:"k8s_address"` // K8S集群内访问地址 (Service Name) - K8SPort int `json:"k8s_port"` // K8S端口 - AdminUser string `json:"admin_user"` // 超管用户名 - AdminPwd string `json:"admin_pwd"` // 超管密码 ⚠️加密存储 - Version string `json:"version"` // 中间件版本 -} -``` - ---- - -## 6. 版本快照结构 (VersionSnapshot) - -```go -// VersionSnapshot 版本快照结构(存储在 project_versions.snapshot_data) -type VersionSnapshot struct { - BasicInfo *BasicInfo `json:"basic_info"` - DeployBusiness *DeployBusiness `json:"deploy_business"` - DeployEnv *DeployEnv `json:"deploy_env"` - DeployMiddleware *DeployMiddleware `json:"deploy_middleware"` -} -``` - ---- - -## 7. 状态常量定义 - -```go -// 生命周期状态 -const ( - LifecycleInit = "INIT" // 已创建,等待填写 - LifecycleDrafting = "DRAFTING" // 填写中 - LifecycleReviewing = "REVIEWING" // 审核中 - LifecycleReleased = "RELEASED" // 已发布 - LifecycleModifying = "MODIFYING" // 变更中 - LifecycleArchived = "ARCHIVED" // 已归档 -) - -// 认证状态 -const ( - CertificationDraft = "draft" // 草稿 - CertificationPending = "pending" // 待审核 - CertificationOfficial = "official" // 正式 -) - -// 版本类型 -const ( - VersionTypeOfficial = "official" // 正式版本 - VersionTypeFillDraft = "fill_draft" // 填写草稿 - VersionTypeModifyDraft = "modify_draft" // 修改草稿 -) - -// 工单类型 -const ( - WorkflowTypeFill = "fill" // 填写工单 - WorkflowTypeModify = "modify" // 修改工单 -) -``` - ---- - -## 8. 敏感字段加密说明 - -以下字段必须使用 **AES-256** 加密存储,密钥使用项目的 `TierOneSecret`: - -| 结构体 | 字段 | 说明 | -|:---|:---|:---| -| DeployBusiness | `admin_password` | 系统超管密码 | -| DeployEnv | `management_pwd` | 管理后台密码 | -| HostInfo | `ssh_pwd` | SSH密码 | -| MiddlewareInfo | `admin_pwd` | 中间件超管密码 | -| ProjectAuthConfig | `tier_one_secret` | 一级TOTP密钥 | -| ProjectAuthConfig | `tier_two_secret` | 二级TOTP密钥 | - -### 加密/解密示例 - -```go -// 加密敏感字段 -func (s *CryptoService) EncryptSensitiveFields(data *DeployBusiness, key []byte) error { - if data.AdminPassword != "" { - encrypted, err := s.EncryptAES256(data.AdminPassword, key) - if err != nil { - return err - } - data.AdminPassword = encrypted - } - return nil -} - -// 解密敏感字段(返回给前端时脱敏) -func (s *CryptoService) MaskSensitiveFields(data *DeployBusiness) { - if data.AdminPassword != "" { - data.AdminPassword = "********" // 脱敏处理 - } -} -``` - ---- - -## 9. 字段校验规则 - -### Namespace 校验 (RFC 1123 DNS 标签规范) - -```go -var namespaceRegex = regexp.MustCompile(`^[a-z][a-z0-9.-]{0,251}[a-z0-9]$`) - -func ValidateNamespace(namespace string) error { - if len(namespace) > 253 { - return errors.New("命名空间长度不能超过253个字符") - } - if !namespaceRegex.MatchString(namespace) { - return errors.New("命名空间只能包含小写字母、数字、'-'和'.',必须以字母开头,以字母或数字结尾") - } - return nil -} -``` - -### IP 地址校验 - -```go -func ValidateIP(ip string) error { - if ip == "" || ip == "无" { - return nil // 允许空值 - } - if net.ParseIP(ip) == nil { - return errors.New("无效的IP地址格式") - } - return nil -} -``` - -### 省市级联校验 - -```go -// 后端需维护省市对应关系表,校验城市是否属于所选省份 -func ValidateProvinceCity(province, city string) error { - validCities, ok := provinceCityMap[province] - if !ok { - return errors.New("无效的省份") - } - for _, c := range validCities { - if c == city { - return nil - } - } - return errors.New("城市不属于所选省份") -} -``` diff --git a/1-AgentSkills/developing-project-management/reference/database-schema.md b/1-AgentSkills/developing-project-management/reference/database-schema.md deleted file mode 100644 index 7ec2a53..0000000 --- a/1-AgentSkills/developing-project-management/reference/database-schema.md +++ /dev/null @@ -1,239 +0,0 @@ -# 数据库 Schema - -## projects 表(项目主表) - -```sql -CREATE TABLE projects ( - id BIGSERIAL PRIMARY KEY, - project_id VARCHAR(64) UNIQUE NOT NULL, - name VARCHAR(128) NOT NULL, - namespace VARCHAR(64) UNIQUE NOT NULL, - - -- 生命周期状态: INIT/DRAFTING/REVIEWING/RELEASED/MODIFYING/ARCHIVED - lifecycle_status VARCHAR(32) DEFAULT 'INIT', - -- 认证状态: draft/pending/official - certification_status VARCHAR(32) DEFAULT 'draft', - - -- 当前正式版本号 - current_version INT DEFAULT 0, - - -- JSONB 存储 - basic_info JSONB, - deploy_business JSONB, - deploy_env JSONB, - deploy_middleware JSONB, - - -- 填写人 - detail_filler_id BIGINT, - detail_filler_name VARCHAR(64), - - -- 审计字段 - created_by BIGINT, - created_by_name VARCHAR(64), - created_at TIMESTAMPTZ DEFAULT NOW(), - updated_at TIMESTAMPTZ DEFAULT NOW(), - deleted_at TIMESTAMPTZ -); - -CREATE INDEX idx_projects_namespace ON projects(namespace); -CREATE INDEX idx_projects_lifecycle ON projects(lifecycle_status); -CREATE INDEX idx_projects_deleted ON projects(deleted_at); -``` - -## project_versions 表(版本历史) - -```sql -CREATE TABLE project_versions ( - id BIGSERIAL PRIMARY KEY, - project_id VARCHAR(64) NOT NULL, - - -- 版本号 (正式版本递增, 草稿为0) - version INT NOT NULL DEFAULT 0, - -- 版本类型: official/fill_draft/modify_draft - version_type VARCHAR(32) NOT NULL, - - -- 基准版本号(草稿基于哪个正式版本创建) - base_version INT DEFAULT 0, - - -- 草稿所属用户 - user_id BIGINT, - user_name VARCHAR(64), - - -- 关联工单 - workflow_id VARCHAR(64), - - -- 完整快照 - snapshot_data JSONB, - - -- 变更信息 - commit_message VARCHAR(255), - committer_id BIGINT, - committer_name VARCHAR(64), - - created_at TIMESTAMPTZ DEFAULT NOW(), - updated_at TIMESTAMPTZ DEFAULT NOW() -); - -CREATE INDEX idx_versions_project ON project_versions(project_id); -CREATE INDEX idx_versions_workflow ON project_versions(workflow_id); -CREATE INDEX idx_versions_user ON project_versions(user_id); -CREATE UNIQUE INDEX idx_versions_project_version ON project_versions(project_id, version) WHERE version > 0; -``` - -## project_workflows 表(项目工单关联) - -```sql -CREATE TABLE project_workflows ( - id BIGSERIAL PRIMARY KEY, - project_id VARCHAR(64) NOT NULL, - workflow_id VARCHAR(64) UNIQUE NOT NULL, - - -- 工单类型: fill(填写)/modify(修改) - workflow_type VARCHAR(32) NOT NULL, - - -- 工单状态 (冗余存储,便于查询) - status VARCHAR(32), - - created_at TIMESTAMPTZ DEFAULT NOW(), - updated_at TIMESTAMPTZ DEFAULT NOW() -); - -CREATE INDEX idx_pw_project ON project_workflows(project_id); -CREATE INDEX idx_pw_status ON project_workflows(status); -``` - -## project_auth_configs 表(授权配置) - -```sql -CREATE TABLE project_auth_configs ( - id BIGSERIAL PRIMARY KEY, - project_id VARCHAR(64) UNIQUE NOT NULL, - - tier_one_secret VARCHAR(128), -- 一级TOTP密钥 (加密存储) - time_offset INT DEFAULT 30, -- 允许时间偏移(秒) - totp_enabled BOOLEAN DEFAULT FALSE, - - tier_two_secret VARCHAR(128), -- 二级TOTP密钥 (来自Watchdog) - - auth_type VARCHAR(32), -- permanent/time_limited - auth_days INT, -- 授权有效期(天) - authorized_at TIMESTAMPTZ, - revoked_at TIMESTAMPTZ, - is_offline BOOLEAN DEFAULT FALSE, - - created_at TIMESTAMPTZ DEFAULT NOW(), - updated_at TIMESTAMPTZ DEFAULT NOW() -); - -CREATE INDEX idx_auth_project ON project_auth_configs(project_id); -``` - -## JSONB 结构定义 - -### basic_info - -```json -{ - "province": "北京市", - "city": "北京市", - "industry_contact": "张三", - "industry_phone": "13800138000", - "project_nature": "market" -} -``` - -### deploy_business - -```json -{ - "deployer_name": "李四", - "deployer_phone": "13900139000", - "deploy_start_time": "2026-01-01", - "deploy_end_time": "2026-01-15", - "system_version": "v2.0.0", - "system_type": "fly-control", - "main_entrance": "https://example.com", - "admin_username": "admin", - "admin_password": "" -} -``` - -### deploy_env - -```json -{ - "hosts": [ - { - "hostname": "master-01", - "internal_ip": "192.168.1.10", - "public_ip": "", - "can_access_public": false, - "ssh_port": 22, - "ssh_user": "root", - "ssh_pwd": "", - "role": "master" - } - ], - "network_type": "internal", - "main_public_ip": "", - "domain_url": "", - "ssl_enabled": false, - "management_type": "bastion", - "management_url": "", - "management_user": "", - "management_pwd": "", - "host_count": 3, - "total_cpu": 24, - "cpu_model": "Intel Xeon", - "total_memory": 64, - "total_storage": 1000 -} -``` - -### deploy_middleware - -```json -{ - "mysql": { - "public_ip": "", - "public_port": 0, - "internal_ip": "192.168.1.10", - "internal_port": 3306, - "k8s_address": "mysql-svc", - "k8s_port": 3306, - "admin_user": "root", - "admin_pwd": "", - "version": "8.0" - }, - "redis": { ... }, - "emqx": { ... }, - "minio": { ... }, - "influxdb": { ... }, - "nacos": { ... }, - "k8s_dashboard": { ... } -} -``` - -## 敏感字段加密说明 - -以下字段必须使用 AES-256 加密存储: - -| 表 | 字段路径 | 说明 | -|:---|:---|:---| -| projects | `deploy_business.admin_password` | 系统超管密码 | -| projects | `deploy_env.hosts[*].ssh_pwd` | SSH密码 | -| projects | `deploy_env.management_pwd` | 管理后台密码 | -| projects | `deploy_middleware.*.admin_pwd` | 中间件超管密码 | -| project_auth_configs | `tier_one_secret` | 一级TOTP密钥 | -| project_auth_configs | `tier_two_secret` | 二级TOTP密钥 | - -## 索引优化建议 - -```sql --- 常用查询优化 -CREATE INDEX idx_projects_lifecycle_cert ON projects(lifecycle_status, certification_status); -CREATE INDEX idx_projects_province ON projects((basic_info->>'province')); - --- JSONB GIN 索引(按需添加) -CREATE INDEX idx_projects_basic_gin ON projects USING GIN (basic_info); -``` diff --git a/1-AgentSkills/developing-project-management/reference/frontend-design.md b/1-AgentSkills/developing-project-management/reference/frontend-design.md deleted file mode 100644 index 4a4ebe0..0000000 --- a/1-AgentSkills/developing-project-management/reference/frontend-design.md +++ /dev/null @@ -1,496 +0,0 @@ -# 前端页面设计规范 - -本文档定义项目详情页面的前端设计规范,包括页面架构、组件设计、交互行为和视觉规范。 - ---- - -## 1. 页面文件结构 - -``` -frontend/src/modules/admin/ -├── pages/ -│ ├── admin/ -│ │ └── ProjectDetail.vue # 超级管理员端项目详情 -│ └── user/ -│ └── UserProjectDetail.vue # 普通用户端项目详情 -├── components/ -│ ├── BasicInfoForm.vue # 基本信息编辑表单 -│ ├── BasicInfoReadonly.vue # 基本信息只读展示 -│ ├── BusinessInfoReadonly.vue # 业务信息只读展示 -│ ├── DeploymentBusinessForm.vue # 部署业务编辑表单 -│ ├── DeploymentEnvironmentForm.vue # 部署环境编辑表单 -│ ├── EnvironmentInfoReadonly.vue # 环境信息只读展示 -│ ├── HostsInfoReadonly.vue # 主机信息只读展示 -│ ├── HostsManagement.vue # 主机管理组件 -│ ├── MiddlewareCardsGrid.vue # 中间件卡片网格 -│ ├── MiddlewareInfoReadonly.vue # 中间件只读展示 -│ ├── AuthorizationManagement.vue # 授权管理 (SuperAdmin Only) -│ ├── VersionHistory.vue # 版本历史 (SuperAdmin Only) -│ ├── SaveConfirmDialog.vue # 保存确认对话框 -│ ├── CopyableField.vue # 可复制字段组件 -│ └── DiffTextField.vue # 差异高亮输入框 -``` - ---- - -## 2. 页面架构设计 - -### 2.1 整体布局 - -采用 **「固定头部 + 固定 Tab 导航 + 可滚动内容区域」** 三段式布局: - -``` -┌─────────────────────────────────────────────────────────────────────────┐ -│ [固定区域] 生命周期状态提示横幅 (Alert Banner) │ -├─────────────────────────────────────────────────────────────────────────┤ -│ [固定区域] 页面头部 Header │ -│ ┌─────────────────────────────────────┬─────────────────────────────┐ │ -│ │ ← 返回 项目名称 │ [查看工单] [打回] [通过] │ │ -│ │ Namespace | 省份 城市 │ [下载配置] [编辑/保存] │ │ -│ │ 状态标签组 │ │ │ -│ └─────────────────────────────────────┴─────────────────────────────┘ │ -├─────────────────────────────────────────────────────────────────────────┤ -│ [固定区域] Tab 导航栏 │ -│ ┌─────────────────────────────────────────────────────────────────────┐│ -│ │ 基本信息 | 部署业务 | 部署环境 | 主机管理 | 中间件 | 授权 | 版本历史 ││ -│ └─────────────────────────────────────────────────────────────────────┘│ -├─────────────────────────────────────────────────────────────────────────┤ -│ [滚动区域] Tab 内容区域 │ -└─────────────────────────────────────────────────────────────────────────┘ -``` - -### 2.2 CSS 布局核心 - -```css -.project-detail-page { - height: 100%; - max-height: 100%; - overflow: hidden; - display: flex; - flex-direction: column; -} - -.header-section { - flex-shrink: 0; - background: rgb(var(--v-theme-surface)); - z-index: 1; -} - -.content-area { - flex: 1 1 auto; - overflow-y: auto; - overflow-x: hidden; - min-height: 0; /* 关键:防止 Flex 子元素撑破父容器 */ - padding-bottom: 24px; -} -``` - ---- - -## 3. 查看/编辑状态分离 - -### 3.1 状态定义 - -| 状态 | 变量名 | 说明 | -|:---|:---|:---| -| **查看状态** | `isEditMode = false` | 默认状态,展示 `*Readonly.vue` 组件 | -| **编辑状态** | `isEditMode = true` | 编辑模式,展示 `*Form.vue` 组件 | - -### 3.2 查看状态交互 - -| 交互 | 实现 | -|:---|:---| -| 一键复制 | `CopyableField` 组件,点击图标复制到剪贴板 | -| 密码查看 | 点击"小眼睛"切换明文/密文 | -| 链接跳转 | URL 字段点击新窗口打开 | - -### 3.3 编辑状态数据流 - -```typescript -// 进入编辑模式 -const enterEditMode = () => { - editForm.value = JSON.parse(JSON.stringify(masterData.value)) // 深拷贝 - isEditMode.value = true -} - -// 脏数据检测 -const hasChanges = computed(() => { - return JSON.stringify(editForm.value) !== JSON.stringify(masterData.value) -}) - -// 退出保护 -const exitEditMode = () => { - if (hasChanges.value) { - exitConfirmDialog.value = true // 弹出确认对话框 - } else { - isEditMode.value = false - } -} -``` - ---- - -## 4. 用户侧 vs 管理侧差异 - -| 特性 | 管理员端 (ProjectDetail.vue) | 用户端 (UserProjectDetail.vue) | -|:---|:---|:---| -| **默认模式** | 查看模式 | 根据工单状态决定 | -| **授权信息 Tab** | ✅ 可见 | ❌ 不可见 | -| **版本历史 Tab** | ✅ 可见 | ❌ 不可见 | -| **主机管理 Tab** | ✅ 可见 | ❌ 不可见 | -| **基本信息** | 可编辑 | 只读(由管理员填写) | -| **编辑操作** | 直接保存(上帝模式) | 草稿 → 提交审核(工单流程) | -| **审批按钮** | ✅ 通过/打回 | ❌ 无 | -| **保存按钮** | 「保存修改」 | 「保存草稿」 | - -### Tab 导航配置 - -```html - - - 基本信息 - 部署业务 - 部署环境 - 主机管理 - 中间件 - 授权信息 - 版本历史 - - - - - 基本信息 - 部署业务 - 部署环境 - 中间件 - -``` - ---- - -## 5. 生命周期状态展示 - -### 5.1 状态标签配置 - -```typescript -// 生命周期状态枚举 -export const LIFECYCLE_STATUS = { - init: '初始化', - drafting: '填写中', - reviewing: '审核中', - released: '已发布', - modifying: '变更中', - archived: '已归档' -} - -// 状态颜色映射 -export const LIFECYCLE_STATUS_COLORS: Record = { - init: 'grey', - drafting: 'info', - reviewing: 'warning', - released: 'success', - modifying: 'primary', - archived: 'grey-darken-1' -} - -// 状态图标 -const LIFECYCLE_STATUS_ICONS: Record = { - init: 'mdi-clock-outline', - drafting: 'mdi-pencil', - reviewing: 'mdi-eye', - released: 'mdi-check-circle', - modifying: 'mdi-sync', - archived: 'mdi-archive' -} -``` - -### 5.2 生命周期提示横幅 - -根据当前状态显示上下文提示: - -```typescript -const lifecycleStatusAlert = computed(() => { - const status = masterData.value?.lifecycle_status - switch (status) { - case 'init': - return { type: 'info', message: '项目已创建,等待指定填写人录入详细信息' } - case 'drafting': - return { type: 'info', message: `项目详情正在由 ${masterData.value.detail_filler_name} 填写中` } - case 'reviewing': - return { type: 'warning', message: '项目详情已提交,等待审核' } - case 'modifying': - return { type: 'info', message: '项目存在活跃的变更工单,主线数据不受影响' } - case 'archived': - return { type: 'warning', message: '项目已归档,仅保留历史数据' } - default: - return null - } -}) -``` - ---- - -## 6. 工单关联与跳转 - -### 6.1 工单按钮显示逻辑 - -```typescript -const showWorkflowButton = computed(() => { - if (!masterData.value?.workflow_id) return false - const status = masterData.value.lifecycle_status - return ['drafting', 'reviewing', 'modifying'].includes(status) -}) - -const workflowButtonText = computed(() => { - const status = masterData.value?.lifecycle_status - switch (status) { - case 'drafting': return '查看填写工单' - case 'reviewing': return '查看审核工单' - case 'modifying': return '查看修改工单' - default: return '查看工单' - } -}) -``` - -### 6.2 多工单场景(MODIFYING 状态) - -当存在多个修改工单时,使用下拉菜单或对话框展示工单列表: - -```html - - - - - {{ wf.workflow_id }} - {{ wf.creator_name }} | {{ formatDate(wf.created_at) }} - - - -``` - ---- - -## 7. 模块字段规范 - -### 7.1 基本信息模块 - -| 字段 | 只读模式 | 编辑模式 | -|:---|:---|:---| -| 项目名称 | 文本 + 复制 | `v-text-field` | -| 命名空间 | 文本 + 复制 | `disabled` 不可编辑 | -| 省份/城市 | 文本 | 级联选择器 | -| 项目性质 | 文本 | `v-select` | - -### 7.2 部署业务模块 - -| 字段 | 只读模式 | 编辑模式 | -|:---|:---|:---| -| 部署人姓名 | 文本 | `v-text-field` 或用户搜索 | -| 业务入口 URL | 可点击链接 | `v-text-field` | -| 超管密码 | 脱敏 `******` + 查看按钮 | `v-text-field` 密码输入 | - -### 7.3 中间件模块 - -采用 **卡片网格** 设计: -- 每个中间件一张卡片,响应式布局 -- 卡片包含:类型图标 + 标题 + IP/Port -- 编辑模式:右上角显示「编辑」「删除」按钮 -- 列表末尾显示「添加中间件」虚线框卡片 - -```typescript -const MIDDLEWARE_ICONS: Record = { - 'mysql': 'mdi-database', - 'redis': 'mdi-database-clock', - 'emqx': 'mdi-broadcast', - 'minio': 'mdi-bucket', - 'influxdb': 'mdi-chart-timeline-variant', - 'nacos': 'mdi-cog-outline', - 'k8s-dashboard': 'mdi-kubernetes' -} -``` - ---- - -## 8. 核心组件设计 - -### 8.1 CopyableField - 可复制字段 - -```html - -``` - -### 8.2 SaveConfirmDialog - 保存确认 - -展示变更 Diff 表格: - -```html - - - 字段修改前修改后 - - - - {{ item.label }} - {{ item.oldValue || '空' }} - {{ item.newValue || '空' }} - - - -``` - -### 8.3 DiffTextField - 差异高亮输入框 - -编辑模式下显示与主线数据的差异: - -```html - - - - - -``` - ---- - -## 9. 视觉设计规范 - -### 9.1 色彩系统 - -| 用途 | Vuetify 类 | -|:---|:---| -| 主色调 | `color="primary"` (Deep Purple) | -| 成功状态 | `color="success"` (Green) | -| 警告状态 | `color="warning"` (Orange) | -| 错误状态 | `color="error"` (Red) | -| 页面背景 | `bg-grey-lighten-4` | - -### 9.2 卡片设计 - -```html - - - -``` - -### 9.3 排版规范 - -| 元素 | 样式类 | -|:---|:---| -| 页面标题 | `text-h4 font-weight-bold` | -| 卡片标题 | `text-h6` | -| 字段标签 | `text-medium-emphasis text-body-2` | -| 字段值 | `text-high-emphasis` | - -### 9.4 间距规范(8px 网格) - -| 间距 | 类 | 值 | -|:---|:---|:---| -| 紧凑 | `pa-2` | 8px | -| 标准 | `pa-4` | 16px | -| 宽松 | `pa-6` | 24px | - ---- - -## 10. 响应式设计 - -### 10.1 断点 - -| 断点 | 宽度 | -|:---|:---| -| xs | < 600px | -| sm | 600px - 960px | -| md | 960px - 1280px | -| lg | 1280px - 1920px | - -### 10.2 中间件卡片响应式 - -```html - - - - - -``` - ---- - -## 11. TypeScript 类型定义 - -```typescript -// 项目详情 -interface ProjectDetail { - id: number - project_id: string - project_name: string - namespace: string - province: string - city: string - project_nature: string - lifecycle_status: string - project_certification: string - workflow_id: string - detail_filler_id: number - detail_filler_name: string - deployment_business: DeploymentBusiness | null - deployment_environment: DeploymentEnvironment | null - middlewares: Middleware[] - hosts: Host[] - draft_data: Record | null -} - -// Diff 项 -interface DiffItem { - field: string - label: string - oldValue: string | number | boolean - newValue: string | number | boolean -} -``` - ---- - -## 12. 组件清单 - -| 组件 | 说明 | 复用范围 | -|:---|:---|:---| -| `BasicInfoForm.vue` | 基本信息编辑表单 | 管理员/用户 | -| `BasicInfoReadonly.vue` | 基本信息只读 | 管理员/用户 | -| `DeploymentBusinessForm.vue` | 业务信息表单 | 管理员/用户 | -| `DeploymentEnvironmentForm.vue` | 环境信息表单 | 管理员/用户 | -| `MiddlewareCardsGrid.vue` | 中间件卡片网格 | 管理员/用户 | -| `AuthorizationManagement.vue` | 授权管理 | 仅管理员 | -| `VersionHistory.vue` | 版本历史 | 仅管理员 | -| `HostsManagement.vue` | 主机管理 | 仅管理员 | -| `SaveConfirmDialog.vue` | 保存确认对话框 | 管理员 | -| `CopyableField.vue` | 可复制字段 | 通用 | -| `DiffTextField.vue` | 差异高亮输入框 | 通用 | diff --git a/1-AgentSkills/developing-project-management/reference/lifecycle-state-machine.md b/1-AgentSkills/developing-project-management/reference/lifecycle-state-machine.md deleted file mode 100644 index fbf3ff8..0000000 --- a/1-AgentSkills/developing-project-management/reference/lifecycle-state-machine.md +++ /dev/null @@ -1,90 +0,0 @@ -# 项目生命周期状态机 - -## 状态定义 - -| 状态 | 说明 | 触发动作 | 权限 | -|:---|:---|:---|:---| -| **INIT** | 项目元数据已创建,等待详细信息录入 | 超级管理员创建项目 | SuperAdmin | -| **DRAFTING** | 正在进行初始信息填写(关联填写工单) | 指定填写人保存/编辑 | 填写人/SuperAdmin | -| **REVIEWING** | 初始信息或变更信息提交审核 | 提交审核 | SuperAdmin | -| **RELEASED** | 审核通过,正常运行中 | 审核通过 | All (View) | -| **MODIFYING** | 存在活跃的变更工单(不影响主线运行) | 发起修改工单 | Owner/SuperAdmin | -| **ARCHIVED** | 软删除状态,不可见但保留数据 | 删除项目 | SuperAdmin | - -## 状态转换图 - -``` -[*] ──创建项目──> INIT - │ - ├──分配填写人──> DRAFTING ──保存草稿──> DRAFTING - │ │ - │ └──提交审核──> REVIEWING - │ │ - │ ┌──审核打回──<────┤ - │ │ │ - │ v └──审核通过──> RELEASED - │ DRAFTING │ - │ │ - │ ┌──发起修改工单──<───────────────┤ - │ │ │ - │ v └──归档删除──> ARCHIVED ──> [*] - │ MODIFYING ──保存草稿──> MODIFYING - │ │ - │ ├──提交变更审核──> REVIEWING - │ │ - │ └──撤销变更──> RELEASED -``` - -## 状态转换条件 - -| From | To | 事件 | 条件 | -|:---|:---|:---|:---| -| INIT | DRAFTING | 分配填写人 | 填写人 ID 有效 | -| DRAFTING | DRAFTING | 保存草稿 | 表单数据有效 | -| DRAFTING | REVIEWING | 提交审核 | 必填字段完整 | -| REVIEWING | DRAFTING | 审核打回 | SuperAdmin 操作 | -| REVIEWING | RELEASED | 审核通过 | SuperAdmin 操作 | -| RELEASED | MODIFYING | 发起修改工单 | 有 View ACL 权限 | -| RELEASED | ARCHIVED | 归档删除 | SuperAdmin 操作 | -| MODIFYING | MODIFYING | 保存草稿 | 表单数据有效 | -| MODIFYING | REVIEWING | 提交变更审核 | 必填字段完整 | -| MODIFYING | RELEASED | 撤销变更/审核通过 | 用户操作/SuperAdmin | - -## Mermaid 状态图 - -```mermaid -stateDiagram-v2 - [*] --> INIT: 创建项目 - - INIT --> DRAFTING: 分配填写人 - - DRAFTING --> DRAFTING: 保存草稿 - DRAFTING --> REVIEWING: 提交审核 - - REVIEWING --> DRAFTING: 审核打回 - REVIEWING --> RELEASED: 审核通过 - - RELEASED --> MODIFYING: 发起修改工单 - RELEASED --> ARCHIVED: 归档删除 - - MODIFYING --> MODIFYING: 保存草稿 - MODIFYING --> REVIEWING: 提交变更审核 - MODIFYING --> RELEASED: 撤销变更/审核通过 - - ARCHIVED --> [*] - - note right of RELEASED: 项目认证状态=official - note right of DRAFTING: 支持多次保存草稿 - note right of MODIFYING: 可同时存在多个变更工单 -``` - -## 状态与认证状态映射 - -| 生命周期状态 | 认证状态 (certification_status) | -|:---|:---| -| INIT | draft | -| DRAFTING | draft | -| REVIEWING | pending | -| RELEASED | official | -| MODIFYING | official (主线不变) | -| ARCHIVED | official (保留) | diff --git a/1-AgentSkills/developing-project-management/reference/version-control-design.md b/1-AgentSkills/developing-project-management/reference/version-control-design.md deleted file mode 100644 index 393a976..0000000 --- a/1-AgentSkills/developing-project-management/reference/version-control-design.md +++ /dev/null @@ -1,448 +0,0 @@ -# 版本控制设计 (Git-like) - -## 设计原则 - -采用**统一版本表**设计,将正式版本和草稿版本存储在同一张表中,通过 `version_type` 字段区分。项目信息采用类似 Git 的分支管理模式: -- **Master 分支**: 由 SuperAdmin 审核维护的正式版本 -- **用户草稿**: 每个用户都有自己的临时分支,提交审核后合并入 Master - -## 版本类型 - -| 版本类型 | 代码 | version 值 | 说明 | -|:---|:---|:---|:---| -| 正式版本 | `official` | 1, 2, 3... (递增) | 审核通过后的正式版本,构成版本历史 | -| 填写草稿 | `fill_draft` | 0 | 项目创建时填写人的草稿 | -| 修改草稿 | `modify_draft` | 0 | 发起变更工单时的草稿 | - -## 版本与工单关系 - -| 关系 | 说明 | -|:---|:---| -| 填写草稿 : 填写工单 | 1:1 关联 | -| 修改草稿 : 修改工单 | 1:1 关联 | -| 正式版本 : 工单 | 审核通过后由草稿转化而来 | -| 项目 : 修改草稿 | 1:N(一个项目可有多个修改草稿) | - -## 版本快照机制 - -每次审核通过后,系统自动生成一个**完整快照**存储到 `project_versions` 表中。 - -### 快照结构 - -```go -// VersionSnapshot 版本快照结构 -type VersionSnapshot struct { - BasicInfo *BasicInfo `json:"basic_info"` - DeployBusiness *DeployBusiness `json:"deploy_business"` - DeployEnv *DeployEnv `json:"deploy_env"` - DeployMiddleware *DeployMiddleware `json:"deploy_middleware"` -} -``` - -### 快照生成时机 - -| 场景 | 版本号 | 版本类型 | 说明 | -|:---|:---|:---|:---| -| 项目首次审批通过 | v1 | official | 项目初始版本 | -| 修改工单审批通过 | v(N+1) | official | 增量版本 | -| **超管直接修改** | v(N+1) | official | **重要:超管直改也必须生成新版本** | -| 用户保存草稿 | 0 | fill_draft/modify_draft | 临时版本,不计入历史 | - -## 超级管理员直改与版本一致性 - -### 问题风险 - -如果超级管理员直接修改 `projects` 表数据而不生成版本历史,会导致: -1. 版本链断裂 -2. 后续基于旧版本的工单 Diff 结果失效或产生误导 -3. 审计日志不完整 - -### 解决方案 - -超级管理员的 "Direct Edit" 操作必须被视为一次**自动审批通过的事务**: - -1. **原子操作**:更新 `projects` 表 + 插入 `project_versions` 表必须在同一数据库事务中完成 -2. **版本归属**: - - `workflow_id` 为空或特定系统标识(如 `DIRECT_EDIT`) - - `committer_id` 记录为 SuperAdmin ID - - `commit_message` 强制填写或自动生成(如 "SuperAdmin Direct Update") -3. **结果**:确保 `projects.current_version` 永远指向最新的 `project_versions.version` - -### 实现代码 - -```go -// SuperAdmin 直接修改项目(必须同时生成版本) -func (s *ProjectService) DirectUpdate(ctx context.Context, req *DirectUpdateRequest) error { - return s.db.Transaction(func(tx *gorm.DB) error { - // 1. 获取当前项目 - var project entity.Project - if err := tx.Where("project_id = ?", req.ProjectID).First(&project).Error; err != nil { - return err - } - - // 2. 更新项目主表 - newVersion := project.CurrentVersion + 1 - if err := tx.Model(&project).Updates(map[string]interface{}{ - "basic_info": req.BasicInfo, - "deploy_business": req.DeployBusiness, - "deploy_env": req.DeployEnv, - "deploy_middleware": req.DeployMiddleware, - "current_version": newVersion, - }).Error; err != nil { - return err - } - - // 3. 同时生成版本记录(关键!) - version := &entity.ProjectVersion{ - ProjectID: req.ProjectID, - Version: newVersion, - VersionType: "official", - BaseVersion: project.CurrentVersion, - SnapshotData: buildSnapshot(req), - CommitMessage: req.CommitMessage, // 或自动生成 - CommitterID: req.OperatorID, - CommitterName: req.OperatorName, - } - if err := tx.Create(version).Error; err != nil { - return err - } - - // 4. 记录审计日志 - return s.auditSvc.Log(ctx, tx, AuditLog{ - Resource: "project", - Action: "direct_update", - ResourceID: req.ProjectID, - Details: map[string]interface{}{"new_version": newVersion}, - }) - }) -} -``` - -## 并发修改与冲突检测 (Optimistic Locking) - -由于超级管理员可能在其他用户编辑草稿期间直接修改项目,需要引入乐观锁机制处理冲突。 - -### 冲突场景 - -``` -时间线: -T1: 用户 A 基于 v3 版本创建草稿 (Draft.base_version = 3) -T2: 超级管理员直接修改项目,版本升级为 v4 (Project.current_version = 4) -T3: 用户 A 提交草稿审核 → 检测到冲突! -``` - -### 处理策略 - -1. **提交时校验**:工单提交/审核接口需校验 `draft.base_version == project.current_version` - -2. **冲突提示**:如果版本不一致,后端返回 `409 Conflict` 错误 - -3. **前端交互**: - - 提示用户:"项目已被修改,当前草稿已过期" - - 提供 **"Rebase" (变基)** 选项:将当前草稿的修改重新应用到最新版本 - - 或提供 **"Diff Check"**:让用户查看当前草稿与最新版本的差异 - -### 冲突检测代码 - -```go -// 提交草稿时检测版本冲突 -func (s *DraftService) SubmitDraft(ctx context.Context, req *SubmitDraftRequest) error { - // 1. 获取草稿 - var draft entity.ProjectVersion - if err := s.db.Where("project_id = ? AND user_id = ? AND version_type IN (?, ?)", - req.ProjectID, req.UserID, "fill_draft", "modify_draft").First(&draft).Error; err != nil { - return err - } - - // 2. 获取项目当前版本 - var project entity.Project - if err := s.db.Where("project_id = ?", req.ProjectID).First(&project).Error; err != nil { - return err - } - - // 3. 乐观锁检查 - if draft.BaseVersion != project.CurrentVersion { - return &VersionConflictError{ - DraftBaseVersion: draft.BaseVersion, - CurrentVersion: project.CurrentVersion, - Message: "项目已被修改,当前草稿已过期,请重新基于最新版本编辑", - } - } - - // 4. 继续提交流程... - return s.workflowTransitioner.TransitionWorkflow(draft.WorkflowID, "complete", ...) -} -``` - -### 错误响应格式 - -```json -{ - "code": 40901, - "message": "版本冲突:项目已被修改", - "data": { - "draft_base_version": 3, - "current_version": 4, - "suggestion": "请点击\"重新加载\"获取最新版本后重新编辑" - } -} -``` - ---- - -## 版本 Diff 算法 - -采用 **JSON Diff** 算法,对比两个版本快照的差异,按模块分组展示。 - -### 差异结构 - -```go -// DiffResult 差异结果(按模块分组) -type DiffResult struct { - Module string `json:"module"` // 模块名称(中文) - ModuleCode string `json:"module_code"` // 模块代码 - FieldDiffs []FieldDiff `json:"field_diffs"` // 字段差异列表 -} - -// FieldDiff 字段差异 -type FieldDiff struct { - FieldPath string `json:"field_path"` // 字段路径 如 "deploy_env.host_count" - FieldName string `json:"field_name"` // 字段中文名 - OldValue interface{} `json:"old_value"` // 旧值 - NewValue interface{} `json:"new_value"` // 新值 - ChangeType string `json:"change_type"` // add/modify/delete -} -``` - -### 字段名映射表 - -```go -// fieldNameMap 字段路径到中文名的映射 -var fieldNameMap = map[string]string{ - // 基本信息 - "basic_info.province": "省份", - "basic_info.city": "城市", - "basic_info.industry_contact": "行业组人员", - "basic_info.industry_phone": "行业组电话", - "basic_info.project_nature": "项目性质", - - // 部署业务 - "deploy_business.deployer_name": "部署人姓名", - "deploy_business.deployer_phone": "部署人电话", - "deploy_business.deploy_start_time": "部署开始时间", - "deploy_business.deploy_end_time": "部署结束时间", - "deploy_business.system_version": "系统版本", - "deploy_business.system_type": "系统类型", - "deploy_business.main_entrance": "业务主入口", - "deploy_business.admin_username": "超管用户名", - - // 部署环境 - "deploy_env.network_type": "网络环境", - "deploy_env.main_public_ip": "主要公网IP", - "deploy_env.domain_url": "域名URL", - "deploy_env.ssl_enabled": "是否开启SSL", - "deploy_env.host_count": "主机台数", - "deploy_env.total_cpu": "CPU总核数", - "deploy_env.total_memory": "内存总大小(GB)", - "deploy_env.total_storage": "存储总大小(GB)", - - // 部署中间件 - "deploy_middleware.mysql.internal_port": "MySQL内网端口", - "deploy_middleware.redis.internal_port": "Redis内网端口", - // ... 其他字段 -} -``` - -### Diff 实现 - -```go -// CompareVersions 比较两个版本的差异 -// @param baseVersion 基准版本(通常是较早的版本或 master) -// @param targetVersion 目标版本(通常是较新的版本或草稿) -// @return []DiffResult 差异结果列表,按模块分组 -func (s *VersionService) CompareVersions( - ctx context.Context, - baseVersion, targetVersion *VersionSnapshot, -) ([]DiffResult, error) { - var results []DiffResult - - // 分模块对比 - modules := []struct { - Name string - Code string - Base interface{} - Target interface{} - }{ - {"基本信息", "basic_info", baseVersion.BasicInfo, targetVersion.BasicInfo}, - {"部署业务", "deploy_business", baseVersion.DeployBusiness, targetVersion.DeployBusiness}, - {"部署环境", "deploy_env", baseVersion.DeployEnv, targetVersion.DeployEnv}, - {"部署中间件", "deploy_middleware", baseVersion.DeployMiddleware, targetVersion.DeployMiddleware}, - } - - for _, m := range modules { - diffs := s.diffJSON(m.Code, m.Base, m.Target) - if len(diffs) > 0 { - results = append(results, DiffResult{ - Module: m.Name, - ModuleCode: m.Code, - FieldDiffs: diffs, - }) - } - } - return results, nil -} - -// diffJSON 对比两个 JSON 对象的差异 -func (s *VersionService) diffJSON(moduleCode string, base, target interface{}) []FieldDiff { - var diffs []FieldDiff - - baseMap := structToMap(base) - targetMap := structToMap(target) - - // 检查修改和删除 - for key, oldVal := range baseMap { - fieldPath := moduleCode + "." + key - if newVal, exists := targetMap[key]; exists { - if !reflect.DeepEqual(oldVal, newVal) { - diffs = append(diffs, FieldDiff{ - FieldPath: fieldPath, - FieldName: getFieldName(fieldPath), - OldValue: oldVal, - NewValue: newVal, - ChangeType: "modify", - }) - } - } else { - diffs = append(diffs, FieldDiff{ - FieldPath: fieldPath, - FieldName: getFieldName(fieldPath), - OldValue: oldVal, - NewValue: nil, - ChangeType: "delete", - }) - } - } - - // 检查新增 - for key, newVal := range targetMap { - if _, exists := baseMap[key]; !exists { - fieldPath := moduleCode + "." + key - diffs = append(diffs, FieldDiff{ - FieldPath: fieldPath, - FieldName: getFieldName(fieldPath), - OldValue: nil, - NewValue: newVal, - ChangeType: "add", - }) - } - } - - return diffs -} -``` - ---- - -## 版本历史查询 - -### 版本列表结构 - -```go -// VersionHistory 版本历史记录 -type VersionHistory struct { - Version int `json:"version"` // 版本号 - VersionType string `json:"version_type"` // 版本类型 - CommitMessage string `json:"commit_message"` // 变更说明 - CommitterID int64 `json:"committer_id"` // 提交人 ID - CommitterName string `json:"committer_name"` // 提交人姓名 - WorkflowID string `json:"workflow_id"` // 关联工单 ID(可跳转) - CreatedAt time.Time `json:"created_at"` // 创建时间 - ChangeSummary string `json:"change_summary"` // 变更摘要(如:修改了 3 个字段) - IsCurrent bool `json:"is_current"` // 是否为当前版本 -} -``` - -### 版本历史 API - -| 方法 | 路径 | 描述 | -|:---|:---|:---| -| POST | `/api/project/version/list` | 获取版本历史列表 | -| POST | `/api/project/version/detail` | 获取指定版本详情(完整快照) | -| POST | `/api/project/version/diff` | 对比两个版本差异 | -| POST | `/api/project/version/diff-with-current` | 对比指定版本与当前版本差异 | - ---- - -## 前端展示设计 - -### 版本历史页面 - -``` -┌─────────────────────────────────────────────────────────────────┐ -│ 项目版本历史 - [项目名称] │ -├─────────────────────────────────────────────────────────────────┤ -│ │ -│ ●──v3 (当前版本) 2026-01-14 15:30 张三 │ -│ │ └─ 变更说明: 更新部署环境信息 │ -│ │ └─ 关联工单: #WF-20260114-001 [点击跳转] │ -│ │ └─ 变更摘要: 修改了 2 个字段 │ -│ │ │ -│ ●──v2 2026-01-10 10:00 李四 │ -│ │ └─ 变更说明: 修改中间件配置 │ -│ │ └─ 关联工单: #WF-20260110-002 │ -│ │ │ -│ ●──v1 (初始版本) 2026-01-05 09:00 王五 │ -│ └─ 变更说明: 项目初始填写 │ -│ └─ 关联工单: #WF-20260105-001 │ -│ │ -│ [查看详情] [对比版本] │ -└─────────────────────────────────────────────────────────────────┘ -``` - -### Diff 对比页面 - -``` -┌─────────────────────────────────────────────────────────────────┐ -│ 版本对比: v2 → v3 │ -├─────────────────────────────────────────────────────────────────┤ -│ 模块: 部署环境 │ -│ ┌───────────────────────────────────────────────────────────┐ │ -│ │ 字段 │ v2 (旧值) │ v3 (新值) │ │ -│ ├───────────────────────────────────────────────────────────┤ │ -│ │ 主机台数 │ 3 │ 5 [修改] │ │ -│ │ 主要公网 IP │ 10.0.0.1 │ 192.168.1.100 [修改] │ │ -│ │ 域名 URL │ - │ www.example.com [新增] │ │ -│ └───────────────────────────────────────────────────────────┘ │ -│ │ -│ 模块: 部署中间件 │ -│ ┌───────────────────────────────────────────────────────────┐ │ -│ │ 字段 │ v2 (旧值) │ v3 (新值) │ │ -│ ├───────────────────────────────────────────────────────────┤ │ -│ │ MySQL.内网端口 │ 3306 │ 3307 [修改] │ │ -│ └───────────────────────────────────────────────────────────┘ │ -│ │ -│ 变更统计: 共 4 个字段变更 (新增: 1, 修改: 3, 删除: 0) │ -└─────────────────────────────────────────────────────────────────┘ -``` - -### 草稿编辑页面 Diff 提示 - -在用户编辑草稿时,实时显示与主线版本的差异: - -``` -┌─────────────────────────────────────────────────────────────────┐ -│ 编辑项目详情 - [项目名称] [保存草稿] [提交审核] │ -├─────────────────────────────────────────────────────────────────┤ -│ ⚠️ 您的草稿基于 v3 版本,与当前版本有以下差异: │ -│ ┌───────────────────────────────────────────────────────────┐ │ -│ │ • 主机台数: 3 → 5 │ │ -│ │ • 系统版本: v2.0.0 → v2.1.0 │ │ -│ └───────────────────────────────────────────────────────────┘ │ -│ │ -│ [基本信息] [部署业务] [部署环境] [部署中间件] │ -│ ───────────────────────────────────────────────────────────── │ -│ 省份: [北京市 ▼] │ -│ 城市: [北京市 ▼] │ -│ ... │ -└─────────────────────────────────────────────────────────────────┘ -``` diff --git a/1-AgentSkills/developing-project-management/scripts/verify-project-module.sh b/1-AgentSkills/developing-project-management/scripts/verify-project-module.sh index 2195667..20cac8f 100644 --- a/1-AgentSkills/developing-project-management/scripts/verify-project-module.sh +++ b/1-AgentSkills/developing-project-management/scripts/verify-project-module.sh @@ -1,231 +1,186 @@ #!/bin/bash # verify-project-module.sh -# 验证 rmdc-project-management 模块的完整性 -# 依赖: go, grep, find -# 用法: ./verify-project-module.sh [project-root-path] +# 验证 developing-project-management Skill 的完整性 -set -e +SKILL_DIR="$(dirname "$0")/.." +REFERENCE_DIR="$SKILL_DIR/reference" -PROJECT_ROOT="${1:-.}" - -echo "==========================================" -echo "RMDC Project Management Module Verification" -echo "==========================================" -echo "Project Root: $PROJECT_ROOT" +echo "========================================" +echo "验证 developing-project-management Skill" +echo "========================================" echo "" -# 颜色定义 -RED='\033[0;31m' -GREEN='\033[0;32m' -YELLOW='\033[1;33m' -NC='\033[0m' # No Color - PASS_COUNT=0 FAIL_COUNT=0 -WARN_COUNT=0 -check_pass() { - echo -e "${GREEN}[PASS]${NC} $1" - ((PASS_COUNT++)) +pass() { + echo "PASS: $1" + PASS_COUNT=$((PASS_COUNT + 1)) } -check_fail() { - echo -e "${RED}[FAIL]${NC} $1" - ((FAIL_COUNT++)) +fail() { + echo "FAIL: $1" + FAIL_COUNT=$((FAIL_COUNT + 1)) } -check_warn() { - echo -e "${YELLOW}[WARN]${NC} $1" - ((WARN_COUNT++)) -} +# ======================================== +# 1. 验证章节分层目录结构 +# ======================================== +echo "--- 1. 验证章节分层目录结构 ---" -# 1. 检查实体定义 -echo "1. Checking Entity Definitions..." -if grep -rq "LifecycleStatus" "$PROJECT_ROOT"/internal/project/entity/*.go 2>/dev/null; then - check_pass "LifecycleStatus field exists in Project entity" -else - check_fail "LifecycleStatus field missing in Project entity" -fi - -if grep -rq "CurrentVersion" "$PROJECT_ROOT"/internal/project/entity/*.go 2>/dev/null; then - check_pass "CurrentVersion field exists for version tracking" -else - check_fail "CurrentVersion field missing for version tracking" -fi - -if grep -rq "CertificationStatus" "$PROJECT_ROOT"/internal/project/entity/*.go 2>/dev/null; then - check_pass "CertificationStatus field exists" -else - check_fail "CertificationStatus field missing" -fi - -# 2. 检查生命周期状态常量 -echo "" -echo "2. Checking Lifecycle Status Constants..." -REQUIRED_STATES=("INIT" "DRAFTING" "REVIEWING" "RELEASED" "MODIFYING" "ARCHIVED") -for state in "${REQUIRED_STATES[@]}"; do - if grep -rq "\"$state\"" "$PROJECT_ROOT"/internal/project/ 2>/dev/null; then - check_pass "Lifecycle state '$state' defined" +# 检查必需的章节目录 +for chapter in "01-architecture-overview" "02-lifecycle-state-machine" "03-permission-model" "04-version-control" "05-database-schema" "06-api-design" "07-frontend-design"; do + if [ -d "$REFERENCE_DIR/$chapter" ]; then + pass "章节目录存在: $chapter" else - check_fail "Lifecycle state '$state' not found" + fail "章节目录缺失: $chapter" fi done -# 3. 检查 API 路由 +# ======================================== +# 2. 验证 reference 文件包含 DDS 追溯信息 +# ======================================== echo "" -echo "3. Checking API Routes..." -REQUIRED_ROUTES=( - "/api/project/list" - "/api/project/detail" - "/api/project/create" - "/api/project/update" - "/api/project/draft/save" - "/api/project/draft/submit" - "/api/project/version/list" - "/api/project/permission/grant" -) -for route in "${REQUIRED_ROUTES[@]}"; do - if grep -rq "$route" "$PROJECT_ROOT"/internal/project/ 2>/dev/null; then - check_pass "Route '$route' registered" +echo "--- 2. 验证 DDS 追溯信息 ---" + +# 检查 DDS-Section 标记 +if grep -r "DDS-Section:" "$REFERENCE_DIR" > /dev/null 2>&1; then + pass "存在 DDS-Section 追溯标记" +else + fail "缺少 DDS-Section 追溯标记" +fi + +# 检查 DDS-Lines 标记 +if grep -r "DDS-Lines:" "$REFERENCE_DIR" > /dev/null 2>&1; then + pass "存在 DDS-Lines 追溯标记" +else + fail "缺少 DDS-Lines 追溯标记" +fi + +# ======================================== +# 3. 验证前端设计文档完整性 +# ======================================== +echo "" +echo "--- 3. 验证前端设计文档完整性 ---" + +FRONTEND_DIR="$REFERENCE_DIR/07-frontend-design" + +for file in "page-architecture.md" "view-edit-states.md" "user-admin-difference.md" "lifecycle-workflow-display.md" "module-design-specs.md" "component-specifications.md" "visual-design-specs.md" "interaction-sequences.md"; do + if [ -f "$FRONTEND_DIR/$file" ]; then + pass "前端设计文档存在: $file" else - check_fail "Route '$route' not found" + fail "前端设计文档缺失: $file" fi done -# 4. 检查权限中间件 +# ======================================== +# 4. 验证后端设计文档完整性 +# ======================================== echo "" -echo "4. Checking Permission Middleware..." -if grep -rq "SuperAdmin\|RequireRole\|RequirePermission" "$PROJECT_ROOT"/internal/project/handler/*.go 2>/dev/null; then - check_pass "Permission middleware applied" +echo "--- 4. 验证后端设计文档完整性 ---" + +# 检查模块依赖文档 +if [ -f "$REFERENCE_DIR/01-architecture-overview/module-dependencies.md" ]; then + pass "模块依赖文档存在" else - check_warn "Permission middleware not detected - verify manually" + fail "模块依赖文档缺失" fi -# 5. 检查版本服务 +# 检查状态机文档 +if [ -f "$REFERENCE_DIR/02-lifecycle-state-machine/lifecycle-states.md" ]; then + pass "生命周期状态文档存在" +else + fail "生命周期状态文档缺失" +fi + +# 检查工单映射文档 +if [ -f "$REFERENCE_DIR/02-lifecycle-state-machine/workflow-state-mapping.md" ]; then + pass "工单状态映射文档存在" +else + fail "工单状态映射文档缺失" +fi + +# 检查权限模型文档 +if [ -f "$REFERENCE_DIR/03-permission-model/acl-permission.md" ]; then + pass "ACL 权限模型文档存在" +else + fail "ACL 权限模型文档缺失" +fi + +# 检查版本控制文档 +if [ -f "$REFERENCE_DIR/04-version-control/version-design.md" ]; then + pass "版本控制设计文档存在" +else + fail "版本控制设计文档缺失" +fi + +# 检查数据库schema文档 +if [ -f "$REFERENCE_DIR/05-database-schema/database-schema.md" ]; then + pass "数据库 Schema 文档存在" +else + fail "数据库 Schema 文档缺失" +fi + +# 检查API文档 +if [ -f "$REFERENCE_DIR/06-api-design/api-endpoints.md" ]; then + pass "API 端点文档存在" +else + fail "API 端点文档缺失" +fi + +# ======================================== +# 5. 验证 SKILL.md 结构 +# ======================================== echo "" -echo "5. Checking Version Service..." -if grep -rq "CompareVersions" "$PROJECT_ROOT"/internal/project/service/*.go 2>/dev/null; then - check_pass "Version comparison service implemented" +echo "--- 5. 验证 SKILL.md 结构 ---" + +SKILL_FILE="$SKILL_DIR/SKILL.md" + +# 检查 frontmatter +if grep -q "^name:" "$SKILL_FILE" && grep -q "^description:" "$SKILL_FILE"; then + pass "SKILL.md frontmatter 结构正确" else - check_fail "Version comparison service not found" + fail "SKILL.md frontmatter 结构缺失" fi -if grep -rq "CreateOfficialVersion" "$PROJECT_ROOT"/internal/project/service/*.go 2>/dev/null; then - check_pass "CreateOfficialVersion method exists" +# 检查章节引用 +if grep -q "reference/07-frontend-design/" "$SKILL_FILE"; then + pass "SKILL.md 引用了前端设计文档" else - check_fail "CreateOfficialVersion method not found" + fail "SKILL.md 未引用前端设计文档" fi -# 6. 检查回调接口 -echo "" -echo "6. Checking Lifecycle Callback Interface..." -if grep -rq "ProjectLifecycleUpdater" "$PROJECT_ROOT"/internal/project/*.go 2>/dev/null; then - check_pass "ProjectLifecycleUpdater interface defined" +# 检查前端相关内容 +if grep -q "Vue3\|Vuetify\|frontend" "$SKILL_FILE"; then + pass "SKILL.md 包含前端开发指导" else - check_fail "ProjectLifecycleUpdater interface not found" + fail "SKILL.md 缺少前端开发指导" fi -CALLBACK_METHODS=("SetLifecycleToDrafting" "SetLifecycleToReviewing" "SetLifecycleToReleased" "SetLifecycleToModifying") -for method in "${CALLBACK_METHODS[@]}"; do - if grep -rq "$method" "$PROJECT_ROOT"/internal/project/ 2>/dev/null; then - check_pass "Callback method '$method' implemented" +# 检查核心章节 +for section in "Plan" "Verify" "Execute" "Pitfalls"; do + if grep -q "## $section" "$SKILL_FILE"; then + pass "SKILL.md 包含 $section 章节" else - check_fail "Callback method '$method' not found" - fi -done - -# 7. 检查敏感字段加密 -echo "" -echo "7. Checking Sensitive Field Encryption..." -if grep -rq "Encrypt\|Decrypt\|AES\|crypto" "$PROJECT_ROOT"/internal/project/service/*.go 2>/dev/null; then - check_pass "Encryption functions detected" -else - check_warn "Encryption functions not detected - verify password field handling" -fi - -# 8. 检查审计日志集成 -echo "" -echo "8. Checking Audit Log Integration..." -if grep -rq "audit\|AuditService\|auditSvc" "$PROJECT_ROOT"/internal/project/service/*.go 2>/dev/null; then - check_pass "Audit log integration detected" -else - check_warn "Audit log integration not detected - verify manually" -fi - -# 9. 检查乐观锁实现 -echo "" -echo "9. Checking Optimistic Lock..." -if grep -rq "base_version\|BaseVersion\|VersionConflict" "$PROJECT_ROOT"/internal/project/ 2>/dev/null; then - check_pass "Optimistic lock (version conflict check) implemented" -else - check_warn "Optimistic lock not detected - concurrent modification may cause issues" -fi - -# 10. 运行单元测试(如果可用) -echo "" -echo "10. Running Unit Tests..." -if [ -d "$PROJECT_ROOT/internal/project" ]; then - cd "$PROJECT_ROOT" - if go test ./internal/project/... -v -short -count=1 2>&1 | head -20; then - check_pass "Unit tests executed" - else - check_warn "Unit tests may have issues - check output above" - fi -else - check_warn "Project directory not found, skipping tests" -fi - -# 11. 检查编译 -echo "" -echo "11. Checking Build..." -if [ -d "$PROJECT_ROOT/internal/project" ]; then - cd "$PROJECT_ROOT" - if go build ./internal/project/... 2>/dev/null; then - check_pass "Module compiles successfully" - else - check_fail "Module compilation failed" - fi -else - check_warn "Project directory not found, skipping build check" -fi - -# 12. 检查 JSONB 字段定义 -echo "" -echo "12. Checking JSONB Field Definitions..." -JSONB_FIELDS=("basic_info" "deploy_business" "deploy_env" "deploy_middleware") -for field in "${JSONB_FIELDS[@]}"; do - if grep -rq "\"$field\"" "$PROJECT_ROOT"/internal/project/entity/*.go 2>/dev/null; then - check_pass "JSONB field '$field' defined" - else - check_fail "JSONB field '$field' not found" + fail "SKILL.md 缺少 $section 章节" fi done +# ======================================== # 总结 +# ======================================== +echo "" +echo "========================================" +echo "验证完成" +echo "========================================" +echo "PASS: $PASS_COUNT" +echo "FAIL: $FAIL_COUNT" echo "" -echo "==========================================" -echo "Verification Summary" -echo "==========================================" -echo -e "Passed: ${GREEN}$PASS_COUNT${NC}" -echo -e "Failed: ${RED}$FAIL_COUNT${NC}" -echo -e "Warnings: ${YELLOW}$WARN_COUNT${NC}" -if [ $FAIL_COUNT -gt 0 ]; then - echo "" - echo -e "${RED}Some checks failed. Please review and fix the issues.${NC}" - echo "" - echo "Common fixes:" - echo " - Missing lifecycle states: Add constants in entity/constants.go" - echo " - Missing routes: Register in handler/router.go" - echo " - Missing callback interface: Implement ProjectLifecycleUpdater" - echo " - Missing version service: Implement version comparison logic" - exit 1 -elif [ $WARN_COUNT -gt 0 ]; then - echo "" - echo -e "${YELLOW}Some warnings detected. Please verify manually.${NC}" +if [ "$FAIL_COUNT" -eq 0 ]; then + echo "所有验证通过!" exit 0 else - echo "" - echo -e "${GREEN}All checks passed!${NC}" - exit 0 + echo "存在 $FAIL_COUNT 个验证失败项" + exit 1 fi diff --git a/1-AgentSkills/developing-rmdc/SKILL.md b/1-AgentSkills/developing-rmdc/SKILL.md new file mode 100644 index 0000000..92c250d --- /dev/null +++ b/1-AgentSkills/developing-rmdc/SKILL.md @@ -0,0 +1,109 @@ +--- +name: developing-rmdc +description: "System-level guidance for RMDC platform development covering cross-module consistency, dependency rules, version compatibility, and global change workflows. Triggered when making changes affecting multiple modules, modifying shared contracts, or planning system-wide updates. Keywords: rmdc-core, rmdc-user-auth, rmdc-jenkins-branch-dac, rmdc-exchange-hub, rmdc-watchdog, rmdc-project-management, rmdc-work-procedure, rmdc-audit-log, cross-module, dependency." +allowed-tools: + - Read + - Glob + - Grep + - Bash +argument-hint: "$ARGUMENTS: [modules...] — scope: cross-module|dependency|version|release" +--- + +# developing-rmdc + +## 概述 +本 Skill 提供 RMDC 系统级开发指导,确保跨模块一致性、依赖管理与版本兼容。 + +## 动态上下文注入 + +### 查看模块结构 +!`ls -la internal/ 2>/dev/null || find . -maxdepth 2 -type d -name "rmdc-*"` + +### 查看模块依赖 +!`grep -rn "import.*rmdc" --include="*.go" | grep -v "_test.go" | head -30` + +--- + +## 模块依赖关系 + +``` +rmdc-core (API Gateway) + ├── rmdc-user-auth (认证/权限) + │ ├── rmdc-work-procedure (工单) + │ └── rmdc-jenkins-branch-dac (Jenkins权限数据) + ├── rmdc-jenkins-branch-dac (构建管理) + │ └── rmdc-audit-log + ├── rmdc-exchange-hub (MQTT网关) + │ └── rmdc-audit-log + ├── rmdc-watchdog (边缘代理) + │ └── rmdc-project-management (一级授权) + ├── rmdc-project-management (项目管理) + │ └── rmdc-audit-log + ├── rmdc-work-procedure (工单) + │ └── rmdc-audit-log + └── rmdc-audit-log (审计) +``` + +--- + +## Plan(规划阶段) + +### 跨模块变更检查 +| 变更类型 | 影响评估 | +|:---|:---| +| JWT Claims 变更 | 影响所有需鉴权模块 | +| RBAC 角色变更 | 影响 user-auth + 所有权限检查点 | +| 审计字段变更 | 影响所有写审计的模块 | +| 工单流程变更 | 影响 user-auth + project-management | +| MQTT Topic 变更 | 影响 exchange-hub + watchdog | + +### 决策点 +- [ ] 识别所有受影响模块 +- [ ] 确定变更顺序(先依赖后被依赖) +- [ ] 确定是否需要版本兼容期 + +--- + +## Verify(验证清单) + +### 依赖一致性 +- [ ] 所有模块使用相同版本的 rmdc-common +- [ ] JWT Claims 定义在所有模块一致 +- [ ] 错误码无冲突 +- [ ] 审计字段格式统一 + +### 接口兼容性 +- [ ] 内部 API 向后兼容 +- [ ] MQTT 消息格式兼容 +- [ ] 数据库 Schema 兼容 + +--- + +## Execute(执行步骤) + +### 跨模块变更流程 +1. 创建变更计划文档 +2. 识别所有受影响模块 +3. 按依赖顺序更新(先 common,后业务) +4. 在每个模块运行验证 +5. 集成测试 +6. 统一发布 + +--- + +## Pitfalls(常见坑) + +1. **依赖版本不一致**:不同模块使用不同版本的 common 包。 +2. **JWT Claims 不同步**:一个模块新增字段,其他模块未解析。 +3. **发布顺序错误**:被依赖模块未先发布。 +4. **审计格式不统一**:不同模块的审计记录格式不同。 +5. **错误码冲突**:不同模块定义了相同的错误码。 + +--- + +## 相关文件 +| 用途 | 路径 | +|:---|:---| +| 模块依赖 | [reference/module-dependencies.md](reference/module-dependencies.md) | +| 术语表 | [reference/terminology.md](reference/terminology.md) | +| 版本兼容 | [reference/version-compatibility.md](reference/version-compatibility.md) | diff --git a/1-AgentSkills/developing-rmdc/reference/module-dependencies.md b/1-AgentSkills/developing-rmdc/reference/module-dependencies.md new file mode 100644 index 0000000..580b4b9 --- /dev/null +++ b/1-AgentSkills/developing-rmdc/reference/module-dependencies.md @@ -0,0 +1,32 @@ +# RMDC 模块依赖关系 + +## 依赖矩阵 + +| 模块 | 依赖模块 | 被依赖模块 | +|:---|:---|:---| +| rmdc-core | user-auth, jenkins-dac, exchange-hub, watchdog, project-mgmt, work-procedure, audit-log | - | +| rmdc-user-auth | work-procedure, jenkins-dac, common | core | +| rmdc-jenkins-branch-dac | audit-log, common | core, user-auth | +| rmdc-exchange-hub | audit-log, common | core | +| rmdc-watchdog | project-mgmt, common | core | +| rmdc-project-management | audit-log, common | core, watchdog | +| rmdc-work-procedure | audit-log, common | core, user-auth | +| rmdc-audit-log | common | jenkins-dac, exchange-hub, project-mgmt, work-procedure | +| rmdc-common | - | 所有模块 | + +## 变更影响传播 + +``` +修改 rmdc-common → 需要重新测试所有模块 +修改 rmdc-user-auth JWT → 需要更新 rmdc-core 中间件 +修改 rmdc-audit-log 字段 → 需要更新所有写审计的模块 +``` + +## 发布顺序 + +1. rmdc-common (基础) +2. rmdc-audit-log (审计基础) +3. rmdc-work-procedure (工单基础) +4. rmdc-jenkins-branch-dac / rmdc-project-management (并行) +5. rmdc-user-auth / rmdc-exchange-hub / rmdc-watchdog (并行) +6. rmdc-core (网关,最后) diff --git a/1-AgentSkills/developing-rmdc/reference/terminology.md b/1-AgentSkills/developing-rmdc/reference/terminology.md new file mode 100644 index 0000000..797a997 --- /dev/null +++ b/1-AgentSkills/developing-rmdc/reference/terminology.md @@ -0,0 +1,18 @@ +# RMDC 术语表 + +| 术语 | 定义 | +|:---|:---| +| 一级授权 | 由 rmdc-project-management 管理的项目级权限 | +| 二级授权 | 由 rmdc-watchdog 执行的 TOTP 动态授权 | +| DAC | Discretionary Access Control,自主访问控制(Jenkins 分支权限) | +| RBAC | Role-Based Access Control,基于角色的访问控制 | +| JWT | JSON Web Token,用户认证令牌 | +| 工单 | 需要审批的变更请求,由 rmdc-work-procedure 管理 | +| 指令生命周期 | MQTT 指令从发送到完成的状态流转 | +| SuperAdmin | 超级管理员,拥有系统全部权限 | +| Admin | 管理员,可管理普通用户和三方用户 | +| Normal | 普通用户 | +| Third | 三方用户,最低权限 | +| 谁注册谁管理 | 用户管理原则:注册人负责管理被注册用户的生命周期 | +| RSA-OAEP | RSA 最优非对称加密填充,用于密码传输加密 | +| bcrypt | 密码哈希算法,用于密码存储 | diff --git a/1-AgentSkills/developing-rmdc/reference/version-compatibility.md b/1-AgentSkills/developing-rmdc/reference/version-compatibility.md new file mode 100644 index 0000000..5b6b0a7 --- /dev/null +++ b/1-AgentSkills/developing-rmdc/reference/version-compatibility.md @@ -0,0 +1,36 @@ +# RMDC 版本兼容策略 + +## 版本号规范 + +采用语义化版本:`MAJOR.MINOR.PATCH` + +- **MAJOR**: 不兼容的 API 变更 +- **MINOR**: 向后兼容的功能新增 +- **PATCH**: 向后兼容的问题修复 + +## 兼容性规则 + +### API 兼容 +- 新增可选字段:兼容 +- 新增必填字段:Breaking Change +- 删除字段:Breaking Change +- 修改字段类型:Breaking Change + +### 数据库兼容 +- 新增可空列:兼容 +- 新增非空列(有默认值):兼容 +- 删除列:Breaking Change +- 修改列类型:需评估 + +### MQTT 消息兼容 +- 新增可选字段:兼容 +- 消息必须包含版本字段 +- 消费者必须忽略未知字段 + +## Breaking Change 处理流程 + +1. 提前通知所有相关模块负责人 +2. 创建新版本接口(v2) +3. 旧版本标记废弃(Deprecated) +4. 设定过渡期(建议 2 个迭代) +5. 过渡期结束后下线旧版本 diff --git a/1-AgentSkills/developing-rmdc/scripts/verify-module-deps.sh b/1-AgentSkills/developing-rmdc/scripts/verify-module-deps.sh new file mode 100644 index 0000000..c94e7af --- /dev/null +++ b/1-AgentSkills/developing-rmdc/scripts/verify-module-deps.sh @@ -0,0 +1,56 @@ +#!/bin/bash +# verify-module-deps.sh - 验证模块依赖一致性 +# 依赖: go, grep +# 用法: ./verify-module-deps.sh + +set -e + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +PROJECT_ROOT="${SCRIPT_DIR}/../../.." + +echo "=== RMDC 模块依赖验证 ===" +echo "" + +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' + +pass() { echo -e "${GREEN}[PASS]${NC} $1"; } +fail() { echo -e "${RED}[FAIL]${NC} $1"; } +warn() { echo -e "${YELLOW}[WARN]${NC} $1"; } + +# 检查 go.mod 中的依赖版本一致性 +check_common_version() { + echo "--- 检查 rmdc-common 版本一致性 ---" + + VERSIONS=$(find "${PROJECT_ROOT}" -name "go.mod" -exec grep "rmdc-common" {} \; 2>/dev/null | \ + grep -oE "v[0-9]+\.[0-9]+\.[0-9]+" | sort | uniq) + + VERSION_COUNT=$(echo "$VERSIONS" | wc -l) + + if [ "$VERSION_COUNT" -eq 1 ]; then + pass "rmdc-common 版本一致: $VERSIONS" + else + fail "rmdc-common 版本不一致: $VERSIONS" + fi +} + +# 检查循环依赖 +check_circular_deps() { + echo "--- 检查循环依赖 ---" + + cd "${PROJECT_ROOT}" + if go mod graph 2>/dev/null | grep -E "rmdc.*rmdc" | head -20; then + warn "发现模块间依赖,请确认无循环" + else + pass "未发现明显循环依赖" + fi +} + +# 执行检查 +check_common_version +check_circular_deps + +echo "" +echo "=== 依赖验证完成 ===" diff --git a/1-AgentSkills/developing-user-auth/SKILL.md b/1-AgentSkills/developing-user-auth/SKILL.md new file mode 100644 index 0000000..8a9cac1 --- /dev/null +++ b/1-AgentSkills/developing-user-auth/SKILL.md @@ -0,0 +1,271 @@ +--- +name: developing-user-auth + +description: > + 指导开发 rmdc-user-auth 用户认证与权限模块(v2.0),覆盖: + JWT认证(HS256、4h有效期、claims注入)、RSA-OAEP加密登录(2048密钥、30天轮换)、 + 密码安全(bcrypt存储、3个月过期、首次登录强制改密 must_change_password)、 + 账户有效期(account_expires_at、非SuperAdmin必须设置有效期)、 + RBAC权限模型(superadmin/admin/normal/third四级角色、谁注册谁管理原则、registered_by_id)、 + 统一权限架构(PermissionModule枚举、jenkins_acls层级权限、project_acls模块级权限、user_permission_caches L2缓存)、 + 用户生命周期(disabled→active→locked状态机、工单审批激活)、 + 用户注册/管理工单集成(接口注入机制、UserStatusUpdater/WorkflowCreator回调、工单由用户接口内部自动创建)。 + 触发场景:修改auth handlers、permission services、user CRUD、workflow callbacks、password policies、JWT/RSA实现。 + Keywords: JWT / RSA-OAEP / bcrypt / RBAC / PermissionModule / jenkins_acls / project_acls / account_expires_at / must_change_password / workflow integration. + +allowed-tools: + - Read + - Glob + - Grep + - Bash + - Edit + - Write + +argument-hint: "$ARGUMENTS: <变更类型> [目标文件] — 变更类型: auth|permission|user-crud|workflow|password-policy|jwt|rsa|account-validity" + +--- + +# developing-user-auth(用户认证模块) + +## 概述 +本 Skill 指导 `rmdc-user-auth` 模块的开发,覆盖认证、权限、用户生命周期、工单集成等核心功能。基于 DDS v2.0 设计规范。 + +## 动态上下文注入 + +### 查看模块结构 +!`find . -type f -name "*.go" -path "*user-auth*" | head -30` + +### 查找权限相关代码 +!`grep -rn "PermissionModule\|CheckPermission\|jenkins_acls\|project_acls" --include="*.go" | head -20` + +### 查看用户表结构 +!`grep -A 60 "type User struct" internal/models/*.go` + +### 查看工单回调接口 +!`grep -rn "UserStatusUpdater\|WorkflowCreator\|ActivateUser" --include="*.go" | head -15` + +--- + +## Plan(规划阶段) + +### 产物清单 +根据 `$ARGUMENTS` 确定变更范围: + +| 变更类型 | 涉及文件 | 下游影响 | +|:---|:---|:---| +| auth | `auth_handler.go`, `auth_service.go` | 前端登录流程、JWT claims、密码过期检查 | +| permission | `permission_handler.go`, `*_permission_service.go`, `*_acl_dao.go` | Jenkins DAC、Project权限、其他模块权限检查 | +| user-crud | `user_handler.go`, `user_service.go`, `user_dao.go` | 工单模块回调、审计日志、账户有效期 | +| workflow | `user_workflow_handler.go`, `*_workflow_service.go` | rmdc-work-procedure 回调 | +| password-policy | `auth_service.go`, `rsa_service.go` | password_expires_at、must_change_password | +| jwt | `auth_middleware.go`, `jwt_utils.go` | 所有需鉴权接口、account_expires_at 检查 | +| rsa | `rsa_service.go`, `rsa_keypair_dao.go` | 前端公钥获取、密钥轮换 | +| account-validity | `user_model.go`, `auth_service.go` | 登录检查、有效期设置规则 | + +### 决策点 +- [ ] 是否涉及 JWT claims 字段变更?→ See: [reference/03-authentication/jwt-claims.md](reference/03-authentication/jwt-claims.md) +- [ ] 是否涉及 RBAC 角色定义变更?→ See: [reference/05-rbac/rbac-roles.md](reference/05-rbac/rbac-roles.md) +- [ ] 是否涉及用户表 schema 变更?→ See: [reference/09-data-model/user-table-schema.md](reference/09-data-model/user-table-schema.md) +- [ ] 是否涉及工单回调逻辑变更?→ See: [reference/06-registration-workflow/](reference/06-registration-workflow/) + [reference/07-management-workflow/](reference/07-management-workflow/) +- [ ] 是否涉及权限模块枚举 PermissionModule 变更?→ See: [reference/08-permission-model/permission-architecture.md](reference/08-permission-model/permission-architecture.md) +- [ ] 是否涉及 jenkins_acls/project_acls 表结构变更?→ See: [reference/09-data-model/permission-tables-schema.md](reference/09-data-model/permission-tables-schema.md) + +--- + +## Verify(验证清单) + +### RBAC 兼容性检查 +- [ ] 角色层级未被破坏:superadmin > admin > normal > third(验证 `reference/05-rbac/rbac-roles.md` 角色层级) +- [ ] "谁注册谁管理"原则未被违反:grep `registered_by_id` 校验逻辑 +- [ ] SuperAdmin 始终具有全部权限:所有 CheckPermission 首先判断 superadmin +- [ ] Admin 不可创建/升级 SuperAdmin +- [ ] 注册权限矩阵正确:SuperAdmin→所有角色,Admin→normal/third,Normal→third + +### JWT/Session 安全检查 +- [ ] JWT 签名算法仍为 HS256(对照 `reference/03-authentication/jwt-claims.md`) +- [ ] Token 有效期未超过 4h +- [ ] Claims 包含必要字段:user_id, username, role, status +- [ ] 仅 status=active 用户可通过校验 +- [ ] 账户有效期 account_expires_at 过期检查已实现 + +### 密码与账户有效期检查 +- [ ] 密码传输使用 RSA-OAEP(SHA-256, 2048) 加密(参考 `reference/03-authentication/login-design.md`) +- [ ] 密码存储使用 bcrypt +- [ ] 密码有效期为 3 个月(password_expires_at) +- [ ] 首次登录/密码重置后 must_change_password = true +- [ ] 非 SuperAdmin 创建用户必须设置 account_expires_at +- [ ] 登录时检查账户有效期并返回 account_expire_days 提醒 + +### 权限模块检查 +- [ ] PermissionModule 枚举定义完整且一致(对照 `reference/08-permission-model/permission-architecture.md`) +- [ ] jenkins_acls 层级继承正确(Org→Repo→Branch)(参考 `reference/08-permission-model/jenkins-acls.md`) +- [ ] project_acls 模块代码与 JSONB 映射正确(参考 `reference/08-permission-model/project-acls.md`) +- [ ] user_permission_caches L2 缓存失效逻辑正确 +- [ ] L1 内存缓存与 L2 DB 缓存同步清除 + +### API 契约检查 +- [ ] 请求/响应字段向后兼容(对照 `reference/10-api-design/api-endpoints.md`) +- [ ] 错误码未被移除或语义变更 +- [ ] 新增字段有默认值(must_change_password, account_expires_at) + +### 工单集成检查 +- [ ] 注册工单:用户初始状态为 disabled(参考 `reference/06-registration-workflow/registration-workflow.md`) +- [ ] 审批通过回调:ActivateUser 状态变更为 active +- [ ] 撤销回调:DeletePendingUser 删除 status=disabled 的用户 +- [ ] 管理工单:ExecuteUserManagement 正确执行 update/enable/disable/delete/reset_password/extend_validity(参考 `reference/07-management-workflow/management-workflow.md`) +- [ ] 工单由用户管理接口内部自动创建(前端不直接调用创建工单接口) + +### 验证命令 +```bash +# 运行单元测试 +go test ./internal/user-auth/... -v + +# 检查 RBAC 定义一致性 +grep -rn "superadmin\|admin\|normal\|third" --include="*.go" | sort | uniq + +# 检查 PermissionModule 使用 +grep -rn "PermissionModule\|ModuleJenkins\|ModuleProject" --include="*.go" + +# 验证 JWT middleware 与账户有效期检查 +grep -rn "account_expires_at\|AccountExpiresAt" --include="*.go" + +# 验证工单回调注入 +grep -rn "UserStatusUpdater\|WorkflowCreator\|SetUserStatusUpdater" --include="*.go" +``` + +--- + +## Execute(执行步骤) + +### 1. 认证相关变更 (auth) +```bash +# 1. 定位认证处理器 +grep -rn "func.*Login\|func.*Register" --include="*.go" + +# 2. 修改认证逻辑(注意 must_change_password 和 account_expires_at 检查) +# 3. 更新登录响应(含 password_expire_days, account_expire_days, must_change_password) +# 4. 验证 RSA 加密流程 +go test ./internal/user-auth/service/auth_service_test.go -v +``` + +### 2. 权限相关变更 (permission) +```bash +# 1. 定位权限检查逻辑 +grep -rn "CheckPermission\|CheckHierarchical\|PermissionModule" --include="*.go" + +# 2. 修改权限逻辑(注意 jenkins_acls 层级继承、project_acls 模块级) +# 3. 更新权限缓存逻辑(L1 内存 + L2 DB user_permission_caches) +# 4. 验证 Jenkins 权限层级 +go test ./internal/user-auth/service/jenkins_permission_service_test.go -v +``` + +### 3. 用户 CRUD 变更 (user-crud) +```bash +# 1. 修改用户服务(注意 registered_by_id/registered_by_name 设置) +# 2. 设置 account_expires_at(非 SuperAdmin 创建时必须) +# 3. 设置 must_change_password = true(新用户/密码重置) +# 4. 同步更新工单回调 executor +# 5. 更新审计日志记录 +go test ./internal/user-auth/service/user_service_test.go -v +``` + +### 4. 工单集成变更 (workflow) +```bash +# 1. 确认接口注入机制(rmdc-core 初始化时注入) +# 2. 实现 UserStatusUpdater 接口(ActivateUser/DeletePendingUser/ExecuteUserManagement) +# 3. 实现 WorkflowCreator 调用(CreateRegistrationWorkflow/CreateManagementWorkflow) +# 4. 验证工单载荷结构(RegistrationWorkflowPayload/ManagementWorkflowPayload) +# 5. 确保前端不直接调用创建工单接口 +``` + +### 5. 账户有效期变更 (account-validity) +```bash +# 1. 添加/修改 users 表的 account_expires_at 字段 +# 2. 修改登录检查逻辑(过期拒绝、7天内提醒) +# 3. 修改用户创建逻辑(根据创建者角色设置有效期选项) +# 4. 实现 extend_validity 管理操作 +``` + +--- + +## Pitfalls(常见坑) + +1. **JWT Claims 变更未同步下游**:修改 claims 字段后,必须通知所有依赖模块更新解析逻辑。参考 `reference/03-authentication/jwt-claims.md` 中的兼容性要求。 + +2. **RBAC 层级破坏**:admin 能创建 superadmin 是严重安全漏洞。每次角色相关变更必须验证 `reference/05-rbac/rbac-roles.md` 中的层级约束。 + +3. **密码过期时间未刷新**:修改密码、重置密码、创建用户时必须刷新 `password_expires_at` 并设置 `must_change_password`。参考 `reference/04-user-lifecycle/user-lifecycle.md`。 + +4. **工单回调状态不一致**:用户注册工单撤销时,必须确认用户仍为 disabled 状态才能删除。参考 `reference/06-registration-workflow/registration-workflow.md` 中的状态映射表。 + +5. **RSA 密钥轮换影响**:密钥过期后自动生成新密钥,前端缓存的旧公钥将无法加密。参考 `reference/03-authentication/login-design.md` 中的轮换策略。 + +6. **权限缓存脏读**:修改权限后必须同时清除对应用户的 L1 内存缓存和 L2 DB 缓存。参考 `reference/08-permission-model/jenkins-acls.md` 中的缓存失效逻辑。 + +7. **账户有效期校验遗漏**:登录流程和 JWT 中间件都需要检查 `account_expires_at`。参考 `reference/04-user-lifecycle/user-lifecycle.md` 中的有效期检查逻辑。 + +8. **工单接口直接调用**:前端不应直接调用创建工单接口。参考 `reference/10-api-design/api-endpoints.md` 中的重要约束。 + +--- + +## Reference 目录结构 + +``` +reference/ +├── 01-overview/ +│ └── module-overview.md # 模块概述 +├── 02-architecture/ +│ ├── module-dependencies.md # 模块依赖关系 +│ ├── interface-injection.md # 接口注入机制 +│ └── tech-stack.md # 技术栈 +├── 03-authentication/ +│ ├── login-design.md # 登录设计 +│ └── jwt-claims.md # JWT Claims 定义 +├── 04-user-lifecycle/ +│ └── user-lifecycle.md # 用户生命周期 +├── 05-rbac/ +│ └── rbac-roles.md # RBAC 角色矩阵 +├── 06-registration-workflow/ +│ └── registration-workflow.md # 注册工单流程 +├── 07-management-workflow/ +│ └── management-workflow.md # 管理工单流程 +├── 08-permission-model/ +│ ├── permission-architecture.md # 统一权限架构 +│ ├── jenkins-acls.md # Jenkins 层级权限 +│ ├── project-acls.md # 项目模块权限 +│ └── business-info-registry.md # 业务信息注册中心 +├── 09-data-model/ +│ ├── user-table-schema.md # 用户表 Schema +│ └── permission-tables-schema.md # 权限表 Schema +├── 10-api-design/ +│ └── api-endpoints.md # API 接口清单 +└── 11-security/ + └── security-compliance.md # 安全与合规 +``` + +--- + +## 相关文件速查 + +| 用途 | 路径 | +|:---|:---| +| 模块概述 | [reference/01-overview/module-overview.md](reference/01-overview/module-overview.md) | +| 模块依赖 | [reference/02-architecture/module-dependencies.md](reference/02-architecture/module-dependencies.md) | +| 接口注入 | [reference/02-architecture/interface-injection.md](reference/02-architecture/interface-injection.md) | +| 登录设计 | [reference/03-authentication/login-design.md](reference/03-authentication/login-design.md) | +| JWT Claims | [reference/03-authentication/jwt-claims.md](reference/03-authentication/jwt-claims.md) | +| 用户生命周期 | [reference/04-user-lifecycle/user-lifecycle.md](reference/04-user-lifecycle/user-lifecycle.md) | +| RBAC 角色 | [reference/05-rbac/rbac-roles.md](reference/05-rbac/rbac-roles.md) | +| 注册工单 | [reference/06-registration-workflow/registration-workflow.md](reference/06-registration-workflow/registration-workflow.md) | +| 管理工单 | [reference/07-management-workflow/management-workflow.md](reference/07-management-workflow/management-workflow.md) | +| 权限架构 | [reference/08-permission-model/permission-architecture.md](reference/08-permission-model/permission-architecture.md) | +| Jenkins 权限 | [reference/08-permission-model/jenkins-acls.md](reference/08-permission-model/jenkins-acls.md) | +| 项目权限 | [reference/08-permission-model/project-acls.md](reference/08-permission-model/project-acls.md) | +| 用户表 Schema | [reference/09-data-model/user-table-schema.md](reference/09-data-model/user-table-schema.md) | +| 权限表 Schema | [reference/09-data-model/permission-tables-schema.md](reference/09-data-model/permission-tables-schema.md) | +| API 端点 | [reference/10-api-design/api-endpoints.md](reference/10-api-design/api-endpoints.md) | +| 安全合规 | [reference/11-security/security-compliance.md](reference/11-security/security-compliance.md) | +| 认证处理器骨架 | [examples/auth-handler-skeleton.go](examples/auth-handler-skeleton.go) | +| 权限检查骨架 | [examples/permission-check-skeleton.go](examples/permission-check-skeleton.go) | +| 工单回调示例 | [examples/workflow-callback-skeleton.go](examples/workflow-callback-skeleton.go) | +| 验证脚本 | [scripts/verify-user-auth.sh](scripts/verify-user-auth.sh) | diff --git a/1-AgentSkills/developing-user-auth/examples/auth-handler-skeleton.go b/1-AgentSkills/developing-user-auth/examples/auth-handler-skeleton.go new file mode 100644 index 0000000..1976772 --- /dev/null +++ b/1-AgentSkills/developing-user-auth/examples/auth-handler-skeleton.go @@ -0,0 +1,145 @@ +package handler + +import ( + "net/http" + "time" + "github.com/gin-gonic/gin" +) + +// AuthHandler 认证处理器骨架 +type AuthHandler struct { + authService *AuthService + rsaService *RSAService +} + +// GetPublicKey 获取 RSA 公钥 +// GET /api/auth/rsa/public-key +func (h *AuthHandler) GetPublicKey(c *gin.Context) { + publicKey, err := h.rsaService.GetCurrentPublicKey() + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "获取公钥失败"}) + return + } + c.JSON(http.StatusOK, gin.H{"public_key": publicKey}) +} + +// Login 用户登录 +// POST /api/auth/login +// Body: {"username": "xxx", "encrypted_password": "RSA加密后的密码"} +func (h *AuthHandler) Login(c *gin.Context) { + var req LoginRequest + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "参数错误"}) + return + } + + // 1. RSA 解密密码 + password, err := h.rsaService.Decrypt(req.EncryptedPassword) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "密码解密失败", "code": "AUTH_005"}) + return + } + + // 2. 验证用户名密码 + user, err := h.authService.ValidateCredentials(req.Username, password) + if err != nil { + c.JSON(http.StatusUnauthorized, gin.H{"error": "用户名或密码错误", "code": "AUTH_001"}) + return + } + + // 3. 检查用户状态 + if user.Status != "active" { + c.JSON(http.StatusUnauthorized, gin.H{"error": "账户未激活或已禁用", "code": "AUTH_003"}) + return + } + + // 4. 检查账户有效期 + if user.AccountExpiresAt != nil && user.AccountExpiresAt.Before(time.Now()) { + c.JSON(http.StatusUnauthorized, gin.H{"error": "账户已过期", "code": "AUTH_007"}) + return + } + + // 5. 检查密码是否已过期 + if user.PasswordExpiresAt != nil && user.PasswordExpiresAt.Before(time.Now()) { + c.JSON(http.StatusUnauthorized, gin.H{"error": "密码已过期,请联系管理员重置", "code": "AUTH_006"}) + return + } + + // 6. 生成 JWT + token, err := h.authService.GenerateJWT(user) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "生成Token失败"}) + return + } + + // 7. 构建响应(含附加标识) + resp := LoginResponse{Token: token, User: user.ToDTO()} + + // 首次登录需强制改密 + if user.MustChangePassword { + resp.MustChangePassword = true + } + + // 密码7天内过期提醒 + if user.PasswordExpiresAt != nil { + days := int(time.Until(*user.PasswordExpiresAt).Hours() / 24) + if days <= 7 && days > 0 { + resp.PasswordExpireDays = days + } + } + + // 账户7天内过期提醒 + if user.AccountExpiresAt != nil { + days := int(time.Until(*user.AccountExpiresAt).Hours() / 24) + if days <= 7 && days > 0 { + resp.AccountExpireDays = days + } + } + + // 更新最后登录时间 + h.authService.UpdateLastLogin(user.ID) + + c.JSON(http.StatusOK, resp) +} + +// ForceChangePassword 首次登录强制改密 +// PUT /api/user/password/force-change +func (h *AuthHandler) ForceChangePassword(c *gin.Context) { + userID := c.GetInt64("user_id") + + var req ForceChangePasswordRequest + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "参数错误"}) + return + } + + // 修改密码并更新相关标识 + err := h.authService.ForceChangePassword(userID, req.NewPassword) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "修改密码失败"}) + return + } + + // 密码修改成功后: + // 1. password_expires_at = now + 90天 + // 2. must_change_password = false + + c.JSON(http.StatusOK, gin.H{"message": "密码修改成功"}) +} + +type LoginRequest struct { + Username string `json:"username" binding:"required"` + EncryptedPassword string `json:"encrypted_password" binding:"required"` +} + +type LoginResponse struct { + Token string `json:"token"` + User UserDTO `json:"user"` + MustChangePassword bool `json:"must_change_password,omitempty"` // 需强制改密 + PasswordExpireDays int `json:"password_expire_days,omitempty"` // 密码剩余天数 + AccountExpireDays int `json:"account_expire_days,omitempty"` // 账户剩余天数 +} + +type ForceChangePasswordRequest struct { + NewPassword string `json:"new_password" binding:"required,min=8"` +} diff --git a/1-AgentSkills/developing-user-auth/examples/permission-check-skeleton.go b/1-AgentSkills/developing-user-auth/examples/permission-check-skeleton.go new file mode 100644 index 0000000..cb8c53e --- /dev/null +++ b/1-AgentSkills/developing-user-auth/examples/permission-check-skeleton.go @@ -0,0 +1,137 @@ +package permission + +import ( + "context" +) + +// PermissionChecker 对外暴露的权限检查接口 +type PermissionChecker struct { + jenkinsPermService *JenkinsPermissionService + projectPermService *ProjectPermissionService + userDao *UserDao + permissionCache *PermissionCache // L1 内存缓存 + permissionCacheDao *PermissionCacheDao // L2 DB 缓存 +} + +// CheckJenkinsPermission 检查 Jenkins 分支权限 +// 层级继承:Org -> Repo -> Branch +func (p *PermissionChecker) CheckJenkinsPermission( + ctx context.Context, + userID int64, + org, repo, branch string, + requireBuild bool, +) (bool, error) { + // 1. 检查是否为 SuperAdmin(直接放行) + user, err := p.userDao.GetByID(userID) + if err != nil { + return false, err + } + if user.Role == "superadmin" { + return true, nil + } + + // 2. 先查 L1 内存缓存 + cacheKey := p.buildCacheKey(userID, org, repo, branch) + if cached, ok := p.permissionCache.Get(cacheKey); ok { + return p.evaluatePermission(cached, requireBuild), nil + } + + // 3. 再查 L2 DB 缓存 (user_permission_caches) + if cached, err := p.permissionCacheDao.GetUserPermissionTree(userID); err == nil { + // 从缓存树中查找权限 + if perm := cached.FindPermission(org, repo, branch); perm != nil { + p.permissionCache.Set(cacheKey, perm) // 回填 L1 + return p.evaluatePermission(perm, requireBuild), nil + } + } + + // 4. 层级权限检查 + return p.jenkinsPermService.CheckHierarchicalPermission( + userID, org, repo, branch, requireBuild, + ) +} + +// CheckHierarchicalPermission 层级权限检查逻辑 +// 优先级:Branch > Repo > Org +func (s *JenkinsPermissionService) CheckHierarchicalPermission( + userID int64, + org, repo, branch string, + requireBuild bool, +) (bool, error) { + // 先查 Branch 级别 + perm, err := s.jenkinsAclDao.FindPermission(userID, org, repo, branch) + if err == nil && perm.CanView { + if !requireBuild || perm.CanBuild { + return true, nil + } + } + + // 再查 Repo 级别(branch 为空) + perm, err = s.jenkinsAclDao.FindPermission(userID, org, repo, "") + if err == nil && perm.CanView { + if !requireBuild || perm.CanBuild { + return true, nil + } + } + + // 最后查 Org 级别(repo 和 branch 都为空) + perm, err = s.jenkinsAclDao.FindPermission(userID, org, "", "") + if err == nil && perm.CanView { + if !requireBuild || perm.CanBuild { + return true, nil + } + } + + return false, nil +} + +// CheckProjectModulePermission 检查项目模块权限 +func (p *PermissionChecker) CheckProjectModulePermission( + ctx context.Context, + userID int64, + userRole string, + projectID string, + moduleCode string, + permissionType string, // "view" or "export" +) (bool, error) { + // 1. SuperAdmin 拥有所有权限 + if userRole == "superadmin" { + return true, nil + } + + // 2. authorization_info 模块仅 SuperAdmin 可访问 + if moduleCode == "authorization_info" { + return false, nil + } + + // 3. 检查是否为项目填写人(自动拥有非授权模块的 view 权限) + if permissionType == "view" { + fillerID, err := p.projectInfoQuerier.GetProjectFillerID(ctx, projectID) + if err == nil && fillerID == userID { + return true, nil + } + } + + // 4. 查询 project_acls 表 + return p.projectPermService.CheckModulePermission( + userID, projectID, moduleCode, permissionType, + ) +} + +// InvalidateCache 权限变更时清除缓存 +func (p *PermissionChecker) InvalidateCache(userID int64) error { + // 清除 L1 内存缓存 + p.permissionCache.DeleteByUser(userID) + + // 清除 L2 DB 缓存 (user_permission_caches) + return p.permissionCacheDao.DeleteUserPermissionCache(userID) +} + +// 权限缓存管理 +// L1: 内存缓存 permissionCache (进程内高速缓存) +// L2: 数据库表 user_permission_caches (JSON格式存储权限树) +// +// 缓存失效时机: +// 1. 权限分配 (POST /api/permissions/jenkins/assign) +// 2. 权限拷贝 (POST /api/permissions/jenkins/copy) +// 3. 权限撤销 (POST /api/permissions/projects/revoke) diff --git a/1-AgentSkills/developing-user-auth/examples/workflow-callback-skeleton.go b/1-AgentSkills/developing-user-auth/examples/workflow-callback-skeleton.go new file mode 100644 index 0000000..037ab48 --- /dev/null +++ b/1-AgentSkills/developing-user-auth/examples/workflow-callback-skeleton.go @@ -0,0 +1,185 @@ +package workflow + +import ( + "context" + "fmt" + "time" +) + +// ===================================================== +// 接口定义(由 rmdc-core 在初始化时注入) +// ===================================================== + +// UserStatusUpdater 用户状态更新接口 +// 由工单模块调用,用于处理工单状态变更后的用户操作 +type UserStatusUpdater interface { + // ActivateUser 激活用户(注册工单审批通过) + ActivateUser(userID int64) error + + // DeletePendingUser 删除待审批用户(工单撤销时,仅删除 status=disabled 的用户) + DeletePendingUser(userID int64) error + + // ExecuteUserManagement 执行用户管理操作(管理工单审批通过) + ExecuteUserManagement(userID int64, action string, payload map[string]interface{}) error +} + +// WorkflowCreator 工单创建接口 +// 由用户模块调用,用于创建用户注册/管理工单 +type WorkflowCreator interface { + // CreateRegistrationWorkflow 创建用户注册工单 + CreateRegistrationWorkflow(ctx context.Context, req *RegistrationWorkflowRequest) (string, error) + + // CreateManagementWorkflow 创建用户管理工单 + CreateManagementWorkflow(ctx context.Context, req *ManagementWorkflowRequest) (string, error) +} + +// ===================================================== +// 工单载荷结构 +// ===================================================== + +// RegistrationWorkflowRequest 注册工单请求 +type RegistrationWorkflowRequest struct { + TargetUserID int64 `json:"target_user_id"` + TargetUsername string `json:"target_username"` + TargetRole string `json:"target_role"` + RegisteredByID int64 `json:"registered_by_id"` + RegisteredByName string `json:"registered_by_name"` + AccountExpiresAt time.Time `json:"account_expires_at"` + RegistrationReason string `json:"registration_reason"` +} + +// ManagementWorkflowRequest 管理工单请求 +type ManagementWorkflowRequest struct { + TargetUserID int64 `json:"target_user_id"` + TargetUsername string `json:"target_username"` + ActionType string `json:"action_type"` // update/enable/disable/delete/reset_password/extend_validity + OperatorID int64 `json:"operator_id"` + OperatorName string `json:"operator_name"` + OriginalData map[string]interface{} `json:"original_data"` + ModifiedData map[string]interface{} `json:"modified_data"` + Reason string `json:"reason"` +} + +// ===================================================== +// UserStatusUpdater 实现示例 +// ===================================================== + +type userStatusUpdaterImpl struct { + userService *UserService +} + +func NewUserStatusUpdater(userService *UserService) UserStatusUpdater { + return &userStatusUpdaterImpl{userService: userService} +} + +// ActivateUser 激活用户(注册工单审批通过时调用) +func (u *userStatusUpdaterImpl) ActivateUser(userID int64) error { + return u.userService.UpdateUserStatus(userID, "active") +} + +// DeletePendingUser 删除待审批用户(工单撤销时调用) +func (u *userStatusUpdaterImpl) DeletePendingUser(userID int64) error { + // 安全检查:只删除 status=disabled 的用户 + user, err := u.userService.GetUserByID(userID) + if err != nil { + return err + } + if user.Status != "disabled" { + return fmt.Errorf("cannot delete user with status: %s", user.Status) + } + return u.userService.DeleteUser(userID) +} + +// ExecuteUserManagement 执行用户管理操作(管理工单审批通过时调用) +func (u *userStatusUpdaterImpl) ExecuteUserManagement( + userID int64, + action string, + payload map[string]interface{}, +) error { + switch action { + case "update": + return u.userService.UpdateUserInfo(userID, payload) + case "enable": + return u.userService.UpdateUserStatus(userID, "active") + case "disable": + return u.userService.UpdateUserStatus(userID, "disabled") + case "delete": + return u.userService.DeleteUser(userID) + case "reset_password": + return u.userService.ResetPassword(userID) + case "extend_validity": + newExpiresAt, ok := payload["new_expires_at"].(time.Time) + if !ok { + return fmt.Errorf("invalid new_expires_at in payload") + } + return u.userService.ExtendValidity(userID, newExpiresAt) + default: + return fmt.Errorf("unknown action: %s", action) + } +} + +// ===================================================== +// rmdc-core 初始化时的注入示例 +// ===================================================== + +func initializeModules() { + // 创建服务实例 + userService := NewUserService(db) + workflowService := NewWorkflowService(db) + + // 1. 将用户状态更新器注入到工单模块 + userStatusUpdater := NewUserStatusUpdater(userService) + workflowService.SetUserStatusUpdater(userStatusUpdater) + + // 2. 将工单创建器注入到用户模块 + workflowCreator := NewWorkflowCreator(workflowService) + userService.SetWorkflowCreator(workflowCreator) +} + +// ===================================================== +// 工单状态回调处理 +// ===================================================== + +// HandleUserRegistrationCallback 处理用户注册工单回调 +func (s *WorkflowService) HandleUserRegistrationCallback( + workflowID string, + event string, // approve/return/revoke + payload map[string]interface{}, +) error { + userID := payload["target_user_id"].(int64) + + switch event { + case "approve": + // 激活用户 + return s.userStatusUpdater.ActivateUser(userID) + case "return": + // 打回,用户状态保持 disabled,前端通知用户修改 + return nil + case "revoke": + // 撤销,删除待审批用户 + return s.userStatusUpdater.DeletePendingUser(userID) + default: + return fmt.Errorf("unknown event: %s", event) + } +} + +// HandleUserManagementCallback 处理用户管理工单回调 +func (s *WorkflowService) HandleUserManagementCallback( + workflowID string, + event string, + payload map[string]interface{}, +) error { + userID := payload["target_user_id"].(int64) + action := payload["action_type"].(string) + + switch event { + case "approve": + // 执行管理操作 + return s.userStatusUpdater.ExecuteUserManagement(userID, action, payload) + case "return": + // 打回,不执行操作 + return nil + default: + return fmt.Errorf("unknown event: %s", event) + } +} diff --git a/1-AgentSkills/developing-user-auth/reference/01-overview/module-overview.md b/1-AgentSkills/developing-user-auth/reference/01-overview/module-overview.md new file mode 100644 index 0000000..2a2452a --- /dev/null +++ b/1-AgentSkills/developing-user-auth/reference/01-overview/module-overview.md @@ -0,0 +1,28 @@ +# 模块概述 + +--- +DDS-Section: 1. 概述 +DDS-Lines: L9-L30 +--- + +## 模块定位 + +`rmdc-user-auth` 提供 RMDC 统一的用户认证、账户生命周期管理与权限服务。 + +## 核心职责 + +| 职责 | 描述 | 关键技术 | +|:---|:---|:---| +| 身份认证 | RSA-OAEP 密码加密 + bcrypt 校验,颁发 4h 有效 JWT | Go + Gin + JWT | +| 账号管理 | 用户 CRUD、密码修改、个人资料更新 | GORM + PostgreSQL | +| 密码策略 | 密码过期、首次登录强制改密、状态控制 | bcrypt + RSA | +| 审批工作流 | 用户注册/管理通过工单审批 | rmdc-work-procedure | +| 权限服务 | Jenkins/Project/Delivery权限检查 | 层级ACL + 模块ACL | +| 系统配置 | RSA密钥对、登录策略、注册开关配置 | PostgreSQL | + +## 版本修订历史 + +| 版本 | 日期 | 修订内容 | +|:---|:---|:---| +| v1.0 | 2026-01-23 | 基于现有代码首次形成 DDS | +| v2.0 | 2026-01-27 | 新增账户有效期、强制改密机制、工单流程详细设计 | diff --git a/1-AgentSkills/developing-user-auth/reference/02-architecture/interface-injection.md b/1-AgentSkills/developing-user-auth/reference/02-architecture/interface-injection.md new file mode 100644 index 0000000..b8841ae --- /dev/null +++ b/1-AgentSkills/developing-user-auth/reference/02-architecture/interface-injection.md @@ -0,0 +1,53 @@ +# 接口注入机制 + +--- +DDS-Section: 2. 系统架构 - 2.3 接口注入机制 +DDS-Lines: L113-L149 +--- + +## 用户模块与工单模块依赖 + +采用接口注入(依赖注入)方式实现模块间回调,避免循环依赖。 + +## 工单模块 → 用户模块回调接口 + +```go +// UserStatusUpdater 用户状态更新接口 +// 由 rmdc-core 在初始化时注入,工单模块状态变更时调用 +type UserStatusUpdater interface { + // UpdateUserStatus 更新用户状态(审批通过时激活) + UpdateUserStatus(userID int64, status string) error + + // ActivateUser 激活用户(注册审批通过) + ActivateUser(userID int64) error + + // DeletePendingUser 删除待审批用户(工单撤销时) + DeletePendingUser(userID int64) error + + // ExecuteUserManagement 执行用户管理操作(管理审批通过) + ExecuteUserManagement(userID int64, action string, payload map[string]interface{}) error +} +``` + +## 用户模块 → 工单模块调用接口 + +```go +// WorkflowCreator 工单创建接口 +// 由 rmdc-core 在初始化时注入,用户模块通过此接口创建工单 +type WorkflowCreator interface { + // CreateRegistrationWorkflow 创建用户注册工单 + CreateRegistrationWorkflow(ctx context.Context, req *RegistrationWorkflowRequest) (string, error) + + // CreateManagementWorkflow 创建用户管理工单 + CreateManagementWorkflow(ctx context.Context, req *ManagementWorkflowRequest) (string, error) +} +``` + +## 注入时机 + +在 `rmdc-core/cmd/main.go` 初始化阶段完成所有接口注入: + +1. 创建 UserService 实例 +2. 创建 WorkflowService 实例 +3. 将 UserStatusUpdater 注入 WorkflowService +4. 将 WorkflowCreator 注入 UserService diff --git a/1-AgentSkills/developing-user-auth/reference/02-architecture/module-dependencies.md b/1-AgentSkills/developing-user-auth/reference/02-architecture/module-dependencies.md new file mode 100644 index 0000000..03f0a0d --- /dev/null +++ b/1-AgentSkills/developing-user-auth/reference/02-architecture/module-dependencies.md @@ -0,0 +1,42 @@ +# 模块依赖关系 + +--- +DDS-Section: 2. 系统架构 - 2.1 模块依赖关系 +DDS-Lines: L33-L73 +--- + +## 依赖关系图 + +``` +rmdc-user-auth +├── 被依赖 ←─────────────────────────────────────┐ +│ ├── rmdc-core (API Gateway 入口) │ +│ ├── rmdc-project-management (用户鉴权/查询) │ +│ └── rmdc-work-procedure (用户状态回调) │ +│ │ +├── 主动依赖 ────────────────────────────────────┘ +│ ├── rmdc-work-procedure (创建工单) +│ ├── rmdc-jenkins-branch-dac (Jenkins资源查询) +│ ├── rmdc-common (公共模块) +│ └── rmdc-audit-log (审计日志) +``` + +## 接口注入关系 + +| 注入方 | 注入接口 | 被注入方 | 用途 | +|:---|:---|:---|:---| +| rmdc-core | UserStatusUpdater | rmdc-work-procedure | 工单状态回调用户模块 | +| rmdc-core | WorkflowCreator | rmdc-user-auth | 用户模块创建工单 | +| rmdc-core | BusinessInfoQuerier | rmdc-user-auth | 权限模块查询业务信息 | +| rmdc-core | ModulePermissionChecker | rmdc-project-management | 业务模块权限检查 | + +## 数据库连接 + +`rmdc-user-auth` 依赖 `models.DatabaseConnections` 提供的多库连接: + +| 连接名 | 用途 | +|:---|:---| +| User | 用户表、权限表 | +| Jenkins | Jenkins 资源元数据 | +| Core | 系统配置 | +| Workflow | 工单数据 | diff --git a/1-AgentSkills/developing-user-auth/reference/02-architecture/tech-stack.md b/1-AgentSkills/developing-user-auth/reference/02-architecture/tech-stack.md new file mode 100644 index 0000000..f2342c2 --- /dev/null +++ b/1-AgentSkills/developing-user-auth/reference/02-architecture/tech-stack.md @@ -0,0 +1,41 @@ +# 技术栈与组件关系 + +--- +DDS-Section: 2. 系统架构 - 2.4-2.6 +DDS-Lines: L151-L171 +--- + +## 技术栈 + +| 技术 | 用途 | 配置 | +|:---|:---|:---| +| Gin | HTTP 路由 | - | +| GORM | ORM | 多库连接 | +| JWT | 访问令牌 | HS256, 4h 有效期 | +| RSA-OAEP | 密码加密 | 2048 位,30 天轮换 | +| bcrypt | 密码存储 | 默认成本 | + +## 路由分层 + +| 路径前缀 | 中间件 | 说明 | +|:---|:---|:---| +| `/api/auth/*` | 无 | 公共接口:RSA公钥、登录、注册 | +| `/api/users` | AuthMiddleware | 用户管理(部分需 RequireAdmin) | +| `/api/user` | AuthMiddleware | 个人资料 | +| `/api/permissions` | AuthMiddleware | 权限管理(部分需 RequireAdmin) | + +## 中间件职责 + +| 中间件 | 职责 | +|:---|:---| +| AuthMiddleware | 解析 Bearer JWT,校验签名/过期/用户状态,注入用户上下文 | +| RequireAdmin | 判定角色包含 `admin`(superadmin/admin) | + +## 组件关系 + +| 层 | 组件 | 职责 | +|:---|:---|:---| +| Handler | auth_handler, user_handler, permission_handler | HTTP 路由处理 | +| Service | AuthService, RSAService, UserService, *PermissionService | 业务逻辑 | +| DAO | UserDao, RSAKeypairDao, *PermissionDao, *AclDao | 数据访问 | +| Pkg | permission.PermissionChecker | 对外暴露权限检查接口 | diff --git a/1-AgentSkills/developing-user-auth/reference/03-authentication/jwt-claims.md b/1-AgentSkills/developing-user-auth/reference/03-authentication/jwt-claims.md new file mode 100644 index 0000000..b5d3b06 --- /dev/null +++ b/1-AgentSkills/developing-user-auth/reference/03-authentication/jwt-claims.md @@ -0,0 +1,68 @@ +# JWT Claims 定义 + +--- +DDS-Section: 3. 认证与登录设计 - 3.3 JWT 中间件行为 +DDS-Lines: L193-L197 +--- + +## Claims 结构 + +```go +type JWTClaims struct { + UserID int64 `json:"user_id"` + Username string `json:"username"` + EnglishName string `json:"english_name"` + Phone string `json:"phone"` + GroupName string `json:"group_name"` + Role string `json:"role"` + DevRole string `json:"dev_role"` + Status string `json:"status"` + jwt.RegisteredClaims +} +``` + +## Claims 字段说明 + +| 字段 | 类型 | 必填 | 来源 | 用途 | +|:---|:---|:---|:---|:---| +| `user_id` | int64 | ✅ | users.id | 唯一用户标识 | +| `username` | string | ✅ | users.username | 中文真实姓名 | +| `english_name` | string | ❌ | users.english_username | 英文昵称 | +| `phone` | string | ❌ | users.phone | 手机号 | +| `group_name` | string | ❌ | users.group_name | 所属小组 | +| `role` | string | ✅ | users.role | 系统角色 | +| `dev_role` | string | ❌ | users.dev_role | 开发角色 | +| `status` | string | ✅ | users.status | 账户状态 | + +## JWT 配置 + +| 配置项 | 值 | 说明 | +|:---|:---|:---| +| 签名算法 | HS256 | HMAC SHA-256 | +| 有效期 | 4 小时 | `exp` claim | +| 签发者 | (空) | `iss` claim 未使用 | +| 秘钥来源 | 环境变量/配置 | `JWT_SECRET` | + +## 下游模块使用 + +其他模块从 Gin Context 中提取用户信息: + +```go +// 获取用户ID +userID := ctx.GetInt64("user_id") + +// 获取角色 +role := ctx.GetString("role") + +// 判断是否管理员 +isAdmin := role == "superadmin" || role == "admin" +``` + +## 兼容性要求 + +⚠️ **重要**:修改 Claims 字段时必须: + +1. 确认所有下游模块解析逻辑兼容 +2. 新增字段需提供默认值 +3. 不可删除或重命名现有必填字段 +4. 更新本文档与 Context 注入键列表 diff --git a/1-AgentSkills/developing-user-auth/reference/03-authentication/login-design.md b/1-AgentSkills/developing-user-auth/reference/03-authentication/login-design.md new file mode 100644 index 0000000..d51764e --- /dev/null +++ b/1-AgentSkills/developing-user-auth/reference/03-authentication/login-design.md @@ -0,0 +1,68 @@ +# 认证与登录设计 + +--- +DDS-Section: 3. 认证与登录设计 +DDS-Lines: L174-L201 +--- + +## 登录流程(RSA + JWT) + +``` +1. 前端调用 GET /api/auth/rsa/public-key 获取公钥(30天有效) +2. 前端用 RSA-OAEP(SHA-256, 2048) 加密密码 +3. 前端提交 POST /api/auth/login (encrypted_password) +4. 后端 RSAService 解密 +5. 后端 AuthService 用 bcrypt 校验 password_hash +6. 生成 HS256 JWT (4h 有效期) +7. 返回 Token + UserDTO + 附加标识 +``` + +## 登录响应附加标识 + +| 字段 | 类型 | 说明 | +|:---|:---|:---| +| `must_change_password` | bool | 首次登录或密码重置后需强制改密 | +| `password_expire_days` | int | 密码剩余有效天数(7天内提示) | +| `account_expire_days` | int | 账户剩余有效天数(7天内提示) | + +## 密钥与密码策略 + +| 配置项 | 值 | 说明 | +|:---|:---|:---| +| RSA 算法 | RSA-OAEP(SHA-256) | 2048 位密钥 | +| RSA 有效期 | 30 天 | 过期自动生成新密钥对 | +| 密码存储 | bcrypt | 默认成本因子 | +| 密码有效期 | 90 天 | 到期需修改密码 | +| JWT 算法 | HS256 | - | +| JWT 有效期 | 4 小时 | 无刷新机制 | + +## JWT 中间件行为 + +| 步骤 | 动作 | +|:---|:---| +| 1 | 解析 `Authorization: Bearer ` | +| 2 | 校验签名与过期时间 | +| 3 | 校验 `claims.Status == "active"` | +| 4 | 校验 `account_expires_at` 未过期 | +| 5 | 注入上下文键 | + +### Context 注入键 + +| 键 | 来源 | +|:---|:---| +| `user_id` | JWT claims | +| `username` | JWT claims | +| `english_name` | JWT claims | +| `phone` | JWT claims | +| `group_name` | JWT claims | +| `role` | JWT claims | +| `dev_role` | JWT claims | +| `status` | JWT claims | + +## 限制说明 + +| TBD/TODO | 说明 | +|:---|:---| +| 刷新接口 | 无,Token 过期需重新登录 | +| 服务端注销 | 无,前端丢弃 Token | +| 登录失败锁定 | 字段存在,逻辑未实现 | diff --git a/1-AgentSkills/developing-user-auth/reference/04-user-lifecycle/user-lifecycle.md b/1-AgentSkills/developing-user-auth/reference/04-user-lifecycle/user-lifecycle.md new file mode 100644 index 0000000..833ff0b --- /dev/null +++ b/1-AgentSkills/developing-user-auth/reference/04-user-lifecycle/user-lifecycle.md @@ -0,0 +1,85 @@ +# 用户生命周期管理 + +--- +DDS-Section: 4. 用户生命周期管理 +DDS-Lines: L203-L288 +--- + +## 用户状态定义 + +| 状态 | 说明 | 触发条件 | +|:---|:---|:---| +| `disabled` | 待审批状态 | 用户注册后默认状态 | +| `active` | 正常激活 | 注册工单审批通过 | +| `locked` | 临时锁定 | 登录失败过多 / 管理员手动锁定 | + +## 状态转换规则 + +| 原状态 | 事件 | 新状态 | +|:---|:---|:---| +| (新建) | 用户注册 | disabled | +| disabled | 保存草稿 | disabled | +| disabled | 工单打回 | disabled | +| disabled | 审批通过 | active | +| disabled | 工单撤销 | (删除) | +| active | 正常使用 | active | +| active | 信息更新 | active | +| active | 登录失败锁定 | locked | +| active | 管理员禁用 | disabled | +| active | 管理员删除 | (删除) | +| locked | 解锁 | active | +| locked | 管理员禁用 | disabled | + +## 账户有效期机制 + +### 有效期设置规则 + +| 用户类型 | 有效期选项 | 默认值 | 说明 | +|:---|:---|:---|:---| +| SuperAdmin 创建 | 无限制/自定义 | 永久 | 可设置任意有效期 | +| Admin 创建 | 1/3/6/12个月 | 3个月 | 必须设置有效期 | +| Normal 创建 | 1/3/6/12个月 | 3个月 | 必须设置有效期 | + +### 有效期字段 + +```go +AccountExpiresAt *time.Time `json:"account_expires_at"` // NULL表示永久 +``` + +### 有效期检查逻辑 + +| 检查点 | 行为 | +|:---|:---| +| 登录时 | 过期则拒绝登录 | +| JWT 中间件 | 校验用户状态时同时检查 | +| 提前提醒 | 7天内登录时返回提醒 | + +## 强制修改密码机制 + +### 触发条件 + +| 条件 | 标识字段 | 处理方式 | +|:---|:---|:---| +| 首次登录 | `must_change_password = true` | 跳转改密页面 | +| 密码重置后 | `must_change_password = true` | 使用临时密码后强制改密 | +| 密码过期 | `password_expires_at` 已过期 | 拒绝登录 | + +### 密码过期时间线 + +``` +创建用户/重置密码 + ↓ +password_expires_at = now + 90天 +must_change_password = true + ↓ +首次登录 → 强制改密 + ↓ +改密后: password_expires_at = now + 90天 + must_change_password = false + ↓ +正常使用 + ↓ +第83天: 提醒 + ↓ +第90天: 强制过期 +``` diff --git a/1-AgentSkills/developing-user-auth/reference/05-rbac/rbac-roles.md b/1-AgentSkills/developing-user-auth/reference/05-rbac/rbac-roles.md new file mode 100644 index 0000000..a295cef --- /dev/null +++ b/1-AgentSkills/developing-user-auth/reference/05-rbac/rbac-roles.md @@ -0,0 +1,86 @@ +# RBAC 角色与权限矩阵 + +--- +DDS-Section: 5. 用户注册与管理权限 +DDS-Lines: L291-L320 +--- + +## 角色层级 + +``` +superadmin (超级管理员) + ↓ + admin (管理员) + ↓ + normal (普通用户) + ↓ + third (三方用户) +``` + +## 角色注册权限矩阵 + +| 操作者角色 | 可注册角色 | 可管理范围 | 说明 | +|:---|:---|:---|:---| +| SuperAdmin | superadmin/admin/normal/third | 所有用户 | 完全权限 | +| Admin | normal/third | 自己注册的用户 | 谁注册谁管理 | +| Normal | third | 自己注册的用户 | 仅可注册三方用户 | +| Third | - | - | 无注册/管理权限 | + +## 管理操作权限矩阵 + +| 操作 | SuperAdmin | Admin | Normal | Third | +|:---|:---|:---|:---|:---| +| 修改用户信息 | ✅ 所有 | ✅ 自己注册的 | ✅ 自己注册的 | ❌ | +| 启用用户 | ✅ 所有 | ✅ 自己注册的 | ✅ 自己注册的 | ❌ | +| 禁用用户 | ✅ 所有 | ✅ 自己注册的 | ✅ 自己注册的 | ❌ | +| 删除用户 | ✅ 所有 | ✅ 自己注册的 | ✅ 自己注册的 | ❌ | +| 重置密码 | ✅ 所有 | ✅ 自己注册的 | ❌ | ❌ | +| 延长有效期 | ✅ 所有 | ❌ | ❌ | ❌ | + +## "谁注册谁管理"原则 + +### 实现机制 + +1. **注册关系记录**:`users.registered_by_id` 记录注册人 ID +2. **权限检查规则**: + - SuperAdmin:可管理所有用户 + - Admin/Normal:只能管理 `registered_by_id == current_user_id` 的用户 +3. **审批归属**:所有用户注册/管理工单由 SuperAdmin 审批 + +### 代码检查点 + +```go +// CheckManagementPermission 检查管理权限 +func CheckManagementPermission(currentRole string, currentUserID, targetUserID int64, targetRegisteredByID int64) bool { + // SuperAdmin 可管理所有 + if currentRole == "superadmin" { + return true + } + + // Admin/Normal 只能管理自己注册的用户 + if currentRole == "admin" || currentRole == "normal" { + return targetRegisteredByID == currentUserID + } + + // Third 无管理权限 + return false +} +``` + +## 注册权限校验 + +```go +// CheckRegistrationPermission 检查注册权限 +func CheckRegistrationPermission(creatorRole, targetRole string) bool { + switch creatorRole { + case "superadmin": + return true // 可注册所有角色 + case "admin": + return targetRole == "normal" || targetRole == "third" + case "normal": + return targetRole == "third" + default: + return false + } +} +``` diff --git a/1-AgentSkills/developing-user-auth/reference/06-registration-workflow/registration-workflow.md b/1-AgentSkills/developing-user-auth/reference/06-registration-workflow/registration-workflow.md new file mode 100644 index 0000000..01ec48b --- /dev/null +++ b/1-AgentSkills/developing-user-auth/reference/06-registration-workflow/registration-workflow.md @@ -0,0 +1,80 @@ +# 用户注册工单流程 + +--- +DDS-Section: 6. 用户注册工单流程 +DDS-Lines: L322-L392 +--- + +## 工单创建时机 + +**重要**:前端不允许直接调用创建工单接口,工单由 `POST /api/users` 内部自动创建。 + +## 流程时序 + +``` +1. 用户 → rmdc-user-auth: POST /api/users (创建用户) +2. rmdc-user-auth: 创建用户记录 (status=disabled, 默认密码) +3. rmdc-user-auth → rmdc-work-procedure: CreateWorkflow(user_registration) +4. rmdc-work-procedure → rmdc-user-auth: workflow_id +5. rmdc-user-auth → 用户: 返回成功,工单ID +6. rmdc-work-procedure → SuperAdmin: 通知待审批 +``` + +## 审批分支 + +| 操作 | 工单状态 | 用户状态 | 后续动作 | +|:---|:---|:---|:---| +| 审批通过 | approved | active | ActivateUser(user_id) | +| 审批打回 | returned | disabled (保持) | 通知修改,用户 PUT /api/users/:id | +| 撤销 | revoked | (删除) | DeletePendingUser(user_id) | +| 重新提交 | pending_review | disabled (保持) | 工单重新进入审批 | + +## 工单状态与用户状态映射 + +| 工单事件 | 工单状态 | 用户状态 | 说明 | +|:---|:---|:---|:---| +| create | pending_review | disabled | 创建用户并提交审核 | +| approve | approved | active | 审核通过,激活用户 | +| return | returned | disabled | 打回修改 | +| resubmit | pending_review | disabled | 重新提交审核 | +| revoke | revoked | (删除) | 撤销工单,删除待审批用户 | + +## 注册工单业务载荷 + +```go +type RegistrationWorkflowPayload struct { + TargetUserID int64 `json:"target_user_id"` // 被注册用户ID + TargetUsername string `json:"target_username"` // 被注册用户名 + TargetRole string `json:"target_role"` // 被注册用户角色 + RegisteredByID int64 `json:"registered_by_id"` // 注册人ID + RegisteredByName string `json:"registered_by_name"` // 注册人姓名 + AccountExpiresAt time.Time `json:"account_expires_at"` // 账户有效期 + RegistrationReason string `json:"registration_reason"` // 注册原因 +} +``` + +## 注册接口设计要点 + +| 要点 | 说明 | +|:---|:---| +| 不暴露密码设置 | 后端自动生成默认密码(如 `Rmdc@2026`) | +| 必须设置有效期 | 非 SuperAdmin 必须选择有效期选项 | +| 自动创建工单 | 用户创建接口内部调用工单模块 | +| 工单初始状态 | 直接进入 `pending_review` | + +## 回调接口 + +当工单状态变更时,工单模块通过 UserStatusUpdater 接口回调用户模块: + +```go +// 审批通过 +func (u *UserStatusUpdaterImpl) ActivateUser(userID int64) error { + return u.userDao.UpdateStatus(userID, "active") +} + +// 撤销工单 +func (u *UserStatusUpdaterImpl) DeletePendingUser(userID int64) error { + // 仅删除 status=disabled 的用户 + return u.userDao.DeleteIfDisabled(userID) +} +``` diff --git a/1-AgentSkills/developing-user-auth/reference/07-management-workflow/management-workflow.md b/1-AgentSkills/developing-user-auth/reference/07-management-workflow/management-workflow.md new file mode 100644 index 0000000..a02c789 --- /dev/null +++ b/1-AgentSkills/developing-user-auth/reference/07-management-workflow/management-workflow.md @@ -0,0 +1,105 @@ +# 用户管理工单流程 + +--- +DDS-Section: 7. 用户管理工单流程 +DDS-Lines: L394-L480 +--- + +## 管理操作类型 + +| 操作代码 | 操作名称 | 说明 | 需审批 | +|:---|:---|:---|:---| +| `update` | 修改用户信息 | 修改基本信息 | 是 | +| `enable` | 启用用户 | disabled → active | 是 | +| `disable` | 禁用用户 | active → disabled | 是 | +| `delete` | 删除用户 | 软删除 | 是 | +| `reset_password` | 重置密码 | 重置为默认密码 | 是 | +| `extend_validity` | 延长有效期 | 延长 account_expires_at | 是 | + +## 工单创建时机 + +**重要**:前端不允许直接调用创建工单接口,工单由用户管理接口内部自动创建。 + +| 前端操作 | 后端接口 | 工单类型 | +|:---|:---|:---| +| 修改用户信息 | `PUT /api/users/:id` | user_management (action=update) | +| 启用用户 | `POST /api/users/:id/enable` | user_management (action=enable) | +| 禁用用户 | `POST /api/users/:id/disable` | user_management (action=disable) | +| 删除用户 | `DELETE /api/users/:id` | user_management (action=delete) | +| 重置密码 | `POST /api/users/:id/reset-password` | user_management (action=reset_password) | +| 延长有效期 | `POST /api/users/:id/extend-validity` | user_management (action=extend_validity) | + +## 流程时序 + +``` +1. 操作人 → rmdc-user-auth: PUT /api/users/:id (或其他管理操作) +2. rmdc-user-auth: CheckManagementPermission +3. rmdc-user-auth: 记录原始数据快照 +4. rmdc-user-auth → rmdc-work-procedure: CreateWorkflow(user_management) +5. rmdc-work-procedure → rmdc-user-auth: workflow_id +6. rmdc-user-auth → 操作人: 返回成功,工单ID + +7. rmdc-work-procedure → SuperAdmin: 通知待审批 + +分支: +- 审批通过: WP → UA: ExecuteUserManagement(action, payload) +- 审批打回: WP → 操作人: 通知修改后重新提交 +``` + +## 管理工单业务载荷 + +```go +type ManagementWorkflowPayload struct { + TargetUserID int64 `json:"target_user_id"` // 目标用户ID + TargetUsername string `json:"target_username"` // 目标用户名 + ActionType string `json:"action_type"` // 操作类型 + OperatorID int64 `json:"operator_id"` // 操作人ID + OperatorName string `json:"operator_name"` // 操作人姓名 + OriginalData map[string]interface{} `json:"original_data"` // 原始数据快照 + ModifiedData map[string]interface{} `json:"modified_data"` // 修改后数据 + Reason string `json:"reason"` // 操作原因 +} +``` + +## 管理操作执行器接口 + +```go +type UserManagementExecutor interface { + UpdateUserInfo(userID int64, payload map[string]interface{}) error + EnableUser(userID int64) error + DisableUser(userID int64) error + DeleteUser(userID int64) error + ResetPassword(userID int64) error + ExtendValidity(userID int64, newExpiresAt time.Time) error +} +``` + +## 操作执行逻辑 + +审批通过后,工单模块调用 `ExecuteUserManagement` 执行实际操作: + +```go +func (e *UserManagementExecutorImpl) ExecuteUserManagement( + userID int64, + action string, + payload map[string]interface{}, +) error { + switch action { + case "update": + return e.UpdateUserInfo(userID, payload) + case "enable": + return e.EnableUser(userID) + case "disable": + return e.DisableUser(userID) + case "delete": + return e.DeleteUser(userID) + case "reset_password": + return e.ResetPassword(userID) + case "extend_validity": + expiresAt := payload["new_expires_at"].(time.Time) + return e.ExtendValidity(userID, expiresAt) + default: + return fmt.Errorf("unknown action: %s", action) + } +} +``` diff --git a/1-AgentSkills/developing-user-auth/reference/08-permission-model/business-info-registry.md b/1-AgentSkills/developing-user-auth/reference/08-permission-model/business-info-registry.md new file mode 100644 index 0000000..e18f8e6 --- /dev/null +++ b/1-AgentSkills/developing-user-auth/reference/08-permission-model/business-info-registry.md @@ -0,0 +1,81 @@ +# BusinessInfoRegistry 注册中心 + +--- +DDS-Section: 8. 权限模型 - 8.3 BusinessInfoRegistry +DDS-Lines: L604-L664 +--- + +## 设计目的 + +权限模块需要查询业务模块信息(如项目填写人),但不应直接依赖具体业务模块。采用统一注册机制解决模块依赖。 + +## 注册接口定义 + +```go +// BusinessInfoQuerier 业务信息查询接口(通用基类) +type BusinessInfoQuerier interface { + GetModuleCode() string +} + +// ProjectInfoQuerier 项目信息查询接口 +type ProjectInfoQuerier interface { + BusinessInfoQuerier + GetProjectFillerID(ctx context.Context, projectID string) (int64, error) +} + +// JenkinsInfoQuerier Jenkins 信息查询接口 +type JenkinsInfoQuerier interface { + BusinessInfoQuerier + GetOrganizations(ctx context.Context) ([]string, error) + GetRepositories(ctx context.Context, org string) ([]string, error) + GetBranches(ctx context.Context, org, repo string) ([]string, error) +} +``` + +## 统一权限检查接口 + +```go +// ModulePermissionChecker 权限检查器接口(通用) +type ModulePermissionChecker interface { + CheckPermission(ctx context.Context, userID int64, userRole string, + resourceID, resourceType, permissionType string) (bool, error) + GetAccessibleResourceIDs(ctx context.Context, userID int64, userRole string, + resourceType string) ([]string, error) +} +``` + +## 注册流程 + +在 `rmdc-core` 初始化阶段完成注册: + +```go +// 1. 创建注册中心 +registry := permission.NewBusinessInfoRegistry() + +// 2. 注册项目信息查询器 +registry.RegisterProjectQuerier(projectService) + +// 3. 注册 Jenkins 信息查询器 +registry.RegisterJenkinsQuerier(jenkinsService) + +// 4. 将注册中心注入权限服务 +projectPermissionService.SetBusinessInfoRegistry(registry) +jenkinsPermissionService.SetBusinessInfoRegistry(registry) + +// 5. 将权限检查器注入业务模块 +projectService.SetPermissionChecker(projectPermissionService) +``` + +## 依赖关系 + +``` +rmdc-core (初始化) + │ + ├── 注册 ProjectInfoQuerier ────────→ BusinessInfoRegistry + ├── 注册 JenkinsInfoQuerier ────────→ BusinessInfoRegistry + │ + ├── 注入 BusinessInfoRegistry ──────→ ProjectPermissionService + ├── 注入 BusinessInfoRegistry ──────→ JenkinsPermissionService + │ + └── 注入 PermissionChecker ─────────→ rmdc-project-management +``` diff --git a/1-AgentSkills/developing-user-auth/reference/08-permission-model/jenkins-acls.md b/1-AgentSkills/developing-user-auth/reference/08-permission-model/jenkins-acls.md new file mode 100644 index 0000000..6bf63fa --- /dev/null +++ b/1-AgentSkills/developing-user-auth/reference/08-permission-model/jenkins-acls.md @@ -0,0 +1,115 @@ +# Jenkins 层级权限 (jenkins_acls) + +--- +DDS-Section: 8. 权限模型 - 8.2 Jenkins 层级权限 +DDS-Lines: L558-L603 +--- + +## 层级结构 + +``` +Organization (组织) + └── Repository (仓库) + └── Branch (分支) +``` + +## 设计原则 + +| 原则 | 说明 | +|:---|:---| +| 层级继承 | 上级权限覆盖下级 | +| 存储最小化 | 一条记录可覆盖子层级 | +| 权限类型 | can_view(查看)、can_build(构建) | + +## 权限表结构 (jenkins_acls) + +| 字段 | 类型 | 说明 | +|:---|:---|:---| +| `id` | int64 | 主键 | +| `user_id` | int64 | 用户 ID | +| `organization_folder` | string | 组织文件夹(必填) | +| `repository_name` | string | 仓库名称(可空 = Org 级权限) | +| `branch_name` | string | 分支名称(可空 = Repo 级权限) | +| `permission_level` | string | 权限层级:org/repo/branch | +| `can_view` | bool | 是否可查看 | +| `can_build` | bool | 是否可构建 | +| `granted_by` | int64 | 授权人 ID | +| `granted_at` | time.Time | 授权时间 | + +## 层级覆盖示例 + +| 记录 | organization | repository | branch | level | 覆盖范围 | +|:---|:---|:---|:---|:---|:---| +| 1 | cmit-dev | (null) | (null) | org | cmit-dev 下所有仓库和分支 | +| 2 | cmit-dev | rmdc-core | (null) | repo | rmdc-core 下所有分支 | +| 3 | cmit-dev | rmdc-core | main | branch | 仅 main 分支 | + +## 权限缓存机制 + +| 层级 | 存储位置 | 说明 | +|:---|:---|:---| +| L1 | 内存 `permissionCache` | 进程内高速缓存 | +| L2 | DB `user_permission_caches` | 用户权限缓存树 JSON | + +### 缓存失效 + +权限变更时必须同时清除 L1 和 L2 缓存: + +```go +func InvalidatePermissionCache(userID int64) error { + // 清除 L1 内存缓存 + permissionCache.Delete(userID) + + // 清除 L2 DB 缓存 + return userPermissionCacheDao.DeleteByUserID(userID) +} +``` + +## 权限检查逻辑 + +```go +// CheckHierarchicalPermission 检查层级权限 +// 1. 先检查 Branch 级权限 +// 2. 若无则检查 Repo 级权限 +// 3. 若无则检查 Org 级权限 +// 4. build 需要 view + build 两个权限 + +func CheckHierarchicalPermission( + userID int64, + org, repo, branch string, + needBuild bool, +) (bool, error) { + // 尝试从 L1 缓存获取 + if cached, ok := permissionCache.Get(userID, org, repo, branch); ok { + return checkCachedPermission(cached, needBuild), nil + } + + // L1 miss,查询 DB + // 1. 先查 Branch 级 + if perm, err := jenkinsAclDao.FindByBranch(userID, org, repo, branch); err == nil && perm != nil { + return checkPermission(perm, needBuild), nil + } + + // 2. 再查 Repo 级 + if perm, err := jenkinsAclDao.FindByRepo(userID, org, repo); err == nil && perm != nil { + return checkPermission(perm, needBuild), nil + } + + // 3. 最后查 Org 级 + if perm, err := jenkinsAclDao.FindByOrg(userID, org); err == nil && perm != nil { + return checkPermission(perm, needBuild), nil + } + + return false, nil +} +``` + +## 懒加载接口 + +按级别逐层加载,减少首次加载数据量: + +| 接口 | 返回 | +|:---|:---| +| `/api/permissions/jenkins/my-tree/organizations` | 用户有权限的组织列表 | +| `/api/permissions/jenkins/my-tree/repositories` | 指定组织下的仓库列表 | +| `/api/permissions/jenkins/my-tree/branches` | 指定仓库下的分支列表 | diff --git a/1-AgentSkills/developing-user-auth/reference/08-permission-model/permission-architecture.md b/1-AgentSkills/developing-user-auth/reference/08-permission-model/permission-architecture.md new file mode 100644 index 0000000..71f9557 --- /dev/null +++ b/1-AgentSkills/developing-user-auth/reference/08-permission-model/permission-architecture.md @@ -0,0 +1,105 @@ +# 统一权限架构 + +--- +DDS-Section: 8. 权限模型 - 8.1 统一权限架构 +DDS-Lines: L483-L556 +--- + +## 设计模式 + +RMDC 采用**专用权限表**设计,针对不同业务场景使用独立权限表结构。 + +## 权限模块枚举 (PermissionModule) + +```go +type PermissionModule string + +const ( + // 用户模块权限 + ModuleUserRegister PermissionModule = "user_register" + ModuleUserManage PermissionModule = "user_manage" + ModuleUserPermission PermissionModule = "user_permission" + + // 项目模块权限 + ModuleProjectCreate PermissionModule = "project_create" + ModuleProjectView PermissionModule = "project_view" + ModuleProjectEdit PermissionModule = "project_edit" + ModuleProjectExport PermissionModule = "project_export" + ModuleProjectAuth PermissionModule = "project_auth" + + // Jenkins 模块权限 + ModuleJenkinsView PermissionModule = "jenkins_view" + ModuleJenkinsBuild PermissionModule = "jenkins_build" + ModuleJenkinsManage PermissionModule = "jenkins_manage" + + // 微服务更新模块权限 + ModuleDeliveryView PermissionModule = "delivery_view" + ModuleDeliveryUpdate PermissionModule = "delivery_update" + + // 工单模块权限 + ModuleWorkflowView PermissionModule = "workflow_view" + ModuleWorkflowApprove PermissionModule = "workflow_approve" +) +``` + +## 模块信息注册表 + +```go +type PermissionModuleInfo struct { + Code PermissionModule `json:"code"` + Name string `json:"name"` + Description string `json:"description"` + ACLTable string `json:"acl_table"` +} + +var PermissionModules = map[PermissionModule]PermissionModuleInfo{ + ModuleJenkinsView: {Code: ModuleJenkinsView, Name: "Jenkins查看", ACLTable: "jenkins_acls"}, + ModuleJenkinsBuild: {Code: ModuleJenkinsBuild, Name: "Jenkins构建", ACLTable: "jenkins_acls"}, + ModuleProjectView: {Code: ModuleProjectView, Name: "项目查看", ACLTable: "project_acls"}, + ModuleProjectEdit: {Code: ModuleProjectEdit, Name: "项目编辑", ACLTable: "project_acls"}, + // ... +} +``` + +## 权限表概览 + +| 权限类型 | 权限表 | 权限粒度 | 说明 | +|:---|:---|:---|:---| +| Jenkins 权限 | `jenkins_acls` | Org/Repo/Branch 层级 | 支持层级继承 | +| 项目权限 | `project_acls` | 项目模块级 | 项目信息访问权限 | +| 用户权限缓存 | `user_permission_caches` | 用户维度 | L2 缓存树 | + +## 权限检查通用规则 + +```go +// 权限检查通用规则: +// 1. SuperAdmin 拥有所有权限,直接放行 +// 2. 根据 PermissionModule 分流到对应的 ACL 表进行检查 +// 3. 权限检查结果存入 L1 内存缓存和 L2 DB 缓存 +// 4. 权限变更时同时清除 L1 和 L2 缓存 + +func CheckPermission(ctx context.Context, userID int64, userRole string, + module PermissionModule, resourceID, permissionType string) (bool, error) { + + // Rule 1: SuperAdmin 直接放行 + if userRole == "superadmin" { + return true, nil + } + + // Rule 2: 根据模块分流检查 + moduleInfo, ok := PermissionModules[module] + if !ok { + return false, fmt.Errorf("unknown module: %s", module) + } + + // 根据 ACLTable 选择对应的检查逻辑 + switch moduleInfo.ACLTable { + case "jenkins_acls": + return checkJenkinsPermission(ctx, userID, resourceID, permissionType) + case "project_acls": + return checkProjectPermission(ctx, userID, resourceID, permissionType) + default: + return false, nil + } +} +``` diff --git a/1-AgentSkills/developing-user-auth/reference/08-permission-model/project-acls.md b/1-AgentSkills/developing-user-auth/reference/08-permission-model/project-acls.md new file mode 100644 index 0000000..92cd3a2 --- /dev/null +++ b/1-AgentSkills/developing-user-auth/reference/08-permission-model/project-acls.md @@ -0,0 +1,94 @@ +# 项目权限 (project_acls) + +--- +DDS-Section: 8. 权限模型 - 8.4 项目权限 +DDS-Lines: L666-L691 +--- + +## 设计原则 + +| 原则 | 说明 | +|:---|:---| +| 权限粒度 | 模块级 | +| SuperAdmin | 默认拥有所有权限 | +| 项目填写人 | 自动获得非授权模块的 view 权限 | + +## 模块代码与 JSONB 映射 + +| 模块代码 | JSONB 字段 | 说明 | +|:---|:---|:---| +| `basic_info` | `projects.basic_info` | 省份、城市、联系人等 | +| `business_info` | `projects.deploy_business` | 部署人、版本、入口等 | +| `environment_info` | `projects.deploy_env` | 主机、网络、管理方式等 | +| `middleware_info` | `projects.deploy_middleware` | MySQL/Redis/EMQX 等 | +| `authorization_info` | `project_auth_configs.*` | TOTP 授权(仅 SuperAdmin) | + +## 权限表结构 (project_acls) + +| 字段 | 类型 | 说明 | +|:---|:---|:---| +| `id` | int64 | 主键 | +| `user_id` | int64 | 用户 ID | +| `project_id` | string | 项目 ID | +| `module_code` | string | 模块代码 | +| `can_view` | bool | 是否可查看 | +| `can_export` | bool | 是否可导出 | +| `granted_by` | int64 | 授权人 ID | +| `granted_at` | time.Time | 授权时间 | + +## 索引设计 + +| 索引名 | 字段 | 用途 | +|:---|:---|:---| +| `idx_project_acl_user` | user_id | 用户维度查询 | +| `idx_project_acl_project` | project_id, module_code | 项目+模块维度查询 | + +## 权限检查规则 + +```go +// CheckProjectModulePermission 检查项目模块权限 +func CheckProjectModulePermission( + ctx context.Context, + userID int64, + userRole string, + projectID string, + moduleCode string, + permissionType string, // "view" or "export" +) (bool, error) { + // Rule 1: SuperAdmin 拥有所有权限 + if userRole == "superadmin" { + return true, nil + } + + // Rule 2: authorization_info 模块仅 SuperAdmin 可访问 + if moduleCode == "authorization_info" { + return false, nil + } + + // Rule 3: 项目填写人自动拥有非授权模块的 view 权限 + if permissionType == "view" { + fillerID, err := businessInfoRegistry.GetProjectFillerID(ctx, projectID) + if err == nil && fillerID == userID { + return true, nil + } + } + + // Rule 4: 查询 project_acls 表 + acl, err := projectAclDao.Find(userID, projectID, moduleCode) + if err != nil { + return false, err + } + if acl == nil { + return false, nil + } + + switch permissionType { + case "view": + return acl.CanView, nil + case "export": + return acl.CanExport, nil + default: + return false, nil + } +} +``` diff --git a/1-AgentSkills/developing-user-auth/reference/09-data-model/permission-tables-schema.md b/1-AgentSkills/developing-user-auth/reference/09-data-model/permission-tables-schema.md new file mode 100644 index 0000000..5b1bfa9 --- /dev/null +++ b/1-AgentSkills/developing-user-auth/reference/09-data-model/permission-tables-schema.md @@ -0,0 +1,120 @@ +# 权限表 Schema + +--- +DDS-Section: 9. 数据模型 - 9.5-9.6 权限表 +DDS-Lines: L768-L777 +--- + +## jenkins_acls 表 + +| 字段 | 类型 | 约束 | 说明 | +|:---|:---|:---|:---| +| `id` | int64 | PK | 主键 | +| `user_id` | int64 | NOT NULL, FK | 用户 ID | +| `organization_folder` | varchar(100) | NOT NULL | 组织文件夹 | +| `repository_name` | varchar(100) | - | 仓库名称(可空 = Org 级) | +| `branch_name` | varchar(100) | - | 分支名称(可空 = Repo 级) | +| `permission_level` | varchar(10) | NOT NULL | org/repo/branch | +| `can_view` | bool | NOT NULL | 查看权限 | +| `can_build` | bool | NOT NULL | 构建权限 | +| `granted_by` | int64 | NOT NULL | 授权人 ID | +| `granted_at` | datetime | NOT NULL | 授权时间 | +| `created_at` | datetime | AUTO | 创建时间 | +| `updated_at` | datetime | AUTO | 更新时间 | + +### 索引设计 + +| 索引 | 字段 | 说明 | +|:---|:---|:---| +| `idx_jenkins_acl_user` | user_id | 用户维度查询 | +| `idx_jenkins_acl_resource` | user_id, organization_folder, repository_name, branch_name | 资源维度查询(层级覆盖) | + +--- + +## project_acls 表 + +| 字段 | 类型 | 约束 | 说明 | +|:---|:---|:---|:---| +| `id` | int64 | PK | 主键 | +| `user_id` | int64 | NOT NULL, FK | 用户 ID | +| `project_id` | varchar(50) | NOT NULL | 项目 ID | +| `module_code` | varchar(30) | NOT NULL | 模块代码 | +| `can_view` | bool | NOT NULL | 查看权限 | +| `can_export` | bool | NOT NULL | 导出权限 | +| `granted_by` | int64 | NOT NULL | 授权人 ID | +| `granted_at` | datetime | NOT NULL | 授权时间 | +| `created_at` | datetime | AUTO | 创建时间 | +| `updated_at` | datetime | AUTO | 更新时间 | + +### 索引设计 + +| 索引 | 字段 | 说明 | +|:---|:---|:---| +| `idx_project_acl_user` | user_id | 用户维度查询 | +| `idx_project_acl_project` | project_id, module_code | 项目+模块维度查询 | +| `uk_project_acl` | user_id, project_id, module_code | 唯一约束 | + +--- + +## user_permission_caches 表 + +| 字段 | 类型 | 约束 | 说明 | +|:---|:---|:---|:---| +| `id` | int64 | PK | 主键 | +| `user_id` | int64 | UNIQUE, NOT NULL | 用户 ID | +| `permission_tree` | jsonb | NOT NULL | 权限树 JSON | +| `updated_at` | datetime | AUTO | 更新时间 | + +### 缓存结构示例 + +```json +{ + "jenkins": { + "cmit-dev": { + "can_view": true, + "can_build": false, + "repositories": { + "rmdc-core": { + "can_view": true, + "can_build": true, + "branches": { + "main": {"can_view": true, "can_build": true}, + "dev": {"can_view": true, "can_build": false} + } + } + } + } + }, + "projects": { + "proj-001": { + "basic_info": {"can_view": true, "can_export": false}, + "business_info": {"can_view": true, "can_export": true} + } + } +} +``` + +--- + +## rsa_keypairs 表 + +| 字段 | 类型 | 约束 | 说明 | +|:---|:---|:---|:---| +| `id` | int64 | PK | 主键 | +| `public_key` | text | NOT NULL | PEM 格式公钥 | +| `private_key` | text | NOT NULL | PEM 格式私钥 | +| `expires_at` | datetime | NOT NULL | 过期时间 | +| `created_at` | datetime | AUTO | 创建时间 | + +--- + +## system_configs 表 + +| 字段 | 类型 | 约束 | 说明 | +|:---|:---|:---|:---| +| `id` | int64 | PK | 主键 | +| `key` | varchar(100) | UNIQUE, NOT NULL | 配置键 | +| `value` | text | - | 配置值 | +| `description` | varchar(255) | - | 描述 | +| `created_at` | datetime | AUTO | 创建时间 | +| `updated_at` | datetime | AUTO | 更新时间 | diff --git a/1-AgentSkills/developing-user-auth/reference/09-data-model/user-table-schema.md b/1-AgentSkills/developing-user-auth/reference/09-data-model/user-table-schema.md new file mode 100644 index 0000000..80ff093 --- /dev/null +++ b/1-AgentSkills/developing-user-auth/reference/09-data-model/user-table-schema.md @@ -0,0 +1,91 @@ +# 用户表 Schema (users) + +--- +DDS-Section: 9. 数据模型 - 9.2 用户表 DDL +DDS-Lines: L695-L764 +--- + +## 表定义 + +```go +type User struct { + ID int64 `gorm:"primaryKey;autoIncrement" json:"id"` + Username string `gorm:"uniqueIndex;not null;size:50" json:"username"` // 中文真实姓名 + EnglishUsername string `gorm:"size:100" json:"english_username"` // 英文用户名(昵称) + PasswordHash string `gorm:"not null;size:255" json:"-"` // 加密后的密码 + AvatarID string `gorm:"size:50;default:'default_1'" json:"avatar_id"` // 头像ID + AvatarFrameID string `gorm:"size:50;default:'default'" json:"avatar_frame_id"` // 头像框ID + Gender string `gorm:"size:10;default:'male'" json:"gender"` // 性别 + Email string `gorm:"uniqueIndex;size:100" json:"email"` // 邮箱 + Phone string `gorm:"size:20" json:"phone"` // 手机号 + ShortNumber string `gorm:"size:10" json:"short_number"` // 短号 + WorkID string `gorm:"size:50" json:"work_id"` // 工号 + GroupName string `gorm:"size:100" json:"group_name"` // 所属小组 + Company string `gorm:"size:100" json:"company"` // 公司名称 + DevRole string `gorm:"size:50;default:'unknown'" json:"dev_role"` // 开发角色 + + // 注册关系 + RegisteredByID int64 `json:"registered_by_id"` // 注册人ID + RegisteredByName string `gorm:"size:64" json:"registered_by_name"` // 注册人姓名 + + // 角色与状态 + Role string `gorm:"not null;size:20;default:'normal'" json:"role"` // 系统角色 + Status string `gorm:"not null;size:20;default:'disabled'" json:"status"` // 状态 + + // 密码策略 + PasswordExpiresAt *time.Time `json:"password_expires_at"` // 密码过期时间 + MustChangePassword bool `gorm:"default:true" json:"must_change_password"` // 是否需强制改密 + + // 账户有效期 + AccountExpiresAt *time.Time `json:"account_expires_at"` // 账户有效期 + + // 登录锁定 + FailedLoginAttempts int `gorm:"default:0" json:"failed_login_attempts"` + LockedUntil *time.Time `json:"locked_until"` + + // MFA + MFASecret string `gorm:"size:100" json:"-"` + + // 审计字段 + CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"` + UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"` + DeletedAt *time.Time `gorm:"index" json:"deleted_at,omitempty"` + LastLoginAt *time.Time `json:"last_login_at"` +} +``` + +## 字段说明 + +| 字段 | 类型 | 约束 | 默认值 | 说明 | +|:---|:---|:---|:---|:---| +| `id` | int64 | PK, AUTO | - | 主键 | +| `username` | varchar(50) | UNIQUE, NOT NULL | - | 中文真实姓名 | +| `english_username` | varchar(100) | - | - | 英文昵称 | +| `password_hash` | varchar(255) | NOT NULL | - | bcrypt 哈希 | +| `email` | varchar(100) | UNIQUE | - | 邮箱 | +| `role` | varchar(20) | NOT NULL | 'normal' | superadmin/admin/normal/third | +| `status` | varchar(20) | NOT NULL | 'disabled' | active/locked/disabled | +| `registered_by_id` | int64 | - | - | 注册人 ID | +| `registered_by_name` | varchar(64) | - | - | 注册人姓名(冗余) | +| `password_expires_at` | datetime | - | - | 密码过期时间 | +| `must_change_password` | bool | - | true | 首次登录强制改密标识 | +| `account_expires_at` | datetime | - | NULL | 账户有效期,NULL=永久 | +| `failed_login_attempts` | int | - | 0 | 登录失败次数 | +| `locked_until` | datetime | - | - | 锁定截止时间 | + +## 索引设计 + +| 索引 | 类型 | 字段 | +|:---|:---|:---| +| PRIMARY | PK | id | +| idx_users_username | UNIQUE | username | +| idx_users_email | UNIQUE | email | +| idx_users_deleted_at | INDEX | deleted_at | + +## v2.0 新增字段 + +| 字段 | 类型 | 说明 | 默认值 | +|:---|:---|:---|:---| +| `registered_by_name` | varchar(64) | 注册人姓名(冗余存储) | NULL | +| `must_change_password` | bool | 是否需要强制改密 | true | +| `account_expires_at` | datetime | 账户有效期 | NULL | diff --git a/1-AgentSkills/developing-user-auth/reference/10-api-design/api-endpoints.md b/1-AgentSkills/developing-user-auth/reference/10-api-design/api-endpoints.md new file mode 100644 index 0000000..4cd31c5 --- /dev/null +++ b/1-AgentSkills/developing-user-auth/reference/10-api-design/api-endpoints.md @@ -0,0 +1,116 @@ +# API 接口设计 + +--- +DDS-Section: 10. 接口设计 (API) +DDS-Lines: L781-L833 +--- + +## 认证接口 (公共) + +| 方法 | 路径 | 说明 | 鉴权 | +|:---|:---|:---|:---| +| GET | `/api/auth/rsa/public-key` | 获取 RSA 公钥 | 公共 | +| POST | `/api/auth/login` | RSA 加密密码登录 | 公共 | +| POST | `/api/auth/register` | 自助注册 | 公共 | + +### POST /api/auth/login + +**Request:** +```json +{ + "username": "string", + "encrypted_password": "string" +} +``` + +**Response:** +```json +{ + "token": "string", + "user": { + "id": 0, + "username": "string", + "role": "string", + "status": "string" + }, + "must_change_password": false, + "password_expire_days": 0, + "account_expire_days": 0 +} +``` + +--- + +## 用户管理接口 + +| 方法 | 路径 | 说明 | 鉴权 | +|:---|:---|:---|:---| +| GET | `/api/users` | 用户列表(分页/筛选) | JWT | +| GET | `/api/users/:id` | 用户详情 | JWT | +| POST | `/api/users` | 创建用户(自动创建注册工单) | JWT + Admin | +| PUT | `/api/users/:id` | 更新用户(草稿/需工单) | JWT + Admin | +| DELETE | `/api/users/:id` | 删除用户 | JWT + SuperAdmin | +| PUT | `/api/user/profile` | 更新本人资料 | JWT | +| PUT | `/api/user/password` | 本人改密 | JWT | +| PUT | `/api/user/password/force-change` | 首次登录强制改密 | JWT | + +### GET /api/users 查询参数 + +| 参数 | 类型 | 说明 | +|:---|:---|:---| +| `page` | int | 页码 | +| `size` | int | 每页数量 | +| `role` | string | 角色筛选 | +| `status` | string | 状态筛选 | +| `group` | string | 小组筛选 | +| `search` | string | 关键词搜索 | +| `scope` | string | 范围:`all`/`created_by_me` | + +--- + +## Jenkins 权限接口 + +| 方法 | 路径 | 说明 | 鉴权 | +|:---|:---|:---|:---| +| GET | `/api/permissions/jenkins/my-tree/organizations` | 我的组织列表 | JWT | +| POST | `/api/permissions/jenkins/my-tree/repositories` | 我的仓库 | JWT | +| POST | `/api/permissions/jenkins/my-tree/branches` | 我的分支 | JWT | +| GET | `/api/permissions/jenkins/my-tree/full` | 我的完整权限树 | JWT | +| GET | `/api/permissions/jenkins/check/:organization/:branch` | 检查分支权限 | JWT | +| GET | `/api/permissions/jenkins/tree` | 全量权限树 | JWT + Admin | +| GET | `/api/permissions/jenkins/users/role/:role` | 按角色查用户 | JWT + Admin | +| GET | `/api/permissions/jenkins/:userId` | 获取用户权限 | JWT + Admin | +| POST | `/api/permissions/jenkins/assign` | 分配权限 | JWT + Admin | +| POST | `/api/permissions/jenkins/copy` | 拷贝权限 | JWT + Admin | +| GET | `/api/permissions/jenkins/user-tree/:userId/organizations` | 懒加载组织 | JWT + Admin | +| GET | `/api/permissions/jenkins/user-tree/:userId/organizations/:org/repositories` | 懒加载仓库 | JWT + Admin | +| GET | `/api/permissions/jenkins/user-tree/:userId/organizations/:org/repositories/:repo/branches` | 懒加载分支 | JWT + Admin | + +--- + +## 项目权限接口 + +| 方法 | 路径 | 说明 | 鉴权 | +|:---|:---|:---|:---| +| GET | `/api/permissions/projects/user/:userId/summary` | 用户项目权限摘要 | JWT + Admin | +| POST | `/api/permissions/projects/grant` | 授予项目模块权限 | JWT + Admin | +| POST | `/api/permissions/projects/revoke` | 撤销项目模块权限 | JWT + Admin | + +--- + +## 系统配置接口 + +| 方法 | 路径 | 说明 | 鉴权 | +|:---|:---|:---|:---| +| GET | `/api/user/system-config` | 获取配置 | JWT | +| PUT | `/api/user/system-config` | 更新配置 | JWT | + +--- + +## 重要约束 + +⚠️ **前端不允许直接调用创建工单接口** + +用户注册/管理工单通过用户管理接口内部自动创建: +- `POST /api/users` → 自动创建 user_registration 工单 +- `PUT /api/users/:id` → 自动创建 user_management 工单(正式修改时) diff --git a/1-AgentSkills/developing-user-auth/reference/11-security/security-compliance.md b/1-AgentSkills/developing-user-auth/reference/11-security/security-compliance.md new file mode 100644 index 0000000..5248932 --- /dev/null +++ b/1-AgentSkills/developing-user-auth/reference/11-security/security-compliance.md @@ -0,0 +1,71 @@ +# 安全与合规 + +--- +DDS-Section: 12. 安全与合规 +DDS-Lines: L930-L944 +--- + +## 传输安全 + +| 要求 | 实现 | +|:---|:---| +| 密码加密 | RSA-OAEP(SHA-256, 2048) | +| 禁止明文 | 登录密码必须加密传输 | + +## 存储安全 + +| 要求 | 实现 | +|:---|:---| +| 密码哈希 | bcrypt,默认成本因子 | +| RSA 私钥 | 仅存 DB,不下发前端 | +| 敏感字段 | json:"-" 标签,不外露 | + +## 账户状态 + +| 状态 | JWT 校验 | +|:---|:---| +| active | ✅ 通过 | +| locked | ❌ 拒绝 | +| disabled | ❌ 拒绝 | + +## 密码策略 + +| 策略 | 值 | 说明 | +|:---|:---|:---| +| 有效期 | 3 个月 | 修改/创建刷新过期时间 | +| 首次登录 | 强制改密 | must_change_password = true | +| 注册状态 | disabled | 需审批激活 | + +## 账户有效期 + +| 创建者 | 要求 | +|:---|:---| +| 非 SuperAdmin | 必须设置有效期 | +| SuperAdmin | 可设置永久 | + +过期账户无法登录。 + +## Token 安全 + +| 配置 | 值 | +|:---|:---| +| 算法 | HS256 | +| 有效期 | 4h | +| 刷新 | 无,需重登 | +| 注销 | 无服务端注销 | + +## 权限检查 + +| 规则 | 说明 | +|:---|:---| +| SuperAdmin | 全通 | +| Admin 限制 | 只能管理/授权 normal/third | +| 授权校验 | 授予者必须具备相应权限 | +| 谁注册谁管理 | 非 SuperAdmin 只能管理自己注册的用户 | + +## TBD/TODO + +| 功能 | 状态 | +|:---|:---| +| 登录失败锁定策略 | 未实现(字段存在) | +| Watchdog 权限检查 | 待补全 | diff --git a/1-AgentSkills/developing-user-auth/scripts/verify-user-auth.sh b/1-AgentSkills/developing-user-auth/scripts/verify-user-auth.sh new file mode 100644 index 0000000..155ad0b --- /dev/null +++ b/1-AgentSkills/developing-user-auth/scripts/verify-user-auth.sh @@ -0,0 +1,315 @@ +#!/bin/bash +# verify-user-auth.sh +# 验证 rmdc-user-auth 模块的设计一致性和代码规范 +# 依赖: bash, grep, sed, find + +set -e + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +SKILL_DIR="$(dirname "$SCRIPT_DIR")" +REFERENCE_DIR="$SKILL_DIR/reference" + +# 颜色定义 +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' # No Color + +# 计数器 +PASS_COUNT=0 +FAIL_COUNT=0 +WARN_COUNT=0 + +# 结果输出 +pass() { + echo -e "${GREEN}[PASS]${NC} $1" + ((PASS_COUNT++)) +} + +fail() { + echo -e "${RED}[FAIL]${NC} $1" + ((FAIL_COUNT++)) +} + +warn() { + echo -e "${YELLOW}[WARN]${NC} $1" + ((WARN_COUNT++)) +} + +echo "================================================" +echo "rmdc-user-auth Skill 验证脚本" +echo "================================================" +echo "" + +# ===================================================== +# 1. Reference 目录章节分层检查 +# ===================================================== +echo "--- 1. Reference 目录章节分层检查 ---" + +# 检查是否存在章节目录 (01-* 开头) +if find "$REFERENCE_DIR" -maxdepth 1 -type d -name '0*-*' | grep -q .; then + CHAPTER_COUNT=$(find "$REFERENCE_DIR" -maxdepth 1 -type d -name '0*-*' | wc -l) + pass "Reference 目录包含 $CHAPTER_COUNT 个章节分层目录" +else + fail "Reference 目录缺少章节分层目录 (需 01-*, 02-* 等)" +fi + +# 检查至少 2 个 reference 文件位于章节子目录 +NON_ROOT_FILES=$(find "$REFERENCE_DIR" -mindepth 2 -type f -name "*.md" | wc -l) +if [ "$NON_ROOT_FILES" -ge 2 ]; then + pass "章节子目录中包含 $NON_ROOT_FILES 个 reference 文件" +else + fail "章节子目录中 reference 文件不足 2 个 (当前: $NON_ROOT_FILES)" +fi + +# 检查 DDS-Section 字段 +DDS_SECTION_COUNT=$(grep -r "DDS-Section:" "$REFERENCE_DIR" 2>/dev/null | wc -l) +if [ "$DDS_SECTION_COUNT" -ge 3 ]; then + pass "Reference 文件包含 $DDS_SECTION_COUNT 处 DDS-Section 标注" +else + fail "Reference 文件 DDS-Section 标注不足 3 处 (当前: $DDS_SECTION_COUNT)" +fi + +# 检查 DDS-Lines 字段 +DDS_LINES_COUNT=$(grep -r "DDS-Lines:" "$REFERENCE_DIR" 2>/dev/null | wc -l) +if [ "$DDS_LINES_COUNT" -ge 3 ]; then + pass "Reference 文件包含 $DDS_LINES_COUNT 处 DDS-Lines 标注" +else + fail "Reference 文件 DDS-Lines 标注不足 3 处 (当前: $DDS_LINES_COUNT)" +fi + +echo "" + +# ===================================================== +# 2. 核心设计要素覆盖检查 +# ===================================================== +echo "--- 2. 核心设计要素覆盖检查 ---" + +# API 端点 +if [ -f "$REFERENCE_DIR/10-api-design/api-endpoints.md" ]; then + pass "API 端点设计文档存在" +else + fail "API 端点设计文档缺失 (reference/10-api-design/api-endpoints.md)" +fi + +# 权限表 Schema +if [ -f "$REFERENCE_DIR/09-data-model/permission-tables-schema.md" ]; then + pass "权限表 Schema 文档存在" +else + fail "权限表 Schema 文档缺失 (reference/09-data-model/permission-tables-schema.md)" +fi + +# 用户表 Schema +if [ -f "$REFERENCE_DIR/09-data-model/user-table-schema.md" ]; then + pass "用户表 Schema 文档存在" +else + fail "用户表 Schema 文档缺失 (reference/09-data-model/user-table-schema.md)" +fi + +# 状态机/工单流程 +if [ -f "$REFERENCE_DIR/06-registration-workflow/registration-workflow.md" ]; then + pass "注册工单流程文档存在" +else + fail "注册工单流程文档缺失 (reference/06-registration-workflow/registration-workflow.md)" +fi + +# 权限模型 +if [ -f "$REFERENCE_DIR/08-permission-model/permission-architecture.md" ]; then + pass "权限架构设计文档存在" +else + fail "权限架构设计文档缺失 (reference/08-permission-model/permission-architecture.md)" +fi + +# 安全模型 +if [ -f "$REFERENCE_DIR/11-security/security-compliance.md" ]; then + pass "安全合规文档存在" +else + fail "安全合规文档缺失 (reference/11-security/security-compliance.md)" +fi + +echo "" + +# ===================================================== +# 3. 关键设计内容检查 +# ===================================================== +echo "--- 3. 关键设计内容检查 ---" + +# 检查 JWT Claims 定义 +if grep -q "user_id.*username.*role.*status" "$REFERENCE_DIR/03-authentication/jwt-claims.md" 2>/dev/null; then + pass "JWT Claims 包含必要字段 (user_id/username/role/status)" +else + fail "JWT Claims 缺少必要字段定义" +fi + +# 检查 RBAC 角色层级 +if grep -q "superadmin.*admin.*normal.*third" "$REFERENCE_DIR/05-rbac/rbac-roles.md" 2>/dev/null; then + pass "RBAC 角色层级定义完整" +else + fail "RBAC 角色层级定义不完整" +fi + +# 检查 account_expires_at 字段 +if grep -q "account_expires_at\|AccountExpiresAt" "$REFERENCE_DIR/09-data-model/user-table-schema.md" 2>/dev/null; then + pass "用户表包含 account_expires_at 字段" +else + fail "用户表缺少 account_expires_at 字段" +fi + +# 检查 must_change_password 字段 +if grep -q "must_change_password\|MustChangePassword" "$REFERENCE_DIR/09-data-model/user-table-schema.md" 2>/dev/null; then + pass "用户表包含 must_change_password 字段" +else + fail "用户表缺少 must_change_password 字段" +fi + +# 检查 jenkins_acls 表定义 +if grep -q "jenkins_acls" "$REFERENCE_DIR/08-permission-model/jenkins-acls.md" 2>/dev/null; then + pass "Jenkins 权限表 (jenkins_acls) 已定义" +else + fail "Jenkins 权限表定义缺失" +fi + +# 检查 project_acls 表定义 +if grep -q "project_acls" "$REFERENCE_DIR/08-permission-model/project-acls.md" 2>/dev/null; then + pass "项目权限表 (project_acls) 已定义" +else + fail "项目权限表定义缺失" +fi + +# 检查工单接口约束 +if grep -q "前端不允许直接调用创建工单接口\|工单由用户管理接口内部自动创建" "$REFERENCE_DIR/10-api-design/api-endpoints.md" 2>/dev/null; then + pass "API 文档包含工单接口约束说明" +else + warn "API 文档可能缺少工单接口约束说明" +fi + +# 检查 PermissionModule 枚举 +if grep -q "PermissionModule" "$REFERENCE_DIR/08-permission-model/permission-architecture.md" 2>/dev/null; then + pass "权限模块枚举 (PermissionModule) 已定义" +else + fail "权限模块枚举定义缺失" +fi + +echo "" + +# ===================================================== +# 4. Examples 和 Scripts 检查 +# ===================================================== +echo "--- 4. Examples 和 Scripts 检查 ---" + +# 检查示例代码 +if [ -f "$SKILL_DIR/examples/auth-handler-skeleton.go" ]; then + pass "认证处理器骨架代码存在" +else + warn "认证处理器骨架代码缺失 (examples/auth-handler-skeleton.go)" +fi + +if [ -f "$SKILL_DIR/examples/permission-check-skeleton.go" ]; then + pass "权限检查骨架代码存在" +else + warn "权限检查骨架代码缺失 (examples/permission-check-skeleton.go)" +fi + +if [ -f "$SKILL_DIR/examples/workflow-callback-skeleton.go" ]; then + pass "工单回调骨架代码存在" +else + warn "工单回调骨架代码缺失 (examples/workflow-callback-skeleton.go)" +fi + +echo "" + +# ===================================================== +# 5. SKILL.md 检查 +# ===================================================== +echo "--- 5. SKILL.md 检查 ---" + +SKILL_MD="$SKILL_DIR/SKILL.md" + +# 检查 frontmatter +if grep -q "^name: developing-user-auth" "$SKILL_MD" 2>/dev/null; then + pass "SKILL.md name 字段正确" +else + fail "SKILL.md name 字段错误或缺失" +fi + +if grep -q "^description:" "$SKILL_MD" 2>/dev/null; then + pass "SKILL.md description 字段存在" +else + fail "SKILL.md description 字段缺失" +fi + +if grep -q "^argument-hint:" "$SKILL_MD" 2>/dev/null; then + pass "SKILL.md argument-hint 字段存在" +else + fail "SKILL.md argument-hint 字段缺失" +fi + +# 检查 Plan/Verify/Execute/Pitfalls 结构 +if grep -q "## Plan" "$SKILL_MD" 2>/dev/null; then + pass "SKILL.md 包含 Plan 章节" +else + fail "SKILL.md 缺少 Plan 章节" +fi + +if grep -q "## Verify" "$SKILL_MD" 2>/dev/null; then + pass "SKILL.md 包含 Verify 章节" +else + fail "SKILL.md 缺少 Verify 章节" +fi + +if grep -q "## Execute" "$SKILL_MD" 2>/dev/null; then + pass "SKILL.md 包含 Execute 章节" +else + fail "SKILL.md 缺少 Execute 章节" +fi + +if grep -q "## Pitfalls" "$SKILL_MD" 2>/dev/null; then + pass "SKILL.md 包含 Pitfalls 章节" +else + fail "SKILL.md 缺少 Pitfalls 章节" +fi + +# 检查动态注入示例 +DYNAMIC_INJECT_COUNT=$(grep -c '!`' "$SKILL_MD" 2>/dev/null || echo "0") +if [ "$DYNAMIC_INJECT_COUNT" -ge 2 ]; then + pass "SKILL.md 包含 $DYNAMIC_INJECT_COUNT 处动态注入示例" +else + fail "SKILL.md 动态注入示例不足 2 处 (当前: $DYNAMIC_INJECT_COUNT)" +fi + +# 检查 Pitfalls 数量 +PITFALL_COUNT=$(grep -c "^\d\." "$SKILL_MD" 2>/dev/null || grep -c "^[0-9]\." "$SKILL_MD" 2>/dev/null || echo "0") +if [ "$PITFALL_COUNT" -ge 3 ]; then + pass "SKILL.md Pitfalls 包含 $PITFALL_COUNT 条" +else + warn "SKILL.md Pitfalls 条目可能不足 (建议 3-8 条)" +fi + +# 检查行数 (应 < 500) +LINE_COUNT=$(wc -l < "$SKILL_MD" 2>/dev/null || echo "0") +if [ "$LINE_COUNT" -lt 500 ]; then + pass "SKILL.md 行数 ($LINE_COUNT) 符合规范 (<500)" +else + warn "SKILL.md 行数 ($LINE_COUNT) 超过建议值 500" +fi + +echo "" + +# ===================================================== +# 汇总 +# ===================================================== +echo "================================================" +echo "验证结果汇总" +echo "================================================" +echo -e "${GREEN}PASS${NC}: $PASS_COUNT" +echo -e "${RED}FAIL${NC}: $FAIL_COUNT" +echo -e "${YELLOW}WARN${NC}: $WARN_COUNT" +echo "" + +if [ "$FAIL_COUNT" -eq 0 ]; then + echo -e "${GREEN}✓ 所有必要检查通过${NC}" + exit 0 +else + echo -e "${RED}✗ 存在 $FAIL_COUNT 项失败,请修复后重新验证${NC}" + exit 1 +fi diff --git a/1-AgentSkills/managing-db-migrations/SKILL.md b/1-AgentSkills/managing-db-migrations/SKILL.md new file mode 100644 index 0000000..352141a --- /dev/null +++ b/1-AgentSkills/managing-db-migrations/SKILL.md @@ -0,0 +1,111 @@ +--- +name: managing-db-migrations +description: "Guides PostgreSQL database migration and rollback for RMDC system. Triggered when adding tables, modifying columns, creating indexes, or evolving schema. Covers migration naming, rollback scripts, field evolution rules, and data migration. Keywords: PostgreSQL, migration, rollback, schema evolution, GORM, ALTER TABLE." +allowed-tools: + - Read + - Glob + - Grep + - Bash + - Edit + - Write +argument-hint: "$ARGUMENTS: [table] — operation: add-table|add-column|modify-column|add-index|rollback" +--- + +# managing-db-migrations + +## 概述 +本 Skill 指导 RMDC 系统的 PostgreSQL 数据库迁移与回滚。 + +## 动态上下文注入 + +### 查看现有迁移文件 +!`ls -la migrations/ 2>/dev/null || find . -name "*migration*" -type f | head -20` + +### 查看表结构定义 +!`grep -rn "type.*struct" --include="*.go" -A 20 | grep -E "gorm:|TableName" | head -30` + +--- + +## Plan(规划阶段) + +### 迁移类型与影响 +| 操作 | 可回滚 | 数据风险 | 锁表影响 | +|:---|:---|:---|:---| +| 新增表 | ✓ | 低 | 无 | +| 新增可空列 | ✓ | 低 | 低 | +| 新增非空列(有默认值) | ✓ | 低 | 中 | +| 修改列类型 | 需评估 | 中 | 高 | +| 删除列 | ✗ | 高 | 低 | +| 新增索引 | ✓ | 低 | 高(大表) | + +### 决策点 +- [ ] 是否需要停机迁移? +- [ ] 是否需要数据迁移脚本? +- [ ] 回滚脚本是否就绪? +- [ ] 是否影响现有查询性能? + +--- + +## Verify(验证清单) + +### 迁移前检查 +- [ ] 迁移文件命名符合规范:`YYYYMMDDHHMMSS_description.sql` +- [ ] 包含 UP 和 DOWN 脚本 +- [ ] 在测试环境验证通过 +- [ ] 回滚脚本已测试 + +### 迁移后检查 +- [ ] 表结构与预期一致 +- [ ] 索引正确创建 +- [ ] 现有数据未丢失 +- [ ] 应用程序正常运行 + +### 验证命令 +```bash +# 检查迁移语法 +psql -h localhost -U rmdc -d rmdc_test -f migration.sql --dry-run + +# 验证回滚 +./scripts/verify-migration-rollback.sh +``` + +--- + +## Execute(执行步骤) + +### 新增列 +```sql +-- UP +ALTER TABLE users ADD COLUMN IF NOT EXISTS new_field VARCHAR(255) DEFAULT ''; + +-- DOWN +ALTER TABLE users DROP COLUMN IF EXISTS new_field; +``` + +### 新增索引(大表使用 CONCURRENTLY) +```sql +-- UP +CREATE INDEX CONCURRENTLY idx_users_email ON users(email); + +-- DOWN +DROP INDEX CONCURRENTLY IF EXISTS idx_users_email; +``` + +--- + +## Pitfalls(常见坑) + +1. **非空列无默认值**:现有数据无法满足约束,迁移失败。 +2. **大表加索引阻塞**:未使用 CONCURRENTLY 导致表锁。 +3. **删除列后回滚**:数据已丢失,无法恢复。 +4. **字段类型变更**:如 `VARCHAR(50)` 改 `VARCHAR(20)`,可能截断数据。 +5. **迁移顺序错误**:依赖的表/列不存在。 + +--- + +## 相关文件 +| 用途 | 路径 | +|:---|:---| +| 命名规范 | [reference/migration-naming.md](reference/migration-naming.md) | +| 回滚策略 | [reference/rollback-policy.md](reference/rollback-policy.md) | +| 字段演进 | [reference/field-evolution-rules.md](reference/field-evolution-rules.md) | diff --git a/1-AgentSkills/managing-db-migrations/examples/migration-template.sql b/1-AgentSkills/managing-db-migrations/examples/migration-template.sql new file mode 100644 index 0000000..f780096 --- /dev/null +++ b/1-AgentSkills/managing-db-migrations/examples/migration-template.sql @@ -0,0 +1,27 @@ +-- Migration Template +-- Migration: YYYYMMDDHHMMSS_description +-- Author: xxx +-- Description: 描述此次迁移的目的 + +-- ==================== UP ==================== +BEGIN; + +-- 添加新列示例 +ALTER TABLE users ADD COLUMN IF NOT EXISTS phone VARCHAR(20); + +-- 添加索引示例(小表) +CREATE INDEX IF NOT EXISTS idx_users_phone ON users(phone); + +-- 添加索引示例(大表,需要在事务外执行) +-- CREATE INDEX CONCURRENTLY idx_users_phone ON users(phone); + +-- 数据迁移示例 +-- UPDATE users SET phone = '' WHERE phone IS NULL; + +COMMIT; + +-- ==================== DOWN ==================== +-- 回滚脚本(默认注释,测试时取消注释) +-- BEGIN; +-- ALTER TABLE users DROP COLUMN IF EXISTS phone; +-- COMMIT; diff --git a/1-AgentSkills/managing-db-migrations/reference/field-evolution-rules.md b/1-AgentSkills/managing-db-migrations/reference/field-evolution-rules.md new file mode 100644 index 0000000..7eef1f9 --- /dev/null +++ b/1-AgentSkills/managing-db-migrations/reference/field-evolution-rules.md @@ -0,0 +1,58 @@ +# 字段演进规则 + +## 安全演进 + +### 新增可空列 +```sql +ALTER TABLE users ADD COLUMN nickname VARCHAR(100); +``` +- ✅ 安全:不影响现有数据 +- ✅ 可回滚 + +### 新增有默认值的列 +```sql +ALTER TABLE users ADD COLUMN status VARCHAR(20) DEFAULT 'active'; +``` +- ✅ 安全:现有行自动填充默认值 +- ⚠️ 大表可能较慢 + +### 扩展字段长度 +```sql +ALTER TABLE users ALTER COLUMN username TYPE VARCHAR(500); +``` +- ✅ 安全:不丢失数据 +- ✅ 可回滚(需确认无超长数据) + +## 危险演进 + +### 缩小字段长度 +```sql +-- 危险!可能截断数据 +ALTER TABLE users ALTER COLUMN username TYPE VARCHAR(50); +``` +- ❌ 可能丢失数据 +- 必须先验证:`SELECT MAX(LENGTH(username)) FROM users;` + +### 修改字段类型 +```sql +-- 危险!可能转换失败 +ALTER TABLE users ALTER COLUMN age TYPE INTEGER USING age::integer; +``` +- ❌ 可能失败或丢失精度 +- 必须先验证数据兼容性 + +### 删除列 +```sql +-- 不可逆! +ALTER TABLE users DROP COLUMN old_field; +``` +- ❌ 数据永久丢失 +- 必须先备份 + +## 推荐流程 + +1. 新增新列(可空) +2. 代码同时写新旧列 +3. 数据迁移(旧列 → 新列) +4. 代码只读写新列 +5. 删除旧列(确认无使用后) diff --git a/1-AgentSkills/managing-db-migrations/reference/migration-naming.md b/1-AgentSkills/managing-db-migrations/reference/migration-naming.md new file mode 100644 index 0000000..95dabc8 --- /dev/null +++ b/1-AgentSkills/managing-db-migrations/reference/migration-naming.md @@ -0,0 +1,49 @@ +# 迁移文件命名规范 + +## 命名格式 + +``` +YYYYMMDDHHMMSS_description.sql +``` + +示例: +- `20260123100000_create_users_table.sql` +- `20260123110000_add_phone_to_users.sql` +- `20260123120000_create_index_users_email.sql` + +## 命名规则 + +1. 时间戳精确到秒,确保唯一性 +2. description 使用小写字母和下划线 +3. 描述清晰表明变更内容 +4. 常用动词:create, add, drop, modify, rename, create_index + +## 文件结构 + +```sql +-- Migration: 20260123100000_create_users_table +-- Author: developer_name +-- Description: 创建用户表 + +-- ==================== UP ==================== +BEGIN; + +CREATE TABLE users ( + id SERIAL PRIMARY KEY, + username VARCHAR(255) NOT NULL UNIQUE, + created_at TIMESTAMP DEFAULT NOW() +); + +COMMIT; + +-- ==================== DOWN ==================== +-- BEGIN; +-- DROP TABLE IF EXISTS users; +-- COMMIT; +``` + +## 注意事项 + +- DOWN 脚本默认注释,测试时取消注释 +- 每个迁移文件只做一件事 +- 复杂迁移拆分成多个文件 diff --git a/1-AgentSkills/managing-db-migrations/reference/rollback-policy.md b/1-AgentSkills/managing-db-migrations/reference/rollback-policy.md new file mode 100644 index 0000000..5e4409a --- /dev/null +++ b/1-AgentSkills/managing-db-migrations/reference/rollback-policy.md @@ -0,0 +1,43 @@ +# 回滚策略 + +## 回滚原则 + +1. **每个 UP 必须有对应的 DOWN** +2. **DOWN 必须经过测试** +3. **不可逆操作需要备份** + +## 可回滚操作 + +| 操作 | 回滚方式 | +|:---|:---| +| CREATE TABLE | DROP TABLE | +| ADD COLUMN | DROP COLUMN | +| CREATE INDEX | DROP INDEX | +| INSERT | DELETE | + +## 不可逆操作(需特殊处理) + +| 操作 | 处理方式 | +|:---|:---| +| DROP TABLE | 迁移前备份表数据 | +| DROP COLUMN | 迁移前备份列数据 | +| TRUNCATE | 迁移前全表备份 | +| 类型变更(缩小) | 验证数据是否超限 | + +## 回滚流程 + +1. 确认回滚范围 +2. 备份当前数据(如需要) +3. 执行 DOWN 脚本 +4. 验证回滚结果 +5. 通知相关方 + +## 紧急回滚 + +```bash +# 回滚最近一次迁移 +psql -h $DB_HOST -U $DB_USER -d $DB_NAME -f migrations/XXXXXX_xxx_down.sql + +# 验证 +psql -h $DB_HOST -U $DB_USER -d $DB_NAME -c "\d table_name" +``` diff --git a/1-AgentSkills/managing-db-migrations/scripts/verify-migration-rollback.sh b/1-AgentSkills/managing-db-migrations/scripts/verify-migration-rollback.sh new file mode 100644 index 0000000..a03ef7e --- /dev/null +++ b/1-AgentSkills/managing-db-migrations/scripts/verify-migration-rollback.sh @@ -0,0 +1,66 @@ +#!/bin/bash +# verify-migration-rollback.sh - 验证迁移回滚 +# 依赖: grep +# 用法: ./verify-migration-rollback.sh + +set -e + +MIGRATION_FILE=$1 + +if [ -z "$MIGRATION_FILE" ]; then + echo "用法: $0 " + exit 1 +fi + +echo "=== 迁移回滚验证 ===" +echo "迁移文件: ${MIGRATION_FILE}" +echo "" + +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' + +pass() { echo -e "${GREEN}[PASS]${NC} $1"; } +fail() { echo -e "${RED}[FAIL]${NC} $1"; } +warn() { echo -e "${YELLOW}[WARN]${NC} $1"; } + +# 检查文件是否存在 +if [ ! -f "$MIGRATION_FILE" ]; then + fail "迁移文件不存在: $MIGRATION_FILE" + exit 1 +fi + +# 检查是否包含 UP 和 DOWN +if grep -q "UP" "$MIGRATION_FILE"; then + pass "包含 UP 脚本" +else + fail "缺少 UP 脚本" +fi + +if grep -q "DOWN" "$MIGRATION_FILE"; then + pass "包含 DOWN 脚本" +else + fail "缺少 DOWN 脚本" +fi + +# 检查 DOWN 脚本是否被注释 +if grep -E "^--.*DROP|^--.*DELETE|^--.*ALTER.*DROP" "$MIGRATION_FILE" > /dev/null; then + warn "DOWN 脚本被注释,请取消注释后测试回滚" +fi + +# 检查是否有危险操作 +if grep -i "DROP TABLE\|TRUNCATE\|DELETE FROM" "$MIGRATION_FILE" | grep -v "^--" > /dev/null; then + warn "包含危险操作(DROP/TRUNCATE/DELETE),请确保已备份" +fi + +# 检查命名规范 +FILENAME=$(basename "$MIGRATION_FILE") +if echo "$FILENAME" | grep -qE "^[0-9]{14}_[a-z_]+\.sql$"; then + pass "文件名符合规范" +else + warn "文件名不符合规范: YYYYMMDDHHMMSS_description.sql" +fi + +echo "" +echo "=== 验证完成 ===" diff --git a/1-AgentSkills/managing-observability/SKILL.md b/1-AgentSkills/managing-observability/SKILL.md new file mode 100644 index 0000000..a1eaa8b --- /dev/null +++ b/1-AgentSkills/managing-observability/SKILL.md @@ -0,0 +1,127 @@ +--- +name: managing-observability +description: "Guides observability implementation including structured logging, metrics, tracing, and audit log alignment for RMDC system. Triggered when adding log statements, defining metrics, implementing traces, or ensuring audit compliance. Keywords: structured log, metrics, trace, audit, Prometheus, OpenTelemetry, rmdc-audit-log." +allowed-tools: + - Read + - Glob + - Grep + - Bash +argument-hint: "$ARGUMENTS: [module] — aspect: logging|metrics|tracing|audit" +--- + +# managing-observability + +## 概述 +本 Skill 指导 RMDC 系统的可观测性实现,确保日志、指标、追踪与审计的一致性。 + +## 动态上下文注入 + +### 查找日志调用 +!`grep -rn "log\.\(Info\|Error\|Warn\|Debug\)" --include="*.go" | head -20` + +### 查找审计相关代码 +!`grep -rn "audit\|Audit\|AuditLog" --include="*.go" | head -20` + +--- + +## Plan(规划阶段) + +### 可观测性维度 +| 维度 | 工具 | 对齐模块 | +|:---|:---|:---| +| 日志 | 结构化日志 | rmdc-audit-log | +| 指标 | Prometheus | - | +| 追踪 | OpenTelemetry | - | +| 审计 | PostgreSQL | rmdc-audit-log | + +### 决策点 +- [ ] 日志级别是否合适? +- [ ] 是否需要添加审计记录? +- [ ] 指标命名是否符合规范? +- [ ] trace_id 是否正确传递? + +--- + +## Verify(验证清单) + +### 日志规范检查 +- [ ] 使用结构化日志格式 +- [ ] 包含 request_id / trace_id +- [ ] 敏感信息已脱敏 +- [ ] 日志级别正确 + +### 审计对齐检查 +- [ ] 关键操作有审计记录 +- [ ] 审计字段完整(who/when/what/where) +- [ ] 审计记录不可篡改 +- [ ] 与 rmdc-audit-log 格式一致 + +### 验证命令 +```bash +# 检查日志调用规范 +grep -rn "log\." --include="*.go" | grep -v "WithFields" | head -20 + +# 检查审计记录 +grep -rn "AuditLog\|audit" --include="*.go" | head -20 +``` + +--- + +## Execute(执行步骤) + +### 添加结构化日志 +```go +import log "github.com/sirupsen/logrus" + +log.WithFields(log.Fields{ + "user_id": userID, + "action": "login", + "request_id": requestID, +}).Info("用户登录成功") +``` + +### 添加审计记录 +```go +auditLog.Record(AuditEntry{ + UserID: userID, + Action: "UPDATE_USER", + ResourceID: targetUserID, + Details: changes, + Timestamp: time.Now(), + IP: clientIP, +}) +``` + +### 添加 Prometheus 指标 +```go +var loginCounter = prometheus.NewCounterVec( + prometheus.CounterOpts{ + Name: "rmdc_user_auth_login_total", + Help: "Total number of login attempts", + }, + []string{"status"}, +) + +// 使用 +loginCounter.WithLabelValues("success").Inc() +``` + +--- + +## Pitfalls(常见坑) + +1. **日志泄露敏感信息**:密码、Token、身份证号等未脱敏直接打印。 +2. **审计字段缺失**:无法追溯操作人(user_id)或操作内容(details)。 +3. **日志级别滥用**:DEBUG 日志在生产环境大量输出影响性能。 +4. **审计记录可被删除**:审计表需要设置写保护,禁止 DELETE/UPDATE。 +5. **trace_id 未传递**:跨服务调用时未将 trace_id 传递到下游,无法串联请求链路。 +6. **指标命名不规范**:未遵循 `模块_资源_动作_单位` 格式。 + +--- + +## 相关文件 +| 用途 | 路径 | +|:---|:---| +| 日志格式 | [reference/log-format.md](reference/log-format.md) | +| 指标命名 | [reference/metrics-naming.md](reference/metrics-naming.md) | +| 审计对齐 | [reference/audit-alignment.md](reference/audit-alignment.md) | diff --git a/1-AgentSkills/managing-observability/examples/structured-log-example.go b/1-AgentSkills/managing-observability/examples/structured-log-example.go new file mode 100644 index 0000000..92c1930 --- /dev/null +++ b/1-AgentSkills/managing-observability/examples/structured-log-example.go @@ -0,0 +1,85 @@ +package logging + +import ( + "time" + + log "github.com/sirupsen/logrus" +) + +// 结构化日志示例 + +// LogUserAction 记录用户操作日志 +func LogUserAction(userID uint, action string, requestID string, details map[string]interface{}) { + fields := log.Fields{ + "user_id": userID, + "action": action, + "request_id": requestID, + "timestamp": time.Now().Format(time.RFC3339), + } + + // 合并详情字段 + for k, v := range details { + fields[k] = v + } + + log.WithFields(fields).Info("用户操作") +} + +// LogAPIRequest 记录 API 请求日志 +func LogAPIRequest(requestID string, method string, path string, statusCode int, duration time.Duration, userID uint) { + log.WithFields(log.Fields{ + "request_id": requestID, + "method": method, + "path": path, + "status_code": statusCode, + "duration_ms": duration.Milliseconds(), + "user_id": userID, + }).Info("API请求") +} + +// LogError 记录错误日志 +func LogError(requestID string, err error, context map[string]interface{}) { + fields := log.Fields{ + "request_id": requestID, + "error": err.Error(), + } + + for k, v := range context { + fields[k] = v + } + + log.WithFields(fields).Error("发生错误") +} + +// 敏感信息脱敏工具 + +// MaskPhone 手机号脱敏 +func MaskPhone(phone string) string { + if len(phone) >= 11 { + return phone[:3] + "****" + phone[7:] + } + return "****" +} + +// MaskEmail 邮箱脱敏 +func MaskEmail(email string) string { + atIndex := -1 + for i, c := range email { + if c == '@' { + atIndex = i + break + } + } + if atIndex > 2 { + return email[:2] + "***" + email[atIndex:] + } + return "***@***" +} + +// MaskIDCard 身份证号脱敏 +func MaskIDCard(idCard string) string { + if len(idCard) >= 18 { + return idCard[:6] + "********" + idCard[14:] + } + return "******" +} diff --git a/1-AgentSkills/managing-observability/reference/audit-alignment.md b/1-AgentSkills/managing-observability/reference/audit-alignment.md new file mode 100644 index 0000000..027cd4d --- /dev/null +++ b/1-AgentSkills/managing-observability/reference/audit-alignment.md @@ -0,0 +1,74 @@ +# 审计日志对齐规范 + +## 与 rmdc-audit-log 对齐 + +所有模块的审计记录必须与 `rmdc-audit-log` 模块的格式保持一致。 + +## 必须字段 + +| 字段 | 类型 | 说明 | +|:---|:---|:---| +| id | uint | 审计记录ID | +| user_id | uint | 操作人ID | +| username | string | 操作人用户名 | +| action | string | 操作类型 | +| resource_type | string | 资源类型 | +| resource_id | string | 资源ID | +| details | json | 操作详情 | +| ip_address | string | 客户端IP | +| user_agent | string | 客户端UA | +| timestamp | timestamp | 操作时间 | +| result | string | 操作结果 success/failed | + +## 操作类型规范 + +| 模块 | 操作类型 | +|:---|:---| +| user-auth | USER_LOGIN, USER_LOGOUT, USER_CREATE, USER_UPDATE, USER_DELETE, PASSWORD_CHANGE, PERMISSION_GRANT | +| jenkins-dac | BUILD_TRIGGER, BUILD_CANCEL, PERMISSION_CHANGE | +| exchange-hub | COMMAND_SEND, COMMAND_COMPLETE | +| watchdog | DEPLOYMENT_START, DEPLOYMENT_COMPLETE, TOTP_VERIFY | +| project-mgmt | PROJECT_CREATE, PROJECT_UPDATE, AUTH_GRANT | +| work-procedure | WORKFLOW_CREATE, WORKFLOW_APPROVE, WORKFLOW_REJECT | + +## 审计表保护 + +审计表必须设置以下保护: +1. 禁止 DELETE 操作 +2. 禁止 UPDATE 操作(除标记字段外) +3. 定期备份 +4. 独立存储(建议) + +```sql +-- 创建只允许 INSERT 的触发器 +CREATE OR REPLACE FUNCTION prevent_audit_modify() +RETURNS TRIGGER AS $$ +BEGIN + RAISE EXCEPTION 'Audit log modification is not allowed'; +END; +$$ LANGUAGE plpgsql; + +CREATE TRIGGER audit_log_protect +BEFORE UPDATE OR DELETE ON audit_logs +FOR EACH ROW EXECUTE FUNCTION prevent_audit_modify(); +``` + +## 审计记录示例 + +```go +// user-auth 模块 +audit.Record(audit.Entry{ + UserID: operatorID, + Username: operatorName, + Action: "USER_CREATE", + ResourceType: "user", + ResourceID: strconv.Itoa(int(newUser.ID)), + Details: map[string]interface{}{ + "username": newUser.Username, + "role": newUser.Role, + }, + IPAddress: c.ClientIP(), + UserAgent: c.Request.UserAgent(), + Result: "success", +}) +``` diff --git a/1-AgentSkills/managing-observability/reference/log-format.md b/1-AgentSkills/managing-observability/reference/log-format.md new file mode 100644 index 0000000..ad1740f --- /dev/null +++ b/1-AgentSkills/managing-observability/reference/log-format.md @@ -0,0 +1,58 @@ +# 日志格式规范 + +## 结构化日志 + +所有日志必须使用结构化格式,禁止字符串拼接。 + +### 正确示例 +```go +log.WithFields(log.Fields{ + "user_id": userID, + "action": "login", + "request_id": requestID, + "duration": duration.Milliseconds(), +}).Info("用户登录成功") +``` + +### 错误示例 +```go +// ❌ 禁止 +log.Info("用户 " + username + " 登录成功,耗时 " + duration.String()) +``` + +## 必须字段 + +| 字段 | 说明 | 示例 | +|:---|:---|:---| +| request_id | 请求唯一标识 | uuid | +| user_id | 操作用户ID | 123 | +| action | 操作类型 | login, create_user | +| duration | 耗时(毫秒) | 150 | + +## 日志级别 + +| 级别 | 使用场景 | +|:---|:---| +| ERROR | 错误,需要关注和处理 | +| WARN | 警告,可能的问题 | +| INFO | 重要业务事件 | +| DEBUG | 调试信息,生产环境关闭 | + +## 敏感信息脱敏 + +必须脱敏的字段: +- 密码(任何形式) +- Token / Secret +- 身份证号 +- 银行卡号 +- 手机号(中间四位) + +```go +// 脱敏工具 +func maskPhone(phone string) string { + if len(phone) >= 11 { + return phone[:3] + "****" + phone[7:] + } + return "****" +} +``` diff --git a/1-AgentSkills/managing-observability/reference/metrics-naming.md b/1-AgentSkills/managing-observability/reference/metrics-naming.md new file mode 100644 index 0000000..064af03 --- /dev/null +++ b/1-AgentSkills/managing-observability/reference/metrics-naming.md @@ -0,0 +1,52 @@ +# 指标命名规范 + +## 命名格式 + +``` +rmdc_{module}_{resource}_{action}_{unit} +``` + +## 命名规则 + +1. 全小写,下划线分隔 +2. 以 `rmdc_` 前缀开头 +3. 包含模块名 +4. 描述清晰的资源和动作 +5. 带单位后缀(如适用) + +## 常用后缀 + +| 后缀 | 说明 | 示例 | +|:---|:---|:---| +| _total | 计数器 | rmdc_user_auth_login_total | +| _seconds | 时间(秒) | rmdc_api_request_duration_seconds | +| _bytes | 大小(字节) | rmdc_file_size_bytes | +| _ratio | 比率 | rmdc_cache_hit_ratio | + +## 示例 + +```go +// 计数器 +rmdc_user_auth_login_total{status="success"} +rmdc_user_auth_login_total{status="failed"} + +// 直方图 +rmdc_user_auth_request_duration_seconds{endpoint="/api/auth/login"} + +// Gauge +rmdc_user_auth_active_sessions +``` + +## 标签规范 + +- 标签名小写下划线 +- 标签值使用小写 +- 避免高基数标签(如 user_id) + +```go +// ✅ 正确 +loginCounter.WithLabelValues("success").Inc() + +// ❌ 错误 - 高基数 +loginCounter.WithLabelValues(userID).Inc() +``` diff --git a/1-AgentSkills/managing-observability/scripts/verify-observability.sh b/1-AgentSkills/managing-observability/scripts/verify-observability.sh new file mode 100644 index 0000000..1c4995c --- /dev/null +++ b/1-AgentSkills/managing-observability/scripts/verify-observability.sh @@ -0,0 +1,128 @@ +#!/bin/bash +# verify-observability.sh - 验证可观测性规范 +# 依赖: grep +# 用法: ./verify-observability.sh [check-type] +# check-type: all|logging|audit|metrics (默认 all) + +set -e + +CHECK_TYPE=${1:-all} +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +PROJECT_ROOT="${SCRIPT_DIR}/../../.." + +echo "=== RMDC 可观测性验证 ===" +echo "检查类型: ${CHECK_TYPE}" +echo "" + +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' + +pass() { echo -e "${GREEN}[PASS]${NC} $1"; } +fail() { echo -e "${RED}[FAIL]${NC} $1"; } +warn() { echo -e "${YELLOW}[WARN]${NC} $1"; } + +# 1. 日志规范检查 +check_logging() { + echo "--- 日志规范检查 ---" + + # 检查是否使用结构化日志 + UNSTRUCTURED=$(grep -rn 'log\.\(Info\|Error\|Warn\)f\?' --include="*.go" "${PROJECT_ROOT}" 2>/dev/null | \ + grep -v "WithFields" | grep -v "_test.go" | head -10) + + if [ -n "$UNSTRUCTURED" ]; then + warn "发现非结构化日志调用:" + echo "$UNSTRUCTURED" + else + pass "日志调用规范" + fi + + # 检查敏感信息泄露 + SENSITIVE=$(grep -rn 'password\|token\|secret' --include="*.go" "${PROJECT_ROOT}" 2>/dev/null | \ + grep -i 'log\.' | grep -v "Mask\|mask\|****" | head -5) + + if [ -n "$SENSITIVE" ]; then + warn "可能泄露敏感信息的日志:" + echo "$SENSITIVE" + else + pass "未发现敏感信息泄露" + fi +} + +# 2. 审计规范检查 +check_audit() { + echo "--- 审计规范检查 ---" + + # 检查是否有审计记录 + AUDIT_CALLS=$(grep -rn "audit\|Audit" --include="*.go" "${PROJECT_ROOT}" 2>/dev/null | \ + grep -v "_test.go" | wc -l) + + if [ "$AUDIT_CALLS" -gt 0 ]; then + pass "存在审计记录调用 ($AUDIT_CALLS 处)" + else + warn "未找到审计记录调用" + fi + + # 检查关键操作是否有审计 + for action in "Login\|login" "Create\|create" "Delete\|delete" "Update\|update"; do + ACTION_AUDIT=$(grep -rn "$action" --include="*.go" "${PROJECT_ROOT}" 2>/dev/null | \ + grep -i "audit" | head -1) + if [ -n "$ACTION_AUDIT" ]; then + pass "操作 $action 有审计" + else + warn "操作 $action 可能缺少审计" + fi + done +} + +# 3. 指标规范检查 +check_metrics() { + echo "--- 指标规范检查 ---" + + # 检查是否使用 prometheus + PROM_USAGE=$(grep -rn "prometheus\|Prometheus" --include="*.go" "${PROJECT_ROOT}" 2>/dev/null | \ + grep -v "_test.go" | wc -l) + + if [ "$PROM_USAGE" -gt 0 ]; then + pass "使用 Prometheus 指标 ($PROM_USAGE 处)" + else + warn "未找到 Prometheus 指标使用" + fi + + # 检查指标命名规范 + METRICS=$(grep -rn 'prometheus\.New' --include="*.go" "${PROJECT_ROOT}" 2>/dev/null | \ + grep -oE 'Name:\s*"[^"]+' | grep -oE '"[^"]+' | tr -d '"') + + if [ -n "$METRICS" ]; then + echo "发现的指标:" + echo "$METRICS" | while read metric; do + if echo "$metric" | grep -qE "^rmdc_"; then + pass " $metric" + else + warn " $metric (建议以 rmdc_ 开头)" + fi + done + fi +} + +# 执行检查 +case $CHECK_TYPE in + logging) check_logging ;; + audit) check_audit ;; + metrics) check_metrics ;; + all) + check_logging + echo "" + check_audit + echo "" + check_metrics + echo "" + echo "=== 所有检查完成 ===" + ;; + *) + echo "未知检查类型: $CHECK_TYPE" + echo "可选: all|logging|audit|metrics" + exit 1 + ;; +esac diff --git a/2-需求转换专业设计/2-DDS转AgentSkills.md b/2-需求转换专业设计/2-DDS转AgentSkills.md new file mode 100644 index 0000000..42b3d06 --- /dev/null +++ b/2-需求转换专业设计/2-DDS转AgentSkills.md @@ -0,0 +1,275 @@ +你是一位专业的 **Agent Skills 架构师** 与 **Claude Code Skills 作者**。你的任务是:把给定的 **RMDC 系统 DDS/PRD/架构说明** 转换为一套**可落地、含设计细节**的 Claude Code Skills(系统级 Skill + 模块级 Skills + 横切 Skills),并输出完整目录树与每个 Skill 的 `SKILL.md`,以及从 DDS 中抽取出的关键设计内容到 `reference/`。 + +--- + +# 0. 核心目标(必须遵守) + +你生成的不只是“工作流提示词”,而是**能指导真实开发/对齐/审查**的 Skill 套件。 + +**硬性要求:每个 Skill 必须“绑定 DDS 设计细节”** + +* 必须从 DDS 中抽取并落盘(reference/): + + * 接口/API(含路径、方法、请求/响应字段、错误码) + * 事件/Topic(字段、版本、幂等键、重试语义) + * DB 表/Schema(字段、索引、约束、迁移策略) + * 状态机/流程(状态、转移、守卫条件、回调、补偿) + * 授权模型(一级/二级、JWT claims、RBAC/DAC) + * 关键时序(跨模块调用链路、Outbox/MQTT/K8S 操作链) +* 如果 DDS 没写清楚:**必须标注 TBD(To Be Defined)**,并输出“最小补充信息清单”,禁止脑补。 + +--- + +# 1. 输入(从 $ARGUMENTS 注入读取 DDS) + +* 源文档路径由参数传入:`$ARGUMENTS` +* 你必须进行动态注入读取输入内容(至少 6 处,且覆盖“结构→关键字→细节”): + +1. 目录与上下文 + +* !`ls -la $(dirname "$ARGUMENTS")` +* !`file "$ARGUMENTS"` + +2. DDS 正文读取(强制至少 3 段) + +* !`sed -n '1,120p' "$ARGUMENTS"` +* !`sed -n '120,240p' "$ARGUMENTS"` +* !`sed -n '240,420p' "$ARGUMENTS"` + +3. 设计细节抽取(强制至少 3 次 grep,分别聚焦 API/事件/DB/状态机/权限) + +* !`grep -nE "API|接口|路径|路由|request|response|错误码|error" "$ARGUMENTS" | head -n 80` +* !`grep -nE "事件|event|MQTT|topic|outbox|消息|payload|幂等|retry" "$ARGUMENTS" | head -n 80` +* !`grep -nE "表|schema|字段|索引|unique|constraint|migration|DDL|PostgreSQL" "$ARGUMENTS" | head -n 80` +* !`grep -nE "状态机|state|transition|流转|工单|workflow|回调|补偿" "$ARGUMENTS" | head -n 80` +* !`grep -nE "RBAC|DAC|鉴权|JWT|claim|一级授权|二级授权|TOTP|权限" "$ARGUMENTS" | head -n 80` + +> 若无法读取文件:明确说明缺少源文档内容,并输出“继续所需的最小信息清单”(接口/事件/表/状态机/依赖/权限/错误码),不得臆造细节。 + +--- + +# 2 章节驱动的 reference 目录分层(强制新增) + +你必须把 DDS 的**章节标题/小节标题**提取出来,并用它们来构建 `reference/` 的分层目录。reference 文件不得全部堆在一个目录里,必须按章节归档。 + +## 2.1 章节提取(必须动态注入至少 2 处) + +你必须在生成任何 reference 文件前,先输出“章节目录草案(TOC)”,并用动态注入从 DDS 中提取标题: + +* !`grep -nE '^(#{1,6}\s+|[0-9]+(\.[0-9]+){0,3}\s+|第[一二三四五六七八九十]+章|第[0-9]+章|[一二三四五六七八九十]+、)' "$ARGUMENTS" | head -n 120` +* !`sed -n '1,200p' "$ARGUMENTS" | nl -ba | sed -n '1,120p'` + +### 标题识别规则(启发式,必须使用) + +按优先级识别章节标题: + +1. Markdown 标题:`# / ## / ### ...` +2. 编号标题:`1 `、`1.1 `、`2.3.4 `(允许末尾带冒号) +3. 中文章标题:`第X章`、`第1章` +4. 中文小节:`一、二、三、` 或 `(一)(二)` + +> 如果识别到的标题少于 3 个:必须进入降级策略(见 2.4),并在最终自检中标记该 DDS “章节结构提取不足” 为 FAIL。 + +--- + +## 2.2 reference 目录命名规范(必须) + +* `reference/` 下目录必须采用 **有序前缀 + slug** 形式,防止乱序: + + * `reference/01-/` + * `reference/01-/02-/`(可选) +* slug 规则: + + * 全小写 + * 非字母数字替换为 `-` + * 连续 `-` 合并 + * 截断到 48 字符以内 +* 章节序号来自 DDS 中的章节顺序(不是字面编号),例如: + + * `01-architecture-overview/` + * `02-security-authz/` + * `03-apis/` + * `04-events-topics/` + * `05-db-schema/` + * `06-state-machine/` + +--- + +## 2.3 reference 文件落盘规则(必须按章节放置) + +你生成 reference 内容时,必须把内容放在对应章节目录下,而不是扁平化: + +* `reference/
/apis.md` +* `reference/
/events-topics.md` +* `reference/
/db-schema.md` +* `reference/
/state-machine.md` +* `reference/
/security-model.md` +* `reference/
/dependencies.md` + +### 章节映射要求(必须) + +* 每条 reference 条目必须包含: + + * `DDS-Section:` 章节标题(原文) + * `DDS-Lines:` 行号范围(如 `L120-L168` 或 “近似行号”) + * `Extract:` 结构化内容(表格/列表) +* SKILL.md 中引用 reference 时必须引用“章节路径”,例如: + + * `See: reference/03-apis/apis.md` + * `See: reference/04-events-topics/events-topics.md` + +--- + +## 2.4 降级策略(无法可靠提取标题时必须执行) + +当标题提取不足(少于 3 个)或 DDS 格式混乱时: + +* 仍然必须分层,但使用保底目录: + + * `reference/00-unknown/` + * `reference/00-unknown/01-apis/` + * `reference/00-unknown/02-events/` + * `reference/00-unknown/03-db/` + * `reference/00-unknown/04-state-machine/` + * `reference/00-unknown/05-security/` +* 同时必须在最终自检中给出 FAIL: + + * FAIL 原因:DDS 标题结构不可识别 + * 修复建议:提供 Markdown 标题、或提供章节目录、或提供导出为 md 的版本 + +--- + +## 2.5 verify.sh 必须检查章节分层(新增校验点) + +每个 Skill 的 `scripts/verify.sh` 必须检查: + +* `reference/` 下至少存在 `01-*` 的章节目录(或降级 `00-unknown`) +* 至少 2 个 reference 文件位于**非根目录**(在章节子目录里) +* 任意 reference 文件中必须出现 `DDS-Section:` 与 `DDS-Lines:` 字段 + +示例检查(可直接用): + +* `find reference -maxdepth 2 -type d -name '01-*' | grep -q .` +* `grep -R "DDS-Section:" -n reference | head -n 5` +* `grep -R "DDS-Lines:" -n reference | head -n 5` + +--- + +# 3. 系统模块(必须按此拆分) + +| 模块 | 职责 | 关键技术 | +| rmdc-core | API Gateway、鉴权、路由 | Go + Gin | +| rmdc-jenkins-branch-dac | Jenkins分支权限(DAC)、构建管理 | Jenkins API, MinIO | +| rmdc-exchange-hub | MQTT消息网关、指令生命周期 | MQTT, PostgreSQL | +| rmdc-watchdog | 边缘代理、K8S操作、二级授权 | K8S API, TOTP | +| rmdc-project-management | 项目管理、一级授权中心 | PostgreSQL | +| rmdc-work-procedure | 工单管理、工单流程、生命周期管理 | 状态机 | +| rmdc-audit-log | 审计日志 | PostgreSQL | +| rmdc-user-auth | 用户认证、权限管理 | JWT, RBAC | + +--- + +# 4. 输出目标(必须一次性给全) + +我将把输出落盘到 Windows 目录: + +* `C:/Users/wddsh/Documents/IdeaProjects/ProjectAGiPrompt/1-AgentSkills` + 但你输出展示路径必须使用 Unix 风格(`/`)。 + +你需要输出: + +1. 全部 Skills 的目录结构树(tree) +2. 每个 Skill 的 SKILL.md 完整内容(frontmatter + body) +3. reference/examples/scripts 中关键文件内容(只给必要内容,但 reference 必须充分) +4. 最后输出全局自检结果(逐条 PASS/FAIL + 修复建议) + +--- + +# 5. Skill 组织架构(必须遵守) + +生成以下 3 类 Skills: +A) 系统级(1个):`rmdc-system`(跨模块一致性、依赖规则、版本/兼容策略、全局变更流程) +B) 模块级(7个):每个模块 1 个 Skill(高频开发:实现步骤 + 依赖影响检查) +C) 横切(至少3个): + +* `designing-contracts`(API/事件/Schema 契约、兼容策略、版本策略) +* `database-migrations`(PostgreSQL迁移与回滚、字段演进) +* `observability-audit`(日志/指标/trace/审计一致性;对齐 rmdc-audit-log) + +> 可按 DDS 增减,但不得少于 3 个横切 Skill。 + +--- + +# 6. 规范约束(硬性) + +## 6.1 Frontmatter(极其重要) + +* name:小写字母/数字/连字符;动名词形式; 包含中文的翻译 +* description:必须【单行】且 <1024 字符;第三人称;包含功能说明 + 触发场景 + 关键词(必须包含模块名)必须是中英文混合的模式,保证中文和英文均可被命中 +* allowed-tools:最小授权原则(默认仅 Read/Grep/Glob/Bash;除非 DDS 明确需要外部工具) +* 必须出现 argument-hint(提示 $ARGUMENTS 格式) + +## 6.2 内容精简与拆分(但必须保留设计细节) + +* 删除 Claude 常识,只保留 **DDS 特有设计** 与 **可操作步骤** +* 重复内容移到 reference/,SKILL.md 只保留“怎么做 + 查哪里 + 怎么验” +* 示例代码放 examples/(只放骨架与关键接口签名) +* scripts/ 必须至少 1 个 `verify.sh`: + + * 能运行并输出 PASS/FAIL + * 检查:契约文件存在、迁移文件存在、接口/事件/表关键字是否匹配 DDS 抽取内容(可用 grep) + * 写清依赖(bash、grep、sed、go test 可选) + +## 6.3 工作流(计划-验证-执行) + +每个 SKILL.md 必须包含以下结构: + +* Plan:产物清单 + 决策点(涉及哪些模块、改动边界、是否影响契约/事件/表) +* Verify:Checklist(可勾选)+ 明确验证点(契约兼容、topic schema、migration 可回滚、RBAC/DAC 不破坏) +* Execute:可操作步骤(命令化短句,按顺序) +* Pitfalls:3~8 条常见坑(必须模块相关,且至少 2 条引用 reference 的具体内容) + +## 6.4 参数与动态上下文 + +* 每个 Skill 必须使用 $ARGUMENTS(模块名、变更类型、输入文档路径、目标目录) +* 每个 Skill 必须至少包含 2 处 !`command` 动态注入示例(查 API/事件/表/运行测试) +* 命令需类 Unix shell 可执行;避免 OS 特定路径 + +## 6.5 质量标准(新增:设计细节覆盖率门槛) + +* 每个模块 Skill 的 reference/ 必须覆盖至少 3 类设计要素(API/事件/DB/状态机/权限/依赖) +* 每个模块 Skill 若 reference 不足,最终自检必须 FAIL,并给补齐策略 +* 每个 SKILL.md 主体 <500 行 + +--- + +# 7. 特殊要求(与模块强相关,必须引用 reference 的设计细节) + +* rmdc-core:鉴权/路由变更影响检查(API契约、错误码、JWT claims)→ 引用 `reference/apis.md` + `reference/security-model.md` +* rmdc-jenkins-branch-dac:DAC 规则回归 + 最小权限;Jenkins API + MinIO 验证点 → 引用 `reference/security-model.md` + `reference/apis.md` +* rmdc-exchange-hub:MQTT topic/指令生命周期契约 + 幂等处理 → 引用 `reference/events-topics.md` + `reference/state-machine.md` +* rmdc-watchdog:K8S API 安全边界 + TOTP 二级授权 → 引用 `reference/security-model.md` + `reference/apis.md` +* rmdc-project-management:一级授权中心字段/状态变更影响 → 引用 `reference/db-schema.md` + `reference/state-machine.md` +* rmdc-audit-log:审计不可篡改/字段完整性/写入链路 → 引用 `reference/db-schema.md` + `reference/dependencies.md` +* rmdc-user-auth:RBAC/JWT/会话安全兼容与回归 → 引用 `reference/security-model.md` + `reference/apis.md` + +--- + +# 8. 生成步骤(必须按顺序输出) + +步骤1:先给出 Skills 清单(系统级/模块级/横切),并为每个 Skill 提供 2~3 个 name 候选,最终选择 1 个 name(附一句理由) +步骤2:输出总目录树(Unix 路径) +步骤3:依次输出每个 Skill 的 SKILL.md(完整内容) +步骤4:输出 supporting files(reference/examples/scripts)按“文件路径 -> 文件内容”逐个输出(reference 必须充分) +步骤5:输出全局 Verify Checklist 自检结果(逐条 PASS/FAIL + 修复建议) + +--- + +# 9. 输出风格(新增:避免“空话”) + +* 禁止只写“检查 API 兼容/检查事件一致性”这种空话 +* 必须写成可执行的审查动作,例如: + + * “在 `reference/events-topics.md` 找到 topic 列表,对照仓库 grep 出 publish/subscribe 点” + * “校验 JWT claims 是否包含 `tenant_id/project_id/role`(来自 `reference/security-model.md`)” + * “migration 必须包含 down SQL;verify.sh grep 检查 `-- +migrate Down` 或回滚段落存在” \ No newline at end of file diff --git a/2-需求转换专业设计/DDS转AgentSkill.md b/2-需求转换专业设计/DDS转AgentSkill.md index 845f31f..8a6d7d2 100644 --- a/2-需求转换专业设计/DDS转AgentSkill.md +++ b/2-需求转换专业设计/DDS转AgentSkill.md @@ -3,9 +3,9 @@ # 输入 - 源文档路径由参数传入:$ARGUMENTS - 你必须使用动态注入读取输入内容(至少 2 处): - - !`ls -la $(dirname "$ARGUMENTS")` - - !`sed -n '1,200p' "$ARGUMENTS"` - - (可选)!`grep -nE "模块|module|service|接口|API|事件|MQTT|topic|表|schema|RBAC|鉴权|状态机" "$ARGUMENTS" | head -n 50` + - !`ls -la $(dirname "$ARGUMENTS")` + - !`sed -n '1,200p' "$ARGUMENTS"` + - (可选)!`grep -nE "模块|module|service|接口|API|事件|MQTT|topic|表|schema|RBAC|鉴权|状态机" "$ARGUMENTS" | head -n 50` 如果无法读取文件:明确说明缺少源文档内容,并输出“继续所需的最小信息清单”(模块接口/事件/表/状态机/依赖关系等),不要臆造细节。 @@ -112,4 +112,8 @@ scripts/ ### claude name命名 rmdc-watchdog-skill watchdog模块的设计文档说明见目录 C:\Users\wddsh\Documents\IdeaProjects\ProjectAGiPrompt\8-CMII-RMDC\6-rmdc-watchdog +请将详细系统设计转换为单独的SKILL + +### claude name命名 rmdc-user-auth-skill +user-auth模块的设计文档说明见C:\Users\wddsh\Documents\IdeaProjects\ProjectAGiPrompt\8-CMII-RMDC\9-rmdc-user-auth\2-user-auth-DDS.md 请将详细系统设计转换为单独的SKILL \ No newline at end of file diff --git a/2-需求转换专业设计/中文表头转换.md b/2-需求转换专业设计/中文表头转换.md new file mode 100644 index 0000000..0cd0685 --- /dev/null +++ b/2-需求转换专业设计/中文表头转换.md @@ -0,0 +1 @@ +请你将下面的AgentSkill的FrontFormatter 转换为中英文的形式,要求翻译的尽可能贴切实际开发,可以修改原始的英文 \ No newline at end of file diff --git a/2-需求转换专业设计/转换的prompt.md b/2-需求转换专业设计/转换的prompt.md index 23f9bec..5b6c143 100644 --- a/2-需求转换专业设计/转换的prompt.md +++ b/2-需求转换专业设计/转换的prompt.md @@ -17,3 +17,10 @@ +你是一位专业的 Agent Skills 架构师与 Claude Code Skills 作者。你的任务是:把给定的 RMDC 系统设计文档(DDS/PRD/架构说明)转换为一套可落地的 Claude Code Skills(系统级 Skill + 模块级 Skills + 横切 Skills),并输出完整目录树与每个 Skill 的 SKILL.md。 + +请你分析并修改 1-AgentSkills/coding-go-gin-gorm +1. 现在我发现LLM很少能够拉到reference里面的详细内容 +2. 现在很多地方定义了api的回复的错误code,请统一回复的Code定义 +3. 回复的Code定义中,不要出现HTTP的错误code,不需要按照模块区分错误Code +4. 回复结构体请统一标准为 1-AgentSkills/coding-go-gin-gorm/reference/api-response-spec.md \ No newline at end of file diff --git a/8-CMII-RMDC/4-rmdc-project-management/2-rmdc-project-management-DDS.md b/8-CMII-RMDC/4-rmdc-project-management/2-rmdc-project-management-DDS.md index 46934a2..3118a92 100644 --- a/8-CMII-RMDC/4-rmdc-project-management/2-rmdc-project-management-DDS.md +++ b/8-CMII-RMDC/4-rmdc-project-management/2-rmdc-project-management-DDS.md @@ -999,4 +999,4 @@ type AuthorizationInfo struct { | [项目管理PRD](1-rmdc-project-management-PRD.md) | 产品需求文档 | | [项目状态说明](4-project-状态说明.md) | 状态转换说明 | | [工单模块DDS](../7-rmdc-work-procedure/1-rmdc-work-procedure-DDS.md) | 工单流程设计 | -| [用户权限DDS](../9-rmdc-user-auth/1-user-auth-DDS.md) | 用户权限设计 | +| [用户权限DDS](../9-rmdc-user-auth/1-user-auth-PRD.md) | 用户权限设计 | diff --git a/8-CMII-RMDC/4-rmdc-project-management/3-china-province-city copy.md b/8-CMII-RMDC/4-rmdc-project-management/3-china-province-city copy.md new file mode 100644 index 0000000..e3d83e3 --- /dev/null +++ b/8-CMII-RMDC/4-rmdc-project-management/3-china-province-city copy.md @@ -0,0 +1,361 @@ +## 省份及地级市列表 + +**河北省** +河北省-石家庄市 +河北省-唐山市 +河北省-秦皇岛市 +河北省-邯郸市 +河北省-邢台市 +河北省-保定市 +河北省-张家口市 +河北省-承德市 +河北省-沧州市 +河北省-廊坊市 +河北省-衡水市 + +**山西省** +山西省-太原市 +山西省-大同市 +山西省-阳泉市 +山西省-长治市 +山西省-晋城市 +山西省-朔州市 +山西省-晋中市 +山西省-运城市 +山西省-忻州市 +山西省-临汾市 +山西省-吕梁市 + +**内蒙古自治区** +内蒙古自治区-呼和浩特市 +内蒙古自治区-包头市 +内蒙古自治区-乌海市 +内蒙古自治区-赤峰市 +内蒙古自治区-通辽市 +内蒙古自治区-鄂尔多斯市 +内蒙古自治区-呼伦贝尔市 +内蒙古自治区-巴彦淖尔市 +内蒙古自治区-乌兰察布市 + +**辽宁省** +辽宁省-沈阳市 +辽宁省-大连市 +辽宁省-鞍山市 +辽宁省-抚顺市 +辽宁省-本溪市 +辽宁省-丹东市 +辽宁省-锦州市 +辽宁省-营口市 +辽宁省-阜新市 +辽宁省-辽阳市 +辽宁省-盘锦市 +辽宁省-铁岭市 +辽宁省-朝阳市 +辽宁省-葫芦岛市 + +**吉林省** +吉林省-长春市 +吉林省-吉林市 +吉林省-四平市 +吉林省-辽源市 +吉林省-通化市 +吉林省-白山市 +吉林省-松原市 +吉林省-白城市 + +**黑龙江省** +黑龙江省-哈尔滨市 +黑龙江省-齐齐哈尔市 +黑龙江省-鸡西市 +黑龙江省-鹤岗市 +黑龙江省-双鸭山市 +黑龙江省-大庆市 +黑龙江省-伊春市 +黑龙江省-佳木斯市 +黑龙江省-七台河市 +黑龙江省-牡丹江市 +黑龙江省-黑河市 +黑龙江省-绥化市 + +**江苏省** +江苏省-南京市 +江苏省-无锡市 +江苏省-徐州市 +江苏省-常州市 +江苏省-苏州市 +江苏省-南通市 +江苏省-连云港市 +江苏省-淮安市 +江苏省-盐城市 +江苏省-扬州市 +江苏省-镇江市 +江苏省-泰州市 +江苏省-宿迁市 + +**浙江省** +浙江省-杭州市 +浙江省-宁波市 +浙江省-温州市 +浙江省-嘉兴市 +浙江省-湖州市 +浙江省-绍兴市 +浙江省-金华市 +浙江省-衢州市 +浙江省-舟山市 +浙江省-台州市 +浙江省-丽水市 + +**安徽省** +安徽省-合肥市 +安徽省-芜湖市 +安徽省-蚌埠市 +安徽省-淮南市 +安徽省-马鞍山市 +安徽省-淮北市 +安徽省-铜陵市 +安徽省-安庆市 +安徽省-黄山市 +安徽省-滁州市 +安徽省-阜阳市 +安徽省-宿州市 +安徽省-六安市 +安徽省-亳州市 +安徽省-池州市 +安徽省-宣城市 + +**福建省** +福建省-福州市 +福建省-厦门市 +福建省-三明市 +福建省-莆田市 +福建省-泉州市 +福建省-漳州市 +福建省-南平市 +福建省-龙岩市 +福建省-宁德市 + +**江西省** +江西省-南昌市 +江西省-景德镇市 +江西省-萍乡市 +江西省-九江市 +江西省-新余市 +江西省-鹰潭市 +江西省-赣州市 +江西省-吉安市 +江西省-宜春市 +江西省-抚州市 +江西省-上饶市 + +**山东省** +山东省-济南市 +山东省-青岛市 +山东省-淄博市 +山东省-枣庄市 +山东省-东营市 +山东省-烟台市 +山东省-潍坊市 +山东省-济宁市 +山东省-泰安市 +山东省-威海市 +山东省-日照市 +山东省-临沂市 +山东省-德州市 +山东省-聊城市 +山东省-滨州市 +山东省-菏泽市 + +**河南省** +河南省-郑州市 +河南省-开封市 +河南省-洛阳市 +河南省-平顶山市 +河南省-安阳市 +河南省-鹤壁市 +河南省-新乡市 +河南省-焦作市 +河南省-濮阳市 +河南省-许昌市 +河南省-漯河市 +河南省-三门峡市 +河南省-南阳市 +河南省-商丘市 +河南省-信阳市 +河南省-周口市 +河南省-驻马店市 + +**湖北省** +湖北省-武汉市 +湖北省-黄石市 +湖北省-十堰市 +湖北省-宜昌市 +湖北省-襄阳市 +湖北省-鄂州市 +湖北省-荆门市 +湖北省-孝感市 +湖北省-荆州市 +湖北省-黄冈市 +湖北省-咸宁市 +湖北省-随州市 + +**湖南省** +湖南省-长沙市 +湖南省-株洲市 +湖南省-湘潭市 +湖南省-衡阳市 +湖南省-邵阳市 +湖南省-岳阳市 +湖南省-常德市 +湖南省-张家界市 +湖南省-益阳市 +湖南省-郴州市 +湖南省-永州市 +湖南省-怀化市 +湖南省-娄底市 + +**广东省** +广东省-广州市 +广东省-韶关市 +广东省-深圳市 +广东省-珠海市 +广东省-汕头市 +广东省-佛山市 +广东省-江门市 +广东省-湛江市 +广东省-茂名市 +广东省-肇庆市 +广东省-惠州市 +广东省-梅州市 +广东省-汕尾市 +广东省-河源市 +广东省-阳江市 +广东省-清远市 +广东省-东莞市 +广东省-中山市 +广东省-潮州市 +广东省-揭阳市 +广东省-云浮市 + +**广西壮族自治区** +广西壮族自治区-南宁市 +广西壮族自治区-柳州市 +广西壮族自治区-桂林市 +广西壮族自治区-梧州市 +广西壮族自治区-北海市 +广西壮族自治区-防城港市 +广西壮族自治区-钦州市 +广西壮族自治区-贵港市 +广西壮族自治区-玉林市 +广西壮族自治区-百色市 +广西壮族自治区-贺州市 +广西壮族自治区-河池市 +广西壮族自治区-来宾市 +广西壮族自治区-崇左市 + +**海南省** +海南省-海口市 +海南省-三亚市 +海南省-三沙市 +海南省-儋州市 + +**四川省** +四川省-成都市 +四川省-自贡市 +四川省-攀枝花市 +四川省-泸州市 +四川省-德阳市 +四川省-绵阳市 +四川省-广元市 +四川省-遂宁市 +四川省-内江市 +四川省-乐山市 +四川省-南充市 +四川省-眉山市 +四川省-宜宾市 +四川省-广安市 +四川省-达州市 +四川省-雅安市 +四川省-巴中市 +四川省-资阳市 + +**贵州省** +贵州省-贵阳市 +贵州省-六盘水市 +贵州省-遵义市 +贵州省-安顺市 +贵州省-毕节市 +贵州省-铜仁市 + +**云南省** +云南省-昆明市 +云南省-曲靖市 +云南省-玉溪市 +云南省-保山市 +云南省-昭通市 +云南省-丽江市 +云南省-普洱市 +云南省-临沧市 + +**西藏自治区** +西藏自治区-拉萨市 +西藏自治区-日喀则市 +西藏自治区-昌都市 +西藏自治区-林芝市 +西藏自治区-山南市 +西藏自治区-那曲市 + +**陕西省** +陕西省-西安市 +陕西省-铜川市 +陕西省-宝鸡市 +陕西省-咸阳市 +陕西省-渭南市 +陕西省-汉中市 +陕西省-延安市 +陕西省-榆林市 +陕西省-安康市 +陕西省-商洛市 + +**甘肃省** +甘肃省-兰州市 +甘肃省-嘉峪关市 +甘肃省-金昌市 +甘肃省-白银市 +甘肃省-天水市 +甘肃省-武威市 +甘肃省-张掖市 +甘肃省-平凉市 +甘肃省-酒泉市 +甘肃省-庆阳市 +甘肃省-定西市 +甘肃省-陇南市 + +**青海省** +青海省-西宁市 +青海省-海东市 + +**宁夏回族自治区** +宁夏回族自治区-银川市 +宁夏回族自治区-石嘴山市 +宁夏回族自治区-吴忠市 +宁夏回族自治区-固原市 +宁夏回族自治区-中卫市 + +**新疆维吾尔自治区** +新疆维吾尔自治区-乌鲁木齐市 +新疆维吾尔自治区-克拉玛依市 +新疆维吾尔自治区-吐鲁番市 +新疆维吾尔自治区-哈密市 + +**直辖市(与省平级)** +北京市 +天津市 +上海市 +重庆市 + +**特别行政区** +香港特别行政区 +澳门特别行政区 + +**台湾省** +台湾省 \ No newline at end of file diff --git a/8-CMII-RMDC/9-rmdc-user-auth/1-user-auth-DDS.md b/8-CMII-RMDC/9-rmdc-user-auth/1-user-auth-PRD.md similarity index 100% rename from 8-CMII-RMDC/9-rmdc-user-auth/1-user-auth-DDS.md rename to 8-CMII-RMDC/9-rmdc-user-auth/1-user-auth-PRD.md diff --git a/8-CMII-RMDC/9-rmdc-user-auth/2-user-auth-DDS.md b/8-CMII-RMDC/9-rmdc-user-auth/2-user-auth-DDS.md new file mode 100644 index 0000000..530b797 --- /dev/null +++ b/8-CMII-RMDC/9-rmdc-user-auth/2-user-auth-DDS.md @@ -0,0 +1,954 @@ +# RMDC 用户认证模块详细设计说明书 (DDS) + +**产品名称**: RMDC 用户认证模块 (rmdc-user-auth) +**版本**: v2.0 +**编制日期**: 2026-01-27 + +--- + +## 1. 概述 + +### 1.1 模块定位 +`rmdc-user-auth` 提供 RMDC 统一的用户认证、账户生命周期管理与权限服务,支持 RSA 加密登录、JWT 鉴权、密码过期策略,以及通过工单驱动的用户注册/管理审批流程。 + +### 1.2 核心职责 +1. **身份认证**:RSA-OAEP 密码加密 + bcrypt 校验,颁发 4h 有效的 JWT。 +2. **账号管理**:用户 CRUD、密码修改、个人资料更新,支持密码过期、首次登录强制改密与状态控制(active/locked/disabled)。 +3. **审批工作流集成**:用户注册/管理通过 `rmdc-work-procedure` 工单审批,遵循"谁注册谁管理"。 +4. **权限服务**: + 1. Jenkins 分支层级权限(Org/Repo/Branch)权限,对外暴露权限检查接口。 + 2. Project 项目权限(数据权限)权限 + 3. DeliveryUpdate 微服务更新权限 +5. **系统配置**:RSA 密钥对、登录策略、注册开关等配置的存取与缓存。 + +### 1.3 版本修订历史 + +| 版本 | 日期 | 修订内容 | +|:---|:---|:---| +| v1.0 | 2026-01-23 | 基于现有代码首次形成 DDS,覆盖认证、工单、权限与数据模型 | +| v2.0 | 2026-01-27 | 新增用户有效期字段、强制改密机制、用户注册/管理工单流程详细设计、模块依赖关系流程图 | + +--- + +## 2. 系统架构 + +### 2.1 模块依赖关系 + +```mermaid +graph TB + subgraph 核心层 + Core[rmdc-core
API Gateway] + end + + subgraph 用户与权限 + UA[rmdc-user-auth
用户认证/权限] + end + + subgraph 业务协作 + PM[rmdc-project-management
项目管理] + WP[rmdc-work-procedure
工单流程] + JB[rmdc-jenkins-branch-dac
Jenkins分支权限] + end + + subgraph 基础设施 + CMN[rmdc-common
公共模块] + Aud[rmdc-audit-log
审计日志] + end + + Core --> UA + Core --> PM + Core --> WP + + UA -->|用户注册/管理工单| WP + UA -->|Jenkins资源查询| JB + UA --> CMN + UA --> Aud + + PM -->|用户鉴权/查询| UA + PM -->|项目权限管理| UA + PM -->|项目工单| WP + + WP -->|用户状态回调| UA + WP -->|项目状态回调| PM +``` + +### 2.2 用户模块与工单模块依赖关系详图 + +```mermaid +graph TB + subgraph rmdc_core["rmdc-core (入口层)"] + INIT[模块初始化] + end + + subgraph rmdc_user_auth["rmdc-user-auth (用户认证层)"] + UH[UserHandler
用户接口] + UWH[UserWorkflowHandler
用户工单接口] + US[UserService
用户服务] + URS[UserRegistrationService
注册工单服务] + UMS[UserManagementService
管理工单服务] + end + + subgraph rmdc_work_procedure["rmdc-work-procedure (工单层)"] + WH[WorkflowHandler
工单接口] + WS[WorkflowService
工单服务] + WC[WorkflowCallbacks
状态回调] + end + + INIT -->|注入用户状态回调| WS + INIT -->|注入工单创建器| US + + UH --> US + UWH --> URS + UWH --> UMS + + URS -->|创建注册工单| WS + UMS -->|创建管理工单| WS + + WC -->|审批通过: 激活用户| US + WC -->|审批打回: 通知修改| URS + WC -->|撤销: 删除待审批用户| US + WC -->|管理审批通过: 执行变更| UMS +``` + +### 2.3 接口注入机制 + +采用 **接口注入(依赖注入)** 方式实现模块间回调,与项目管理模块保持一致的架构模式。 + +#### 2.3.1 工单模块回调用户模块的接口 + +```go +// UserStatusUpdater 用户状态更新接口 +// 由 rmdc-core 在初始化时注入,工单模块状态变更时调用 +type UserStatusUpdater interface { + // UpdateUserStatus 更新用户状态(审批通过时激活) + UpdateUserStatus(userID int64, status string) error + + // ActivateUser 激活用户(注册审批通过) + ActivateUser(userID int64) error + + // DeletePendingUser 删除待审批用户(工单撤销时) + DeletePendingUser(userID int64) error + + // ExecuteUserManagement 执行用户管理操作(管理审批通过) + ExecuteUserManagement(userID int64, action string, payload map[string]interface{}) error +} +``` + +#### 2.3.2 用户模块调用工单模块的接口 + +```go +// WorkflowCreator 工单创建接口 +// 由 rmdc-core 在初始化时注入,用户模块通过此接口创建工单 +type WorkflowCreator interface { + // CreateRegistrationWorkflow 创建用户注册工单 + CreateRegistrationWorkflow(ctx context.Context, req *RegistrationWorkflowRequest) (string, error) + + // CreateManagementWorkflow 创建用户管理工单 + CreateManagementWorkflow(ctx context.Context, req *ManagementWorkflowRequest) (string, error) +} +``` + +### 2.4 技术栈 +- Gin (HTTP 路由) +- GORM (ORM,依赖 `models.DatabaseConnections` 提供多库连接:User/Jenkins/Core/Workflow) +- JWT (HS256) +- RSA-OAEP (2048) 前端密码加密,密钥 30 天轮换 +- bcrypt (密码存储) +- 内部模块依赖:`rmdc-work-procedure`(工单)、`rmdc-jenkins-branch-dac`(Jenkins 资源)、`rmdc-common`(日志、返回码、模型) + +### 2.5 路由与中间件分层 +- `/api/auth/*`:免 token,含获取 RSA 公钥、登录、注册。 +- `/api/users`、`/api/user`、`/api/permissions`:需 `AuthMiddleware(jwtSecret)` 校验 JWT,部分再叠加 `RequireAdmin()`。 +- 中间件: + - `AuthMiddleware`:解析 Bearer JWT,校验签名和用户状态为 `active`,注入用户上下文。 + - `RequireAdmin`:判定角色包含 `admin` 字样(superadmin/admin)。 + +### 2.6 组件关系 +- Handler 层:`auth_handler`、`user_handler`、`permission_handler` 注册 HTTP 路由。 +- Service 层:`AuthService`、`RSAService`、`UserService`、`JenkinsPermissionService`、`SystemConfigService`、注册/管理工单服务。 +- DAO 层:`UserDao`、`RSAKeypairDao`、`JenkinsPermissionDao`、`BasePermissionDao`、`SystemConfigDao`、`JenkinsDao`。 +- 公共接口:`pkg/permission.PermissionChecker` 对外暴露权限检查,供其他模块调用。 + +--- + +## 3. 认证与登录设计 + +### 3.1 登录流程(RSA + JWT) +1. 前端调用 `GET /api/auth/rsa/public-key` 获取当前有效公钥(30 天有效,过期自动轮换)。 +2. 前端用 RSA-OAEP(SHA-256, 2048) 加密密码,提交 `POST /api/auth/login`,字段 `encrypted_password`。 +3. 后端 `RSAService` 解密,`AuthService` 用 bcrypt 校验 `users.password_hash`。 +4. 生成 HS256 JWT,默认有效期 4h,Claims 包含用户基础信息与 `status`。 +5. 返回 Token 与用户信息,并返回以下标识: + - `must_change_password`: 首次登录或密码重置后需强制修改密码 + - `password_expire_days`: 密码剩余有效天数(7 天内提示) + - `account_expire_days`: 账户剩余有效天数(7 天内提示) + +### 3.2 密钥与密码策略 +- RSA 密钥:`rsa_keypairs` 表存储 PEM,对外只暴露公钥;过期自动生成新对,异步清理过期数据。 +- 密码哈希:bcrypt 默认成本;密码有效期 3 个月(注册/重置/直改都会刷新过期时间)。 +- 账户状态:active/locked/disabled;JWT 校验时必须 active。 +- 首次登录强制改密:新注册用户或重置密码后,`must_change_password` 标识为 true。 +- 登录失败计数:字段存在,逻辑留有 TODO(未实现锁定策略)。 + +### 3.3 JWT 中间件行为 +- 解析 Authorization: Bearer ,校验签名与过期。 +- 校验 `claims.Status == "active"`,否则 401。 +- 注入上下文键:`user_id/username/english_name/phone/group_name/role/dev_role/status`。 + +### 3.4 缺省/限制 +- 无刷新接口;Token 过期需重新登录。 +- 无服务器端注销接口(前端丢弃 Token)。 + +--- + +## 4. 用户生命周期管理 + +### 4.1 用户状态定义 + +| 状态 | 说明 | 触发条件 | +|:---|:---|:---| +| `disabled` | 待审批状态 | 用户注册后默认状态 | +| `active` | 正常激活 | 注册工单审批通过 | +| `locked` | 临时锁定 | 登录失败次数过多 / 管理员手动锁定 | + +### 4.2 用户生命周期状态机 + +```mermaid +stateDiagram-v2 + [*] --> disabled: 用户注册 + + disabled --> disabled: 保存草稿 + disabled --> disabled: 工单打回,等待修改 + disabled --> active: 注册工单审批通过 + disabled --> [*]: 工单撤销,删除用户 + + active --> active: 正常使用 + active --> active: 信息更新 + active --> locked: 登录失败锁定 + active --> disabled: 管理员禁用 + active --> [*]: 管理员删除 + + locked --> active: 解锁 + locked --> disabled: 管理员禁用 + + note right of disabled: 用户刚注册,等待审批 + note right of active: 正常使用状态 + note right of locked: 临时锁定,可解锁 +``` + +### 4.3 用户有效期机制 + +#### 4.3.1 有效期设置规则 + +| 用户类型 | 有效期选项 | 默认值 | 说明 | +|:---|:---|:---|:---| +| SuperAdmin 创建的用户 | 无限制 / 自定义 | 永久 | SuperAdmin 可设置任意有效期 | +| Admin 创建的用户 | 1个月/3个月/6个月/1年 | 3个月 | 必须设置有效期 | +| Normal 创建的用户 | 1个月/3个月/6个月/1年 | 3个月 | 必须设置有效期 | + +#### 4.3.2 有效期字段设计 + +```go +// 用户表新增字段 +AccountExpiresAt *time.Time `json:"account_expires_at"` // 账户有效期 +``` + +#### 4.3.3 有效期检查逻辑 + +1. **登录时检查**:若 `account_expires_at` 不为空且已过期,拒绝登录并返回账户已过期提示 +2. **接口检查**:JWT 中间件校验用户状态时,同时检查账户有效期 +3. **提前提醒**:账户有效期剩余 7 天内,登录时返回提醒信息 + +### 4.4 强制修改密码机制 + +#### 4.4.1 触发条件 + +| 条件 | 字段标识 | 处理方式 | +|:---|:---|:---| +| 首次登录 | `must_change_password = true` | 登录成功后跳转改密页面 | +| 密码重置后 | `must_change_password = true` | 使用临时密码登录后强制改密 | +| 密码过期 | `password_expires_at` 已过期 | 拒绝登录,提示密码已过期 | + +#### 4.4.2 密码过期时间线 + +```mermaid +flowchart TB + A["创建用户 / 重置密码"] + B["password_expires_at = now + 90天\nmust_change_password = true"] + C["首次登录"] + D["强制修改密码"] + E["修改密码后:\npassword_expires_at = now + 90天\nmust_change_password = false"] + F["正常使用"] + G["第83天:提醒"] + H["第90天:强制过期"] + + A --> B --> C --> D --> E --> F --> G --> H + +``` + +--- + +## 5. 用户注册与管理权限 + +### 5.1 角色与注册/管理权限矩阵 + +| 操作者角色 | 可注册角色 | 可管理范围 | 说明 | +|:---|:---|:---|:---| +| SuperAdmin | superadmin/admin/normal/third | 所有用户 | 完全权限 | +| Admin | normal/third | 自己注册的用户 | 遵循"谁注册谁管理" | +| Normal | third | 自己注册的用户 | 仅可注册三方用户 | +| Third | - | - | 无注册和管理权限 | + +### 5.2 管理操作权限矩阵 + +| 操作 | SuperAdmin | Admin | Normal | Third | +|:---|:---|:---|:---|:---| +| 修改用户信息 | ✅ 所有用户 | ✅ 自己注册的 | ✅ 自己注册的 | ❌ | +| 启用用户 | ✅ 所有用户 | ✅ 自己注册的 | ✅ 自己注册的 | ❌ | +| 禁用用户 | ✅ 所有用户 | ✅ 自己注册的 | ✅ 自己注册的 | ❌ | +| 删除用户 | ✅ 所有用户 | ✅ 自己注册的 | ✅ 自己注册的 | ❌ | +| 重置密码 | ✅ 所有用户 | ✅ 自己注册的 | ❌ | ❌ | +| 延长有效期 | ✅ 所有用户 | ❌ | ❌ | ❌ | + +### 5.3 "谁注册谁管理"原则 + +1. **注册关系记录**:用户表中 `registered_by_id` 记录注册人 ID +2. **权限检查规则**: + - SuperAdmin:可管理所有用户 + - Admin/Normal:只能管理 `registered_by_id == current_user_id` 的用户 +3. **审批归属**:所有用户注册/管理工单由 SuperAdmin 审批 + +--- + +## 6. 用户注册工单流程 + +### 6.1 工单流程概述 + +```mermaid +sequenceDiagram + participant User as 注册发起人 + participant UA as rmdc-user-auth + participant WP as rmdc-work-procedure + participant SA as SuperAdmin + + User->>UA: POST /api/users (创建用户) + UA->>UA: 创建用户记录
(status=disabled, 默认密码) + UA->>WP: CreateWorkflow(user_registration) + WP-->>UA: workflow_id + UA-->>User: 返回成功,工单ID + + WP->>SA: 通知:新用户注册待审批 + + alt 审批通过 + SA->>WP: POST /api/workflow/approve + WP->>UA: ActivateUser(user_id) + UA->>UA: status = active + UA-->>WP: success + WP->>User: 通知:用户注册已通过 + else 审批打回 + SA->>WP: POST /api/workflow/return + WP->>User: 通知:请修改用户信息 + User->>UA: PUT /api/users/:id (修改信息) + User->>WP: POST /api/workflow/resubmit + else 撤销 + User->>WP: POST /api/workflow/revoke + WP->>UA: DeletePendingUser(user_id) + UA->>UA: DELETE users WHERE status=disabled + end +``` + +### 6.2 工单状态与用户状态映射 + +| 工单事件 | 工单状态 | 用户状态 | 说明 | +|:---|:---|:---|:---| +| create | pending_review | disabled | 创建用户并提交审核 | +| approve | approved | active | 审核通过,激活用户 | +| return | returned | disabled (保持) | 打回修改,用户状态不变 | +| resubmit | pending_review | disabled (保持) | 重新提交审核 | +| revoke | revoked | (删除) | 撤销工单,删除待审批用户 | + +### 6.3 注册工单业务载荷 + +```go +// RegistrationWorkflowPayload 注册工单业务载荷 +type RegistrationWorkflowPayload struct { + TargetUserID int64 `json:"target_user_id"` // 被注册用户ID + TargetUsername string `json:"target_username"` // 被注册用户名 + TargetRole string `json:"target_role"` // 被注册用户角色 + RegisteredByID int64 `json:"registered_by_id"` // 注册人ID + RegisteredByName string `json:"registered_by_name"` // 注册人姓名 + AccountExpiresAt time.Time `json:"account_expires_at"` // 账户有效期 + RegistrationReason string `json:"registration_reason"` // 注册原因 +} +``` + +### 6.4 注册接口设计要点 + +1. **不暴露密码设置**:注册接口不接收密码参数,后端自动生成默认密码(如 `Rmdc@2026`) +2. **必须设置有效期**:非 SuperAdmin 创建用户时,必须选择有效期(1个月/3个月/6个月/1年) +3. **自动创建工单**:用户创建接口内部调用工单模块创建审批工单 +4. **工单初始状态**:直接进入 `pending_review`,由 SuperAdmin 审批 + +--- + +## 7. 用户管理工单流程 + +### 7.1 管理操作类型 + +| 操作类型代码 | 操作名称 | 说明 | 需要审批 | +|:---|:---|:---|:---| +| `update` | 修改用户信息 | 修改基本信息(姓名、邮箱等) | 是 | +| `enable` | 启用用户 | 将 disabled 用户改为 active | 是 | +| `disable` | 禁用用户 | 将 active 用户改为 disabled | 是 | +| `delete` | 删除用户 | 软删除用户 | 是 | +| `reset_password` | 重置密码 | 重置为默认密码 | 是 | +| `extend_validity` | 延长有效期 | 延长账户有效期 | 是 | + +### 7.2 工单流程 + +> **重要**:前端不允许直接调用创建工单接口,工单由用户管理接口(如 `PUT /api/users/:id`)内部自动创建。 + +```mermaid +sequenceDiagram + participant Operator as 操作人 + participant UA as rmdc-user-auth + participant WP as rmdc-work-procedure + participant SA as SuperAdmin + + Operator->>UA: PUT /api/users/:id (修改用户信息) + Note right of Operator: 或 POST /api/users/:id/enable|disable|delete + UA->>UA: CheckManagementPermission + UA->>UA: 记录原始数据快照 + UA->>WP: CreateWorkflow(user_management) + WP-->>UA: workflow_id + UA-->>Operator: 返回成功,工单ID + + WP->>SA: 通知:用户管理待审批 + + alt 审批通过 + SA->>WP: POST /api/workflow/approve + WP->>UA: ExecuteUserManagement(action, payload) + UA->>UA: 执行对应操作(update/enable/disable/delete) + UA-->>WP: success + WP->>Operator: 通知:管理操作已通过 + else 审批打回 + SA->>WP: POST /api/workflow/return + WP->>Operator: 通知:请重新提交 + end +``` + +### 7.3 管理工单业务载荷 + +```go +// ManagementWorkflowPayload 管理工单业务载荷 +type ManagementWorkflowPayload struct { + TargetUserID int64 `json:"target_user_id"` // 目标用户ID + TargetUsername string `json:"target_username"` // 目标用户名 + ActionType string `json:"action_type"` // 操作类型 + OperatorID int64 `json:"operator_id"` // 操作人ID + OperatorName string `json:"operator_name"` // 操作人姓名 + OriginalData map[string]interface{} `json:"original_data"` // 原始数据快照 + ModifiedData map[string]interface{} `json:"modified_data"` // 修改后数据 + Reason string `json:"reason"` // 操作原因 +} +``` + +### 7.4 管理操作执行器 + +```go +// UserManagementExecutor 用户管理执行器 +type UserManagementExecutor interface { + // UpdateUserInfo 更新用户信息 + UpdateUserInfo(userID int64, payload map[string]interface{}) error + + // EnableUser 启用用户 + EnableUser(userID int64) error + + // DisableUser 禁用用户 + DisableUser(userID int64) error + + // DeleteUser 删除用户 + DeleteUser(userID int64) error + + // ResetPassword 重置密码 + ResetPassword(userID int64) error + + // ExtendValidity 延长有效期 + ExtendValidity(userID int64, newExpiresAt time.Time) error +} +``` + +--- + +## 8. 权限模型 + +### 8.1 统一权限架构 + +RMDC 系统采用**专用权限表**的设计模式,针对不同业务场景使用独立的权限表结构,以满足复杂的权限控制需求。同时定义统一的权限模块枚举,便于扩展和维护。 + +#### 8.1.1 权限模块枚举 (PermissionModule) + +```go +// PermissionModule 权限模块枚举 +type PermissionModule string + +const ( + // 用户模块权限 + ModuleUserRegister PermissionModule = "user_register" // 用户注册 + ModuleUserManage PermissionModule = "user_manage" // 用户管理 + ModuleUserPermission PermissionModule = "user_permission" // 用户权限管理 + + // 项目模块权限 + ModuleProjectCreate PermissionModule = "project_create" // 项目创建 + ModuleProjectView PermissionModule = "project_view" // 项目查看 + ModuleProjectEdit PermissionModule = "project_edit" // 项目编辑 + ModuleProjectExport PermissionModule = "project_export" // 项目导出 + ModuleProjectAuth PermissionModule = "project_auth" // 项目授权管理 + + // Jenkins 模块权限 + ModuleJenkinsView PermissionModule = "jenkins_view" // Jenkins 查看 + ModuleJenkinsBuild PermissionModule = "jenkins_build" // Jenkins 构建 + ModuleJenkinsManage PermissionModule = "jenkins_manage" // Jenkins 权限管理 + + // 微服务更新模块权限 + ModuleDeliveryView PermissionModule = "delivery_view" // 微服务查看 + ModuleDeliveryUpdate PermissionModule = "delivery_update" // 微服务更新 + + // 工单模块权限 + ModuleWorkflowView PermissionModule = "workflow_view" // 工单查看 + ModuleWorkflowApprove PermissionModule = "workflow_approve" // 工单审批 +) + +// PermissionModuleInfo 模块信息 +type PermissionModuleInfo struct { + Code PermissionModule `json:"code"` // 模块代码 + Name string `json:"name"` // 模块名称 + Description string `json:"description"` // 模块描述 + ACLTable string `json:"acl_table"` // 关联的ACL表 +} + +// 模块注册表 +var PermissionModules = map[PermissionModule]PermissionModuleInfo{ + ModuleJenkinsView: {Code: ModuleJenkinsView, Name: "Jenkins查看", ACLTable: "jenkins_acls"}, + ModuleJenkinsBuild: {Code: ModuleJenkinsBuild, Name: "Jenkins构建", ACLTable: "jenkins_acls"}, + ModuleProjectView: {Code: ModuleProjectView, Name: "项目查看", ACLTable: "project_acls"}, + ModuleProjectEdit: {Code: ModuleProjectEdit, Name: "项目编辑", ACLTable: "project_acls"}, + // ... 其他模块 +} +``` + +#### 8.1.2 权限表概览 + +| 权限类型 | 权限表 | 权限粒度 | 说明 | +|:---|:---|:---|:---| +| **Jenkins 权限** | `jenkins_acls` | Org/Repo/Branch 层级 | 支持层级继承的 CI/CD 权限 | +| **项目权限** | `project_acls` | 项目模块级 | 项目信息访问权限 | +| **用户权限缓存** | `user_permission_caches` | 用户维度 | 用户所有权限的 L2 缓存树 | + +#### 8.1.3 权限检查通用规则 + +```go +// 权限检查通用规则: +// 1. SuperAdmin 拥有所有权限,直接放行 +// 2. 根据 PermissionModule 分流到对应的 ACL 表进行检查 +// 3. 权限检查结果存入 L1 内存缓存和 L2 DB 缓存,提升性能 +// 4. 权限变更时同时清除 L1 和 L2 缓存 +``` + +### 8.2 Jenkins 层级权限 (jenkins_acls) + +#### 8.2.1 层级结构 + +``` +Organization (组织) + └── Repository (仓库) + └── Branch (分支) +``` + +#### 8.2.2 设计原则 +- **层级继承**:上级权限可覆盖下级(如 Org 级权限覆盖其下所有 Repo/Branch) +- **存储最小化**:一条记录可覆盖子层级,减少数据冗余 +- **权限类型**:`can_view`(查看,对应 `jenkins_view`)、`can_build`(构建,对应 `jenkins_build`) + +#### 8.2.3 权限表设计 (jenkins_acls) + +| 字段 | 说明 | +|:---|:---| +| `user_id` | 用户 ID | +| `organization_folder` | 组织文件夹(必填) | +| `repository_name` | 仓库名称(可空,空表示 Org 级权限) | +| `branch_name` | 分支名称(可空,空表示 Repo 级权限) | +| `permission_level` | 权限层级:org/repo/branch | +| `can_view` | 是否可查看 | +| `can_build` | 是否可构建 | +| `granted_by` | 授权人 ID | +| `granted_at` | 授权时间 | + +#### 8.2.4 权限缓存机制 + +- **L1 内存缓存**:`permissionCache`,进程内高速缓存 +- **L2 DB 缓存**:`user_permission_caches` 表,存储用户所有权限的缓存树 JSON +- **懒加载接口**:按 organizations → repositories → branches 逐级加载 +- **缓存失效**:权限变更时同时清除 L1 和 L2 缓存 + +#### 8.2.5 权限检查逻辑 + +```go +// CheckHierarchicalPermission 检查层级权限 +// 1. 先检查 Branch 级权限 +// 2. 若无则检查 Repo 级权限 +// 3. 若无则检查 Org 级权限 +// 4. 支持 can_build 需求判断(build 需要 view + build 两个权限) +``` + +### 8.3 BusinessInfoRegistry 注册中心 + +权限模块需要查询业务模块信息(如项目填写人),但不应直接依赖具体业务模块。采用**统一注册机制**解决模块依赖。 + +```mermaid +graph TB + subgraph rmdc_core["rmdc-core (入口层)"] + INIT[模块初始化] + end + + subgraph rmdc_user_auth["rmdc-user-auth (权限层)"] + REG[BusinessInfoRegistry
业务信息注册中心] + PS[ProjectPermissionService] + JS[JenkinsPermissionService] + end + + subgraph business["业务模块层"] + PM[rmdc-project-management] + JB[rmdc-jenkins-branch-dac] + end + + INIT -->|注册业务查询器| REG + INIT -->|注入权限检查器| PM + INIT -->|注入权限检查器| JB + + PS --> REG + JS --> REG + REG -.->|查询业务信息| PM + REG -.->|查询Jenkins信息| JB +``` + +#### 8.3.1 注册接口定义 + +```go +// BusinessInfoQuerier 业务信息查询接口(通用基类) +type BusinessInfoQuerier interface { + GetModuleCode() string +} + +// ProjectInfoQuerier 项目信息查询接口 +type ProjectInfoQuerier interface { + BusinessInfoQuerier + GetProjectFillerID(ctx context.Context, projectID string) (int64, error) +} + +// JenkinsInfoQuerier Jenkins 信息查询接口 +type JenkinsInfoQuerier interface { + BusinessInfoQuerier + GetOrganizations(ctx context.Context) ([]string, error) + GetRepositories(ctx context.Context, org string) ([]string, error) + GetBranches(ctx context.Context, org, repo string) ([]string, error) +} + +// ModulePermissionChecker 权限检查器接口(通用) +type ModulePermissionChecker interface { + CheckPermission(ctx context.Context, userID int64, userRole string, + resourceID, resourceType, permissionType string) (bool, error) + GetAccessibleResourceIDs(ctx context.Context, userID int64, userRole string, + resourceType string) ([]string, error) +} +``` + +### 8.4 项目权限(ProjectACL) + +#### 8.4.1 设计原则 +- 权限粒度:**模块级**(basic_info/business_info/environment_info/middleware_info/authorization_info) +- SuperAdmin 默认拥有所有权限,无需存储 ACL 记录 +- 项目填写人自动获得非授权模块的 view 权限 + +#### 8.4.2 模块代码与 JSONB 映射 + +| 模块代码 | JSONB 字段 | 说明 | +|:---|:---|:---| +| `basic_info` | `projects.basic_info` | 省份、城市、联系人等 | +| `business_info` | `projects.deploy_business` | 部署人、版本、入口等 | +| `environment_info` | `projects.deploy_env` | 主机、网络、管理方式等 | +| `middleware_info` | `projects.deploy_middleware` | MySQL/Redis/EMQX 等 | +| `authorization_info` | `project_auth_configs.*` | TOTP 授权(仅 SuperAdmin) | + +#### 8.4.3 权限检查规则 + +```go +// CheckProjectModulePermission 规则: +// 1. SuperAdmin 拥有所有权限 +// 2. authorization_info 模块仅 SuperAdmin 可访问 +// 3. 项目填写人自动拥有非授权模块的 view 权限 +// 4. 其他用户查询 project_acls 表 +``` + +--- + +## 9. 数据模型 + +### 9.1 核心表概览 +| 表 | 作用 | 关键字段 | +|:---|:---|:---| +| `users` | 账户信息 | username, english_username, password_hash, role, status, registered_by_id, password_expires_at, account_expires_at, must_change_password, failed_login_attempts, locked_until | +| `rsa_keypairs` | RSA 密钥对 | public_key/ private_key (PEM), expires_at | +| `jenkins_acls` | Jenkins 层级权限 | user_id, organization_folder, repository_name?, branch_name?, permission_level, can_view, can_build, granted_by | +| `project_acls` | 项目模块级权限 | user_id, project_id, module_code, can_view, can_export, granted_by | +| `user_permission_caches` | 用户权限 L2 缓存 | user_id, permission_tree (JSON), updated_at | +| `system_configs` | 系统配置 | key, value, description | + +### 9.2 用户表 DDL (users) + +```go +// User 用户表 +type User struct { + ID int64 `gorm:"primaryKey;autoIncrement" json:"id"` + Username string `gorm:"uniqueIndex;not null;size:50" json:"username"` // 中文真实姓名 + EnglishUsername string `gorm:"size:100" json:"english_username"` // 英文用户名(昵称) + PasswordHash string `gorm:"not null;size:255" json:"-"` // 加密后的密码 + AvatarID string `gorm:"size:50;default:'default_1'" json:"avatar_id"` // 头像ID + AvatarFrameID string `gorm:"size:50;default:'default'" json:"avatar_frame_id"` // 头像框ID + Gender string `gorm:"size:10;default:'male'" json:"gender"` // 性别: male/female + Email string `gorm:"uniqueIndex;size:100" json:"email"` // 邮箱 + Phone string `gorm:"size:20" json:"phone"` // 手机号 + ShortNumber string `gorm:"size:10" json:"short_number"` // 短号 + WorkID string `gorm:"size:50" json:"work_id"` // 工号 + GroupName string `gorm:"size:100" json:"group_name"` // 所属小组 + Company string `gorm:"size:100" json:"company"` // 公司名称 + DevRole string `gorm:"size:50;default:'unknown'" json:"dev_role"` // 开发角色 + + // 注册关系 + RegisteredByID int64 `json:"registered_by_id"` // 注册人ID + RegisteredByName string `gorm:"size:64" json:"registered_by_name"` // 注册人姓名(冗余) + + // 角色与状态 + Role string `gorm:"not null;size:20;default:'normal'" json:"role"` // 系统角色 + Status string `gorm:"not null;size:20;default:'disabled'" json:"status"` // 状态 + + // 密码策略 + PasswordExpiresAt *time.Time `json:"password_expires_at"` // 密码过期时间 + MustChangePassword bool `gorm:"default:true" json:"must_change_password"` // 是否需要强制改密 + + // 账户有效期(新增) + AccountExpiresAt *time.Time `json:"account_expires_at"` // 账户有效期 + + // 登录锁定 + FailedLoginAttempts int `gorm:"default:0" json:"failed_login_attempts"` + LockedUntil *time.Time `json:"locked_until"` + + // MFA + MFASecret string `gorm:"size:100" json:"-"` + + // 审计字段 + CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"` + UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"` + DeletedAt *time.Time `gorm:"index" json:"deleted_at,omitempty"` + LastLoginAt *time.Time `json:"last_login_at"` +} +``` + +### 9.3 用户表新增字段说明 + +| 字段 | 类型 | 说明 | 默认值 | +|:---|:---|:---|:---| +| `registered_by_name` | varchar(64) | 注册人姓名(冗余存储,便于展示) | NULL | +| `must_change_password` | bool | 是否需要强制修改密码 | true | +| `account_expires_at` | datetime | 账户有效期,NULL表示永久有效 | NULL | + +### 9.4 RSA 密钥表(rsa_keypairs) +- 存储 PEM 文本及过期时间;`RSAService` 负责加载、轮换、缓存与过期清理。 + +### 9.5 Jenkins 权限表(jenkins_acls) +- 层级列可空实现覆盖:org=不空,repo/branch 为空;repo 级:branch 空;branch 级:三列全填。 +- `permission_level` 归档实际层级,便于查询。 +- 详见 [8.2 Jenkins 层级权限](#82-jenkins-层级权限-jenkins_acls)。 + +### 9.6 项目权限表(project_acls) +- 模块级权限控制:user_id + project_id + module_code 组合唯一。 +- 权限类型:can_view(查看)、can_export(导出)。 +- 索引:`idx_project_acl_user`(用户维度)、`idx_project_acl_project`(项目+模块维度)。 +- SuperAdmin 不存储记录,权限检查时直接放行。 + +--- + +## 10. 接口设计 (API) + +### 10.1 认证 +| 方法 | 路径 | 说明 | 鉴权 | +|:---|:---|:---|:---| +| GET | `/api/auth/rsa/public-key` | 获取 RSA 公钥 | 公共 | +| POST | `/api/auth/login` | RSA 加密密码登录,返回 JWT | 公共 | +| POST | `/api/auth/register` | 自助注册(创建 disabled 用户) | 公共 | + +### 10.2 用户管理 +| 方法 | 路径 | 说明 | 鉴权 | +|:---|:---|:---|:---| +| GET | `/api/users` | 列表,支持角色/状态/组/搜索/Scope | JWT | +| GET | `/api/users/:id` | 用户详情 + 权限 | JWT | +| POST | `/api/users` | 创建用户(自动创建注册工单) | JWT + Admin | +| PUT | `/api/users/:id` | 更新用户(仅用于保存草稿,正式修改需工单) | JWT + Admin | +| DELETE | `/api/users/:id` | 删除用户(仅SuperAdmin可直接删除) | JWT + SuperAdmin | +| PUT | `/api/user/profile` | 更新本人资料(头像/密码) | JWT | +| PUT | `/api/user/password` | 本人改密(校验旧密码) | JWT | +| PUT | `/api/user/password/force-change` | 首次登录强制改密 | JWT | + +### 10.3 权限(Jenkins) +| 方法 | 路径 | 说明 | 鉴权 | +|:---|:---|:---|:---| +| GET | `/api/permissions/jenkins/my-tree/organizations` | 我的组织列表 | JWT | +| POST | `/api/permissions/jenkins/my-tree/repositories` | 我的仓库(按 org) | JWT | +| POST | `/api/permissions/jenkins/my-tree/branches` | 我的分支(按 org/repo) | JWT | +| GET | `/api/permissions/jenkins/my-tree/full` | 我的权限树(缓存/重建) | JWT | +| GET | `/api/permissions/jenkins/check/:organization/:branch` | 检查分支权限 | JWT | +| GET | `/api/permissions/jenkins/tree` | 全量权限树(Admin) | JWT + Admin | +| GET | `/api/permissions/jenkins/users/role/:role` | 按角色查用户 | JWT + Admin | +| GET | `/api/permissions/jenkins/:userId` | 获取用户 Jenkins 权限 | JWT + Admin | +| POST | `/api/permissions/jenkins/assign` | 分配权限(层级覆盖) | JWT + Admin | +| POST | `/api/permissions/jenkins/copy` | 拷贝权限 | JWT + Admin | +| GET | `/api/permissions/jenkins/user-tree/:userId/organizations` | 懒加载组织 | JWT + Admin | +| GET | `/api/permissions/jenkins/user-tree/:userId/organizations/:org/repositories` | 懒加载仓库 | JWT + Admin | +| GET | `/api/permissions/jenkins/user-tree/:userId/organizations/:org/repositories/:repo/branches` | 懒加载分支 | JWT + Admin | + +### 10.4 项目权限接口 +| 方法 | 路径 | 说明 | 鉴权 | +|:---|:---|:---|:---| +| GET | `/api/permissions/projects/user/:userId/summary` | 获取用户项目权限摘要 | JWT + Admin | +| POST | `/api/permissions/projects/grant` | 授予项目模块权限 | JWT + Admin | +| POST | `/api/permissions/projects/revoke` | 撤销项目模块权限 | JWT + Admin | + +> **注意**:前端不允许直接调用创建工单接口,用户注册/管理工单通过用户管理接口(如 `POST /api/users`、`PUT /api/users/:id`)内部自动创建。 + +### 10.5 系统配置 +| 方法 | 路径 | 说明 | 鉴权 | +|:---|:---|:---|:---| +| GET | `/api/user/system-config` | 获取配置(为空时返回默认值) | JWT | +| PUT | `/api/user/system-config` | 更新配置 | JWT | + +--- + +## 11. 业务流程 + +### 11.1 登录流程 +```mermaid +sequenceDiagram + participant FE as 前端 + participant UA as rmdc-user-auth + + FE->>UA: GET /api/auth/rsa/public-key + UA-->>FE: 公钥 + FE->>UA: POST /api/auth/login (encrypted_password) + UA->>UA: RSA 解密 + bcrypt 校验 + UA->>UA: 检查账户状态/有效期/密码过期 + UA->>UA: 生成 JWT(4h) + UA-->>FE: token + userDTO + 附加标识 + + Note over FE,UA: 附加标识:
must_change_password
password_expire_days
account_expire_days + + alt must_change_password = true + FE->>FE: 跳转强制改密页面 + FE->>UA: PUT /api/user/password/force-change + UA->>UA: 更新密码,重置过期时间 + UA-->>FE: 改密成功 + end +``` + +### 11.2 用户注册工单 +```mermaid +sequenceDiagram + participant User as 已登录用户 + participant UA as rmdc-user-auth + participant WP as rmdc-work-procedure + participant SA as SuperAdmin + + User->>UA: POST /api/users (创建用户) + UA->>UA: 权限检查:可注册该角色? + UA->>UA: 创建用户(status=disabled, 默认密码) + UA->>UA: 设置 account_expires_at + UA->>UA: 设置 must_change_password = true + UA->>WP: CreateWorkflow(user_registration, pending_review) + WP-->>UA: workflow_id + UA-->>User: 返回成功 + + WP->>SA: 通知待审批 + + alt 审批通过 + SA->>WP: approve + WP->>UA: ActivateUser + UA->>UA: status = active + else 打回 + SA->>WP: return + WP->>User: 通知修改 + User->>UA: PUT /api/users/:id + User->>WP: resubmit + else 撤销 + User->>WP: revoke + WP->>UA: DeletePendingUser + UA->>UA: DELETE (status=disabled) + end +``` + +### 11.3 用户管理工单 + +> **重要**:前端不允许直接调用创建工单接口,工单由用户管理接口内部自动创建。 + +```mermaid +sequenceDiagram + participant Operator as 操作人 + participant UA as rmdc-user-auth + participant WP as rmdc-work-procedure + participant SA as SuperAdmin + + Operator->>UA: PUT /api/users/:id (修改用户) + Note right of Operator: 或 POST /api/users/:id/enable|disable|delete + UA->>UA: CheckManagementPermission + UA->>UA: 记录原始数据快照 + UA->>WP: CreateWorkflow(user_management) + WP-->>UA: workflow_id + UA-->>Operator: 返回成功 + + WP->>SA: 通知待审批 + + alt 审批通过 + SA->>WP: approve + WP->>UA: ExecuteUserManagement + UA->>UA: 执行操作(update/enable/disable/delete) + else 打回 + SA->>WP: return + WP->>Operator: 通知修改后重新提交 + end +``` + +--- + +## 12. 安全与合规 + +- **传输安全**:登录密码必须使用 RSA-OAEP(SHA-256, 2048) 加密;禁止明文密码传输。 +- **存储安全**:密码使用 bcrypt;RSA 私钥仅存 DB,不下发前端;敏感字段不外露。 +- **账户状态**:仅 active 用户可通过 JWT 校验;locked/disabled 一律拒绝。 +- **密码策略**: + - 默认 3 个月过期;修改/创建均刷新过期时间 + - 注册返回 disabled,需审批激活 + - 首次登录或密码重置后强制修改密码 +- **账户有效期**:非 SuperAdmin 创建的用户必须设置有效期,过期后无法登录 +- **Token**:HS256,4h 过期;无刷新,需重登;超时后自动失效。 +- **权限检查**:SuperAdmin 全通;Admin 只能管理/授权 normal/third;权限分配需校验授予者是否具备权限。 +- **"谁注册谁管理"**:非 SuperAdmin 只能管理自己注册的用户 +- **TODO**:登录失败次数与锁定策略尚未实现;Watchdog 权限检查待补全。 + +--- + +## 13. 相关文档 + +| 文档 | 说明 | +|:---|:---| +| [用户认证PRD](1-user-auth-PRD.md) | 产品需求文档 | +| [用户权限设计](权限设计部分/3-用户部分-权限设计.md) | 用户注册管理权限设计 | +| [项目管理DDS](../4-rmdc-project-management/2-rmdc-project-management-DDS.md) | 参考文档结构(项目工单流程) | +| [工单模块DDS](../7-rmdc-work-procedure/1-rmdc-work-procedure-DDS.md) | 工单流程设计 |