# 版本管理方案

## 🎯 问题

前端每次更新版本，后端也需要手动同步版本信息，容易出错且效率低。

## ✅ 解决方案

### 方案一：自动生成 version.json（已实现）⭐⭐⭐

前端打包时自动生成 `version.json` 文件，后端可以直接读取。

#### 工作流程

```
1. 开发者修改 package.json 中的 version
   ↓
2. 运行 pnpm run build
   ↓
3. Vite 自动读取 package.json 版本
   ↓
4. 构建完成后生成 dist/version.json
   ↓
5. 部署到服务器
   ↓
6. 后端通过 HTTP 请求获取前端版本
```

#### 生成的 version.json 格式

```json
{
  "version": "0.0.1",
  "buildTime": "2025-12-30T10:30:00.000Z",
  "gitCommit": "abc123",
  "environment": "production"
}
```

#### 后端获取版本的方式

**方式 1：直接读取静态文件（推荐）**

如果前端部署在 Nginx/CDN，后端可以直接访问：

```bash
curl https://your-domain.com/version.json
```

后端 API 实现示例：

```typescript
// Node.js / Elysia 示例
app.get("/api/app/version", async (ctx) => {
  // 从前端静态资源读取版本
  const response = await fetch("https://your-frontend-domain.com/version.json");
  const frontendVersion = await response.json();

  const { platform, currentVersion } = ctx.query;

  return {
    version: frontendVersion.version,
    forceUpdate: compareVersion(currentVersion, frontendVersion.version) < 0,
    updateMessage: "修复了一些问题",
    updateUrl: platform === "ios"
      ? "https://apps.apple.com/app/xxx"
      : "https://play.google.com/store/apps/details?id=xxx",
    minSupportVersion: "0.0.1",
  };
});
```

**方式 2：部署时同步到后端**

在 CI/CD 中，部署前端时自动同步 version.json 到后端：

```yaml
# GitHub Actions / GitLab CI 示例
- name: Deploy Frontend
  run: |
    pnpm run build
    # 上传到前端服务器
    rsync -avz dist/ user@frontend-server:/var/www/html/
    # 同时将 version.json 复制到后端
    scp dist/version.json user@backend-server:/app/frontend-version.json
```

后端直接读取本地文件：

```typescript
import fs from "node:fs";

app.get("/api/app/version", async (ctx) => {
  const versionFile = fs.readFileSync("/app/frontend-version.json", "utf-8");
  const { version } = JSON.parse(versionFile);

  return {
    version,
    forceUpdate: false,
    // ...
  };
});
```

---

## 📝 使用步骤

### 1. 修改版本号

只需修改 `package.json` 中的 `version` 字段：

```json
{
  "version": "1.2.3" // 只改这里
}
```

### 2. 构建项目

```bash
pnpm run build
```

构建完成后会在 `dist/` 目录生成 `version.json`：

```
dist/
├── index.html
├── assets/
└── version.json  ← 自动生成
```

### 3. 部署

将整个 `dist/` 目录部署到服务器，`version.json` 会一起部署。

---

## 🔧 原生应用版本同步

### iOS (Xcode)

修改 `ios/App/App/Info.plist`：

```xml
<key>CFBundleShortVersionString</key>
<string>1.2.3</string>  <!-- 与 package.json 保持一致 -->
```

### Android (build.gradle)

修改 `android/app/build.gradle`：

```gradle
versionName "1.2.3"  // 与 package.json 保持一致
```

### 自动化脚本（可选）

创建 `scripts/sync-version.js`：

```javascript
#!/usr/bin/env node
import { execSync } from "node:child_process";
import fs from "node:fs";

const packageJson = JSON.parse(fs.readFileSync("./package.json", "utf-8"));
const version = packageJson.version;

console.log(`Syncing version ${version} to native projects...`);

// iOS
const infoPlist = "./ios/App/App/Info.plist";
let plistContent = fs.readFileSync(infoPlist, "utf-8");
plistContent = plistContent.replace(
  /<key>CFBundleShortVersionString<\/key>\s*<string>.*?<\/string>/,
  `<key>CFBundleShortVersionString</key>\n\t<string>${version}</string>`
);
fs.writeFileSync(infoPlist, plistContent);

// Android (需要安装 gradle 解析库或手动更新)
console.log("Please manually update android/app/build.gradle versionName");

console.log("✓ Version synced!");
```

在 `package.json` 中添加脚本：

```json
{
  "scripts": {
    "version:sync": "node scripts/sync-version.js",
    "build:ios": "npm run version:sync && ionic cap sync ios",
    "build:android": "npm run version:sync && ionic cap sync android"
  }
}
```

---

## 🎨 版本号规范

遵循**语义化版本 (Semantic Versioning)**：

```
主版本号.次版本号.修订号
   ↓        ↓       ↓
  1    .    2   .   3

- 主版本号：不兼容的 API 修改
- 次版本号：向下兼容的功能新增
- 修订号：向下兼容的问题修正
```

### 版本更新示例

- `0.0.1` → `0.0.2` - 修复 bug
- `0.0.2` → `0.1.0` - 新增功能
- `0.1.0` → `1.0.0` - 重大更新

---

## 🚀 CI/CD 集成

### GitHub Actions 示例

```yaml
name: Build and Deploy

on:
  push:
    branches: [main]

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3

      - name: Setup Node.js
        uses: actions/setup-node@v3
        with:
          node-version: "18"

      - name: Install dependencies
        run: pnpm install

      - name: Build
        run: pnpm run build
        env:
          NODE_ENV: production

      - name: Deploy to server
        run: |
          rsync -avz dist/ user@server:/var/www/html/

      - name: Notify backend
        run: |
          curl -X POST https://your-backend.com/api/webhook/frontend-deployed \
            -H "Content-Type: application/json" \
            -d '{"version": "'$(node -p "require('./package.json').version")'"}'
```

---

## 📊 方案对比

| 方案 | 优点 | 缺点 | 推荐度 |
|-----|------|------|--------|
| **自动生成 version.json** | 完全自动化、前后端解耦 | 需要后端额外请求 | ⭐⭐⭐⭐⭐ |
| 手动同步 | 简单直接 | 容易忘记、出错 | ⭐ |
| 共享配置文件 | 统一管理 | 需要 Monorepo | ⭐⭐⭐ |
| 环境变量 | 灵活 | 部署时需手动设置 | ⭐⭐ |

---

## ✅ 总结

1. **前端**：只需修改 `package.json` 的 `version` 字段
2. **构建**：运行 `pnpm run build` 自动生成 `version.json`
3. **后端**：通过 HTTP 请求或读取文件获取版本信息
4. **原生应用**：可选使用脚本自动同步版本号

**一次配置，永久受益！** 🎉

## 后端 API 实现示例 - 版本检查接口

```ts
// 方案二：从本地文件读取（适用于前后端部署在同一服务器）
import fs from "node:fs";

import { Elysia, t } from "elysia";

// 版本比较工具函数
function compareVersion(version1: string, version2: string): number {
  const v1Parts = version1.split(".").map(Number);
  const v2Parts = version2.split(".").map(Number);
  const maxLength = Math.max(v1Parts.length, v2Parts.length);

  for (let i = 0; i < maxLength; i++) {
    const v1Part = v1Parts[i] || 0;
    const v2Part = v2Parts[i] || 0;

    if (v1Part < v2Part)
      return -1;
    if (v1Part > v2Part)
      return 1;
  }

  return 0;
}

// 方案一：从前端静态资源读取版本（推荐）
async function getFrontendVersionFromURL(): Promise<{
  version: string;
  buildTime: string;
  gitCommit: string;
  environment: string;
}> {
  const response = await fetch("https://your-frontend-domain.com/version.json");
  return await response.json();
}

function getFrontendVersionFromFile(): {
  version: string;
  buildTime: string;
  gitCommit: string;
  environment: string;
} {
  const content = fs.readFileSync("/app/frontend-version.json", "utf-8");
  return JSON.parse(content);
}

// 应用商店链接配置
const APP_STORE_URLS = {
  ios: "https://apps.apple.com/app/id123456789",
  android: "https://play.google.com/store/apps/details?id=riwa.ionic.app",
};

// 版本策略配置（可存储在数据库）
const VERSION_POLICIES = {
  minSupportVersion: "0.0.1", // 最低支持版本
  forceUpdateVersions: ["0.0.1"], // 需要强制更新的版本列表
  updateMessages: {
    "zh-CN": "修复了一些问题并优化了性能",
    "en-US": "Bug fixes and performance improvements",
  },
};

// Elysia 路由定义
const app = new Elysia()
  .get(
    "/api/app/version",
    async ({ query, headers }) => {
      const { platform, currentVersion } = query;
      const lang = headers["accept-language"]?.split(",")[0] || "en-US";

      try {
        // 获取前端版本信息
        const frontendVersion = await getFrontendVersionFromURL();
        // 或使用本地文件: const frontendVersion = getFrontendVersionFromFile()

        // 判断是否需要更新
        const hasUpdate = compareVersion(currentVersion, frontendVersion.version) < 0;

        // 判断是否强制更新
        let forceUpdate = VERSION_POLICIES.forceUpdateVersions.includes(currentVersion);

        // 检查是否低于最低支持版本
        if (compareVersion(currentVersion, VERSION_POLICIES.minSupportVersion) < 0) {
          forceUpdate = true;
        }

        // 获取更新链接
        const updateUrl = platform === "ios"
          ? APP_STORE_URLS.ios
          : platform === "android"
            ? APP_STORE_URLS.android
            : "";

        // 获取更新说明（多语言）
        const updateMessage = VERSION_POLICIES.updateMessages[lang]
          || VERSION_POLICIES.updateMessages["en-US"];

        return {
          version: frontendVersion.version,
          buildNumber: Number.parseInt(frontendVersion.version.replace(/\./g, "")),
          buildTime: frontendVersion.buildTime,
          gitCommit: frontendVersion.gitCommit,
          forceUpdate,
          updateMessage,
          updateUrl,
          minSupportVersion: VERSION_POLICIES.minSupportVersion,
          releaseNotes: [
            "修复了已知问题",
            "优化了应用性能",
            "改进了用户界面",
          ],
        };
      }
      catch (error) {
        console.error("Failed to get frontend version:", error);

        // 降级处理：返回当前版本，不强制更新
        return {
          version: currentVersion,
          forceUpdate: false,
          updateMessage: "",
          updateUrl: "",
        };
      }
    },
    {
      query: t.Object({
        platform: t.Union([t.Literal("ios"), t.Literal("android"), t.Literal("web")]),
        currentVersion: t.String(),
      }),
      response: t.Object({
        version: t.String(),
        buildNumber: t.Optional(t.Number()),
        buildTime: t.Optional(t.String()),
        gitCommit: t.Optional(t.String()),
        forceUpdate: t.Boolean(),
        updateMessage: t.Optional(t.String()),
        updateUrl: t.Optional(t.String()),
        minSupportVersion: t.Optional(t.String()),
        releaseNotes: t.Optional(t.Array(t.String())),
      }),
    }
  );

export default app;

/**
 * 使用示例：
 *
 * 1. 启动后端服务
 * 2. 前端请求：GET /api/app/version?platform=ios&currentVersion=0.0.1
 * 3. 后端响应：
 * {
 *   "version": "0.0.2",
 *   "buildTime": "2025-12-30T11:42:43.425Z",
 *   "forceUpdate": false,
 *   "updateMessage": "修复了一些问题并优化了性能",
 *   "updateUrl": "https://apps.apple.com/app/id123456789",
 *   "minSupportVersion": "0.0.1"
 * }
 */

/**
 * 数据库存储方案（可选）
 *
 * 如果需要更灵活的版本策略管理，可以将配置存储在数据库：
 *
 * CREATE TABLE app_versions (
 *   id SERIAL PRIMARY KEY,
 *   platform VARCHAR(20) NOT NULL,
 *   version VARCHAR(20) NOT NULL,
 *   min_support_version VARCHAR(20),
 *   force_update BOOLEAN DEFAULT FALSE,
 *   update_message_zh TEXT,
 *   update_message_en TEXT,
 *   update_url TEXT,
 *   release_notes JSONB,
 *   created_at TIMESTAMP DEFAULT NOW()
 * );
 *
 * 然后从数据库查询版本策略：
 * const policy = await db.query(
 *   'SELECT * FROM app_versions WHERE platform = $1 ORDER BY created_at DESC LIMIT 1',
 *   [platform]
 * )
 */
```
