# 发布脚本使用说明

## 📋 概述

发布脚本 (`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: ${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: ${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': `${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: ${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 认证，格式为 `${token}`
   - 所有接口请求都需要在请求头中包含 `Authorization: ${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': `${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': `${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': `${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: 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: 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)

