# 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 (选题生成) ```json { "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 (内容生成) ```json { "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 (海报生成) ```json { "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 新增配置服务 ```java // com/zowoyoo/zwypicture/service/AIGCConfigService.java @Service @Slf4j public class AIGCConfigService { @Autowired private ExternalServiceClient pythonClient; @Autowired private RedisTemplate 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> getStyles() { // 1. 查缓存 List> cached = (List>) redisTemplate.opsForValue().get(CACHE_KEY_STYLES); if (cached != null) { return cached; } // 2. 从 Python 获取 try { Map response = pythonClient.get( "content-generate", "/api/v2/aigc/config/styles", Map.class ); List> styles = (List>) 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> getAudiences() { List> cached = (List>) redisTemplate.opsForValue().get(CACHE_KEY_AUDIENCES); if (cached != null) { return cached; } try { Map response = pythonClient.get( "content-generate", "/api/v2/aigc/config/audiences", Map.class ); List> audiences = (List>) 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 getStyleById(String styleId) { return getStyles().stream() .filter(s -> styleId.equals(s.get("id"))) .findFirst() .orElseThrow(() -> new BusinessException( ErrorCode.NOT_FOUND, "风格不存在: " + styleId)); } /** * 根据 ID 获取人群 */ public Map 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 ```java // 改造前 (传 ID) private TopicGenerateResponse callAIService(TopicGenerateRequest request) { Map 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 style = aigcConfigService.getStyleById( request.getStyleIds().get(0)); Map audience = aigcConfigService.getAudienceById( request.getAudienceIds().get(0)); // 3. 构建 V2 请求 Map 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 requestBody = Map.of( "engine", "topic_generate", "params", params, "async_mode", false ); // 4. 调用 V2 接口 Map response = externalServiceClient.post( SERVICE_NAME, "/api/v2/aigc/execute", requestBody, Map.class ); // 5. 解析响应 return parseTopicResponse(response); } private TopicGenerateResponse parseTopicResponse(Map response) { if (!(Boolean) response.get("success")) { throw new BusinessException(ErrorCode.OPERATION_ERROR, (String) response.get("error")); } @SuppressWarnings("unchecked") Map data = (Map) response.get("data"); @SuppressWarnings("unchecked") List> topics = (List>) data.get("topics"); // 转换为 TopicGenerateResponse TopicGenerateResponse result = new TopicGenerateResponse(); result.setTopics(topics.stream() .map(this::convertToTopic) .collect(Collectors.toList())); return result; } ``` ### 3.3 改造 ContentGenerateServiceImpl ```java 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 style = aigcConfigService.getStyleById( request.getStyleIds().get(0)); Map audience = aigcConfigService.getAudienceById( request.getAudienceIds().get(0)); // 2. 构建 V2 请求 Map 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 requestBody = Map.of( "engine", "content_generate", "params", params, "async_mode", false ); // 3. 调用 V2 接口 Map response = externalServiceClient.post( SERVICE_NAME, "/api/v2/aigc/execute", requestBody, Map.class ); return parseContentResponse(response); } ``` ### 3.4 更新 ExternalServicesConfig ```yaml # 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 ```java // AIGCConfigController.java @RestController @RequestMapping("/api/aigc/config") public class AIGCConfigController { @Autowired private AIGCConfigService aigcConfigService; @GetMapping("/styles") public BaseResponse>> getStyles() { return ResultUtils.success(aigcConfigService.getStyles()); } @GetMapping("/audiences") public BaseResponse>> getAudiences() { return ResultUtils.success(aigcConfigService.getAudiences()); } @PostMapping("/refresh") @AuthCheck(mustRole = "admin") public BaseResponse 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 端测试 ```bash # 直接测试 (不启动服务) 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 端测试 ```java @SpringBootTest public class AIGCIntegrationTest { @Autowired private AIGCConfigService aigcConfigService; @Autowired private TopicGenerateService topicGenerateService; @Test public void testGetStyles() { List> 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 端: [开发团队]