673 lines
18 KiB
Markdown
673 lines
18 KiB
Markdown
# 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<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
|
||
|
||
```java
|
||
// 改造前 (传 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
|
||
|
||
```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<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
|
||
|
||
```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<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 端测试
|
||
|
||
```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<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 端: [开发团队]
|