第 04 章

文档翻译

文档上传、异步翻译任务、进度轮询、译文 / 原文下载(MinIO 预签名 URL)与删除。

4.1 功能与限制

支持的文档格式(8 种,可经 app.document.supported-formats 配置)
pdf doc docx xls xlsx ppt pptx txt

上传限制:单文件 ≤ 50MB(spring.servlet.multipart.max-file-size), 单请求 ≤ 100MB;超限抛 MaxUploadSizeExceededException → HTTP 413 FILE_TOO_LARGE。 解析器按扩展名自动选择:PDF 用 Apache PDFBox 3,Office 系列用 Apache POI 5.2,txt 按行读取。

计费规则
usedPoints = 页数 × 1    // document-points-per-page = 1,即 1 点 / 页
// 扣费时机:POST /start/{id} 启动翻译时一次性预扣(写入 usedPointsSnapshot)
// 点数不足且会员额度不足:HTTP 402 INSUFFICIENT_POINTS,任务不会启动
任务状态机(ProcessingStatusEnum)
PENDING(1) 待处理 UPLOADING(2) 上传中 PROCESSING(3) 处理中 PAUSED(4) 已暂停 COMPLETED(5) 已完成 FAILED(6) 失败 CANCELLED(7) 已取消

正常链路:PENDING → PROCESSING → COMPLETED;任一步骤重试后仍失败则置 FAILED(含 errorMessage)。 翻译类型可选 STANDARD 标准翻译 / PROFESSIONAL 专业翻译 / FAST 快速翻译 / ACCURATE 精准翻译。

4.2 接口总览(前缀 /api/document)

POST/api/document/upload上传文档(multipart,存入 MinIO)
POST/api/document/start/{id}启动异步翻译任务(预扣点数)
GET/api/document/list文档任务分页列表
GET/api/document/progress/{id}查询翻译进度
GET/api/document/download-url/{id}获取译文预签名下载链接(600 秒)
GET/api/document/original-url/{id}获取原文预签名下载链接
GET/api/document/download/{id}直接流式下载译文文件
DELETE/api/document/delete/{id}删除文档任务

4.3 接口详情

POST

/api/document/upload

上传文档到 MinIO 并创建翻译任务记录(此时状态为 PENDING,尚未翻译、尚未扣费)。请求为 multipart/form-data。

请求参数(form-data)
字段类型必填默认值说明
fileFile必填—文档文件,扩展名须在 8 种白名单内,≤ 50MB
sourceLanguageString必填—源语言(可为 auto)
targetLanguageString必填—目标语言
translationTypeString可选STANDARD翻译类型枚举名
translationEngineString可选QWEN文档翻译默认走通义千问
useTerminologyBoolean可选false是否应用个人术语库
priorityInteger可选—任务优先级

响应 data 为 DocumentTranslation 实体,关键字段: id、originalFilename、fileType、fileSize、sourceLanguage / targetLanguage、 status(PENDING)、progress、downloadCount、characterCount、createdAt 等。

POST

/api/document/start/{id}

启动异步翻译任务:按页数预扣点数(1 点 / 页),提交至 document.translation.executor 线程池(4 核心 / 8 最大 / 50 队列)后立即返回。

响应 data(StartTranslationResponse)
{
  "id": 55,
  "originalFilename": "产品手册.docx",
  "fileSize": 1048576,
  "sourceLanguage": "zh",
  "targetLanguage": "en",
  "translationEngine": "QWEN",
  "status": "PROCESSING",       // 枚举名
  "progress": 0,
  "createdAt": "2026-09-06T10:00:00",
  "usedPoints": 12,              // 本次预扣点数(= 页数),无快照时为 0
  "pointsBalance": 88            // 扣减后余额,可为 null
}
GET

/api/document/list

分页查询当前用户的文档翻译任务。响应 data 为 MyBatis-Plus 分页对象(0 起始 page)。

Query 参数
参数必填说明
page可选默认 0(0 起始)
size可选每页条数,默认 10
GET

/api/document/progress/{id}

轮询翻译进度。前端建议每 2–3 秒调用一次,直至 status 为 COMPLETED 或 FAILED。

响应 data(DocumentProgressResponse)
{
  "documentId": 55,
  "taskId": "doc-task-55",
  "fileName": "产品手册.docx",
  "progress": 65,                    // 0–100 百分比
  "status": "PROCESSING",
  "statusMessage": "正在翻译第 8/12 页",
  "totalPages": 12,
  "processedPages": 8,
  "currentPage": 8,
  "estimatedCompletionTime": "2026-09-06T10:05:00",
  "processingTime": 120000,          // 已耗时(毫秒)
  "remainingTime": 60000,            // 预计剩余(毫秒)
  "errorMessage": null,
  "qualityScore": null,
  "useTerminology": false,
  "translationEngine": "QWEN"
}
GET

/api/document/download-url/{id}

获取译文文件的 MinIO 预签名下载链接,有效期 600 秒(内部 60 秒 × 10 次重放)。

响应 data(FileUrlResponse)
{
  "url": "http://minio:9002/translation-dev/translated/xxx.docx?X-Amz-...",
  "filename": "产品手册_翻译.docx"
}

⚠️ 任务未完成或无译文文件时抛 IllegalArgumentException("翻译结果不存在") → HTTP 400。 链接过期后需重新调用本接口获取。

GET

/api/document/original-url/{id}

获取原始文件的预签名下载链接,结构与 download-url 一致(FileUrlResponse)。

GET

/api/document/download/{id}

由服务端代理,直接以二进制流下载译文文件(响应为 byte[],不经过 ApiResponse 统一包装)。适合无法访问 MinIO 直链的内网环境。

DELETE

/api/document/delete/{id}

删除文档翻译任务记录。

响应 data(DeleteResponse)
{ "id": 55, "success": true }

4.4 异步任务生命周期

  1. 上传 upload 校验格式与大小 → 原文上传 MinIO → 创建 DocumentTranslation 记录(PENDING,未扣费)。
  2. 启动 start 统计总页数 → 按 1 点 / 页预扣点数(快照到 usedPointsSnapshot)→ 状态置 PROCESSING → 提交线程池异步执行。
  3. 解析与翻译 按扩展名选择解析器提取文本行 → 分批调用翻译引擎(带术语库上下文)→ 单批失败重试,重试仍失败保留原文占位不中断整体任务。
  4. 回写与上传 译文按原格式回写文档字节(PDFBox / POI)→ 译文上传 MinIO → 状态置 COMPLETED、progress 100。
  5. 轮询与下载 客户端轮询 progress/{id} 至终态 → 调 download-url/{id} 取 600 秒预签名链接下载,或直接走 download/{id} 流式下载。

全程接口均为「提交即返回」模式:start 不等待翻译结束,进度只能通过 progress/{id} 轮询获得(本系统无文档级 SSE 推送)。

4.5 完整调用示例

# 1. 上传文档
DOC_ID=$(curl -s -X POST http://localhost:8090/api/document/upload \
  -H "Authorization: Bearer $TOKEN" \
  -F "file=@./产品手册.docx" \
  -F "sourceLanguage=zh" \
  -F "targetLanguage=en" \
  -F "translationEngine=QWEN" \
  | jq -r '.data.id')

# 2. 启动翻译(预扣点数)
curl -X POST http://localhost:8090/api/document/start/$DOC_ID \
  -H "Authorization: Bearer $TOKEN"

# 3. 轮询进度直至 COMPLETED / FAILED
curl http://localhost:8090/api/document/progress/$DOC_ID \
  -H "Authorization: Bearer $TOKEN"

# 4. 获取译文下载链接(600 秒有效)
curl http://localhost:8090/api/document/download-url/$DOC_ID \
  -H "Authorization: Bearer $TOKEN"
async function translateDocument(token, file) {
  // 1. 上传
  const form = new FormData();
  form.append('file', file);
  form.append('sourceLanguage', 'zh');
  form.append('targetLanguage', 'en');
  const uploaded = await fetch('/api/document/upload', {
    method: 'POST',
    headers: { 'Authorization': `Bearer ${token}` },  // 浏览器勿手动设 Content-Type
    body: form
  }).then(r => r.json());
  const docId = uploaded.data.id;

  // 2. 启动
  await fetch(`/api/document/start/${docId}`, {
    method: 'POST',
    headers: { 'Authorization': `Bearer ${token}` }
  });

  // 3. 轮询进度
  while (true) {
    const p = await fetch(`/api/document/progress/${docId}`, {
      headers: { 'Authorization': `Bearer ${token}` }
    }).then(r => r.json());
    console.log(p.data.progress + '%');
    if (['COMPLETED', 'FAILED'].includes(p.data.status)) return p.data;
    await new Promise(r => setTimeout(r, 3000));
  }
}