TravelContentCreator/docs/JAVA_ADAPTER_PLAN.md
jinye_huang 73cc12fe65 docs: 新增 Java 端适配方案
- PythonAIGCClient 统一客户端设计
- 选题/内容/海报三个 Service 层
- 提示词管理迁移到 Java 端 (数据库)
- 灰度切换策略
- 实施步骤和工时估算
2025-12-10 16:49:07 +08:00

17 KiB
Raw Blame History

Java 端适配方案

更新时间: 2024-12-10

概述

本文档描述 Java 端如何对接新的 Python AIGC 服务,包括:

  1. 新增 AIGC 服务适配层
  2. 提示词管理迁移到 Java 端
  3. 灰度切换策略

一、架构设计

┌─────────────────────────────────────────────────────────────┐
│                      Java 端 (主控)                          │
│                                                             │
│  ┌─────────────────┐  ┌─────────────────┐                  │
│  │ PromptManager   │  │ AIGCService     │                  │
│  │ (提示词管理)     │  │ (AIGC 服务调用)  │                  │
│  └────────┬────────┘  └────────┬────────┘                  │
│           │                    │                            │
│           ▼                    ▼                            │
│  ┌─────────────────────────────────────────┐               │
│  │         PythonApiClient                  │               │
│  │  (统一 HTTP 客户端)                       │               │
│  └─────────────────────────────────────────┘               │
└─────────────────────────────────────────────────────────────┘
                              │
                              ▼
┌─────────────────────────────────────────────────────────────┐
│                   Python AIGC 服务                           │
│                                                             │
│  POST /api/v2/aigc/execute                                  │
│  - engine: topic_generate / content_generate / poster_smart_v2 │
└─────────────────────────────────────────────────────────────┘

二、Java 端新增模块

2.1 目录结构

src/main/java/com/zowoyoo/zwypicture/
├── aigc/                          # 新增: AIGC 模块
│   ├── client/
│   │   └── PythonAIGCClient.java  # Python API 客户端
│   ├── service/
│   │   ├── TopicGenerateService.java
│   │   ├── ContentGenerateService.java
│   │   └── PosterGenerateService.java
│   ├── dto/
│   │   ├── request/
│   │   │   ├── TopicGenerateRequest.java
│   │   │   ├── ContentGenerateRequest.java
│   │   │   └── PosterGenerateRequest.java
│   │   └── response/
│   │       ├── TopicGenerateResponse.java
│   │       ├── ContentGenerateResponse.java
│   │       └── PosterGenerateResponse.java
│   └── controller/
│       └── AIGCController.java    # API 入口
│
├── prompt/                        # 新增: 提示词管理
│   ├── entity/
│   │   └── PromptTemplate.java    # 提示词实体
│   ├── repository/
│   │   └── PromptTemplateRepository.java
│   ├── service/
│   │   └── PromptService.java
│   └── controller/
│       └── PromptController.java

2.2 配置

# application.yml
aigc:
  python:
    # 新服务地址
    base-url: http://localhost:8001
    # 旧服务地址 (灰度切换用)
    legacy-url: http://localhost:8000
    # 超时配置
    connect-timeout: 5000
    read-timeout: 60000
  # 灰度开关
  use-new-service: true

三、核心代码设计

3.1 Python API 客户端

package com.zowoyoo.zwypicture.aigc.client;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.web.reactive.function.client.WebClient;
import reactor.core.publisher.Mono;

@Slf4j
@Component
@RequiredArgsConstructor
public class PythonAIGCClient {

    private final WebClient webClient;

    @Value("${aigc.python.base-url}")
    private String baseUrl;

    /**
     * 统一执行 AIGC 引擎
     */
    public <T> Mono<T> execute(String engine, Object params, Class<T> responseType) {
        AIGCExecuteRequest request = AIGCExecuteRequest.builder()
                .engine(engine)
                .params(params)
                .asyncMode(false)
                .build();

        return webClient.post()
                .uri(baseUrl + "/api/v2/aigc/execute")
                .bodyValue(request)
                .retrieve()
                .bodyToMono(responseType)
                .doOnError(e -> log.error("AIGC 调用失败: engine={}, error={}", engine, e.getMessage()));
    }

    /**
     * 执行选题生成
     */
    public Mono<TopicGenerateResponse> generateTopics(TopicGenerateRequest request) {
        return execute("topic_generate", request, TopicGenerateResponse.class);
    }

    /**
     * 执行内容生成
     */
    public Mono<ContentGenerateResponse> generateContent(ContentGenerateRequest request) {
        return execute("content_generate", request, ContentGenerateResponse.class);
    }

    /**
     * 执行海报生成
     */
    public Mono<PosterGenerateResponse> generatePoster(PosterGenerateRequest request) {
        return execute("poster_smart_v2", request, PosterGenerateResponse.class);
    }
}

3.2 请求/响应 DTO

// === 选题生成 ===
@Data
@Builder
public class TopicGenerateRequest {
    private Integer numTopics = 5;
    private String month;           // "2024-12"
    private SubjectInfo subject;    // 主体信息
    private StyleInfo style;        // 风格
    private AudienceInfo audience;  // 人群
    private HotTopics hotTopics;    // 热点
}

@Data
public class TopicGenerateResponse {
    private Boolean success;
    private TopicData data;
    private String error;

    @Data
    public static class TopicData {
        private List<Topic> topics;
        private Integer count;
    }
}

// === 内容生成 ===
@Data
@Builder
public class ContentGenerateRequest {
    private Topic topic;            // 选题
    private SubjectInfo subject;    // 主体信息
    private StyleInfo style;
    private AudienceInfo audience;
    private ReferenceContent reference;  // 参考内容
    private Boolean enableJudge = true;
}

@Data
public class ContentGenerateResponse {
    private Boolean success;
    private ContentData data;

    @Data
    public static class ContentData {
        private GeneratedContent content;
        private Topic topic;
        private Boolean judged;
    }
}

// === 海报生成 ===
@Data
@Builder
public class PosterGenerateRequest {
    private String category;        // 景点/美食/酒店/民宿/活动/攻略
    private String name;
    private String description;
    private String price;
    private String location;
    private String features;        // 逗号分隔
    private String imageUrl;
    private String overrideLayout;  // 可选
    private String overrideTheme;   // 可选
}

@Data
public class PosterGenerateResponse {
    private Boolean success;
    private PosterData data;

    @Data
    public static class PosterData {
        private String previewBase64;   // 预览图
        private Object fabricJson;      // Fabric.js JSON
        private String layout;
        private String theme;
        private Map<String, Object> content;
    }
}

3.3 Service 层

@Slf4j
@Service
@RequiredArgsConstructor
public class TopicGenerateService {

    private final PythonAIGCClient pythonClient;
    private final PromptService promptService;  // 提示词服务

    /**
     * 生成选题
     */
    public Mono<List<Topic>> generateTopics(
            Long scenicSpotId,
            Long productId,
            String styleId,
            String audienceId,
            String month,
            int count
    ) {
        // 1. 从数据库获取完整对象
        SubjectInfo subject = buildSubjectInfo(scenicSpotId, productId);
        StyleInfo style = promptService.getStyle(styleId);
        AudienceInfo audience = promptService.getAudience(audienceId);

        // 2. 构建请求
        TopicGenerateRequest request = TopicGenerateRequest.builder()
                .numTopics(count)
                .month(month)
                .subject(subject)
                .style(style)
                .audience(audience)
                .build();

        // 3. 调用 Python 服务
        return pythonClient.generateTopics(request)
                .map(response -> {
                    if (!response.getSuccess()) {
                        throw new AIGCException("选题生成失败: " + response.getError());
                    }
                    return response.getData().getTopics();
                });
    }

    private SubjectInfo buildSubjectInfo(Long scenicSpotId, Long productId) {
        // 从数据库查询景区和产品信息,组装成 SubjectInfo
        // ...
    }
}

3.4 Controller 层

@Slf4j
@RestController
@RequestMapping("/api/v2/aigc")
@RequiredArgsConstructor
public class AIGCController {

    private final TopicGenerateService topicService;
    private final ContentGenerateService contentService;
    private final PosterGenerateService posterService;

    /**
     * 生成选题
     */
    @PostMapping("/topic/generate")
    public Mono<ApiResponse<List<Topic>>> generateTopics(
            @RequestBody TopicGenerateRequestVO request
    ) {
        return topicService.generateTopics(
                request.getScenicSpotId(),
                request.getProductId(),
                request.getStyleId(),
                request.getAudienceId(),
                request.getMonth(),
                request.getCount()
        ).map(ApiResponse::success);
    }

    /**
     * 生成内容
     */
    @PostMapping("/content/generate")
    public Mono<ApiResponse<GeneratedContent>> generateContent(
            @RequestBody ContentGenerateRequestVO request
    ) {
        return contentService.generateContent(request)
                .map(ApiResponse::success);
    }

    /**
     * 生成海报
     */
    @PostMapping("/poster/generate")
    public Mono<ApiResponse<PosterResult>> generatePoster(
            @RequestBody PosterGenerateRequestVO request
    ) {
        return posterService.generatePoster(request)
                .map(ApiResponse::success);
    }
}

四、提示词管理迁移

4.1 数据库表设计

CREATE TABLE prompt_template (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    name VARCHAR(100) NOT NULL COMMENT '模板名称',
    type VARCHAR(50) NOT NULL COMMENT '类型: style/audience/system',
    version VARCHAR(20) DEFAULT 'v1.0.0',
    content TEXT NOT NULL COMMENT '模板内容',
    variables JSON COMMENT '变量定义',
    model_params JSON COMMENT '模型参数',
    status TINYINT DEFAULT 1 COMMENT '状态: 1启用 0禁用',
    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
    updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    UNIQUE KEY uk_name_version (name, version)
) COMMENT '提示词模板';

-- 预置风格
INSERT INTO prompt_template (name, type, content) VALUES
('xiaohongshu', 'style', '小红书种草风格活泼有趣善用emoji...'),
('gonglue', 'style', '攻略分享风格,干货满满,条理清晰...'),
('tuijian', 'style', '真诚推荐风格,真实体验,娓娓道来...');

-- 预置人群
INSERT INTO prompt_template (name, type, content) VALUES
('qinzi', 'audience', '亲子家庭,关注安全、趣味、教育...'),
('zhoubianyou', 'audience', '周边游用户,关注性价比、交通便利...'),
('gaoshe', 'audience', '高消费人群,关注品质、私密、独特...');

4.2 PromptService

@Service
@RequiredArgsConstructor
public class PromptService {

    private final PromptTemplateRepository repository;

    /**
     * 获取风格配置
     */
    public StyleInfo getStyle(String styleId) {
        PromptTemplate template = repository.findByNameAndType(styleId, "style")
                .orElseThrow(() -> new NotFoundException("风格不存在: " + styleId));

        return StyleInfo.builder()
                .id(styleId)
                .name(template.getName())
                .content(template.getContent())
                .build();
    }

    /**
     * 获取人群配置
     */
    public AudienceInfo getAudience(String audienceId) {
        PromptTemplate template = repository.findByNameAndType(audienceId, "audience")
                .orElseThrow(() -> new NotFoundException("人群不存在: " + audienceId));

        return AudienceInfo.builder()
                .id(audienceId)
                .name(template.getName())
                .content(template.getContent())
                .build();
    }

    /**
     * 获取所有风格列表 (供前端下拉)
     */
    public List<StyleInfo> listStyles() {
        return repository.findByTypeAndStatus("style", 1)
                .stream()
                .map(this::toStyleInfo)
                .collect(Collectors.toList());
    }

    /**
     * 获取所有人群列表
     */
    public List<AudienceInfo> listAudiences() {
        return repository.findByTypeAndStatus("audience", 1)
                .stream()
                .map(this::toAudienceInfo)
                .collect(Collectors.toList());
    }
}

4.3 管理接口

@RestController
@RequestMapping("/api/v2/prompt")
@RequiredArgsConstructor
public class PromptController {

    private final PromptService promptService;

    // 列表
    @GetMapping("/styles")
    public ApiResponse<List<StyleInfo>> listStyles() {
        return ApiResponse.success(promptService.listStyles());
    }

    @GetMapping("/audiences")
    public ApiResponse<List<AudienceInfo>> listAudiences() {
        return ApiResponse.success(promptService.listAudiences());
    }

    // CRUD
    @PostMapping
    public ApiResponse<PromptTemplate> create(@RequestBody PromptTemplate template) {
        return ApiResponse.success(promptService.create(template));
    }

    @PutMapping("/{id}")
    public ApiResponse<PromptTemplate> update(
            @PathVariable Long id,
            @RequestBody PromptTemplate template
    ) {
        return ApiResponse.success(promptService.update(id, template));
    }

    @DeleteMapping("/{id}")
    public ApiResponse<Void> delete(@PathVariable Long id) {
        promptService.delete(id);
        return ApiResponse.success();
    }
}

五、灰度切换策略

5.1 配置开关

@Configuration
@ConfigurationProperties(prefix = "aigc")
@Data
public class AIGCConfig {
    private boolean useNewService = false;  // 灰度开关
    private PythonConfig python = new PythonConfig();

    @Data
    public static class PythonConfig {
        private String baseUrl;     // 新服务
        private String legacyUrl;   // 旧服务
    }
}

5.2 切换逻辑

@Service
@RequiredArgsConstructor
public class AIGCRouter {

    private final AIGCConfig config;
    private final PythonAIGCClient newClient;
    private final LegacyAIGCClient legacyClient;

    public <T> Mono<T> route(String engine, Object params, Class<T> responseType) {
        if (config.isUseNewService()) {
            return newClient.execute(engine, params, responseType);
        } else {
            return legacyClient.execute(engine, params, responseType);
        }
    }
}

5.3 测试流程

1. 部署新 Python 服务到 8001 端口
2. Java 端配置 aigc.use-new-service=false (默认用旧服务)
3. 测试环境手动切换为 true验证新服务
4. 验证通过后,生产环境切换为 true
5. 观察一段时间后,移除旧服务

六、实施步骤

阶段 任务 工时
Phase 1 新增 PythonAIGCClient + DTO 0.5 天
Phase 2 新增 Service 层 (选题/内容/海报) 1 天
Phase 3 新增 Controller + 前端对接 0.5 天
Phase 4 提示词管理迁移 (数据库 + API) 1 天
Phase 5 灰度测试 + 切换 1 天

总计: 4 天


七、API 对照表

功能 Java 新接口 Python 引擎
选题生成 POST /api/v2/aigc/topic/generate topic_generate
内容生成 POST /api/v2/aigc/content/generate content_generate
海报生成 POST /api/v2/aigc/poster/generate poster_smart_v2
风格列表 GET /api/v2/prompt/styles - (Java 本地)
人群列表 GET /api/v2/prompt/audiences - (Java 本地)