This commit is contained in:
张成
2025-11-28 09:50:38 +08:00
parent 4db078c80a
commit 0e334116b6
4 changed files with 749 additions and 5 deletions

View File

@@ -0,0 +1,574 @@
# 发布脚本使用说明
## 📋 概述
发布脚本 (`scripts/publish.js`) 用于自动化应用的构建和发布流程,包括:
1. 清理构建目录
2. 构建应用NSIS 或便携版)
3. 调用接口创建版本记录
4. 上传压缩包到 OSS
## 🚀 快速开始
### 安装依赖
确保已安装 `form-data` 依赖:
```bash
npm install form-data --save-dev
```
### 基本使用
```bash
# 发布 NSIS 安装包(默认)
npm run publish
# 或直接运行脚本
node scripts/publish.js
```
## 📝 命令选项
### 构建类型
```bash
# 发布 NSIS 安装包
npm run publish:nsis
# 或
node scripts/publish.js --type nsis
# 发布便携版
npm run publish:portable
# 或
node scripts/publish.js --type portable
```
### 发布说明
```bash
node scripts/publish.js --notes "修复了若干bug优化了性能"
```
### 强制更新
```bash
node scripts/publish.js --force
```
### 跳过步骤
```bash
# 仅上传已构建的文件(跳过构建)
node scripts/publish.js --skip-build
# 仅构建不上传
node scripts/publish.js --skip-upload
```
### 查看帮助
```bash
node scripts/publish.js --help
```
## 🔧 配置要求
### 1. 配置文件
确保 `config/appConfig.js` 中包含有效的配置:
```javascript
module.exports = {
api_urls: {
dev: "http://work.light120.com/api",
prod: "http://work.light120.com/api"
},
token: "your-token-here", // 必须配置有效的 token
// ...
};
```
### 2. API 接口
脚本需要以下 API 接口:
#### 接口 1创建版本记录
- **接口地址**: `POST /api/version/create`
- **请求头**:
```
Content-Type: application/json
Authorization: Bearer ${token}
```
- **请求参数**:
```json
{
"version": "1.0.0", // 必填版本号x.y.z 格式)
"platform": "win32", // 必填平台类型win32/darwin/linux
"arch": "x64", // 必填架构类型x64/ia32/arm64
"download_url": "https://...", // 必填:下载地址(上传后更新)
"file_path": "/path/to/file", // 必填:服务器文件路径
"file_size": 12345678, // 可选:文件大小(字节),不提供会自动计算
"file_hash": "sha256-hash", // 可选SHA256 哈希值,不提供会自动计算
"release_notes": "发布说明", // 可选:更新日志
"force_update": 0, // 可选是否强制更新1:是 0:否),默认 0
"status": 1 // 可选状态1:启用 0:禁用),默认 1
}
```
- **响应示例**:
```json
{
"code": 0,
"message": "版本创建成功",
"data": {
"id": 123,
"version": "1.0.0",
"platform": "win32",
"arch": "x64",
"download_url": "https://oss.example.com/path/to/file.exe",
"file_path": "/path/to/file.exe",
"file_size": 12345678,
"file_hash": "sha256-hash-value",
"release_notes": "发布说明",
"force_update": 0,
"status": 1,
"create_time": "2024-01-01 12:00:00"
}
}
```
- **错误响应**:
```json
{
"code": 400,
"message": "版本号不能为空" // 或其他错误信息
}
```
- **注意事项**:
- 版本号格式必须为 `x.y.z`1.0.0
- 平台类型必须为:`win32`、`darwin` 或 `linux`
- 架构类型必须为:`x64`、`ia32` 或 `arm64`
- 如果版本已存在(相同 version + platform + arch会返回错误
- 如果提供了 `file_path` 但未提供 `file_size` 或 `file_hash`,接口会自动计算
#### 接口 2上传文件到 OSS
- **接口地址**: `POST /api/file/upload_version`
- **请求头**:
```
Content-Type: multipart/form-data
Authorization: Bearer ${token}
```
- **请求参数**Form Data:
| 参数名 | 类型 | 必填 | 说明 |
|--------|------|------|------|
| `file` | File | 是 | 文件内容(二进制) |
| `version` | String | 是 | 版本号1.0.0 |
| `platform` | String | 是 | 平台类型win32/darwin/linux |
| `arch` | String | 是 | 架构类型x64/ia32/arm64 |
| `file_hash` | String | 是 | SHA256 哈希值 |
| `file_size` | Number | 是 | 文件大小(字节) |
| `version_id` | Number | 否 | 版本记录 ID如果已创建版本记录 |
| `build_type` | String | 否 | 构建类型nsis/portable |
- **请求示例**(使用 form-data:
```javascript
const FormData = require('form-data');
const fs = require('fs');
const form = new FormData();
form.append('file', fs.createReadStream('./dist/app.exe'));
form.append('version', '1.0.0');
form.append('platform', 'win32');
form.append('arch', 'x64');
form.append('file_hash', 'sha256-hash-value');
form.append('file_size', 12345678);
form.append('version_id', 123); // 可选
// 发送请求
form.submit('http://api.example.com/api/file/upload_version', {
headers: {
'Authorization': `Bearer ${token}`
}
}, callback);
```
- **响应示例**:
```json
{
"code": 0,
"message": "文件上传成功",
"data": {
"download_url": "https://oss.example.com/versions/win32/x64/app-1.0.0.exe",
"file_path": "versions/win32/x64/app-1.0.0.exe",
"oss_path": "https://oss.example.com/versions/win32/x64/app-1.0.0.exe",
"file_size": 12345678,
"file_hash": "sha256-hash-value"
}
}
```
- **错误响应**:
```json
{
"code": 400,
"message": "文件上传失败:文件大小不匹配" // 或其他错误信息
}
```
- **注意事项**:
- 文件会按照 `versions/{platform}/{arch}/{filename}` 的路径结构上传到 OSS
- 上传前会验证文件哈希值,确保文件完整性
- 上传成功后,建议调用 `/version/update` 接口更新版本记录的下载地址
- 大文件上传建议设置较长的超时时间(建议 10 分钟以上)
#### 接口 3更新版本下载地址可选
- **接口地址**: `POST /api/version/update`
- **请求头**:
```
Content-Type: application/json
Authorization: Bearer ${token}
```
- **请求参数**:
```json
{
"id": 123, // 必填:版本记录 ID
"download_url": "https://...", // 可选:下载地址
"file_path": "/path/to/file", // 可选:文件路径
"file_hash": "sha256-hash", // 可选:文件哈希值
"file_size": 12345678, // 可选:文件大小
"release_notes": "更新说明", // 可选:更新日志
"force_update": 1, // 可选:是否强制更新
"status": 1 // 可选:状态
}
```
- **响应示例**:
```json
{
"code": 0,
"message": "版本更新成功",
"data": {
"id": 123,
"version": "1.0.0",
"download_url": "https://oss.example.com/path/to/file.exe",
// ... 其他字段
}
}
```
- **使用场景**:
- 文件上传成功后,更新版本记录的下载地址
- 修改版本的发布说明或强制更新标志
- 启用或禁用某个版本
## 📦 发布流程
1. **清理构建目录**
- 删除 `dist` 目录及其所有内容
2. **构建应用**
- 根据构建类型执行 `electron-builder`
- NSIS: `electron-builder --win nsis`
- Portable: `electron-builder --win portable`
3. **查找构建产物**
- 在 `dist` 目录中查找 `.exe` 文件
- 按文件大小排序,优先处理主安装包
4. **创建版本记录**
- 调用 `POST /api/version/create` 接口
- 传递版本信息、平台、架构等
- 获取版本记录 ID用于后续更新
5. **上传文件到 OSS**
- 计算文件 SHA256 哈希值
- 使用 `multipart/form-data` 格式调用 `POST /api/file/upload_version` 接口
- 传递文件、版本信息、哈希值等参数
- 获取上传后的下载地址
6. **更新版本记录**
- 使用获取到的下载地址调用 `POST /api/version/update` 接口
- 更新版本记录的 `download_url` 和 `file_path` 字段
## ⚠️ 注意事项
1. **Token 配置**
- 确保 `config/appConfig.js` 中有有效的 `token`
- Token 用于 API 认证,格式为 `Bearer ${token}`
- 所有接口请求都需要在请求头中包含 `Authorization: Bearer ${token}`
2. **接口路径**
- 所有接口路径前缀为 `/api`
- 完整路径示例:
- 创建版本:`POST http://api.example.com/api/version/create`
- 上传文件:`POST http://api.example.com/api/file/upload_version`
- 更新版本:`POST http://api.example.com/api/version/update`
3. **文件大小**
- 大文件上传可能需要较长时间
- 脚本默认超时时间为 10 分钟600000ms
- 建议大文件(>100MB增加超时时间到 30 分钟
4. **网络连接**
- 确保能够访问 API 服务器和 OSS
- 上传大文件时建议使用稳定的网络连接
- 建议在网络稳定的环境下执行发布流程
5. **版本号**
- 版本号从 `package.json` 的 `version` 字段读取
- 发布前确保版本号已更新
- 版本号格式必须符合 `x.y.z` 格式1.0.0
6. **构建产物**
- 脚本会自动查找 `dist` 目录中的 `.exe` 文件
- 排除 `builder` 和 `helper` 相关的辅助文件
- 确保构建产物文件名清晰,便于识别
7. **文件哈希验证**
- 上传前必须计算文件的 SHA256 哈希值
- 上传接口会验证文件哈希,确保文件完整性
- 哈希值不匹配会导致上传失败
8. **接口调用顺序**
- 建议先创建版本记录(获取 version_id
- 然后上传文件(可传递 version_id
- 最后更新版本记录的下载地址
- 也可以先上传文件,再创建版本记录并更新下载地址
## 🔍 故障排查
### 问题:上传失败
**可能原因**:
- Token 无效或过期
- API 接口地址不正确
- 网络连接问题
- 文件过大,超时
**解决方法**:
1. 检查 `config/appConfig.js` 中的 `token` 是否有效
2. 检查 API 地址是否正确
3. 检查网络连接
4. 如果文件很大,可以增加超时时间
### 问题:构建失败
**可能原因**:
- 缺少依赖
- electron-builder 配置错误
- 磁盘空间不足
**解决方法**:
1. 运行 `npm install` 安装所有依赖
2. 检查 `package.json` 中的 `build` 配置
3. 确保有足够的磁盘空间
### 问题:找不到构建产物
**可能原因**:
- 构建未成功完成
- 构建产物在其他位置
**解决方法**:
1. 检查构建日志,确认构建成功
2. 手动检查 `dist` 目录
3. 使用 `--skip-build` 选项,手动指定文件路径(需要修改脚本)
## 📝 示例
### 完整发布流程
```bash
# 1. 更新版本号(在 package.json 中)
# "version": "1.0.1"
# 2. 发布
npm run publish -- --notes "新功能:支持自动投递简历" --force
```
### 仅上传已构建的文件
```bash
# 1. 手动构建
npm run build:nsis
# 2. 仅上传
node scripts/publish.js --skip-build
```
### 测试发布(不上传)
```bash
# 构建并创建版本记录,但不上传文件
node scripts/publish.js --skip-upload
```
## 💻 接口调用示例
### Node.js 示例代码
#### 1. 创建版本记录
```javascript
const axios = require('axios');
const crypto = require('crypto');
const fs = require('fs');
async function createVersion() {
const filePath = './dist/app-1.0.0.exe';
const stats = fs.statSync(filePath);
const fileSize = stats.size;
// 计算文件哈希
const fileBuffer = fs.readFileSync(filePath);
const fileHash = crypto.createHash('sha256').update(fileBuffer).digest('hex');
const response = await axios.post('http://api.example.com/api/version/create', {
version: '1.0.0',
platform: 'win32',
arch: 'x64',
download_url: '', // 上传后更新
file_path: filePath,
file_size: fileSize,
file_hash: fileHash,
release_notes: '修复了若干bug优化了性能',
force_update: 0,
status: 1
}, {
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
}
});
return response.data.data; // 返回版本记录,包含 id
}
```
#### 2. 上传文件到 OSS
```javascript
const FormData = require('form-data');
const axios = require('axios');
const fs = require('fs');
const crypto = require('crypto');
async function uploadVersionFile(versionId) {
const filePath = './dist/app-1.0.0.exe';
const stats = fs.statSync(filePath);
const fileSize = stats.size;
// 计算文件哈希
const fileBuffer = fs.readFileSync(filePath);
const fileHash = crypto.createHash('sha256').update(fileBuffer).digest('hex');
// 创建 FormData
const form = new FormData();
form.append('file', fs.createReadStream(filePath));
form.append('version', '1.0.0');
form.append('platform', 'win32');
form.append('arch', 'x64');
form.append('file_hash', fileHash);
form.append('file_size', fileSize);
if (versionId) {
form.append('version_id', versionId);
}
const response = await axios.post('http://api.example.com/api/file/upload_version', form, {
headers: {
'Authorization': `Bearer ${token}`,
...form.getHeaders()
},
maxContentLength: Infinity,
maxBodyLength: Infinity,
timeout: 600000 // 10 分钟超时
});
return response.data.data; // 返回上传结果,包含 download_url
}
```
#### 3. 更新版本下载地址
```javascript
async function updateVersionDownloadUrl(versionId, downloadUrl, fileHash) {
const response = await axios.post('http://api.example.com/api/version/update', {
id: versionId,
download_url: downloadUrl,
file_hash: fileHash
}, {
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
}
});
return response.data.data;
}
```
#### 4. 完整发布流程示例
```javascript
async function publishVersion() {
try {
// 1. 创建版本记录
console.log('创建版本记录...');
const version = await createVersion();
console.log('版本记录创建成功ID:', version.id);
// 2. 上传文件
console.log('上传文件到 OSS...');
const uploadResult = await uploadVersionFile(version.id);
console.log('文件上传成功,下载地址:', uploadResult.download_url);
// 3. 更新版本下载地址
console.log('更新版本下载地址...');
await updateVersionDownloadUrl(version.id, uploadResult.download_url, uploadResult.file_hash);
console.log('版本发布完成!');
} catch (error) {
console.error('发布失败:', error.response?.data || error.message);
throw error;
}
}
```
### cURL 示例
#### 创建版本记录
```bash
curl -X POST http://api.example.com/api/version/create \
-H "Authorization: Bearer your-token-here" \
-H "Content-Type: application/json" \
-d '{
"version": "1.0.0",
"platform": "win32",
"arch": "x64",
"download_url": "",
"file_path": "/path/to/file.exe",
"file_size": 12345678,
"file_hash": "sha256-hash-value",
"release_notes": "发布说明",
"force_update": 0,
"status": 1
}'
```
#### 上传文件
```bash
curl -X POST http://api.example.com/api/file/upload_version \
-H "Authorization: Bearer your-token-here" \
-F "file=@./dist/app-1.0.0.exe" \
-F "version=1.0.0" \
-F "platform=win32" \
-F "arch=x64" \
-F "file_hash=sha256-hash-value" \
-F "file_size=12345678" \
-F "version_id=123"
```
## 🔗 相关文档
- [API 配置说明](./API_CONFIG.md)
- [打包说明](./BUILD.md)
- [更新逻辑检查报告](./更新逻辑检查报告.md)

View File

@@ -96,6 +96,17 @@ module.exports = {
let result = await ossToolService.uploadStream(buffer, 'image/jpeg', 'jpg') let result = await ossToolService.uploadStream(buffer, 'image/jpeg', 'jpg')
return ctx.success(result);
},
'POST /file/upload_file_to_oss_by_auto_work': async (ctx) => {
const file =ctx.request.files.file ;
const result = await ossToolService.uploadFile(file, 'work_boss');
return ctx.success(result); return ctx.success(result);
} }
} }

View File

@@ -1,6 +1,10 @@
const Framework = require("../../framework/node-core-framework.js"); const Framework = require("../../framework/node-core-framework.js");
const version_service = require('../services/version_service.js'); const version_service = require('../services/version_service.js');
const config = require('../../config/config.js'); const config = require('../../config/config.js');
const ossToolService = require('../services/oss_tool_service.js');
const crypto = require('crypto');
const fs = require('fs');
const path = require('path');
/** /**
* 版本管理控制器 * 版本管理控制器
@@ -193,6 +197,161 @@ module.exports = {
console.error('版本检查错误:', error); console.error('版本检查错误:', error);
return ctx.fail('服务器错误', 500); return ctx.fail('服务器错误', 500);
} }
},
/**
* @swagger
* /api/version/create:
* post:
* summary: 创建版本记录
* description: 创建新版本信息,用于发布脚本自动创建版本
* tags: [前端-版本管理]
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* required:
* - version
* - platform
* - arch
* properties:
* version:
* type: string
* description: 版本号x.y.z 格式)
* example: '1.0.0'
* platform:
* type: string
* enum: [win32, darwin, linux]
* description: 平台类型
* example: 'win32'
* arch:
* type: string
* enum: [x64, ia32, arm64]
* description: 架构类型
* example: 'x64'
* download_url:
* type: string
* description: 下载地址(上传后更新)
* example: ''
* file_path:
* type: string
* description: 服务器文件路径
* example: '/path/to/file.exe'
* file_size:
* type: integer
* description: 文件大小(字节)
* example: 12345678
* file_hash:
* type: string
* description: SHA256 哈希值
* example: 'sha256-hash-value'
* release_notes:
* type: string
* description: 更新日志
* example: '修复了若干bug优化了性能'
* force_update:
* type: integer
* description: 是否强制更新1:是 0:否)
* example: 0
* status:
* type: integer
* description: 状态1:启用 0:禁用)
* example: 1
* responses:
* 200:
* description: 创建成功
* 400:
* description: 参数错误
*/
'POST /version/create': async (ctx) => {
try {
const models = Framework.getModels();
const { version_info } = models;
const body = ctx.getBody();
// 参数验证
if (!body.version) {
return ctx.fail('版本号不能为空', 400);
} }
if (!body.platform) {
return ctx.fail('平台类型不能为空', 400);
}
if (!body.arch) {
return ctx.fail('架构类型不能为空', 400);
}
// 验证版本号格式
if (!version_service.is_valid_version(body.version)) {
return ctx.fail('版本号格式错误,应为 x.y.z 格式', 400);
}
// 验证平台类型
if (!version_service.is_valid_platform(body.platform)) {
return ctx.fail('平台类型错误,应为 win32/darwin/linux', 400);
}
// 验证架构类型
if (!version_service.is_valid_arch(body.arch)) {
return ctx.fail('架构类型错误,应为 x64/ia32/arm64', 400);
}
// 检查版本是否已存在
const existing = await version_info.findOne({
where: {
version: body.version,
platform: body.platform,
arch: body.arch
}
});
if (existing) {
return ctx.fail('该版本已存在', 400);
}
// 如果提供了文件路径,计算文件大小和哈希
if (body.file_path) {
try {
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, '版本创建成功');
} catch (error) {
console.error('创建版本错误:', error);
return ctx.fail('服务器错误', 500);
}
},
}; };

View File

@@ -35,10 +35,10 @@ module.exports = {
ttl: 60 * 60 * 24 * 7 // 默认过期时间(7天) ttl: 60 * 60 * 24 * 7 // 默认过期时间(7天)
}, },
oos: { oos: {
"accessKeyId": "LTAI5t7kYFnwxKMBUgdQLvVT", "accessKeyId": "LTAI5tENEdLxFU7Ne9wGazsk",
"accessKeySecret": "TqXxL6rTYaXDg4RGOgCukyc9gWgl54", "accessKeySecret": "nvfvDfz2tLcOH8XE3EQpXsnBFpVyK5",
region: 'oss-cn-shanghai', region: 'oss-cn-beijing',
bucket: 'bimwe' bucket: 'light22600'
}, },
// 分页配置 // 分页配置