- PythonAIGCClient 统一客户端设计 - 选题/内容/海报三个 Service 层 - 提示词管理迁移到 Java 端 (数据库) - 灰度切换策略 - 实施步骤和工时估算
17 KiB
17 KiB
Java 端适配方案
更新时间: 2024-12-10
概述
本文档描述 Java 端如何对接新的 Python AIGC 服务,包括:
- 新增 AIGC 服务适配层
- 提示词管理迁移到 Java 端
- 灰度切换策略
一、架构设计
┌─────────────────────────────────────────────────────────────┐
│ 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 配置
# 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 客户端
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
// === 选题生成 ===
@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 层
@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 层
@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 数据库表设计
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
@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 管理接口
@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 配置开关
@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 切换逻辑
@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 本地) |