docs: 新增 Java 端适配方案
- PythonAIGCClient 统一客户端设计 - 选题/内容/海报三个 Service 层 - 提示词管理迁移到 Java 端 (数据库) - 灰度切换策略 - 实施步骤和工时估算
This commit is contained in:
parent
44bf652cb0
commit
73cc12fe65
567
docs/JAVA_ADAPTER_PLAN.md
Normal file
567
docs/JAVA_ADAPTER_PLAN.md
Normal file
@ -0,0 +1,567 @@
|
||||
# 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 配置
|
||||
|
||||
```yaml
|
||||
# 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 客户端
|
||||
|
||||
```java
|
||||
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
|
||||
|
||||
```java
|
||||
// === 选题生成 ===
|
||||
@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 层
|
||||
|
||||
```java
|
||||
@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 层
|
||||
|
||||
```java
|
||||
@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 数据库表设计
|
||||
|
||||
```sql
|
||||
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
|
||||
|
||||
```java
|
||||
@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 管理接口
|
||||
|
||||
```java
|
||||
@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 配置开关
|
||||
|
||||
```java
|
||||
@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 切换逻辑
|
||||
|
||||
```java
|
||||
@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 本地) |
|
||||
Loading…
x
Reference in New Issue
Block a user