diff --git a/_doc/发布的接口文档.md b/_doc/发布的接口文档.md new file mode 100644 index 0000000..48e8635 --- /dev/null +++ b/_doc/发布的接口文档.md @@ -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) + diff --git a/api/controller_front/file.js b/api/controller_front/file.js index 1aed454..9b6bf34 100644 --- a/api/controller_front/file.js +++ b/api/controller_front/file.js @@ -96,6 +96,17 @@ module.exports = { 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); } } \ No newline at end of file diff --git a/api/controller_front/version.js b/api/controller_front/version.js index 8a83743..2642844 100644 --- a/api/controller_front/version.js +++ b/api/controller_front/version.js @@ -1,6 +1,10 @@ const Framework = require("../../framework/node-core-framework.js"); const version_service = require('../services/version_service.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); 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); + } + }, + }; diff --git a/config/config.js b/config/config.js index 9c2812f..023ce53 100644 --- a/config/config.js +++ b/config/config.js @@ -35,10 +35,10 @@ module.exports = { ttl: 60 * 60 * 24 * 7 // 默认过期时间(7天) }, oos: { - "accessKeyId": "LTAI5t7kYFnwxKMBUgdQLvVT", - "accessKeySecret": "TqXxL6rTYaXDg4RGOgCukyc9gWgl54", - region: 'oss-cn-shanghai', - bucket: 'bimwe' + "accessKeyId": "LTAI5tENEdLxFU7Ne9wGazsk", + "accessKeySecret": "nvfvDfz2tLcOH8XE3EQpXsnBFpVyK5", + region: 'oss-cn-beijing', + bucket: 'light22600' }, // 分页配置