SharedTranslateController.java
package com.yumu.noveltranslator.adapter.in.rest.shared;
import com.yumu.noveltranslator.port.dto.common.Result;
import com.yumu.noveltranslator.port.dto.entity.TaskStatusResponse;
import com.yumu.noveltranslator.port.dto.translation.TranslationResultResponse;
import com.yumu.noveltranslator.port.dto.translation.RagTranslationResponse;
import com.yumu.noveltranslator.port.dto.translation.RagTranslationRequest;
import com.yumu.noveltranslator.domain.model.Document;
import com.yumu.noveltranslator.domain.model.TranslationTask;
import com.yumu.noveltranslator.enums.ErrorCodeEnum;
import com.yumu.noveltranslator.port.in.RagTranslationPort;
import com.yumu.noveltranslator.port.in.TranslationTaskPort;
import com.yumu.noveltranslator.util.SecurityUtil;
import jakarta.validation.Valid;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
/**
* 共享翻译接口(Web 和插件共用)
* 路径前缀: /v1/translate
*/
@RestController
@RequestMapping("/v1/translate")
@RequiredArgsConstructor
public class SharedTranslateController {
private final TranslationTaskPort translationTaskPort;
private final RagTranslationPort ragTranslationPort;
/**
* 查询翻译任务状态
* GET /v1/translate/task/{taskId}
*/
@GetMapping("/task/{taskId}")
public Result<TaskStatusResponse> getTaskStatus(@PathVariable String taskId) {
TranslationTask task = translationTaskPort.getTaskByTaskId(taskId);
if (task == null) {
return Result.error(ErrorCodeEnum.NOT_FOUND, "任务不存在");
}
return Result.ok(translationTaskPort.toTaskStatusResponse(task));
}
/**
* 取消翻译任务
* DELETE /v1/translate/task/{taskId}
*/
@DeleteMapping("/task/{taskId}")
@PreAuthorize("isAuthenticated()")
public Result cancelTask(@PathVariable String taskId) {
Long userId = SecurityUtil.getRequiredUserId();
if (translationTaskPort.cancelTask(taskId, userId)) {
return Result.ok(null);
} else {
return Result.error(ErrorCodeEnum.INVALID_STATE, "取消失败,任务可能已完成或正在处理");
}
}
/**
* 删除翻译历史记录
* DELETE /v1/translate/history/{taskId}
*/
@DeleteMapping("/history/{taskId}")
@PreAuthorize("isAuthenticated()")
public Result deleteHistory(@PathVariable String taskId) {
Long userId = SecurityUtil.getRequiredUserId();
if (translationTaskPort.deleteHistory(taskId, userId)) {
return Result.ok(null);
} else {
return Result.error(ErrorCodeEnum.NOT_FOUND, "记录不存在");
}
}
/**
* 获取翻译结果
* GET /v1/translate/task/{taskId}/result
*/
@GetMapping("/task/{taskId}/result")
public Result<TranslationResultResponse> getTranslationResult(@PathVariable String taskId) {
TranslationResultResponse result = translationTaskPort.getTranslationResult(taskId);
if (result == null) {
return Result.error(ErrorCodeEnum.NOT_FOUND, "任务不存在或结果不可用");
}
return Result.ok(result);
}
/**
* 下载翻译结果
* GET /v1/translate/task/{taskId}/download
*/
@GetMapping("/task/{taskId}/download")
@PreAuthorize("isAuthenticated()")
public ResponseEntity<byte[]> downloadTranslation(@PathVariable String taskId) {
Long userId = SecurityUtil.getRequiredUserId();
String filePath = translationTaskPort.getDownloadPath(taskId, userId);
if (filePath == null) {
return ResponseEntity.notFound().build();
}
try {
byte[] fileContent = Files.readAllBytes(Paths.get(filePath));
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_OCTET_STREAM);
headers.setContentDispositionFormData("attachment", "translated_" + taskId);
return new ResponseEntity<>(fileContent, headers, HttpStatus.OK);
} catch (IOException e) {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
}
}
/**
* 文档流式翻译(SSE)— 直接上传文件并翻译
* POST /v1/translate/document/stream
*/
@PostMapping(value = "/document/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public SseEmitter streamDocumentTranslate(
@RequestParam("file") MultipartFile file,
@RequestParam(value = "sourceLang", defaultValue = "auto") String sourceLang,
@RequestParam(value = "targetLang", defaultValue = "zh") String targetLang,
@RequestParam(value = "mode", defaultValue = "fast") String mode) {
return translationTaskPort.streamTranslateDocument(file, sourceLang, targetLang, mode);
}
/**
* 文档流式翻译(SSE)— 基于已上传的文档
* POST /v1/translate/document/stream/{docId}
*/
@PostMapping(value = "/document/stream/{docId}", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public SseEmitter streamDocumentTranslateById(
@PathVariable Long docId,
@RequestParam(value = "targetLang", defaultValue = "zh") String targetLang,
@RequestParam(value = "mode", defaultValue = "fast") String mode) {
return translationTaskPort.streamTranslateDocumentById(docId, targetLang, mode);
}
/**
* RAG 翻译记忆查询
* POST /v1/translate/rag
*/
@PostMapping("/rag")
public Result<RagTranslationResponse> queryRag(@RequestBody @Valid RagTranslationRequest request) {
Long userId = SecurityUtil.getCurrentUserId().orElse(null);
RagTranslationResponse response = ragTranslationPort.searchSimilarWithModes(
userId, request.getText(), request.getTargetLang(),
request.getEngine() != null ? List.of(request.getEngine()) : List.of());
return Result.ok(response);
}
}