feat: 添加应用版本管理功能;生成 version.json 文件并更新环境变量配置

This commit is contained in:
2025-12-30 19:18:24 +07:00
parent 5b1ebac9c4
commit b6132ea30d
12 changed files with 520 additions and 22 deletions

View File

@@ -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
VITE_TRADINGVIEW_DATA_API_URL=https://demo-feed-data.tradingview.com

View File

@@ -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
VITE_TRADINGVIEW_DATA_API_URL=https://demo-feed-data.tradingview.com

474
VERSION_MANAGEMENT.md Normal file
View File

@@ -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
<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 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(
/<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 { 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&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]
* )
*/
```

View File

@@ -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",

3
pnpm-lock.yaml generated
View File

@@ -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)

25
scripts/build.ts Normal file
View File

@@ -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}`);
},
};
}

View File

@@ -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);

View File

@@ -10,5 +10,5 @@
},
"allowSyntheticDefaultImports": true
},
"include": ["vite.config.ts", "tailwind.config.ts"]
"include": ["vite.config.ts", "tailwind.config.ts", "scripts/**/*.ts"]
}

3
types/env.d.ts vendored
View File

@@ -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;

View File

@@ -1,12 +0,0 @@
open float Y 开盘价 每一个纬度(例如天纬度,就是一天的第一笔成交价格)的第一笔成交价格
high float Y 最高价
low float Y 最低价
close float Y 收盘价
settle float Y 结算价 每一个纬度(某一个时间段之内的金额总和 / 交易量)
接口
代币化分类接口
代币化列表接口
代币化详情接口
代币化数据接口
买卖接口

View File

@@ -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: {