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);
    }
}