From b6132ea30dc43d5f04d1cab9ded6c1096ba3654b Mon Sep 17 00:00:00 2001 From: Seven Date: Tue, 30 Dec 2025 19:18:24 +0700 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E5=BA=94=E7=94=A8?= =?UTF-8?q?=E7=89=88=E6=9C=AC=E7=AE=A1=E7=90=86=E5=8A=9F=E8=83=BD=EF=BC=9B?= =?UTF-8?q?=E7=94=9F=E6=88=90=20version.json=20=E6=96=87=E4=BB=B6=E5=B9=B6?= =?UTF-8?q?=E6=9B=B4=E6=96=B0=E7=8E=AF=E5=A2=83=E5=8F=98=E9=87=8F=E9=85=8D?= =?UTF-8?q?=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .env.development | 3 +- .env.production | 3 +- app.update.md => APP_UPDATE.md | 0 VERSION_MANAGEMENT.md | 474 ++++++++++++++++++++++++++++++++ package.json | 3 +- pnpm-lock.yaml | 3 + scripts/build.ts | 25 ++ src/composables/useAppUpdate.ts | 4 +- tsconfig.node.json | 2 +- types/env.d.ts | 3 +- vault.md | 12 - vite.config.ts | 10 +- 12 files changed, 520 insertions(+), 22 deletions(-) rename app.update.md => APP_UPDATE.md (100%) create mode 100644 VERSION_MANAGEMENT.md create mode 100644 scripts/build.ts delete mode 100644 vault.md diff --git a/.env.development b/.env.development index 3d63d7f..a376891 100644 --- a/.env.development +++ b/.env.development @@ -1,4 +1,3 @@ VITE_API_URL=http://192.168.1.3:9528 VITE_TRADINGVIEW_LIBRARY_URL=http://192.168.1.5:6173 -VITE_TRADINGVIEW_DATA_API_URL=https://demo-feed-data.tradingview.com -VITE_APP_VERSION=0.0.1 \ No newline at end of file +VITE_TRADINGVIEW_DATA_API_URL=https://demo-feed-data.tradingview.com \ No newline at end of file diff --git a/.env.production b/.env.production index 2c09e7e..28ba411 100644 --- a/.env.production +++ b/.env.production @@ -1,4 +1,3 @@ VITE_API_URL=http://192.168.1.3:9527 VITE_TRADINGVIEW_LIBRARY_URL=http://192.168.1.5:6173 -VITE_TRADINGVIEW_DATA_API_URL=https://demo-feed-data.tradingview.com -VITE_APP_VERSION=0.0.1 \ No newline at end of file +VITE_TRADINGVIEW_DATA_API_URL=https://demo-feed-data.tradingview.com \ No newline at end of file diff --git a/app.update.md b/APP_UPDATE.md similarity index 100% rename from app.update.md rename to APP_UPDATE.md diff --git a/VERSION_MANAGEMENT.md b/VERSION_MANAGEMENT.md new file mode 100644 index 0000000..c06c232 --- /dev/null +++ b/VERSION_MANAGEMENT.md @@ -0,0 +1,474 @@ +# 版本管理方案 + +## 🎯 问题 + +前端每次更新版本,后端也需要手动同步版本信息,容易出错且效率低。 + +## ✅ 解决方案 + +### 方案一:自动生成 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 '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 +CFBundleShortVersionString +1.2.3 +``` + +### Android (build.gradle) + +修改 `android/app/build.gradle`: + +```gradle +versionName "1.2.3" // 与 package.json 保持一致 +``` + +### 自动化脚本(可选) + +创建 `scripts/sync-version.js`: + +```javascript +#!/usr/bin/env node +import fs from 'fs' +import { execSync } from 'child_process' + +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( + /CFBundleShortVersionString<\/key>\s*.*?<\/string>/, + `CFBundleShortVersionString\n\t${version}` +) +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 { 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() +} + +// 方案二:从本地文件读取(适用于前后端部署在同一服务器) +import fs from 'fs' + +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: 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¤tVersion=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] + * ) + */ + +``` \ No newline at end of file diff --git a/package.json b/package.json index c2f0264..0a17724 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,7 @@ "preview": "vite preview", "run:ios": "ionic capacitor run ios -l --external", "run:android": "ionic capacitor run android -l --external", - "proxy": "ionic config set -g proxy http://192.168.1.36:9527", + "proxy": "ionic config set -g proxy http://192.168.1.3:9527", "test:e2e": "cypress run", "test:unit": "vitest", "lint": "eslint", @@ -81,6 +81,7 @@ "eslint": "^9.39.1", "eslint-plugin-format": "^1.1.0", "eslint-plugin-vue": "^10.6.2", + "jiti": "^2.6.1", "jsdom": "^27.3.0", "lint-staged": "^16.2.7", "simple-git-hooks": "^2.13.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0a9730f..d3ae835 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -195,6 +195,9 @@ importers: eslint-plugin-vue: specifier: ^10.6.2 version: 10.6.2(@stylistic/eslint-plugin@5.6.1(eslint@9.39.1(jiti@2.6.1)))(@typescript-eslint/parser@8.49.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1))(vue-eslint-parser@10.2.0(eslint@9.39.1(jiti@2.6.1))) + jiti: + specifier: ^2.6.1 + version: 2.6.1 jsdom: specifier: ^27.3.0 version: 27.3.0(postcss@8.5.6) diff --git a/scripts/build.ts b/scripts/build.ts new file mode 100644 index 0000000..80ccd96 --- /dev/null +++ b/scripts/build.ts @@ -0,0 +1,25 @@ +import fs from "node:fs"; +import path from "node:path"; +import process from "node:process"; + +export interface VersionPluginOptions { + version: string; +} + +export function generateVersion(options: VersionPluginOptions) { + return { + name: "generate-version", + closeBundle() { + const versionInfo = { + version: options.version, + buildTime: new Date().toISOString(), + environment: process.env.NODE_ENV, + }; + + // 写入到 dist 目录 + const distPath = path.resolve(process.cwd(), "dist/version.json"); + fs.writeFileSync(distPath, JSON.stringify(versionInfo, null, 2)); + console.log(`✓ Generated version.json: ${options.version}`); + }, + }; +} diff --git a/src/composables/useAppUpdate.ts b/src/composables/useAppUpdate.ts index 919816b..a9083f2 100644 --- a/src/composables/useAppUpdate.ts +++ b/src/composables/useAppUpdate.ts @@ -58,8 +58,8 @@ export function useAppUpdate() { return appInfo.version; } - // Web 平台从环境变量或 package.json 获取 - return import.meta.env.VITE_APP_VERSION; + // Web 平台从编译时注入的全局变量获取(来自 package.json) + return __APP_VERSION__; } catch (error) { console.error("获取当前版本失败:", error); diff --git a/tsconfig.node.json b/tsconfig.node.json index a2365d8..b35a97e 100644 --- a/tsconfig.node.json +++ b/tsconfig.node.json @@ -10,5 +10,5 @@ }, "allowSyntheticDefaultImports": true }, - "include": ["vite.config.ts", "tailwind.config.ts"] + "include": ["vite.config.ts", "tailwind.config.ts", "scripts/**/*.ts"] } diff --git a/types/env.d.ts b/types/env.d.ts index 31ee272..acaa264 100644 --- a/types/env.d.ts +++ b/types/env.d.ts @@ -12,9 +12,10 @@ interface ImportMetaEnv { readonly VITE_API_URL: string; readonly VITE_TRADINGVIEW_LIBRARY_URL: string; readonly VITE_TRADINGVIEW_DATA_API_URL: string; - readonly VITE_APP_VERSION: string; } interface ImportMeta { readonly env: ImportMetaEnv; } + +declare const __APP_VERSION__: string; diff --git a/vault.md b/vault.md deleted file mode 100644 index badf08e..0000000 --- a/vault.md +++ /dev/null @@ -1,12 +0,0 @@ -open float Y 开盘价 每一个纬度(例如天纬度,就是一天的第一笔成交价格)的第一笔成交价格 -high float Y 最高价 -low float Y 最低价 -close float Y 收盘价 -settle float Y 结算价 每一个纬度(某一个时间段之内的金额总和 / 交易量) - -接口 -代币化分类接口 -代币化列表接口 -代币化详情接口 -代币化数据接口 -买卖接口 diff --git a/vite.config.ts b/vite.config.ts index e9cc781..d53b27c 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -1,3 +1,4 @@ +import fs from "node:fs"; import path from "node:path"; import process from "node:process"; import tailwindcss from "@tailwindcss/vite"; @@ -11,9 +12,13 @@ import icons from "unplugin-icons/vite"; import { IonicResolver } from "unplugin-vue-components/resolvers"; import components from "unplugin-vue-components/vite"; import { defineConfig } from "vite"; +import { generateVersion } from "./scripts/build"; dotenv.config({ path: `.env.${process.env.NODE_ENV}` }); +const packageJson = JSON.parse(fs.readFileSync("./package.json", "utf-8")); +const appVersion = packageJson.version; + // https://vitejs.dev/config/ export default defineConfig({ plugins: [ @@ -34,9 +39,12 @@ export default defineConfig({ directoryAsNamespace: true, resolvers: [IonicResolver(), iconsResolver({ prefix: "i" })], }), + generateVersion({ + version: appVersion, + }), ], define: { - APP_VERSION: JSON.stringify(process.env.VITE_APP_VERSION || "0.0.1"), + __APP_VERSION__: JSON.stringify(appVersion), }, resolve: { alias: {