This commit is contained in:
张成
2025-11-26 12:39:21 +08:00
parent 5d59000960
commit 7858459118
12 changed files with 1785 additions and 2 deletions

View File

@@ -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, '状态更新成功');
}
};

View File

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

View File

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

View File

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

86
api/model/version_info.js Normal file
View File

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

View File

@@ -0,0 +1,145 @@
const crypto = require('crypto');
const fs = require('fs');
const path = require('path');
/**
* 版本服务
* 提供版本比较、文件哈希计算等功能
*/
class VersionService {
/**
* 比较两个版本号
* @param {string} version1 - 版本号1x.y.z 格式)
* @param {string} version2 - 版本号2x.y.z 格式)
* @returns {number} 返回 1 表示 version1 > version2-1 表示 version1 < version20 表示相等
*/
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<string>} 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<number>} 文件大小(字节)
*/
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();