更新rmdc-project-management的SKILL

This commit is contained in:
zeaslity
2026-01-22 11:58:03 +08:00
parent 0d511d9a03
commit 93624efdab
4 changed files with 1446 additions and 72 deletions

View File

@@ -1,7 +1,7 @@
---
name: developing-project-management
description: Guides development of rmdc-project-management module including project lifecycle management, version control (Git-like), ACL permissions, TOTP authorization, and workflow integration. Triggered when modifying project CRUD, draft/version APIs, permission grants, or authorization features. Keywords: project mangement, project lifecycle, version snapshot, ACL, TOTP, workflow callback, SuperAdmin.
argument-hint: "<change-type> [target]" where change-type is one of: api|entity|service|migration|frontend|auth. Example: "api draft-submit" or "migration add-field"
description: Guides development of rmdc-project-management module including project lifecycle management, Git-like version control with snapshot/diff, ACL permissions, TOTP authorization, and workflow integration. Triggered when modifying project CRUD, draft/version APIs, permission grants, or authorization features. Keywords: project lifecycle, version snapshot, diff algorithm, ACL, TOTP, workflow callback, SuperAdmin, optimistic lock.
argument-hint: "<change-type> [target]" where change-type is one of: api|entity|service|migration|frontend|auth|version. Example: "api draft-submit" or "version diff-algorithm"
allowed-tools:
- Read
- Glob
@@ -17,9 +17,10 @@ allowed-tools:
## 模块定位
- **核心职责**: 项目 CRUD、版本控制(Git-like)、细粒度 ACL 权限、一级 TOTP 授权
- **核心职责**: 项目 CRUD、Git-like 版本控制、细粒度 ACL 权限、一级 TOTP 授权
- **技术栈**: Go + Gin + GORM + PostgreSQL (JSONB)
- **架构**: 模块化单体,通过接口注入与 `rmdc-work-procedure` 工单模块协作
- **版本控制思想**: 类似 Git 的分支管理Master 主线 + 用户草稿分支)
## 动态上下文注入
@@ -29,8 +30,8 @@ allowed-tools:
# 查看项目管理模块目录结构
!`find . -path "*/rmdc-project-management/*" -name "*.go" | head -20`
# 查找生命周期状态相关代码
!`grep -rn "lifecycle_status\|LifecycleStatus" --include="*.go" | head -15`
# 查找版本控制相关代码
!`grep -rn "VersionSnapshot\|CompareVersions\|DiffResult" --include="*.go" | head -15`
```
---
@@ -41,32 +42,23 @@ allowed-tools:
根据 `$ARGUMENTS` 确定变更范围:
| 变更类型 | 产物文件 | 影响模块 |
|:---|:---|:---|
| `api` | `handler/*.go`, `router.go` | rmdc-core 路由注册 |
| `entity` | `entity/*.go` | 数据库迁移、DTO 映射 |
| `service` | `service/*.go` | 业务逻辑、版本快照 |
| `migration` | `migrations/*.sql` | 数据库 Schema |
| `frontend` | `pages/*.vue`, `components/*.vue` | 前端联调 |
| `auth` | `service/auth_*.go` | TOTP 授权、Exchange-Hub 交互 |
| 变更类型 | 产物文件 | 影响模块 | 参考文档 |
|:---|:---|:---|:---|
| `api` | `handler/*.go`, `router.go` | rmdc-core 路由注册 | `reference/api-endpoints.md` |
| `entity` | `entity/*.go` | 数据库迁移、DTO 映射 | `reference/data-structures.md` |
| `service` | `service/*.go` | 业务逻辑、版本快照 | `reference/version-control-design.md` |
| `migration` | `migrations/*.sql` | 数据库 Schema | `reference/database-schema.md` |
| `frontend` | `pages/*.vue`, `components/*.vue` | 前端联调 | `reference/frontend-design.md` |
| `auth` | `service/auth_*.go` | TOTP 授权、Exchange-Hub 交互 | `reference/acl-permission-model.md` |
| `version` | `service/version_*.go` | 版本快照、Diff 算法 | `reference/version-control-design.md` |
### 决策点
1. **是否涉及生命周期状态变更?**
- 若涉及,必须同步更新状态机转换逻辑
- 检查 `reference/lifecycle-state-machine.md`
2. **是否修改版本快照结构?**
- 若涉及,需评估历史版本兼容性
- 更新 `VersionSnapshot` 结构体
3. **是否变更 ACL 权限模型?**
- 若涉及,需同步 `rmdc-user-auth` 模块
- 检查 `reference/acl-permission-model.md`
4. **是否影响工单模块回调?**
- 若涉及,需更新 `ProjectLifecycleUpdater` 接口实现
- 检查 `reference/workflow-state-mapping.md`
1. **是否涉及生命周期状态变更?** → 检查 `reference/lifecycle-state-machine.md`
2. **是否修改版本快照结构?** → 检查 `reference/version-control-design.md` 第5节
3. **是否涉及并发修改冲突?** → 检查乐观锁实现base_version 校验)
4. **是否变更 ACL 权限模型?** → 检查 `reference/acl-permission-model.md`
5. **是否影响工单模块回调?** → 检查 `reference/workflow-state-mapping.md`
---
@@ -77,20 +69,25 @@ allowed-tools:
- [ ] **生命周期状态机完整性**: 所有状态转换有明确的触发条件和权限控制
- [ ] **版本快照一致性**: `projects` 表与 `project_versions` 表数据同步
- [ ] **乐观锁检查**: 并发修改时 `base_version == current_version` 校验存在
- [ ] **ACL 权限验证**: 接口权限注解与业务逻辑一致
- [ ] **超管直改版本生成**: SuperAdmin 直接修改必须同时生成版本记录(原子事务)
- [ ] **Diff 算法正确性**: 版本对比结果按模块分组,字段路径完整,中文名映射正确
- [ ] **ACL 权限验证**: 接口权限注解与业务逻辑一致,授权模块仅 SuperAdmin 可见
- [ ] **工单回调幂等**: 状态更新操作具备幂等性
- [ ] **敏感字段加密**: 密码字段使用 AES-256 加密存储
- [ ] **审计日志**: 所有写操作记录到 `rmdc-audit-log`
- [ ] **TOTP 授权安全**: 一级密钥仅 SuperAdmin 可访问
- [ ] **Namespace 校验**: 符合 RFC 1123 DNS 标签规范
### 验证命令
```bash
# 检查实体字段与数据库 Schema 一致性
!`grep -rn "gorm:\"" entity/project.go | head -20`
# 检查版本服务实现
!`grep -rn "CompareVersions\|CreateOfficialVersion\|VersionSnapshot" service/*.go`
# 检查 API 路由权限注解
!`grep -rn "RequireRole\|RequirePermission" handler/*.go`
# 检查乐观锁实现
!`grep -rn "base_version\|BaseVersion\|VersionConflict\|409" --include="*.go"`
# 检查敏感字段加密
!`grep -rn "EncryptAES\|DecryptAES\|admin_password\|ssh_pwd" --include="*.go"`
# 运行模块单元测试
go test ./internal/project/... -v -cover
@@ -102,19 +99,26 @@ go test ./internal/project/... -v -cover
### API 开发流程
1. **定义请求/响应结构体**`dto/project_dto.go`
2. **实现 Service 方法**`service/project_service.go`
3. **实现 Handler 方法**`handler/project_handler.go`
4. **注册路由**`router.go` (注意权限中间件)
5. **编写单元测试**`*_test.go`
1. 定义请求/响应结构体 → `dto/project_dto.go`
2. 实现 Service 方法 → `service/project_service.go`
3. 实现 Handler 方法 → `handler/project_handler.go`
4. 注册路由 → `router.go` (注意权限中间件)
5. 编写单元测试 → `*_test.go`
### 版本快照变更流程
1. 更新 `VersionSnapshot` 结构体定义
2. 确保 `CompareVersions` Diff 算法兼容新字段
3. 添加字段到 Diff 结果的字段名映射表
1. 更新 `VersionSnapshot` 结构体定义`reference/data-structures.md`
2. 更新字段名映射表 `fieldNameMap` → 确保 Diff 显示中文名
3. 确保 `CompareVersions` Diff 算法兼容新字段
4. 测试历史版本查看功能不受影响
### SuperAdmin 直改流程
1. 更新 `projects` 表 + 插入 `project_versions` 表**必须在同一事务**
2. `workflow_id` 设为空或 `DIRECT_EDIT` 标识
3. `committer_id` 记录 SuperAdmin ID
4. 更新 `current_version` 字段
### 生命周期状态变更流程
1. 更新 `reference/lifecycle-state-machine.md` 状态图
@@ -122,32 +126,25 @@ go test ./internal/project/... -v -cover
3. 同步更新 `ProjectLifecycleUpdater` 接口实现
4. 验证与工单模块的状态映射表一致
### 授权功能变更流程
1. 检查 `project_auth_configs` 表结构
2. 更新 `AuthorizationInfo` 结构体
3. 确保 TOTP 密钥生成/验证逻辑正确
4. 测试与 Exchange-Hub 的授权指令下发
---
## Pitfalls常见问题
1. **超管直改未生成版本**: SuperAdmin 直接修改 `projects` 表时,必须同时插入 `project_versions` 记录,否则版本链断裂
1. **超管直改未生成版本**: SuperAdmin 直接修改 `projects` 表时,必须同时插入 `project_versions` 记录,否则版本链断裂,后续 Diff 失效
2. **草稿基准版本过期**: 用户 A 基于 v3 创建草稿,超管修改产生 v4用户 A 提交时需检测冲突并提示 Rebase
2. **草稿基准版本过期**: 用户 A 基于 v3 创建草稿,超管修改产生 v4用户 A 提交时需检测冲突`draft.base_version != project.current_version`)并返回 409 Conflict
3. **工单回调重复处理**: 工单模块可能重试回调,`ProjectLifecycleUpdater` 实现必须幂等
4. **ACL 权限遗漏授权模块**: `authorization_info` 模块仅 SuperAdmin 可见,其他角色查询时需过滤
5. **密码字段明文泄露**: `AdminPassword``SSHPwd` 等字段响应时必须脱敏或不返回
5. **密码字段明文泄露**: `AdminPassword``SSHPwd` 等字段响应时必须脱敏(返回 `********`
6. **省市级联校验缺失**: 前端省市级联选择后,后端需校验省市对应关系有效性
6. **Namespace 唯一性**: 创建项目时必须校验 `namespace` 全局唯一且符合 RFC 1123 DNS 标签规范(小写字母开头,只含小写字母/数字/-/.
7. **Namespace 唯一性**: 创建项目时必须校验 `namespace` 全局唯一且符合 RFC 1123 DNS 标签规范
7. **JSONB 字段空值处理**: `basic_info``deploy_business` 等 JSONB 字段为空时,需返回空对象 `{}` 而非 `null`
8. **JSONB 字段空值处理**: `basic_info``deploy_business` 等 JSONB 字段为空时,需返回空对象 `{}` 而非 `null`
8. **版本号混淆**: 草稿版本号为 0正式版本从 1 开始递增,切勿混淆;正式版本必须保证唯一性
---
@@ -156,29 +153,34 @@ go test ./internal/project/... -v -cover
```
rmdc-project-management
├── → rmdc-user-auth (用户鉴权、ACL 权限查询)
├── rmdc-work-procedure (工单创建状态转换)
├── rmdc-work-procedure (工单创建/状态转换 + 回调更新生命周期)
├── → rmdc-audit-log (操作审计记录)
├── → rmdc-exchange-hub (授权指令下发)
└── ← rmdc-core (路由注册、依赖注入)
```
## 关键接口
## 关键接口速查
| 类别 | 路径 | 权限 |
|:---|:---|:---|
| 项目列表 | `POST /api/project/list` | Login |
| 项目详情 | `POST /api/project/detail` | View ACL |
| 创建项目 | `POST /api/project/create` | SuperAdmin |
| 直接更新 | `POST /api/project/update` | SuperAdmin |
| 保存草稿 | `POST /api/project/draft/save` | View ACL |
| 提交审核 | `POST /api/project/draft/submit` | View ACL |
| 版本历史 | `POST /api/project/version/list` | View ACL |
| 权限分配 | `POST /api/project/permission/grant` | SuperAdmin |
| 类别 | 路径 | 权限 | 说明 |
|:---|:---|:---|:---|
| 项目列表 | `POST /api/project/list` | Login | 自动过滤 ACL |
| 项目详情 | `POST /api/project/detail` | View ACL | Master 版本 |
| 创建项目 | `POST /api/project/create` | SuperAdmin | 同时创建填写工单 |
| 直接更新 | `POST /api/project/update` | SuperAdmin | 必须生成新版本 |
| 保存草稿 | `POST /api/project/draft/save` | View ACL | 更新草稿快照 |
| 提交审核 | `POST /api/project/draft/submit` | View ACL | 检测版本冲突 |
| 版本历史 | `POST /api/project/version/list` | View ACL | 仅 official 类型 |
| 版本对比 | `POST /api/project/version/diff` | View ACL | 按模块分组 |
| 权限分配 | `POST /api/project/permission/grant` | SuperAdmin | 模块级权限 |
## 相关文档
- 生命周期状态机: `reference/lifecycle-state-machine.md`
- API 端点清单: `reference/api-endpoints.md`
- 数据库 Schema: `reference/database-schema.md`
- ACL 权限模型: `reference/acl-permission-model.md`
- 工单状态映射: `reference/workflow-state-mapping.md`
| 文档 | 内容 |
|:---|:---|
| `reference/lifecycle-state-machine.md` | 生命周期状态机、状态转换条件 |
| `reference/version-control-design.md` | 版本快照、Diff 算法、乐观锁、冲突检测 |
| `reference/database-schema.md` | DDL、索引、JSONB 结构示例 |
| `reference/data-structures.md` | 实体定义、枚举常量、字段校验规则 |
| `reference/acl-permission-model.md` | RBAC/ACL 权限模型、权限检查流程 |
| `reference/workflow-state-mapping.md` | 工单状态与项目生命周期映射、回调接口 |
| `reference/api-endpoints.md` | API 清单、请求/响应示例 |

View File

@@ -0,0 +1,428 @@
# 数据结构定义
本文档定义项目管理模块中所有核心数据结构包括实体、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("城市不属于所选省份")
}
```

View File

@@ -0,0 +1,496 @@
# 前端页面设计规范
本文档定义项目详情页面的前端设计规范,包括页面架构、组件设计、交互行为和视觉规范。
---
## 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
<!-- 管理员端 -->
<v-tabs v-model="activeTab">
<v-tab value="basic">基本信息</v-tab>
<v-tab value="business">部署业务</v-tab>
<v-tab value="environment">部署环境</v-tab>
<v-tab value="hosts">主机管理</v-tab>
<v-tab value="middlewares">中间件</v-tab>
<v-tab value="authorization" v-if="isSuperAdmin">授权信息</v-tab>
<v-tab value="version-history" v-if="isSuperAdmin">版本历史</v-tab>
</v-tabs>
<!-- 用户端 -->
<v-tabs v-model="activeTab">
<v-tab value="basic">基本信息</v-tab>
<v-tab value="business">部署业务</v-tab>
<v-tab value="environment">部署环境</v-tab>
<v-tab value="middlewares">中间件</v-tab>
</v-tabs>
```
---
## 5. 生命周期状态展示
### 5.1 状态标签配置
```typescript
// 生命周期状态枚举
export const LIFECYCLE_STATUS = {
init: '初始化',
drafting: '填写中',
reviewing: '审核中',
released: '已发布',
modifying: '变更中',
archived: '已归档'
}
// 状态颜色映射
export const LIFECYCLE_STATUS_COLORS: Record<string, string> = {
init: 'grey',
drafting: 'info',
reviewing: 'warning',
released: 'success',
modifying: 'primary',
archived: 'grey-darken-1'
}
// 状态图标
const LIFECYCLE_STATUS_ICONS: Record<string, string> = {
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
<v-menu v-if="multipleWorkflows">
<template v-slot:activator="{ props }">
<v-btn v-bind="props" color="info" variant="tonal">
查看工单 ({{ workflowCount }})
<v-icon end>mdi-chevron-down</v-icon>
</v-btn>
</template>
<v-list>
<v-list-item
v-for="wf in relatedWorkflows"
:key="wf.workflow_id"
@click="navigateToWorkflow(wf.workflow_id)"
>
<v-list-item-title>{{ wf.workflow_id }}</v-list-item-title>
<v-list-item-subtitle>{{ wf.creator_name }} | {{ formatDate(wf.created_at) }}</v-list-item-subtitle>
</v-list-item>
</v-list>
</v-menu>
```
---
## 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<string, string> = {
'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
<template>
<div class="copyable-field d-flex align-center gap-2">
<span class="field-value">{{ displayValue }}</span>
<v-btn icon="mdi-content-copy" size="x-small" variant="text" @click="copyToClipboard">
<v-tooltip activator="parent" location="top">复制</v-tooltip>
</v-btn>
</div>
</template>
```
### 8.2 SaveConfirmDialog - 保存确认
展示变更 Diff 表格:
```html
<v-table density="compact">
<thead>
<tr><th>字段</th><th>修改前</th><th>修改后</th></tr>
</thead>
<tbody>
<tr v-for="item in diffItems" :key="item.field">
<td>{{ item.label }}</td>
<td class="text-error">{{ item.oldValue || '空' }}</td>
<td class="text-success">{{ item.newValue || '空' }}</td>
</tr>
</tbody>
</v-table>
```
### 8.3 DiffTextField - 差异高亮输入框
编辑模式下显示与主线数据的差异:
```html
<v-text-field
v-model="inputValue"
:label="label"
:class="{ 'diff-highlight': hasDiff }"
:hint="hasDiff ? `主线值: ${masterValue}` : ''"
persistent-hint
>
<template v-slot:prepend-inner v-if="hasDiff">
<v-icon color="warning" size="small">mdi-alert-circle</v-icon>
</template>
</v-text-field>
<style scoped>
.diff-highlight :deep(.v-field__outline) {
--v-field-border-color: rgb(var(--v-theme-warning));
}
</style>
```
---
## 9. 视觉设计规范
### 9.1 色彩系统
| 用途 | Vuetify 类 |
|:---|:---|
| 主色调 | `color="primary"` (Deep Purple) |
| 成功状态 | `color="success"` (Green) |
| 警告状态 | `color="warning"` (Orange) |
| 错误状态 | `color="error"` (Red) |
| 页面背景 | `bg-grey-lighten-4` |
### 9.2 卡片设计
```html
<v-card elevation="2" rounded="lg" class="pa-4">
<!-- 圆角 8px, 阴影 level-2, 内边距 16px -->
</v-card>
```
### 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
<v-row>
<v-col v-for="mw in middlewares" :key="mw.type"
cols="12" sm="6" md="4" lg="3"
>
<MiddlewareCard :data="mw" />
</v-col>
</v-row>
```
---
## 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<string, unknown> | 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` | 差异高亮输入框 | 通用 |

View File

@@ -0,0 +1,448 @@
# 版本控制设计 (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 │ │
│ └───────────────────────────────────────────────────────────┘ │
│ │
│ [基本信息] [部署业务] [部署环境] [部署中间件] │
│ ───────────────────────────────────────────────────────────── │
│ 省份: [北京市 ▼] │
│ 城市: [北京市 ▼] │
│ ... │
└─────────────────────────────────────────────────────────────────┘
```