TravelContentCreator/docs/JAVA_MIGRATION_GUIDE.md

18 KiB
Raw Blame History

Java 端改造指南

文档版本: 1.0.0
更新日期: 2024-12-09
Python 端状态: 已完成


1. 概述

Python 端 V2 API 已完成改造Java 端需要进行以下改造以完成整体迁移:

  1. 改为传完整对象 - 不再传 ID传完整的景区/产品/风格/人群对象
  2. 调用 V2 接口 - 从 /api/v1/tweet/* 改为 /api/v2/aigc/execute
  3. 缓存风格/人群配置 - 从 Python 端获取并缓存

2. Python V2 API 接口文档

2.1 配置查询接口

获取风格列表

GET /api/v2/aigc/config/styles

响应:
{
  "styles": [
    {
      "id": "gonglue",
      "name": "攻略风",
      "description": "以实用信息为主,包含详细的游玩路线、时间安排、费用预算等,语言平实靠谱",
      "icon": "📝",
      "order": 1
    },
    {
      "id": "tuijian",
      "name": "极力推荐风",
      "description": "热情洋溢的推荐风格,强调产品亮点和独特体验,语言感染力强",
      "icon": "🔥",
      "order": 2
    }
  ],
  "count": 2
}

获取人群列表

GET /api/v2/aigc/config/audiences

响应:
{
  "audiences": [
    {
      "id": "qinzi",
      "name": "亲子向",
      "description": "家庭出游,有小孩同行,关注安全性、趣味性和教育意义",
      "icon": "👨‍👩‍👧",
      "order": 1
    },
    {
      "id": "zhoubianyou",
      "name": "周边游",
      "description": "短途出游,周末或小长假,追求便捷和性价比",
      "icon": "🚗",
      "order": 2
    },
    {
      "id": "gaoshe",
      "name": "高奢酒店",
      "description": "高净值人群,追求品质与独特体验,价格敏感度低",
      "icon": "🏨",
      "order": 3
    }
  ],
  "count": 3
}

获取全部配置 (推荐)

GET /api/v2/aigc/config/all

响应:
{
  "styles": [...],
  "audiences": [...],
  "styles_count": 2,
  "audiences_count": 3
}

2.2 引擎执行接口

统一执行入口

POST /api/v2/aigc/execute

请求体:
{
  "engine": "topic_generate",  // 引擎 ID
  "params": { ... },           // 引擎参数 (完整对象)
  "async_mode": false          // 是否异步
}

响应 (同步模式):
{
  "success": true,
  "data": { ... },             // 引擎返回的数据
  "error": null,
  "error_code": null
}

响应 (异步模式):
{
  "success": true,
  "task_id": "abc123",
  "status": "pending",
  "estimated_duration": 30
}

2.3 引擎参数规范

topic_generate (选题生成)

{
  "engine": "topic_generate",
  "params": {
    "month": "2025-01",
    "num_topics": 5,
    "scenic_spot": {
      "id": 1,
      "name": "天津冒险湾",
      "description": "天津最大的水上乐园...",
      "address": "天津市滨海新区...",
      "location": "天津市",
      "highlights": ["水上过山车", "儿童戏水区"]
    },
    "product": {
      "id": 10,
      "name": "家庭套票",
      "price": 299,
      "original_price": 399,
      "description": "含2大1小门票..."
    },
    "style": {
      "id": "gonglue",
      "name": "攻略风"
    },
    "audience": {
      "id": "qinzi",
      "name": "亲子向"
    },
    "prompt_version": "latest"
  }
}

content_generate (内容生成)

{
  "engine": "content_generate",
  "params": {
    "topic": {
      "index": 1,
      "date": "2025-01-15",
      "title": "寒假遛娃好去处",
      "object": "天津冒险湾",
      "product": "家庭套票",
      "style": "攻略风",
      "targetAudience": "亲子向",
      "logic": "寒假期间家庭出游需求旺盛"
    },
    "scenic_spot": { ... },
    "product": { ... },
    "style": { "id": "gonglue", "name": "攻略风" },
    "audience": { "id": "qinzi", "name": "亲子向" },
    "refer_content": "",
    "enable_judge": true,
    "prompt_version": "latest"
  }
}

poster_generate (海报生成)

{
  "engine": "poster_generate",
  "params": {
    "template_id": "vibrant",
    "poster_content": {
      "title": "寒假特惠",
      "subtitle": "家庭套票限时优惠",
      "price": "299",
      "original_price": "399"
    },
    "image_urls": ["https://..."],
    "output_format": "png"
  }
}

3. Java 端改造清单

3.1 新增配置服务

// com/zowoyoo/zwypicture/service/AIGCConfigService.java

@Service
@Slf4j
public class AIGCConfigService {
    
    @Autowired
    private ExternalServiceClient pythonClient;
    
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;
    
    private static final String CACHE_KEY_STYLES = "aigc:config:styles";
    private static final String CACHE_KEY_AUDIENCES = "aigc:config:audiences";
    private static final long CACHE_TTL_HOURS = 1;
    
    /**
     * 获取所有风格配置
     */
    @SuppressWarnings("unchecked")
    public List<Map<String, Object>> getStyles() {
        // 1. 查缓存
        List<Map<String, Object>> cached = (List<Map<String, Object>>) 
            redisTemplate.opsForValue().get(CACHE_KEY_STYLES);
        if (cached != null) {
            return cached;
        }
        
        // 2. 从 Python 获取
        try {
            Map<String, Object> response = pythonClient.get(
                "content-generate",
                "/api/v2/aigc/config/styles",
                Map.class
            );
            
            List<Map<String, Object>> styles = 
                (List<Map<String, Object>>) response.get("styles");
            
            // 3. 缓存
            redisTemplate.opsForValue().set(
                CACHE_KEY_STYLES, 
                styles, 
                CACHE_TTL_HOURS, 
                TimeUnit.HOURS
            );
            
            return styles;
        } catch (Exception e) {
            log.error("获取风格配置失败", e);
            throw new BusinessException(ErrorCode.SYSTEM_ERROR, "获取风格配置失败");
        }
    }
    
    /**
     * 获取所有人群配置
     */
    @SuppressWarnings("unchecked")
    public List<Map<String, Object>> getAudiences() {
        List<Map<String, Object>> cached = (List<Map<String, Object>>) 
            redisTemplate.opsForValue().get(CACHE_KEY_AUDIENCES);
        if (cached != null) {
            return cached;
        }
        
        try {
            Map<String, Object> response = pythonClient.get(
                "content-generate",
                "/api/v2/aigc/config/audiences",
                Map.class
            );
            
            List<Map<String, Object>> audiences = 
                (List<Map<String, Object>>) response.get("audiences");
            
            redisTemplate.opsForValue().set(
                CACHE_KEY_AUDIENCES, 
                audiences, 
                CACHE_TTL_HOURS, 
                TimeUnit.HOURS
            );
            
            return audiences;
        } catch (Exception e) {
            log.error("获取人群配置失败", e);
            throw new BusinessException(ErrorCode.SYSTEM_ERROR, "获取人群配置失败");
        }
    }
    
    /**
     * 根据 ID 获取风格
     */
    public Map<String, Object> getStyleById(String styleId) {
        return getStyles().stream()
            .filter(s -> styleId.equals(s.get("id")))
            .findFirst()
            .orElseThrow(() -> new BusinessException(
                ErrorCode.NOT_FOUND, "风格不存在: " + styleId));
    }
    
    /**
     * 根据 ID 获取人群
     */
    public Map<String, Object> getAudienceById(String audienceId) {
        return getAudiences().stream()
            .filter(a -> audienceId.equals(a.get("id")))
            .findFirst()
            .orElseThrow(() -> new BusinessException(
                ErrorCode.NOT_FOUND, "人群不存在: " + audienceId));
    }
    
    /**
     * 刷新缓存
     */
    public void refreshCache() {
        redisTemplate.delete(CACHE_KEY_STYLES);
        redisTemplate.delete(CACHE_KEY_AUDIENCES);
        getStyles();
        getAudiences();
        log.info("AIGC 配置缓存已刷新");
    }
}

3.2 改造 TopicGenerateServiceImpl

// 改造前 (传 ID)
private TopicGenerateResponse callAIService(TopicGenerateRequest request) {
    Map<String, Object> requestBody = new HashMap<>();
    requestBody.put("dates", datesStr);
    requestBody.put("styleIds", request.getStyleIds());
    requestBody.put("audienceIds", request.getAudienceIds());
    requestBody.put("scenicSpotIds", request.getScenicSpotIds());
    requestBody.put("productIds", request.getProductIds());
    
    return externalServiceClient.post(SERVICE_NAME, "topics", requestBody, ...);
}

// 改造后 (传完整对象)
@Autowired
private AIGCConfigService aigcConfigService;

private TopicGenerateResponse callAIService(TopicGenerateRequest request) {
    // 1. 查询完整对象
    ScenicSpot scenicSpot = null;
    if (request.getScenicSpotIds() != null && !request.getScenicSpotIds().isEmpty()) {
        scenicSpot = scenicSpotService.getById(
            Long.parseLong(request.getScenicSpotIds().get(0)));
    }
    
    Product product = null;
    if (request.getProductIds() != null && !request.getProductIds().isEmpty()) {
        product = productService.getById(
            Long.parseLong(request.getProductIds().get(0)));
    }
    
    // 2. 获取风格/人群配置
    Map<String, Object> style = aigcConfigService.getStyleById(
        request.getStyleIds().get(0));
    Map<String, Object> audience = aigcConfigService.getAudienceById(
        request.getAudienceIds().get(0));
    
    // 3. 构建 V2 请求
    Map<String, Object> params = new HashMap<>();
    params.put("month", datesStr);
    params.put("num_topics", request.getNumTopics());
    
    if (scenicSpot != null) {
        params.put("scenic_spot", Map.of(
            "id", scenicSpot.getId(),
            "name", scenicSpot.getName(),
            "description", Optional.ofNullable(scenicSpot.getDescription()).orElse(""),
            "address", Optional.ofNullable(scenicSpot.getAddress()).orElse(""),
            "location", Optional.ofNullable(scenicSpot.getLocation()).orElse(""),
            "highlights", Optional.ofNullable(scenicSpot.getHighlights()).orElse(List.of())
        ));
    }
    
    if (product != null) {
        params.put("product", Map.of(
            "id", product.getId(),
            "name", product.getName(),
            "price", Optional.ofNullable(product.getPrice()).orElse(BigDecimal.ZERO),
            "original_price", Optional.ofNullable(product.getOriginalPrice()).orElse(BigDecimal.ZERO),
            "description", Optional.ofNullable(product.getDescription()).orElse("")
        ));
    }
    
    params.put("style", Map.of(
        "id", style.get("id"),
        "name", style.get("name")
    ));
    
    params.put("audience", Map.of(
        "id", audience.get("id"),
        "name", audience.get("name")
    ));
    
    Map<String, Object> requestBody = Map.of(
        "engine", "topic_generate",
        "params", params,
        "async_mode", false
    );
    
    // 4. 调用 V2 接口
    Map<String, Object> response = externalServiceClient.post(
        SERVICE_NAME,
        "/api/v2/aigc/execute",
        requestBody,
        Map.class
    );
    
    // 5. 解析响应
    return parseTopicResponse(response);
}

private TopicGenerateResponse parseTopicResponse(Map<String, Object> response) {
    if (!(Boolean) response.get("success")) {
        throw new BusinessException(ErrorCode.OPERATION_ERROR, 
            (String) response.get("error"));
    }
    
    @SuppressWarnings("unchecked")
    Map<String, Object> data = (Map<String, Object>) response.get("data");
    
    @SuppressWarnings("unchecked")
    List<Map<String, Object>> topics = (List<Map<String, Object>>) data.get("topics");
    
    // 转换为 TopicGenerateResponse
    TopicGenerateResponse result = new TopicGenerateResponse();
    result.setTopics(topics.stream()
        .map(this::convertToTopic)
        .collect(Collectors.toList()));
    
    return result;
}

3.3 改造 ContentGenerateServiceImpl

private ContentGenerateResponse callAIService(ContentGenerateRequest.ContentsInfo request) {
    // 1. 查询完整对象
    ScenicSpot scenicSpot = scenicSpotService.getById(
        Long.parseLong(request.getScenicSpotIds().get(0)));
    Product product = productService.getById(
        Long.parseLong(request.getProductIds().get(0)));
    
    Map<String, Object> style = aigcConfigService.getStyleById(
        request.getStyleIds().get(0));
    Map<String, Object> audience = aigcConfigService.getAudienceById(
        request.getAudienceIds().get(0));
    
    // 2. 构建 V2 请求
    Map<String, Object> params = new HashMap<>();
    params.put("topic", request.getTopic());
    params.put("scenic_spot", convertScenicSpotToMap(scenicSpot));
    params.put("product", convertProductToMap(product));
    params.put("style", Map.of("id", style.get("id"), "name", style.get("name")));
    params.put("audience", Map.of("id", audience.get("id"), "name", audience.get("name")));
    params.put("enable_judge", true);
    
    Map<String, Object> requestBody = Map.of(
        "engine", "content_generate",
        "params", params,
        "async_mode", false
    );
    
    // 3. 调用 V2 接口
    Map<String, Object> response = externalServiceClient.post(
        SERVICE_NAME,
        "/api/v2/aigc/execute",
        requestBody,
        Map.class
    );
    
    return parseContentResponse(response);
}

3.4 更新 ExternalServicesConfig

# application.yml

external-services:
  content-generate:
    enabled: true
    base-url: http://localhost:8000
    connect-timeout: 30
    read-timeout: 180  # 内容生成可能需要较长时间
    endpoints:
      # V2 接口 (新)
      execute: /api/v2/aigc/execute
      styles: /api/v2/aigc/config/styles
      audiences: /api/v2/aigc/config/audiences
      engines: /api/v2/aigc/engines
      
      # V1 接口 (旧,待废弃)
      topics: /api/v1/tweet/topics
      content: /api/v1/tweet/content

3.5 新增前端 API

// AIGCConfigController.java

@RestController
@RequestMapping("/api/aigc/config")
public class AIGCConfigController {
    
    @Autowired
    private AIGCConfigService aigcConfigService;
    
    @GetMapping("/styles")
    public BaseResponse<List<Map<String, Object>>> getStyles() {
        return ResultUtils.success(aigcConfigService.getStyles());
    }
    
    @GetMapping("/audiences")
    public BaseResponse<List<Map<String, Object>>> getAudiences() {
        return ResultUtils.success(aigcConfigService.getAudiences());
    }
    
    @PostMapping("/refresh")
    @AuthCheck(mustRole = "admin")
    public BaseResponse<Void> refreshCache() {
        aigcConfigService.refreshCache();
        return ResultUtils.success(null);
    }
}

4. 迁移步骤

4.1 准备阶段

  1. 确认 Python 服务已部署并可访问
  2. 测试 Python V2 API 接口
  3. 在 Java 项目中添加新的配置项

4.2 实施阶段

  1. 新增 AIGCConfigService
  2. 新增 AIGCConfigController
  3. 改造 TopicGenerateServiceImpl.callAIService()
  4. 改造 ContentGenerateServiceImpl.callAIService()
  5. 改造 PosterGenerateServiceImpl (如需要)
  6. 更新 application.yml 配置

4.3 测试阶段

  1. 单元测试
  2. 集成测试 (Java → Python)
  3. 端到端测试 (前端 → Java → Python)
  4. 性能测试

4.4 上线阶段

  1. 灰度发布
  2. 监控告警配置
  3. 回滚方案准备

5. 测试验证

5.1 Python 端测试

# 直接测试 (不启动服务)
cd /root/TravelContentCreator
PYTHONPATH=. python3 tests/test_v2_api_e2e.py --direct

# HTTP 测试 (需要启动服务)
python3 -m uvicorn api.main:app --host 0.0.0.0 --port 8000
python3 tests/test_v2_api_e2e.py --server http://localhost:8000

5.2 Java 端测试

@SpringBootTest
public class AIGCIntegrationTest {
    
    @Autowired
    private AIGCConfigService aigcConfigService;
    
    @Autowired
    private TopicGenerateService topicGenerateService;
    
    @Test
    public void testGetStyles() {
        List<Map<String, Object>> styles = aigcConfigService.getStyles();
        assertFalse(styles.isEmpty());
        assertTrue(styles.stream().anyMatch(s -> "gonglue".equals(s.get("id"))));
    }
    
    @Test
    public void testGenerateTopics() {
        TopicGenerateRequest request = new TopicGenerateRequest();
        request.setDates(List.of("2025-01-15"));
        request.setNumTopics(3);
        request.setScenicSpotIds(List.of("1"));
        request.setProductIds(List.of("10"));
        request.setStyleIds(List.of("gonglue"));
        request.setAudienceIds(List.of("qinzi"));
        
        TopicGenerateResponse response = topicGenerateService.generateAndSaveTopic(1L, request);
        
        assertNotNull(response);
        assertFalse(response.getTopics().isEmpty());
    }
}

6. 常见问题

Q1: 风格/人群 ID 从哪里来?

A: 风格/人群 ID 定义在 Python 端的 prompts/style/prompts/audience/ 目录中。Java 端通过 /api/v2/aigc/config/styles/api/v2/aigc/config/audiences 接口获取可用列表,并缓存到 Redis。

Q2: 如何新增风格/人群?

A:

  1. 在 Python 端 prompts/style/prompts/audience/ 目录下创建新的 YAML 文件
  2. 调用 Java 端 /api/aigc/config/refresh 刷新缓存
  3. 前端自动获取新的选项

Q3: 旧接口何时废弃?

A: 待 Java 端完全迁移到 V2 接口后Python 端的 V1 接口 (/api/v1/tweet/*) 将被废弃。建议保留 1-2 个版本周期作为过渡。

Q4: 超时如何处理?

A:

  • 选题生成: 建议 60 秒超时
  • 内容生成: 建议 120 秒超时 (含审核)
  • 海报生成: 建议 180 秒超时

7. 联系方式

如有问题,请联系:

  • Python 端: [开发团队]
  • Java 端: [开发团队]