initializeApp() 函数执行完成后触发版本检查currentVersion:当前版本latestVersion:最新版本信息downloadProgress:下载进度updateStatus:更新状态(idle, checking, available, downloading, installing, completed, error)checkVersion():调用 /api/v1/auth/settings/package/version 获取版本信息downloadPackage(version):调用 /api/v1/auth/settings/package/download 下载安装包installPackage(version):调用 /api/v1/auth/settings/package/install 安装包src/states/versionUpdateSlice.ts - Redux状态管理src/API/versionUpdate.ts - API调用封装src/services/VersionUpdateService.ts - 业务逻辑服务src/components/VersionUpdateModal.tsx - 版本更新提示组件src/components/DownloadProgressModal.tsx - 下载进度组件src/utils/versionUtils.ts - 版本处理工具src/utils/mqttUtils.ts - MQTT消息处理工具src/pages/index/index.tsx - 在服务器连接检查之后、登录之前添加版本检查调用src/app.tsx - 在initializeApp()中添加版本检查调用(作为备用检查)src/pages/system/SettingsModal/sections/SystemHome/index.tsx - 添加版本信息显示和手动检查更新按钮src/types/global.d.ts - 添加版本更新相关类型定义VersionUpdateService.checkForUpdates() - 检查更新主流程VersionUpdateService.startDownload() - 调用下载API并订阅进度VersionUpdateService.installUpdate() - 调用安装APIVersionUpdateService.handleMqttDownloadMessage() - 处理MQTT下载进度消息versionUtils.compareVersions() - 版本比较mqttUtils.subscribeDownloadProgress() - 订阅下载进度MQTT消息mqttUtils.unsubscribeDownloadProgress() - 取消订阅下载进度mqttUtils.getClient() - 获取MQTT连接客户端sequenceDiagram
participant App as 应用启动
participant Init as initializeApp()
participant VU as VersionUpdateService
participant API as versionUpdateAPI
participant MQTT as MQTT消息
participant Modal as VersionUpdateModal
participant Progress as DownloadProgressModal
participant Redux as versionUpdateSlice
App->>Init: 应用启动
Init->>VU: checkForUpdates()
VU->>API: checkVersion()
API-->>VU: 版本信息响应
VU->>VU: 比较版本
VU->>Redux: 更新状态 (newVersionAvailable)
VU->>Modal: 显示更新提示
Modal->>Modal: 用户选择"立即更新"
Modal->>VU: startDownload()
VU->>API: downloadPackage()
VU->>VU: 订阅MQTT下载进度
VU->>Progress: 显示下载进度
loop 下载中
MQTT->>VU: MODULE/TASK/DOWNLOAD/PACKAGE 消息
VU->>VU: 解析MQTT消息
VU->>Redux: 更新进度状态
VU->>Progress: 更新进度条
end
VU->>VU: 下载完成/失败
VU->>VU: 取消MQTT订阅
VU->>API: installPackage()
API-->>VU: 安装完成
VU->>Redux: 更新状态 (completed)
VU->>Modal: 显示安装完成提示
flowchart TD
A[应用启动] --> B[initializeApp完成]
B --> C[checkForUpdates]
C --> D{API调用成功?}
D -->|否| E[错误处理]
D -->|是| F[获取版本信息]
F --> G{有新版本?}
G -->|否| H[结束]
G -->|是| I[显示VersionUpdateModal]
I --> J{用户选择}
J -->|立即更新| K[显示DownloadProgressModal]
J -->|稍后提醒| L[记录提醒时间]
J -->|跳过版本| M[记录跳过版本]
K --> N[startDownload]
N --> O[API下载包]
O --> P{下载进度更新}
P --> Q{下载完成?}
Q -->|否| P
Q -->|是| R[installPackage]
R --> S{安装成功?}
S -->|是| T[显示成功提示]
S -->|否| U[显示错误提示]
L --> V[下次启动检查]
M --> W[版本升级时检查]
classDiagram
class VersionUpdateService {
+checkForUpdates()
+startDownload(version)
+installUpdate(version)
-compareVersions(current, latest)
-handleError(error)
}
class versionUpdateAPI {
+checkVersion()
+downloadPackage(version)
+installPackage(version)
}
class versionUpdateSlice {
+currentVersion
+latestVersion
+downloadProgress
+updateStatus
+setUpdateStatus()
+setDownloadProgress()
}
class VersionUpdateModal {
+versionInfo
+onUpdate()
+onSkip()
+onRemindLater()
}
class DownloadProgressModal {
+progress
+onCancel()
}
VersionUpdateService --> versionUpdateAPI
VersionUpdateService --> versionUpdateSlice
VersionUpdateModal --> VersionUpdateService
DownloadProgressModal --> VersionUpdateService
initializeApp() → VersionUpdateService.checkForUpdates() → API调用获取版本信息 → 状态更新startDownload() → 下载进度更新 → installUpdate() → 完成interface VersionInfo {
name: string;
version_pulled: boolean;
new_version_available: boolean;
current_version: string;
newest_version: string;
newest_package_size: number;
newest_time: string;
newest_md5_sum: string;
}
interface DownloadProgress {
loaded: number;
total: number;
speed: number; // bytes per second
eta: number; // estimated time of arrival in seconds
percentage: number; // 下载进度百分比 (0.0 - 1.0)
timeSpent: number; // 已用时(秒)
status: 'IN_PROGRESS' | 'SUCCESS' | 'FAILURE'; // 下载状态
message: string; // 失败原因
}
interface MqttDownloadMessage {
name: string; // 包名,目前为 "backend"
content_length: number; // 包总大小(bytes)
download_size: number; // 已下载大小(bytes)
percentage: number; // 下载进度百分比 (0.0 - 1.0)
speed: number; // 平均速度(MB/s)
time_spent: number; // 已用时(秒)
status: 'IN_PROGRESS' | 'SUCCESS' | 'FAILURE'; // 下载状态
msg: string; // 失败原因
}
interface UpdateState {
status: 'idle' | 'checking' | 'available' | 'downloading' | 'installing' | 'completed' | 'error';
versionInfo: VersionInfo | null;
downloadProgress: DownloadProgress | null;
error: string | null;
lastChecked: Date | null;
skippedVersions: string[]; // 用户跳过的版本列表
remindLaterTime: Date | null; // 稍后提醒的时间戳
userChoice: 'none' | 'update_now' | 'remind_later' | 'skip_version'; // 用户的选择记录
}
index.tsx 检查登录状态(现有实现):
isLoggedIn(userInfo) 检查登录状态版本更新检查在用户登录成功后、应用初始化前执行
完整的时序流程:
initializeApp() 完成 → checkForUpdates() → API获取版本信息VersionUpdateModalDownloadProgressModal → 开始下载触发条件:用户点击"立即更新"按钮
执行逻辑:
userChoice = 'update_now'VersionUpdateModalDownloadProgressModalVersionUpdateService.startDownload()/api/v1/auth/settings/package/download/api/v1/auth/settings/package/install 安装触发条件:用户点击"稍后提醒"按钮
执行逻辑:
userChoice = 'remind_later'remindLaterTime = Date.now() + REMIND_INTERVAL (例如24小时后)VersionUpdateModalremindLaterTimeremindLaterTime,则重新显示更新提示remindLaterTime,则跳过此次更新检查触发条件:用户点击"跳过此版本"按钮
执行逻辑:
userChoice = 'skip_version'newest_version 添加到 skippedVersions 数组VersionUpdateModalskippedVersions 中skippedVersions 列表flowchart TD
A[显示VersionUpdateModal] --> B{用户选择}
B -->|立即更新| C[设置userChoice='update_now']
B -->|稍后提醒| D[设置userChoice='remind_later']
B -->|跳过版本| E[设置userChoice='skip_version']
C --> F[关闭Modal]
D --> F
E --> F
F --> G[开始下载流程]
G --> H{下载成功?}
H -->|是| I[自动安装]
H -->|否| J[显示错误]
I --> K{安装成功?}
K -->|是| L[显示成功提示]
K -->|否| M[显示安装失败]
D --> N[记录remindLaterTime]
N --> O[下次启动检查]
E --> P[添加到skippedVersions]
P --> Q[新版本发布时检查]
interface PersistentUpdateState {
skippedVersions: string[]; // ["1.8.8", "1.9.0"]
remindLaterTime: number | null; // timestamp
lastUserChoice: 'update_now' | 'remind_later' | 'skip_version' | null;
lastCheckedVersion: string; // 上次检查的版本号
}
Topic: MODULE/TASK/DOWNLOAD/PACKAGE
消息示例:
{
"name": "backend",
"content_length": 156024824,
"download_size": 56024824,
"percentage": 0.56,
"speed": 1.16,
"time_spent": 120,
"status": "IN_PROGRESS",
"msg": ""
}
字段详细说明:
name: 包名称,目前固定为 "backend"content_length: 包总大小(字节)download_size: 已下载大小(字节)percentage: 下载进度百分比(0.0-1.0)speed: 平均下载速度(MB/s)time_spent: 已用时(秒)status: 下载状态
"IN_PROGRESS": 下载中"SUCCESS": 下载成功"FAILURE": 下载失败msg: 失败原因(当status为"FAILURE"时填充)触发时机:用户选择"立即更新"并开始下载时
处理逻辑:
// VersionUpdateService.startDownload()
const startDownload = async (version: string) => {
try {
// 1. 调用下载API
await versionUpdateAPI.downloadPackage(version);
// 2. 订阅MQTT消息
mqttUtils.subscribeDownloadProgress(
'MODULE/TASK/DOWNLOAD/PACKAGE',
handleMqttDownloadMessage
);
// 3. 显示下载进度窗口
dispatch(setUpdateStatus('downloading'));
} catch (error) {
handleError(error);
}
};
消息处理器: VersionUpdateService.handleMqttDownloadMessage()
处理逻辑:
const handleMqttDownloadMessage = (message: MqttDownloadMessage) => {
// 1. 转换数据格式
const progress: DownloadProgress = {
loaded: message.download_size,
total: message.content_length,
speed: message.speed * 1024 * 1024, // MB/s 转换为 bytes/s
percentage: message.percentage,
timeSpent: message.time_spent,
status: message.status,
message: message.msg,
eta: calculateETA(message) // 计算剩余时间
};
// 2. 更新Redux状态
dispatch(updateDownloadProgress(progress));
// 3. 处理不同状态
switch (message.status) {
case 'IN_PROGRESS':
// 继续显示进度
break;
case 'SUCCESS':
// 下载完成,准备安装
handleDownloadComplete();
break;
case 'FAILURE':
// 下载失败,显示错误
handleDownloadError(message.msg);
break;
}
};
触发时机:下载完成或失败时
处理逻辑:
const handleDownloadComplete = () => {
// 1. 取消MQTT订阅
mqttUtils.unsubscribeDownloadProgress('MODULE/TASK/DOWNLOAD/PACKAGE');
// 2. 更新状态为安装中
dispatch(setUpdateStatus('installing'));
// 3. 开始安装流程
installUpdate();
};
const handleDownloadError = (errorMessage: string) => {
// 1. 取消MQTT订阅
mqttUtils.unsubscribeDownloadProgress('MODULE/TASK/DOWNLOAD/PACKAGE');
// 2. 更新错误状态
dispatch(setUpdateError(errorMessage));
dispatch(setUpdateStatus('error'));
};
// src/utils/mqttUtils.ts
import { getMqttBrokerUrl } from '../API/config';
// MQTT客户端类型(需要安装mqtt库)
import mqtt, { MqttClient, IClientOptions } from 'mqtt';
class MqttUtils {
private client: MqttClient | null = null;
private subscriptions: Map<string, (message: any) => void> = new Map();
private isConnecting: boolean = false;
// 获取或创建MQTT连接
private async getClient(): Promise<MqttClient> {
if (this.client && this.client.connected) {
return this.client;
}
if (this.isConnecting) {
// 等待连接完成
return new Promise((resolve, reject) => {
const checkConnection = () => {
if (this.client && this.client.connected) {
resolve(this.client);
} else if (!this.isConnecting) {
reject(new Error('MQTT连接失败'));
} else {
setTimeout(checkConnection, 100);
}
};
checkConnection();
});
}
this.isConnecting = true;
try {
const brokerUrl = getMqttBrokerUrl();
const options: IClientOptions = {
clientId: `version-update-${Date.now()}`,
clean: true,
reconnectPeriod: 5000, // 重连间隔
connectTimeout: 10000, // 连接超时
};
this.client = mqtt.connect(brokerUrl, options);
return new Promise((resolve, reject) => {
this.client!.on('connect', () => {
console.log('MQTT连接成功');
this.isConnecting = false;
resolve(this.client!);
});
this.client!.on('error', (error) => {
console.error('MQTT连接失败:', error);
this.isConnecting = false;
reject(error);
});
this.client!.on('message', (topic, message) => {
this.handleIncomingMessage(topic, message);
});
});
} catch (error) {
this.isConnecting = false;
throw error;
}
}
// 订阅下载进度消息
async subscribeDownloadProgress(topic: string, callback: (message: MqttDownloadMessage) => void) {
try {
const client = await this.getClient();
this.subscriptions.set(topic, callback);
await client.subscribeAsync(topic, { qos: 1 });
console.log(`已订阅MQTT主题: ${topic}`);
} catch (error) {
console.error('MQTT订阅失败:', error);
throw error;
}
}
// 取消订阅下载进度消息
async unsubscribeDownloadProgress(topic: string) {
try {
if (this.client && this.client.connected) {
await this.client.unsubscribeAsync(topic);
this.subscriptions.delete(topic);
console.log(`已取消订阅MQTT主题: ${topic}`);
}
} catch (error) {
console.error('MQTT取消订阅失败:', error);
}
}
// 处理接收到的MQTT消息
private handleIncomingMessage(topic: string, message: Buffer) {
const callback = this.subscriptions.get(topic);
if (callback) {
try {
// 解析MQTT消息并调用回调
const parsedMessage = JSON.parse(message.toString());
callback(parsedMessage);
} catch (error) {
console.error('MQTT消息解析失败:', error);
}
}
}
// 断开连接(可选,用于清理资源)
disconnect() {
if (this.client) {
this.client.end();
this.client = null;
this.subscriptions.clear();
}
}
}
export const mqttUtils = new MqttUtils();
说明:
config.ts 中的 getMqttBrokerUrl() 获取MQTT broker地址src/utils/mqttUtils.ts - MQTT工具类(新增)src/services/VersionUpdateService.ts - 添加MQTT消息处理方法src/states/versionUpdateSlice.ts - 添加进度更新actionsrc/components/DownloadProgressModal.tsx - 更新UI显示逻辑创建 cypress/support/mock/handlers/versionUpdate.ts:
/**
* 版本更新相关Mock Handlers
*/
// 检查版本 - 有新版本
export function mockCheckVersionHasUpdate() {
cy.intercept('GET', '/dr/api/v1/auth/settings/package/version', {
statusCode: 200,
body: {
code: "0x000000",
data: {
name: "backend",
version_pulled: true,
new_version_available: true,
current_version: "1.14.1",
newest_version: "1.15.0",
newest_package_size: 104857600, // 100MB
newest_time: "2025-12-18T07:00:00Z",
newest_md5_sum: "abc123..."
}
}
}).as('checkVersionHasUpdate');
}
// 检查版本 - 无新版本
export function mockCheckVersionNoUpdate() {
cy.intercept('GET', '/dr/api/v1/auth/settings/package/version', {
statusCode: 200,
body: {
code: "0x000000",
data: {
new_version_available: false,
current_version: "1.14.1"
}
}
}).as('checkVersionNoUpdate');
}
// 下载包API
export function mockDownloadPackage() {
cy.intercept('POST', '/dr/api/v1/auth/settings/package/download', {
statusCode: 200,
body: { code: "0x000000", data: {} }
}).as('downloadPackage');
}
// 安装包API
export function mockInstallPackage() {
cy.intercept('POST', '/dr/api/v1/auth/settings/package/install', {
statusCode: 200,
body: { code: "0x000000", data: {} }
}).as('installPackage');
}
在 cypress/support/pageObjects/LoginPage.ts 中添加版本更新相关方法:
// 版本更新弹窗相关方法
getVersionUpdateModal() {
return cy.get('[data-testid="version-update-modal"]');
}
getUpdateNowButton() {
return cy.get('[data-testid="update-now-btn"]');
}
getRemindLaterButton() {
return cy.get('[data-testid="remind-later-btn"]');
}
getSkipVersionButton() {
return cy.get('[data-testid="skip-version-btn"]');
}
// 下载进度弹窗相关方法
getDownloadProgressModal() {
return cy.get('[data-testid="download-progress-modal"]');
}
getDownloadProgressBar() {
return cy.get('[data-testid="download-progress-bar"]');
}
getDownloadCancelButton() {
return cy.get('[data-testid="download-cancel-btn"]');
}
创建 cypress/e2e/versionUpdate.cy.ts:
import { mockLoginSuccess } from '../support/mock/handlers/user';
import {
mockCheckVersionHasUpdate,
mockCheckVersionNoUpdate,
mockDownloadPackage,
mockInstallPackage
} from '../support/mock/handlers/versionUpdate';
import LoginPage from '../support/pageObjects/LoginPage';
describe('Version Update', () => {
const loginPage = new LoginPage();
beforeEach(() => {
mockLoginSuccess();
});
it('should check for updates after login', () => {
mockCheckVersionHasUpdate();
// 登录流程
loginPage.visit();
loginPage.login('admin', '123456');
cy.wait('@loginSuccess');
// 等待版本检查完成
cy.wait('@checkVersionHasUpdate');
// 验证版本更新弹窗出现
loginPage.getVersionUpdateModal().should('be.visible');
});
it('should start download when user clicks update now', () => {
mockCheckVersionHasUpdate();
mockDownloadPackage();
// 登录并等待版本检查
loginPage.visit();
loginPage.login('admin', '123456');
cy.wait('@loginSuccess');
cy.wait('@checkVersionHasUpdate');
// 点击立即更新
loginPage.getUpdateNowButton().click();
// 验证下载弹窗出现
loginPage.getDownloadProgressModal().should('be.visible');
// 验证API调用
cy.wait('@downloadPackage');
});
it('should skip version when user chooses to skip', () => {
mockCheckVersionHasUpdate();
loginPage.visit();
loginPage.login('admin', '123456');
cy.wait('@loginSuccess');
cy.wait('@checkVersionHasUpdate');
// 点击跳过版本
loginPage.getSkipVersionButton().click();
// 验证弹窗关闭
loginPage.getVersionUpdateModal().should('not.exist');
});
it('should not show update modal when no new version available', () => {
mockCheckVersionNoUpdate();
loginPage.visit();
loginPage.login('admin', '123456');
cy.wait('@loginSuccess');
cy.wait('@checkVersionNoUpdate');
// 验证没有版本更新弹窗
loginPage.getVersionUpdateModal().should('not.exist');
});
});
启动测试环境:
# 启动开发服务器
npm run h5:electron
# 或者启动浏览器版本用于调试
npm run h5:browser
# 运行e2e测试
npm run e2e
登录后自动触发版本检查
正常更新流程
用户选择跳过更新
稍后提醒
无新版本时不显示弹窗
版本检查失败
下载中断
安装失败
启动时间影响
内存占用
不同操作系统
不同网络环境
版本号格式异常
并发更新检查
下载文件损坏
安装权限不足
磁盘空间不足
网络超时
应用重启失败