From 78584591180cda079e2e52ad18ec3ccb4a87e287 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BC=A0=E6=88=90?= Date: Wed, 26 Nov 2025 12:39:21 +0800 Subject: [PATCH] 1 --- _doc/服务端升级功能逻辑说明.md | 320 +++++++++++++++ _sql/create_version_info_table.sql | 20 + admin/src/api/system/version_server.js | 64 +++ admin/src/router/component-map.js | 2 + admin/src/views/system/version.vue | 282 ++++++++++++++ api/controller_admin/version.js | 517 +++++++++++++++++++++++++ api/controller_front/file.js | 86 +++- api/controller_front/user.js | 65 ++++ api/controller_front/version.js | 198 ++++++++++ api/model/version_info.js | 86 ++++ api/services/version_service.js | 145 +++++++ config/config.js | 2 +- 12 files changed, 1785 insertions(+), 2 deletions(-) create mode 100644 _doc/服务端升级功能逻辑说明.md create mode 100644 _sql/create_version_info_table.sql create mode 100644 admin/src/api/system/version_server.js create mode 100644 admin/src/views/system/version.vue create mode 100644 api/controller_admin/version.js create mode 100644 api/controller_front/version.js create mode 100644 api/model/version_info.js create mode 100644 api/services/version_service.js diff --git a/_doc/服务端升级功能逻辑说明.md b/_doc/服务端升级功能逻辑说明.md new file mode 100644 index 0000000..fe1e4fc --- /dev/null +++ b/_doc/服务端升级功能逻辑说明.md @@ -0,0 +1,320 @@ +# 服务端升级功能逻辑说明 + +## 概述 + +本文档说明服务端需要实现的升级功能逻辑,配合客户端的自动升级功能。 + +## API 接口要求 + +### GET /version/check + +**功能**:检查是否有新版本可用 + +**请求参数**(Query 参数): +- `current_version`: 当前版本号(x.y.z 格式,如 "1.0.0") +- `platform`: 平台类型(如 "win32", "darwin", "linux") +- `arch`: 架构类型(如 "x64", "ia32", "arm64") +- `sn_code`: 设备序列号(可选,用于权限控制) + +**响应格式**: +```json +{ + "code": 0, + "message": "success", + "data": { + "version": "1.1.0", + "download_url": "http://work.light120.com/downloads/app-1.1.0.exe", + "release_notes": "修复了一些bug,新增了xxx功能", + "force_update": false, + "file_size": 52428800, + "file_hash": "abc123def456..." + } +} +``` + +**字段说明**: +- `version`: 最新版本号(x.y.z 格式) +- `download_url`: 更新包下载地址(完整的 HTTP/HTTPS URL) +- `release_notes`: 更新日志(可选,字符串) +- `force_update`: 是否强制更新(可选,布尔值,默认 false) +- `file_size`: 文件大小(字节,可选) +- `file_hash`: 文件 SHA256 哈希值(可选,用于校验文件完整性) + +**业务逻辑**: + +1. **版本比较**: + - 从数据库查询最新版本信息(根据 platform 和 arch) + - 比较请求中的 `current_version` 与数据库中的最新版本 + - 如果最新版本 > 当前版本,返回更新信息 + - 如果版本相同或更旧,返回 `code: 0, data: null` 或提示"已是最新版本" + +2. **版本号格式**: + - 格式:x.y.z(如 1.0.0, 1.1.0, 2.0.0) + - 比较规则:主版本号 > 次版本号 > 修订号 + - 示例:1.1.0 > 1.0.0, 2.0.0 > 1.9.9 + +3. **平台和架构过滤**: + - 只返回匹配 platform 和 arch 的版本信息 + - 如果某个平台没有新版本,返回空结果 + +4. **序列号验证**(可选): + - 可以根据 sn_code 验证设备权限 + - 如果启用权限控制,未授权的设备返回错误 + +5. **错误处理**: + - 参数缺失:返回 `code: 400, message: "参数错误"` + - 服务器错误:返回 `code: 500, message: "服务器错误"` + - 无新版本:返回 `code: 0, data: null` 或 `has_update: false` + +## 数据库设计建议 + +### 版本信息表(version_info) + +| 字段名 | 类型 | 说明 | 约束 | +|--------|------|------|------| +| id | INT/BIGINT | 主键 | PRIMARY KEY, AUTO_INCREMENT | +| version | VARCHAR(20) | 版本号(x.y.z 格式) | NOT NULL, UNIQUE | +| platform | VARCHAR(20) | 平台类型(win32/darwin/linux) | NOT NULL | +| arch | VARCHAR(20) | 架构类型(x64/ia32/arm64) | NOT NULL | +| download_url | VARCHAR(500) | 下载地址 | NOT NULL | +| file_path | VARCHAR(500) | 服务器文件路径 | NOT NULL | +| file_size | BIGINT | 文件大小(字节) | DEFAULT 0 | +| file_hash | VARCHAR(64) | SHA256 哈希值 | | +| release_notes | TEXT | 更新日志 | | +| force_update | TINYINT(1) | 是否强制更新 | DEFAULT 0 | +| status | TINYINT(1) | 状态(1:启用 0:禁用) | DEFAULT 1 | +| create_time | DATETIME | 创建时间 | NOT NULL, DEFAULT CURRENT_TIMESTAMP | +| last_modify_time | DATETIME | 最后修改时间 | NOT NULL, DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP | + +**索引建议**: +- PRIMARY KEY (id) +- UNIQUE KEY (version, platform, arch) +- INDEX (platform, arch, status) + +### 版本发布历史表(version_history)(可选) + +用于记录版本发布历史,便于管理: + +| 字段名 | 类型 | 说明 | +|--------|------|------| +| id | INT/BIGINT | 主键 | +| version_id | INT | 关联 version_info.id | +| release_type | VARCHAR(20) | 发布类型(stable/beta/alpha) | +| create_time | DATETIME | 发布时间 | +| last_modify_time | DATETIME | 最后修改时间 | + +## 业务逻辑流程 + +### 1. 版本检查流程 + +``` +客户端请求 + ↓ +接收参数(current_version, platform, arch, sn_code) + ↓ +验证参数有效性 + ↓ +查询数据库最新版本(按 platform + arch + status=1) + ↓ +版本号比较 + ↓ +有更新? + ├─ 是 → 构建返回数据(包含文件信息) + └─ 否 → 返回空结果或提示"已是最新版本" + ↓ +返回响应 +``` + +### 2. 版本管理流程 + +**新增版本**: +1. 上传安装包文件到服务器指定目录 +2. 计算文件 SHA256 哈希值 +3. 获取文件大小 +4. 插入数据库记录(status=1 表示启用) + +**禁用版本**: +1. 更新 status=0(不删除记录,保留历史) + +**删除版本**: +1. 删除数据库记录 +2. 删除服务器上的文件 + +### 3. 文件存储建议 + +**目录结构**: +``` +/uploads/ + └── versions/ + ├── win32/ + │ ├── x64/ + │ │ ├── app-1.0.0.exe + │ │ └── app-1.1.0.exe + │ └── ia32/ + └── darwin/ + └── x64/ + └── app-1.1.0.dmg +``` + +**文件命名规则**: +- 格式:`app-{version}.{ext}` +- 示例:`app-1.1.0.exe`, `app-1.1.0.dmg` + +**下载 URL 生成**: +- 基础 URL:`http://work.light120.com/downloads/` +- 完整 URL:`http://work.light120.com/downloads/app-1.1.0.exe` + +## 关键逻辑说明 + +### 1. 版本号比较逻辑 + +**字符串比较规则**: +- 将版本号按 "." 分割成数组:["1", "1", "0"] +- 逐位比较数字大小 +- 主版本号优先级最高,依次递减 + +**示例**: +```javascript +// 伪代码 +compareVersion("1.1.0", "1.0.0") → true // 1.1.0 > 1.0.0 +compareVersion("2.0.0", "1.9.9") → true // 2.0.0 > 1.9.9 +compareVersion("1.0.0", "1.0.0") → false // 相等 +``` + +### 2. 文件哈希计算 + +**计算方法**: +- 使用 SHA256 算法 +- 读取文件内容,计算哈希值 +- 返回小写的十六进制字符串 + +**用途**: +- 客户端下载后校验文件完整性 +- 防止文件被篡改 +- 确保文件下载完整 + +### 3. 强制更新逻辑 + +**force_update 字段**: +- `true`: 强制更新,客户端必须更新才能使用 +- `false`: 可选更新,客户端可以选择稍后更新 + +**业务场景**: +- 安全漏洞修复 → 强制更新 +- 重大功能更新 → 强制更新 +- 小版本更新 → 可选更新 + +### 4. 平台和架构支持 + +**支持列表**: +- Windows: `win32` + `x64` / `ia32` +- macOS: `darwin` + `x64` / `arm64` +- Linux: `linux` + `x64` / `arm64` + +**查询逻辑**: +- 必须同时匹配 platform 和 arch +- 如果某个组合没有版本,返回空结果 + +## 安全考虑 + +### 1. 文件下载安全 + +- **HTTPS 下载**:优先使用 HTTPS 协议 +- **文件校验**:提供 SHA256 哈希值供客户端校验 +- **文件大小验证**:返回文件大小,客户端可以验证下载完整性 + +### 2. 权限控制 + +- **序列号验证**:可以根据 sn_code 控制哪些设备可以更新 +- **版本状态**:使用 status 字段控制版本是否可用 +- **IP 限制**:可以限制下载 IP 范围(可选) + +### 3. 防止恶意更新 + +- **文件类型验证**:只允许上传 .exe、.dmg、.AppImage 等安装包 +- **文件大小限制**:设置最大文件大小限制 +- **版本号验证**:验证版本号格式是否正确 + +## 返回状态码说明 + +| code | 说明 | 处理方式 | +|------|------|----------| +| 0 | 成功 | 检查 data 是否为空判断是否有更新 | +| 400 | 参数错误 | 客户端提示参数错误 | +| 401 | 未授权 | 客户端提示未授权 | +| 404 | 未找到版本 | 客户端提示版本不存在 | +| 500 | 服务器错误 | 客户端提示服务器错误,稍后重试 | + +## 示例场景 + +### 场景1:有新版本 + +**请求**: +``` +GET /version/check?current_version=1.0.0&platform=win32&arch=x64&sn_code=GHJU +``` + +**响应**: +```json +{ + "code": 0, + "message": "success", + "data": { + "version": "1.1.0", + "download_url": "http://work.light120.com/downloads/app-1.1.0.exe", + "release_notes": "1. 修复了登录问题\n2. 新增自动升级功能", + "force_update": false, + "file_size": 52428800, + "file_hash": "a1b2c3d4e5f6..." + } +} +``` + +### 场景2:已是最新版本 + +**请求**: +``` +GET /version/check?current_version=1.1.0&platform=win32&arch=x64&sn_code=GHJU +``` + +**响应**: +```json +{ + "code": 0, + "message": "已是最新版本", + "data": null +} +``` + +### 场景3:参数错误 + +**请求**: +``` +GET /version/check?current_version=1.0.0 +``` + +**响应**: +```json +{ + "code": 400, + "message": "缺少必要参数:platform, arch", + "data": null +} +``` + +## 注意事项 + +1. **日期字段命名**:统一使用 `create_time` 和 `last_modify_time` +2. **版本号格式**:严格按照 x.y.z 格式,便于版本比较 +3. **文件存储**:建议使用 CDN 或静态文件服务器,提高下载速度 +4. **日志记录**:记录版本检查请求,便于统计和分析 +5. **缓存策略**:可以考虑缓存最新版本信息,减少数据库查询 +6. **灰度发布**:可以通过 sn_code 控制部分设备先更新(可选) + +## 实现优先级 + +1. **基础功能**:版本检查、版本比较、返回下载信息 +2. **文件管理**:文件上传、文件存储、哈希计算 +3. **安全功能**:文件校验、权限控制 +4. **管理功能**:版本管理后台、历史记录 + diff --git a/_sql/create_version_info_table.sql b/_sql/create_version_info_table.sql new file mode 100644 index 0000000..2bfadc2 --- /dev/null +++ b/_sql/create_version_info_table.sql @@ -0,0 +1,20 @@ +-- 版本信息表 +CREATE TABLE IF NOT EXISTS `version_info` ( + `id` INT(11) NOT NULL AUTO_INCREMENT COMMENT '主键ID', + `version` VARCHAR(20) NOT NULL COMMENT '版本号(x.y.z 格式)', + `platform` VARCHAR(20) NOT NULL COMMENT '平台类型(win32/darwin/linux)', + `arch` VARCHAR(20) NOT NULL COMMENT '架构类型(x64/ia32/arm64)', + `download_url` VARCHAR(500) NOT NULL COMMENT '下载地址', + `file_path` VARCHAR(500) NOT NULL COMMENT '服务器文件路径', + `file_size` BIGINT(20) DEFAULT 0 COMMENT '文件大小(字节)', + `file_hash` VARCHAR(64) DEFAULT NULL COMMENT 'SHA256 哈希值', + `release_notes` TEXT COMMENT '更新日志', + `force_update` TINYINT(1) DEFAULT 0 COMMENT '是否强制更新(1:是 0:否)', + `status` TINYINT(1) DEFAULT 1 COMMENT '状态(1:启用 0:禁用)', + `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `last_modify_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '最后修改时间', + PRIMARY KEY (`id`), + UNIQUE KEY `uk_version_platform_arch` (`version`, `platform`, `arch`), + KEY `idx_platform_arch_status` (`platform`, `arch`, `status`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='版本信息表'; + diff --git a/admin/src/api/system/version_server.js b/admin/src/api/system/version_server.js new file mode 100644 index 0000000..e4a5ff9 --- /dev/null +++ b/admin/src/api/system/version_server.js @@ -0,0 +1,64 @@ +/** + * 版本管理 API 服务 + */ + +class VersionServer { + /** + * 分页查询版本列表 + * @param {Object} param - 查询参数 + * @param {Object} param.seachOption - 搜索条件 + * @param {Object} param.pageOption - 分页选项 + * @returns {Promise} + */ + page(param) { + return window.framework.http.post('/version/list', param) + } + + /** + * 获取版本详情 + * @param {Object} data - 版本数据(包含id) + * @returns {Promise} + */ + detail(data) { + return window.framework.http.post('/version/detail', data) + } + + /** + * 创建版本 + * @param {Object} data - 版本数据 + * @returns {Promise} + */ + add(data) { + return window.framework.http.post('/version/create', data) + } + + /** + * 更新版本 + * @param {Object} data - 版本数据 + * @returns {Promise} + */ + update(data) { + return window.framework.http.post('/version/update', data) + } + + /** + * 删除版本 + * @param {Object} row - 版本数据 + * @returns {Promise} + */ + del(row) { + return window.framework.http.post('/version/delete', { id: row.id }) + } + + /** + * 更新版本状态 + * @param {Object} data - 包含id和status + * @returns {Promise} + */ + updateStatus(data) { + return window.framework.http.post('/version/update_status', data) + } +} + +export default new VersionServer() + diff --git a/admin/src/router/component-map.js b/admin/src/router/component-map.js index 5a14c96..7ea7c04 100644 --- a/admin/src/router/component-map.js +++ b/admin/src/router/component-map.js @@ -19,6 +19,7 @@ import TaskStatus from '@/views/task/task_status.vue' // 系统设置模块 import SystemConfig from '@/views/system/system_config.vue' +import Version from '@/views/system/version.vue' import JobTypes from '@/views/work/job_types.vue' // 首页模块 @@ -49,6 +50,7 @@ const componentMap = { // 系统设置模块 'system/system_config': SystemConfig, + 'system/version': Version, 'work/job_types': JobTypes, 'home/index': HomeIndex, } diff --git a/admin/src/views/system/version.vue b/admin/src/views/system/version.vue new file mode 100644 index 0000000..c0fb439 --- /dev/null +++ b/admin/src/views/system/version.vue @@ -0,0 +1,282 @@ + + + + + + diff --git a/api/controller_admin/version.js b/api/controller_admin/version.js new file mode 100644 index 0000000..ab61110 --- /dev/null +++ b/api/controller_admin/version.js @@ -0,0 +1,517 @@ +/** + * 版本管理API - 后台管理 + * 提供版本信息的CRUD操作和版本控制功能 + */ + +const Framework = require("../../framework/node-core-framework.js"); +const version_service = require('../services/version_service.js'); +const config = require('../../config/config.js'); + +module.exports = { + /** + * @swagger + * /admin_api/version/list: + * post: + * summary: 获取版本列表 + * description: 分页获取所有版本信息 + * tags: [后台-版本管理] + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * seachOption: + * type: object + * description: 搜索条件 + * properties: + * key: + * type: string + * description: 搜索字段(version/platform/arch) + * value: + * type: string + * description: 搜索值 + * platform: + * type: string + * description: 平台筛选(win32/darwin/linux) + * arch: + * type: string + * description: 架构筛选(x64/ia32/arm64) + * status: + * type: integer + * description: 状态筛选(1:启用 0:禁用) + * pageOption: + * type: object + * description: 分页选项 + * properties: + * page: + * type: integer + * description: 页码 + * pageSize: + * type: integer + * description: 每页数量 + * responses: + * 200: + * description: 获取成功 + */ + 'POST /version/list': async (ctx) => { + const models = Framework.getModels(); + const { version_info } = models; + const { op } = models; + const body = ctx.getBody(); + + const seachOption = body.seachOption || {}; + const pageOption = body.pageOption || {}; + + // 获取分页参数 + const page = pageOption.page || 1; + const pageSize = pageOption.pageSize || 20; + const limit = pageSize; + const offset = (page - 1) * pageSize; + + const where = {}; + + // 平台筛选 + if (seachOption.platform) { + where.platform = seachOption.platform; + } + + // 架构筛选 + if (seachOption.arch) { + where.arch = seachOption.arch; + } + + // 状态筛选 + if (seachOption.status !== undefined && seachOption.status !== null) { + where.status = seachOption.status; + } + + // 搜索:版本号、平台、架构 + if (seachOption.key && seachOption.value) { + const key = seachOption.key; + const value = seachOption.value; + + if (key === 'version') { + where.version = { [op.like]: `%${value}%` }; + } else if (key === 'platform') { + where.platform = { [op.like]: `%${value}%` }; + } else if (key === 'arch') { + where.arch = { [op.like]: `%${value}%` }; + } + } + + const result = await version_info.findAndCountAll({ + where, + limit, + offset, + order: [['create_time', 'DESC']] + }); + + return ctx.success({ + rows: result.rows, + count: result.count + }); + }, + + /** + * @swagger + * /admin_api/version/detail: + * post: + * summary: 获取版本详情 + * description: 根据版本ID获取详细信息 + * tags: [后台-版本管理] + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * required: + * - id + * properties: + * id: + * type: integer + * description: 版本ID + * responses: + * 200: + * description: 获取成功 + */ + 'POST /version/detail': async (ctx) => { + const models = Framework.getModels(); + const { version_info } = models; + const { id } = ctx.getBody(); + + if (!id) { + return ctx.fail('版本ID不能为空'); + } + + const version = await version_info.findByPk(id); + + if (!version) { + return ctx.fail('版本不存在'); + } + + return ctx.success(version); + }, + + /** + * @swagger + * /admin_api/version/create: + * post: + * summary: 创建版本 + * description: 创建新版本信息 + * tags: [后台-版本管理] + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * required: + * - version + * - platform + * - arch + * - download_url + * - file_path + * properties: + * version: + * type: string + * description: 版本号(x.y.z 格式) + * platform: + * type: string + * description: 平台类型 + * arch: + * type: string + * description: 架构类型 + * download_url: + * type: string + * description: 下载地址 + * file_path: + * type: string + * description: 服务器文件路径 + * file_size: + * type: integer + * description: 文件大小(字节) + * file_hash: + * type: string + * description: SHA256 哈希值 + * release_notes: + * type: string + * description: 更新日志 + * force_update: + * type: integer + * description: 是否强制更新(1:是 0:否) + * status: + * type: integer + * description: 状态(1:启用 0:禁用) + * responses: + * 200: + * description: 创建成功 + */ + 'POST /version/create': async (ctx) => { + const models = Framework.getModels(); + const { version_info } = models; + const body = ctx.getBody(); + + // 参数验证 + if (!body.version) { + return ctx.fail('版本号不能为空'); + } + if (!body.platform) { + return ctx.fail('平台类型不能为空'); + } + if (!body.arch) { + return ctx.fail('架构类型不能为空'); + } + if (!body.download_url) { + return ctx.fail('下载地址不能为空'); + } + if (!body.file_path) { + return ctx.fail('文件路径不能为空'); + } + + // 验证版本号格式 + if (!version_service.is_valid_version(body.version)) { + return ctx.fail('版本号格式错误,应为 x.y.z 格式'); + } + + // 验证平台类型 + if (!version_service.is_valid_platform(body.platform)) { + return ctx.fail('平台类型错误,应为 win32/darwin/linux'); + } + + // 验证架构类型 + if (!version_service.is_valid_arch(body.arch)) { + return ctx.fail('架构类型错误,应为 x64/ia32/arm64'); + } + + // 检查版本是否已存在 + const existing = await version_info.findOne({ + where: { + version: body.version, + platform: body.platform, + arch: body.arch + } + }); + + if (existing) { + return ctx.fail('该版本已存在'); + } + + // 如果提供了文件路径,计算文件大小和哈希 + if (body.file_path) { + try { + const fs = require('fs'); + const path = require('path'); + const full_path = path.resolve(body.file_path); + + if (fs.existsSync(full_path)) { + // 计算文件大小 + if (!body.file_size) { + const stats = fs.statSync(full_path); + body.file_size = stats.size; + } + + // 计算文件哈希 + if (!body.file_hash) { + body.file_hash = await version_service.calculate_file_hash(full_path); + } + } + } catch (error) { + console.error('计算文件信息失败:', error); + } + } + + // 创建版本 + const version = await version_info.create({ + version: body.version, + platform: body.platform, + arch: body.arch, + download_url: body.download_url, + file_path: body.file_path, + file_size: body.file_size || 0, + file_hash: body.file_hash || '', + release_notes: body.release_notes || '', + force_update: body.force_update || 0, + status: body.status !== undefined ? body.status : 1 + }); + + return ctx.success(version, '版本创建成功'); + }, + + /** + * @swagger + * /admin_api/version/update: + * post: + * summary: 更新版本 + * description: 更新版本信息 + * tags: [后台-版本管理] + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * required: + * - id + * properties: + * id: + * type: integer + * description: 版本ID + * version: + * type: string + * description: 版本号 + * platform: + * type: string + * description: 平台类型 + * arch: + * type: string + * description: 架构类型 + * download_url: + * type: string + * description: 下载地址 + * file_path: + * type: string + * description: 文件路径 + * release_notes: + * type: string + * description: 更新日志 + * force_update: + * type: integer + * description: 是否强制更新 + * status: + * type: integer + * description: 状态 + * responses: + * 200: + * description: 更新成功 + */ + 'POST /version/update': async (ctx) => { + const models = Framework.getModels(); + const { version_info } = models; + const { op } = models; + const body = ctx.getBody(); + + if (!body.id) { + return ctx.fail('版本ID不能为空'); + } + + const version = await version_info.findByPk(body.id); + if (!version) { + return ctx.fail('版本不存在'); + } + + // 如果更新了版本号、平台或架构,检查是否重复 + if (body.version || body.platform || body.arch) { + const check_version = body.version || version.version; + const check_platform = body.platform || version.platform; + const check_arch = body.arch || version.arch; + + const existing = await version_info.findOne({ + where: { + version: check_version, + platform: check_platform, + arch: check_arch, + id: { [op.ne]: body.id } + } + }); + + if (existing) { + return ctx.fail('该版本已存在'); + } + } + + // 如果更新了文件路径,重新计算文件大小和哈希 + if (body.file_path && body.file_path !== version.file_path) { + try { + const fs = require('fs'); + const path = require('path'); + const full_path = path.resolve(body.file_path); + + if (fs.existsSync(full_path)) { + const stats = fs.statSync(full_path); + body.file_size = stats.size; + body.file_hash = await version_service.calculate_file_hash(full_path); + } + } catch (error) { + console.error('计算文件信息失败:', error); + } + } + + // 更新版本 + await version_info.update({ + version: body.version, + platform: body.platform, + arch: body.arch, + download_url: body.download_url, + file_path: body.file_path, + file_size: body.file_size, + file_hash: body.file_hash, + release_notes: body.release_notes, + force_update: body.force_update, + status: body.status + }, { + where: { id: body.id } + }); + + return ctx.success(null, '版本更新成功'); + }, + + /** + * @swagger + * /admin_api/version/delete: + * post: + * summary: 删除版本 + * description: 删除指定的版本信息 + * tags: [后台-版本管理] + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * required: + * - id + * properties: + * id: + * type: integer + * description: 版本ID + * responses: + * 200: + * description: 删除成功 + */ + 'POST /version/delete': async (ctx) => { + const models = Framework.getModels(); + const { version_info } = models; + const { id } = ctx.getBody(); + + if (!id) { + return ctx.fail('版本ID不能为空'); + } + + const result = await version_info.destroy({ + where: { id } + }); + + if (result === 0) { + return ctx.fail('版本不存在'); + } + + return ctx.success(null, '版本删除成功'); + }, + + /** + * @swagger + * /admin_api/version/update_status: + * post: + * summary: 更新版本状态 + * description: 启用或禁用版本 + * tags: [后台-版本管理] + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * required: + * - id + * - status + * properties: + * id: + * type: integer + * description: 版本ID + * status: + * type: integer + * description: 状态(1:启用 0:禁用) + * responses: + * 200: + * description: 更新成功 + */ + 'POST /version/update_status': async (ctx) => { + const models = Framework.getModels(); + const { version_info } = models; + const { id, status } = ctx.getBody(); + + if (!id) { + return ctx.fail('版本ID不能为空'); + } + + if (status === undefined || status === null) { + return ctx.fail('状态不能为空'); + } + + const result = await version_info.update({ + status: status + }, { + where: { id } + }); + + if (result[0] === 0) { + return ctx.fail('版本不存在'); + } + + return ctx.success(null, '状态更新成功'); + } +}; + diff --git a/api/controller_front/file.js b/api/controller_front/file.js index ea9fa4b..1aed454 100644 --- a/api/controller_front/file.js +++ b/api/controller_front/file.js @@ -1,7 +1,91 @@ const Framework = require("../../framework/node-core-framework.js"); const ossToolService = require('../services/oss_tool_service.js'); -module.exports = { +module.exports = { + /** + * @swagger + * /api/file/upload_oss: + * post: + * summary: 上传文件到OSS + * description: 将Base64编码的文件上传到阿里云OSS + * tags: [前端-文件管理] + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * required: + * - fileBase64 + * properties: + * fileBase64: + * type: string + * format: base64 + * description: Base64编码的文件内容 + * example: '/9j/4AAQSkZJRgABAQEAYABgAAD...' + * responses: + * 200: + * description: 上传成功 + * content: + * application/json: + * schema: + * type: object + * properties: + * code: + * type: integer + * description: 状态码,0表示成功 + * example: 0 + * message: + * type: string + * description: 响应消息 + * example: 'success' + * data: + * type: object + * properties: + * success: + * type: boolean + * description: 是否成功 + * example: true + * name: + * type: string + * description: OSS文件名称 + * path: + * type: string + * description: OSS文件URL + * ossPath: + * type: string + * description: OSS完整路径 + * fileType: + * type: string + * description: 文件类型 + * fileSize: + * type: integer + * description: 文件大小(字节) + * originalName: + * type: string + * description: 原始文件名 + * suffix: + * type: string + * description: 文件后缀 + * storagePath: + * type: string + * description: 存储路径 + * 400: + * description: 参数错误 + * content: + * application/json: + * schema: + * type: object + * properties: + * code: + * type: integer + * example: 400 + * message: + * type: string + * example: '缺少必要参数:fileBase64' + * 500: + * description: 服务器错误或上传失败 + */ 'POST /file/upload_oss': async (ctx) => { const body = ctx.getBody(); const { fileBase64 } = body; diff --git a/api/controller_front/user.js b/api/controller_front/user.js index f7db8a3..d0ea90d 100644 --- a/api/controller_front/user.js +++ b/api/controller_front/user.js @@ -1,6 +1,71 @@ const Framework = require("../../framework/node-core-framework.js"); module.exports = { + /** + * @swagger + * /api/user/login: + * post: + * summary: 用户登录 + * description: 通过设备SN码登录,返回token和用户信息 + * tags: [前端-用户管理] + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * required: + * - sn_code + * - device_id + * properties: + * sn_code: + * type: string + * description: 设备SN码 + * example: 'GHJU' + * device_id: + * type: string + * description: 设备ID + * example: 'device_123456' + * responses: + * 200: + * description: 登录成功 + * content: + * application/json: + * schema: + * type: object + * properties: + * code: + * type: integer + * description: 状态码,0表示成功 + * example: 0 + * message: + * type: string + * description: 响应消息 + * example: 'success' + * data: + * type: object + * properties: + * token: + * type: string + * description: 认证token + * example: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...' + * user: + * type: object + * description: 用户信息 + * 400: + * description: 参数错误或用户不存在 + * content: + * application/json: + * schema: + * type: object + * properties: + * code: + * type: integer + * example: 400 + * message: + * type: string + * example: '用户不存在' + */ "POST /user/login": async (ctx) => { const { sn_code, device_id } = ctx.getBody(); diff --git a/api/controller_front/version.js b/api/controller_front/version.js new file mode 100644 index 0000000..8a83743 --- /dev/null +++ b/api/controller_front/version.js @@ -0,0 +1,198 @@ +const Framework = require("../../framework/node-core-framework.js"); +const version_service = require('../services/version_service.js'); +const config = require('../../config/config.js'); + +/** + * 版本管理控制器 + * 提供版本检查、版本管理等功能 + */ +module.exports = { + /** + * @swagger + * /api/version/check: + * get: + * summary: 检查是否有新版本 + * description: 根据当前版本号、平台和架构检查是否有可用更新 + * tags: [前端-版本管理] + * parameters: + * - in: query + * name: current_version + * required: true + * schema: + * type: string + * pattern: '^\d+\.\d+\.\d+$' + * description: 当前版本号(x.y.z 格式,如 1.0.0) + * example: '1.0.0' + * - in: query + * name: platform + * required: true + * schema: + * type: string + * enum: [win32, darwin, linux] + * description: 平台类型 + * example: 'win32' + * - in: query + * name: arch + * required: true + * schema: + * type: string + * enum: [x64, ia32, arm64] + * description: 架构类型 + * example: 'x64' + * - in: query + * name: sn_code + * required: false + * schema: + * type: string + * description: 设备序列号(可选,用于权限控制) + * example: 'GHJU' + * responses: + * 200: + * description: 检查成功 + * content: + * application/json: + * schema: + * type: object + * properties: + * code: + * type: integer + * description: 状态码,0表示成功 + * example: 0 + * message: + * type: string + * description: 响应消息 + * example: 'success' + * data: + * type: object + * nullable: true + * description: 版本信息,null表示已是最新版本 + * properties: + * version: + * type: string + * description: 最新版本号 + * example: '1.1.0' + * download_url: + * type: string + * description: 下载地址 + * example: 'http://work.light120.com/downloads/app-1.1.0.exe' + * release_notes: + * type: string + * description: 更新日志 + * example: '修复了一些bug,新增了xxx功能' + * force_update: + * type: boolean + * description: 是否强制更新 + * example: false + * file_size: + * type: integer + * description: 文件大小(字节) + * example: 52428800 + * file_hash: + * type: string + * description: SHA256 哈希值 + * example: 'abc123def456...' + * 400: + * description: 参数错误 + * content: + * application/json: + * schema: + * type: object + * properties: + * code: + * type: integer + * example: 400 + * message: + * type: string + * example: '缺少必要参数:current_version' + * 500: + * description: 服务器错误 + */ + 'GET /version/check': async (ctx) => { + try { + // 获取请求参数 + const query = ctx.query || {}; + const current_version = query.current_version; + const platform = query.platform; + const arch = query.arch; + const sn_code = query.sn_code; // 可选,用于权限控制 + + // 参数验证 + if (!current_version) { + return ctx.fail('缺少必要参数:current_version', 400); + } + if (!platform) { + return ctx.fail('缺少必要参数:platform', 400); + } + if (!arch) { + return ctx.fail('缺少必要参数:arch', 400); + } + + // 验证版本号格式 + if (!version_service.is_valid_version(current_version)) { + return ctx.fail('版本号格式错误,应为 x.y.z 格式', 400); + } + + // 验证平台类型 + if (!version_service.is_valid_platform(platform)) { + return ctx.fail('平台类型错误,应为 win32/darwin/linux', 400); + } + + // 验证架构类型 + if (!version_service.is_valid_arch(arch)) { + return ctx.fail('架构类型错误,应为 x64/ia32/arm64', 400); + } + + // 获取模型 + const { version_info } = Framework.getModels(); + + // 查询所有启用状态的版本(按 platform + arch + status=1) + const all_versions = await version_info.findAll({ + where: { + platform: platform, + arch: arch, + status: 1 + } + }); + + // 如果没有找到版本信息 + if (!all_versions || all_versions.length === 0) { + return ctx.success(null, '未找到该平台的版本信息'); + } + + // 按版本号排序(降序) + all_versions.sort((a, b) => { + return version_service.compare_version(b.version, a.version); + }); + + const latest = all_versions[0]; + if (!latest) { + return ctx.success(null, '已是最新版本'); + } + + // 比较版本 + const has_update = version_service.has_new_version(current_version, latest.version); + + // 如果没有更新 + if (!has_update) { + return ctx.success(null, '已是最新版本'); + } + + // 构建返回数据 + const result = { + version: latest.version, + download_url: latest.download_url, + release_notes: latest.release_notes || '', + force_update: latest.force_update === 1, + file_size: latest.file_size || 0, + file_hash: latest.file_hash || '' + }; + + return ctx.success(result, 'success'); + + } catch (error) { + console.error('版本检查错误:', error); + return ctx.fail('服务器错误', 500); + } + } +}; + diff --git a/api/model/version_info.js b/api/model/version_info.js new file mode 100644 index 0000000..cdfc377 --- /dev/null +++ b/api/model/version_info.js @@ -0,0 +1,86 @@ +const Sequelize = require('sequelize'); + +/** + * 版本信息表模型 + * 存储应用版本信息,支持多平台多架构 + */ +module.exports = (db) => { + const version_info = db.define("version_info", { + // 版本基本信息 + version: { + comment: '版本号(x.y.z 格式)', + type: Sequelize.STRING(20), + allowNull: false + }, + platform: { + comment: '平台类型(win32/darwin/linux)', + type: Sequelize.STRING(20), + allowNull: false + }, + arch: { + comment: '架构类型(x64/ia32/arm64)', + type: Sequelize.STRING(20), + allowNull: false + }, + + // 文件信息 + download_url: { + comment: '下载地址', + type: Sequelize.STRING(500), + allowNull: false + }, + file_path: { + comment: '服务器文件路径', + type: Sequelize.STRING(500), + allowNull: false + }, + file_size: { + comment: '文件大小(字节)', + type: Sequelize.BIGINT, + allowNull: true, + defaultValue: 0 + }, + file_hash: { + comment: 'SHA256 哈希值', + type: Sequelize.STRING(64), + allowNull: true + }, + + // 更新信息 + release_notes: { + comment: '更新日志', + type: Sequelize.TEXT, + allowNull: true + }, + force_update: { + comment: '是否强制更新(1:是 0:否)', + type: Sequelize.TINYINT(1), + allowNull: false, + defaultValue: 0 + }, + + // 状态信息 + status: { + comment: '状态(1:启用 0:禁用)', + type: Sequelize.TINYINT(1), + allowNull: false, + defaultValue: 1 + }, + + }, { + timestamps: false, + indexes: [ + { + unique: true, + fields: ['version', 'platform', 'arch'] + }, + { + unique: false, + fields: ['platform', 'arch', 'status'] + } + ] + }); + + return version_info; +}; + diff --git a/api/services/version_service.js b/api/services/version_service.js new file mode 100644 index 0000000..063a391 --- /dev/null +++ b/api/services/version_service.js @@ -0,0 +1,145 @@ +const crypto = require('crypto'); +const fs = require('fs'); +const path = require('path'); + +/** + * 版本服务 + * 提供版本比较、文件哈希计算等功能 + */ +class VersionService { + /** + * 比较两个版本号 + * @param {string} version1 - 版本号1(x.y.z 格式) + * @param {string} version2 - 版本号2(x.y.z 格式) + * @returns {number} 返回 1 表示 version1 > version2,-1 表示 version1 < version2,0 表示相等 + */ + compare_version(version1, version2) { + if (!version1 || !version2) { + throw new Error('版本号不能为空'); + } + + // 验证版本号格式 + const version_pattern = /^\d+\.\d+\.\d+$/; + if (!version_pattern.test(version1) || !version_pattern.test(version2)) { + throw new Error('版本号格式错误,应为 x.y.z 格式'); + } + + // 将版本号按 "." 分割成数组 + const v1_parts = version1.split('.').map(Number); + const v2_parts = version2.split('.').map(Number); + + // 逐位比较 + for (let i = 0; i < 3; i++) { + if (v1_parts[i] > v2_parts[i]) { + return 1; + } else if (v1_parts[i] < v2_parts[i]) { + return -1; + } + } + + return 0; + } + + /** + * 检查是否有新版本 + * @param {string} current_version - 当前版本号 + * @param {string} latest_version - 最新版本号 + * @returns {boolean} 是否有新版本 + */ + has_new_version(current_version, latest_version) { + try { + const result = this.compare_version(latest_version, current_version); + return result > 0; + } catch (error) { + return false; + } + } + + /** + * 计算文件的 SHA256 哈希值 + * @param {string} file_path - 文件路径 + * @returns {Promise} SHA256 哈希值(小写十六进制字符串) + */ + async calculate_file_hash(file_path) { + return new Promise((resolve, reject) => { + // 检查文件是否存在 + if (!fs.existsSync(file_path)) { + return reject(new Error(`文件不存在: ${file_path}`)); + } + + // 创建哈希对象 + const hash = crypto.createHash('sha256'); + const stream = fs.createReadStream(file_path); + + stream.on('data', (data) => { + hash.update(data); + }); + + stream.on('end', () => { + const hash_value = hash.digest('hex'); + resolve(hash_value); + }); + + stream.on('error', (error) => { + reject(error); + }); + }); + } + + /** + * 获取文件大小 + * @param {string} file_path - 文件路径 + * @returns {Promise} 文件大小(字节) + */ + async get_file_size(file_path) { + return new Promise((resolve, reject) => { + if (!fs.existsSync(file_path)) { + return reject(new Error(`文件不存在: ${file_path}`)); + } + + fs.stat(file_path, (error, stats) => { + if (error) { + return reject(error); + } + resolve(stats.size); + }); + }); + } + + /** + * 验证版本号格式 + * @param {string} version - 版本号 + * @returns {boolean} 是否有效 + */ + is_valid_version(version) { + if (!version || typeof version !== 'string') { + return false; + } + const version_pattern = /^\d+\.\d+\.\d+$/; + return version_pattern.test(version); + } + + /** + * 验证平台类型 + * @param {string} platform - 平台类型 + * @returns {boolean} 是否有效 + */ + is_valid_platform(platform) { + const valid_platforms = ['win32', 'darwin', 'linux']; + return valid_platforms.includes(platform); + } + + /** + * 验证架构类型 + * @param {string} arch - 架构类型 + * @returns {boolean} 是否有效 + */ + is_valid_arch(arch) { + const valid_archs = ['x64', 'ia32', 'arm64']; + return valid_archs.includes(arch); + } +} + +// 导出单例 +module.exports = new VersionService(); + diff --git a/config/config.js b/config/config.js index 71597f9..9c2812f 100644 --- a/config/config.js +++ b/config/config.js @@ -63,7 +63,7 @@ module.exports = { }, // 白名单URL - 不需要token验证的接口 - "allowUrls": ["/admin_api/sys_user/login", "/admin_api/sys_user/authorityMenus", "/admin_api/sys_user/register", "/api/user/loginByWeixin", "/file/", "/sys_file/", "/admin_api/win_data/viewLogInfo", "/api/user/wx_auth", '/api/docs', 'api/swagger.json', 'payment/notify', 'payment/refund-notify', 'wallet/transfer_notify', 'user/sms/send', 'user/sms/verify'], + "allowUrls": ["/admin_api/sys_user/login", "/admin_api/sys_user/authorityMenus", "/admin_api/sys_user/register", "/api/user/loginByWeixin", "/file/", "/sys_file/", "/admin_api/win_data/viewLogInfo", "/api/user/wx_auth", '/api/docs', 'api/swagger.json', 'payment/notify', 'payment/refund-notify', 'wallet/transfer_notify', 'user/sms/send', 'user/sms/verify', '/api/version/check'], // AI服务配置