18 KiB
18 KiB
Java 端改造指南
文档版本: 1.0.0
更新日期: 2024-12-09
Python 端状态: ✅ 已完成
1. 概述
Python 端 V2 API 已完成改造,Java 端需要进行以下改造以完成整体迁移:
- 改为传完整对象 - 不再传 ID,传完整的景区/产品/风格/人群对象
- 调用 V2 接口 - 从
/api/v1/tweet/*改为/api/v2/aigc/execute - 缓存风格/人群配置 - 从 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 准备阶段
- 确认 Python 服务已部署并可访问
- 测试 Python V2 API 接口
- 在 Java 项目中添加新的配置项
4.2 实施阶段
- 新增
AIGCConfigService - 新增
AIGCConfigController - 改造
TopicGenerateServiceImpl.callAIService() - 改造
ContentGenerateServiceImpl.callAIService() - 改造
PosterGenerateServiceImpl(如需要) - 更新
application.yml配置
4.3 测试阶段
- 单元测试
- 集成测试 (Java → Python)
- 端到端测试 (前端 → Java → Python)
- 性能测试
4.4 上线阶段
- 灰度发布
- 监控告警配置
- 回滚方案准备
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:
- 在 Python 端
prompts/style/或prompts/audience/目录下创建新的 YAML 文件 - 调用 Java 端
/api/aigc/config/refresh刷新缓存 - 前端自动获取新的选项
Q3: 旧接口何时废弃?
A: 待 Java 端完全迁移到 V2 接口后,Python 端的 V1 接口 (/api/v1/tweet/*) 将被废弃。建议保留 1-2 个版本周期作为过渡。
Q4: 超时如何处理?
A:
- 选题生成: 建议 60 秒超时
- 内容生成: 建议 120 秒超时 (含审核)
- 海报生成: 建议 180 秒超时
7. 联系方式
如有问题,请联系:
- Python 端: [开发团队]
- Java 端: [开发团队]