1
This commit is contained in:
320
_doc/服务端升级功能逻辑说明.md
Normal file
320
_doc/服务端升级功能逻辑说明.md
Normal file
@@ -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. **管理功能**:版本管理后台、历史记录
|
||||
|
||||
20
_sql/create_version_info_table.sql
Normal file
20
_sql/create_version_info_table.sql
Normal file
@@ -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='版本信息表';
|
||||
|
||||
64
admin/src/api/system/version_server.js
Normal file
64
admin/src/api/system/version_server.js
Normal file
@@ -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()
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
282
admin/src/views/system/version.vue
Normal file
282
admin/src/views/system/version.vue
Normal file
@@ -0,0 +1,282 @@
|
||||
<template>
|
||||
<div class="content-view">
|
||||
<div class="table-head-tool">
|
||||
<Button type="primary" @click="showAddWarp">新增版本</Button>
|
||||
<Form ref="formInline" :model="gridOption.param.seachOption" inline :label-width="80">
|
||||
<FormItem :label-width="20" class="flex">
|
||||
<Select v-model="gridOption.param.seachOption.key" style="width: 120px"
|
||||
:placeholder="seachTypePlaceholder">
|
||||
<Option v-for="item in seachTypes" :value="item.key" :key="item.key">{{ item.value }}</Option>
|
||||
</Select>
|
||||
<Input class="ml10" v-model="gridOption.param.seachOption.value" style="width: 200px" search
|
||||
placeholder="请输入关键字" @on-search="query(1)" />
|
||||
</FormItem>
|
||||
<FormItem label="平台">
|
||||
<Select v-model="gridOption.param.seachOption.platform" style="width: 120px" clearable @on-change="query(1)">
|
||||
<Option value="win32">Windows</Option>
|
||||
<Option value="darwin">macOS</Option>
|
||||
<Option value="linux">Linux</Option>
|
||||
</Select>
|
||||
</FormItem>
|
||||
<FormItem label="架构">
|
||||
<Select v-model="gridOption.param.seachOption.arch" style="width: 120px" clearable @on-change="query(1)">
|
||||
<Option value="x64">x64</Option>
|
||||
<Option value="ia32">ia32</Option>
|
||||
<Option value="arm64">arm64</Option>
|
||||
</Select>
|
||||
</FormItem>
|
||||
<FormItem label="状态">
|
||||
<Select v-model="gridOption.param.seachOption.status" style="width: 120px" clearable @on-change="query(1)">
|
||||
<Option :value="1">启用</Option>
|
||||
<Option :value="0">禁用</Option>
|
||||
</Select>
|
||||
</FormItem>
|
||||
<FormItem>
|
||||
<Button type="primary" @click="query(1)">查询</Button>
|
||||
<Button type="default" @click="resetQuery" class="ml10">重置</Button>
|
||||
</FormItem>
|
||||
</Form>
|
||||
</div>
|
||||
<div class="table-body">
|
||||
<tables :columns="listColumns" :value="gridOption.data" :pageOption="gridOption.param.pageOption"
|
||||
@changePage="query"></tables>
|
||||
</div>
|
||||
<editModal ref="editModal" :columns="editColumns" :rules="gridOption.rules"></editModal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import versionServer from '@/api/system/version_server.js'
|
||||
|
||||
export default {
|
||||
data() {
|
||||
let rules = {}
|
||||
rules["version"] = [
|
||||
{ required: true, message: '请填写版本号', trigger: 'blur' },
|
||||
{ pattern: /^\d+\.\d+\.\d+$/, message: '版本号格式错误,应为 x.y.z 格式', trigger: 'blur' }
|
||||
]
|
||||
rules["platform"] = [{ required: true, message: '请选择平台', trigger: 'change' }]
|
||||
rules["arch"] = [{ required: true, message: '请选择架构', trigger: 'change' }]
|
||||
rules["download_url"] = [{ required: true, message: '请填写下载地址', trigger: 'blur' }]
|
||||
rules["file_path"] = [{ required: true, message: '请填写文件路径', trigger: 'blur' }]
|
||||
|
||||
return {
|
||||
seachTypes: [
|
||||
{ key: 'version', value: '版本号' },
|
||||
{ key: 'platform', value: '平台' },
|
||||
{ key: 'arch', value: '架构' }
|
||||
],
|
||||
gridOption: {
|
||||
param: {
|
||||
seachOption: {
|
||||
key: 'version',
|
||||
value: '',
|
||||
platform: null,
|
||||
arch: null,
|
||||
status: null
|
||||
},
|
||||
pageOption: {
|
||||
page: 1,
|
||||
pageSize: 20
|
||||
}
|
||||
},
|
||||
data: [],
|
||||
rules: rules
|
||||
},
|
||||
listColumns: [
|
||||
{ title: 'ID', key: 'id', minWidth: 80 },
|
||||
{ title: '版本号', key: 'version', minWidth: 120 },
|
||||
{
|
||||
title: '平台',
|
||||
key: 'platform',
|
||||
minWidth: 100,
|
||||
render: (h, params) => {
|
||||
const platformMap = {
|
||||
'win32': { text: 'Windows', color: 'blue' },
|
||||
'darwin': { text: 'macOS', color: 'green' },
|
||||
'linux': { text: 'Linux', color: 'orange' }
|
||||
}
|
||||
const platform = platformMap[params.row.platform] || { text: params.row.platform, color: 'default' }
|
||||
return h('Tag', { props: { color: platform.color } }, platform.text)
|
||||
}
|
||||
},
|
||||
{
|
||||
title: '架构',
|
||||
key: 'arch',
|
||||
minWidth: 80,
|
||||
render: (h, params) => {
|
||||
return h('Tag', { props: { color: 'cyan' } }, params.row.arch.toUpperCase())
|
||||
}
|
||||
},
|
||||
{
|
||||
title: '文件大小',
|
||||
key: 'file_size',
|
||||
minWidth: 120,
|
||||
render: (h, params) => {
|
||||
const size = params.row.file_size || 0
|
||||
const size_mb = (size / 1024 / 1024).toFixed(2)
|
||||
return h('span', `${size_mb} MB`)
|
||||
}
|
||||
},
|
||||
{
|
||||
title: '强制更新',
|
||||
key: 'force_update',
|
||||
minWidth: 100,
|
||||
render: (h, params) => {
|
||||
return h('Tag', {
|
||||
props: { color: params.row.force_update === 1 ? 'red' : 'default' }
|
||||
}, params.row.force_update === 1 ? '是' : '否')
|
||||
}
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
key: 'status',
|
||||
minWidth: 100,
|
||||
render: (h, params) => {
|
||||
return h('Tag', {
|
||||
props: { color: params.row.status === 1 ? 'success' : 'default' }
|
||||
}, params.row.status === 1 ? '启用' : '禁用')
|
||||
}
|
||||
},
|
||||
{ title: '下载地址', key: 'download_url', minWidth: 300 },
|
||||
{ title: '创建时间', key: 'create_time', minWidth: 150 },
|
||||
{
|
||||
title: '操作',
|
||||
key: 'action',
|
||||
width: 250,
|
||||
type: 'template',
|
||||
render: (h, params) => {
|
||||
let btns = [
|
||||
{
|
||||
title: '编辑',
|
||||
type: 'primary',
|
||||
click: () => {
|
||||
this.showEditWarp(params.row)
|
||||
},
|
||||
},
|
||||
{
|
||||
title: params.row.status === 1 ? '禁用' : '启用',
|
||||
type: params.row.status === 1 ? 'warning' : 'success',
|
||||
click: () => {
|
||||
this.toggleStatus(params.row)
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '删除',
|
||||
type: 'error',
|
||||
click: () => {
|
||||
this.delConfirm(params.row)
|
||||
},
|
||||
},
|
||||
]
|
||||
return window.framework.uiTool.getBtn(h, btns)
|
||||
},
|
||||
}
|
||||
],
|
||||
editColumns: [
|
||||
{ title: '版本号', key: 'version', type: 'text', required: true, placeholder: '例如:1.0.0' },
|
||||
{
|
||||
title: '平台',
|
||||
key: 'platform',
|
||||
type: 'select',
|
||||
required: true,
|
||||
options: [
|
||||
{ value: 'win32', label: 'Windows' },
|
||||
{ value: 'darwin', label: 'macOS' },
|
||||
{ value: 'linux', label: 'Linux' }
|
||||
]
|
||||
},
|
||||
{
|
||||
title: '架构',
|
||||
key: 'arch',
|
||||
type: 'select',
|
||||
required: true,
|
||||
options: [
|
||||
{ value: 'x64', label: 'x64' },
|
||||
{ value: 'ia32', label: 'ia32' },
|
||||
{ value: 'arm64', label: 'arm64' }
|
||||
]
|
||||
},
|
||||
{ title: '下载地址', key: 'download_url', type: 'text', required: true, placeholder: '完整的HTTP/HTTPS URL' },
|
||||
{ title: '文件路径', key: 'file_path', type: 'text', required: true, placeholder: '服务器文件路径' },
|
||||
{ title: '文件大小(字节)', key: 'file_size', type: 'number', placeholder: '可选,会自动计算' },
|
||||
{ title: '文件哈希(SHA256)', key: 'file_hash', type: 'text', placeholder: '可选,会自动计算' },
|
||||
{ title: '更新日志', key: 'release_notes', type: 'textarea', placeholder: '版本更新说明' },
|
||||
{ title: '强制更新', key: 'force_update', type: 'switch' },
|
||||
{ title: '状态', key: 'status', type: 'switch' }
|
||||
]
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.query(1)
|
||||
},
|
||||
methods: {
|
||||
query(page) {
|
||||
this.gridOption.param.pageOption.page = page
|
||||
versionServer.page(this.gridOption.param).then(res => {
|
||||
this.gridOption.data = res.data.rows
|
||||
this.gridOption.param.pageOption.total = res.data.count
|
||||
})
|
||||
},
|
||||
showAddWarp() {
|
||||
this.$refs.editModal.showModal()
|
||||
},
|
||||
showEditWarp(row) {
|
||||
this.$refs.editModal.showModal(row)
|
||||
},
|
||||
toggleStatus(row) {
|
||||
const new_status = row.status === 1 ? 0 : 1
|
||||
const action = new_status === 1 ? '启用' : '禁用'
|
||||
|
||||
this.$Modal.confirm({
|
||||
title: '确认操作',
|
||||
content: `确定要${action}该版本吗?`,
|
||||
onOk: async () => {
|
||||
try {
|
||||
await versionServer.updateStatus({ id: row.id, status: new_status })
|
||||
this.$Message.success(`${action}成功!`)
|
||||
this.query(1)
|
||||
} catch (error) {
|
||||
this.$Message.error(`${action}失败:${error.message || '未知错误'}`)
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
delConfirm(row) {
|
||||
window.framework.uiTool.delConfirm(async () => {
|
||||
await versionServer.del(row)
|
||||
this.$Message.success('删除成功!')
|
||||
this.query(1)
|
||||
})
|
||||
},
|
||||
resetQuery() {
|
||||
this.gridOption.param.seachOption = {
|
||||
key: 'version',
|
||||
value: '',
|
||||
platform: null,
|
||||
arch: null,
|
||||
status: null
|
||||
}
|
||||
this.query(1)
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
seachTypePlaceholder() {
|
||||
const selected = this.seachTypes.find(item => item.key === this.gridOption.param.seachOption.key)
|
||||
return selected ? selected.value : '请选择搜索类型'
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.ml10 {
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.flex {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
</style>
|
||||
|
||||
517
api/controller_admin/version.js
Normal file
517
api/controller_admin/version.js
Normal 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, '状态更新成功');
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
|
||||
198
api/controller_front/version.js
Normal file
198
api/controller_front/version.js
Normal 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
86
api/model/version_info.js
Normal 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;
|
||||
};
|
||||
|
||||
145
api/services/version_service.js
Normal file
145
api/services/version_service.js
Normal file
@@ -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<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();
|
||||
|
||||
@@ -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服务配置
|
||||
|
||||
Reference in New Issue
Block a user