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