Files
msh-system/.cursor/plans/mealplan_image_ai_generation_e8e123a0.plan.md

476 lines
20 KiB
Markdown
Raw Normal View History

---
name: MealPlan Image AI Generation
overview: 将 models-integration 项目中 KieAI 图像/视频生成接口复刻到 crmeb-front并在 mealPlan 菜品图片 URL 无效时,通过 KieAI NanoBanana API 生成图片、上传 OSS、返回有效 URL。
todos:
- id: replicate-kieai-config
content: 在 crmeb-common 中新增 KieAIConfig 配置类,在 application-sophia.yml 中添加 kie-ai 配置
status: completed
- id: replicate-kieai-dtos
content: 在 crmeb-common 中复刻 KieAI 相关 DTOTextToImageInput, CreateTaskRequest, CreateTaskResponse, QueryTaskResponse, NanoBananaRequest, NanoBananaResponse 等)
status: completed
- id: replicate-kieai-helper
content: 在 crmeb-service 中复刻 NanoBananaHelperAPI 调用工具类)
status: completed
- id: replicate-kieai-service
content: 在 crmeb-service 中复刻 KieAI 图像生成 Service 接口及实现ToolKieAIService / ToolKieAIServiceImpl
status: completed
- id: replicate-kieai-controller
content: 在 crmeb-front 中新增 KieAIController暴露文生图、图编辑、任务查询等 REST 接口
status: completed
- id: create-cache-table
content: 创建 v2_dish_image_cache 数据库表及对应 MyBatis Entity/Dao/XML
status: completed
- id: extend-oss-service
content: 扩展 OssService 支持 InputStream 上传AI 生成的图片无需先落盘)
status: completed
- id: implement-dish-image-service
content: 实现 DishImageServiceURL 检测 + KieAI 文生图 + 轮询等待 + 下载图片 + OSS 上传 + 缓存写入
status: completed
- id: modify-calculator-service
content: 修改 ToolCalculatorServiceImpl.generateMealPlan() 集成 DishImageService
status: completed
- id: error-handling
content: 实现降级策略KieAI 生成失败时使用默认占位图,不阻断主流程
status: completed
isProject: false
---
# MealPlan 菜品图片 KieAI 生成 + OSS 上传方案
## 一、现状分析
### 1.1 问题
`[ToolCalculatorServiceImpl.generateMealPlan()](msh_crmeb_22/crmeb-service/src/main/java/com/zbkj/service/service/impl/tool/ToolCalculatorServiceImpl.java)`11 道菜品的图片 URL 是硬编码的,指向 `https://uthink2025.oss-cn-shanghai.aliyuncs.com/recipes/xxx.jpg`。如果这些图片在 OSS 上不存在,前端会显示裂图。
### 1.2 已有基础设施
- **crmeb 项目**: Coze API SDK (v0.2.3)、阿里云 OSS 上传 (`[OssServiceImpl](msh_crmeb_22/crmeb-service/src/main/java/com/zbkj/service/service/impl/OssServiceImpl.java)`)、OSS 配置存储在 `system_config` 表中
- **models-integration 项目**: 已有完整的 KieAI 集成,包括:
- `NanoBanana` 文生图 API`POST {baseUrl}/predictions`
- `Sora2` 文生视频 / 图生视频 API`POST {baseUrl}/api/v1/jobs/createTask`
- 任务状态轮询、回调通知机制
- 文件上传接口(`POST {uploadBaseUrl}/api/file-url-upload`
### 1.3 KieAI API 核心信息
| 项目 | 值 |
| --------------- | ---------------------------------------------------- |
| API Base URL | `https://api.kie.ai` |
| Upload Base URL | `https://kieai.redpandaai.co` |
| 文生图端点 | `POST /predictions` |
| 查询任务端点 | `GET /predictions/{taskId}` |
| 认证方式 | `Authorization: Bearer {apiToken}` |
| 文生图模型 | `google/nano-banana` |
| 图片编辑模型 | `google/nano-banana-edit` / `google/nano-banana-pro` |
| 任务状态值 | `queuing` / `processing` / `completed` / `failed` |
---
## 二、整体架构
### 2.1 KieAI 接口复刻架构
```mermaid
graph TD
subgraph crmebFront [crmeb-front]
KieAICtrl[KieAIController]
end
subgraph crmebService [crmeb-service]
ToolKieAISvc[ToolKieAIService]
ToolKieAISvcImpl[ToolKieAIServiceImpl]
KieAIHelper[KieAIHelper]
end
subgraph crmebCommon [crmeb-common]
KieAICfg[KieAIConfig]
DTOs[DTOs: TextToImageInput etc.]
end
subgraph external [External]
KieAIAPI["KieAI API (api.kie.ai)"]
end
KieAICtrl --> ToolKieAISvc
ToolKieAISvc --> ToolKieAISvcImpl
ToolKieAISvcImpl --> KieAIHelper
KieAIHelper --> KieAICfg
KieAIHelper --> DTOs
KieAIHelper --> KieAIAPI
```
### 2.2 菜品图片生成流程
```mermaid
sequenceDiagram
participant Client
participant CalculatorService
participant DishImageService
participant DB as v2_dish_image_cache
participant KieAI as KieAI NanoBanana API
participant OSS as 阿里云OSS
Client->>CalculatorService: POST /calculator/calculate
CalculatorService->>CalculatorService: generateMealPlan()
loop 每道菜品
CalculatorService->>DishImageService: ensureValidImage(dishName, originalUrl)
DishImageService->>DB: 查询缓存(dishName)
alt 缓存命中
DB-->>DishImageService: 返回缓存的 oss_url
else 缓存未命中
DishImageService->>DishImageService: HTTP HEAD 检查 originalUrl
alt 原始URL有效
DishImageService->>DB: 写入缓存
else 原始URL无效(404)
DishImageService->>KieAI: createTextToImageTask(prompt)
KieAI-->>DishImageService: 返回 taskId
DishImageService->>KieAI: waitForTaskCompletion(taskId)
KieAI-->>DishImageService: 返回 output URLs
DishImageService->>DishImageService: 下载图片 byte[]
DishImageService->>OSS: 上传到 recipes/ 目录
OSS-->>DishImageService: 返回 OSS URL
DishImageService->>DB: 写入缓存
end
end
DishImageService-->>CalculatorService: 返回有效 URL
end
CalculatorService-->>Client: NutritionCalculateResponse
```
---
## 三、实现步骤
### 步骤 1复刻 KieAI 配置到 crmeb-common
**新增文件:** `crmeb-common/src/main/java/com/zbkj/common/config/KieAIConfig.java`
`[models-integration KieAIConfig](models-integration/src/main/java/com/integration/api/config/KieAIConfig.java)` 复刻,保留核心字段:
```java
@Configuration
@ConfigurationProperties(prefix = "kie-ai")
public class KieAIConfig {
private String baseUrl = "https://api.kie.ai";
private String apiToken; // API Token
private String apiUploadBaseUrl = "https://kieai.redpandaai.co";
private String apiCallbackUrl; // 回调 URL
private Integer connectTimeout = 30000;
private Integer readTimeout = 60000;
private Integer pollInterval = 2000; // 轮询间隔
private Integer maxWaitTime = 300000; // 最大等待时间
private String defaultOutputFormat = "png";
private String defaultImageSize = "1:1"; // 菜品图默认正方形
}
```
**修改:** `application-sophia.yml` 新增配置段:
```yaml
kie-ai:
base-url: https://api.kie.ai
api-token: 484661585fe62c5bcb77e6d392ba8ee8
api-callback-url: https://sophia-shop.uj345.cc/api/front/kieai/callback
api-upload-base-url: https://kieai.redpandaai.co
connect-timeout: 30000
read-timeout: 60000
poll-interval: 2000
max-wait-time: 300000
default-output-format: png
default-image-size: "1:1"
```
### 步骤 2复刻 KieAI DTOs 到 crmeb-common
**新增文件位置:** `crmeb-common/src/main/java/com/zbkj/common/request/kieai/``crmeb-common/src/main/java/com/zbkj/common/response/kieai/`
`[models-integration DTOs](models-integration/src/main/java/com/integration/api/dto/)` 复刻以下类:
| 源文件 (models-integration) | 目标位置 (crmeb-common) | 说明 |
| ------------------------- | --------------------------------------------- | --------- |
| `TextToImageInput.java` | `request/kieai/KieAITextToImageInput.java` | 文生图输入参数 |
| `ImageEditInput.java` | `request/kieai/KieAIImageEditInput.java` | 图编辑输入参数 |
| `CreateTaskRequest.java` | `request/kieai/KieAICreateTaskRequest.java` | 创建任务请求 |
| `NanoBananaRequest.java` | `request/kieai/KieAINanoBananaRequest.java` | 前端传入的完整请求 |
| `CreateTaskResponse.java` | `response/kieai/KieAICreateTaskResponse.java` | 创建任务响应 |
| `QueryTaskResponse.java` | `response/kieai/KieAIQueryTaskResponse.java` | 查询任务响应 |
| `NanoBananaResponse.java` | `response/kieai/KieAINanoBananaResponse.java` | 通用响应包装 |
核心 DTO 结构(`KieAIQueryTaskResponse`
```java
public class KieAIQueryTaskResponse {
private String id; // 任务ID
private String status; // queuing/processing/completed/failed
private String created_at;
private String finished_at;
private String model;
private Object input;
private List<String> output; // 生成的图片URL列表
private String error;
}
```
### 步骤 3复刻 KieAI Helper 到 crmeb-service
**新增文件:** `crmeb-service/src/main/java/com/zbkj/service/helper/KieAIHelper.java`
`[NanoBananaHelper](models-integration/src/main/java/com/integration/api/helper/NanoBananaHelper.java)` 复刻,核心方法:
- `createHeaders()` -- 构建 `Authorization: Bearer {token}` 请求头
- `createTask(KieAICreateTaskRequest)` -- POST `/predictions` 创建文生图任务
- `queryTask(String taskId)` -- GET `/predictions/{taskId}` 查询任务
- `buildTextToImageRequest(KieAITextToImageInput, callbackUrl)` -- 构建请求
- `isApiTokenConfigured()` -- 验证 Token 配置
使用项目中已有的 `RestTemplate`(如果 crmeb 中没有 RestTemplate Bean需要注册一个
### 步骤 4复刻 KieAI Service 到 crmeb-service
**新增文件:**
- `crmeb-service/src/main/java/com/zbkj/service/service/tool/ToolKieAIService.java`(接口)
- `crmeb-service/src/main/java/com/zbkj/service/service/impl/tool/ToolKieAIServiceImpl.java`(实现)
`[NanoBananaServiceImpl](models-integration/src/main/java/com/integration/api/service/impl/NanoBananaServiceImpl.java)` 复刻核心方法:
```java
public interface ToolKieAIService {
/** 文生图 - 创建任务 */
KieAICreateTaskResponse createTextToImageTask(KieAINanoBananaRequest request);
/** 查询任务状态 */
KieAIQueryTaskResponse queryTask(String taskId);
/** 同步等待任务完成(轮询) */
KieAIQueryTaskResponse waitForTaskCompletion(String taskId, long maxWaitTime);
/** 图编辑 - 创建任务 */
KieAICreateTaskResponse createImageEditTask(KieAINanoBananaRequest request);
}
```
**注意:** 复刻时简化设计 -- 不需要 `NanoBananaTask` 数据库表和 `Article` 表关联逻辑,因为 crmeb 中的主要使用场景是内部调用(菜品图片生成),任务记录可选。
### 步骤 5复刻 KieAI Controller 到 crmeb-front
**新增文件:** `crmeb-front/src/main/java/com/zbkj/front/controller/KieAIController.java`
`[KieAI2ImageController](models-integration/src/main/java/com/integration/api/controller/KieAI2ImageController.java)` 复刻,暴露以下接口:
| 接口 | 方法 | 说明 |
| ------------------------------------- | ---- | ------ |
| `/api/front/kieai/text-to-image` | POST | 文生图 |
| `/api/front/kieai/image-edit` | POST | 图编辑 |
| `/api/front/kieai/task/{taskId}` | GET | 查询任务 |
| `/api/front/kieai/task/{taskId}/wait` | GET | 同步等待任务 |
| `/api/front/kieai/callback` | POST | 回调通知 |
同时需要在安全白名单中添加 `/api/front/kieai/**` 路径(参考 Coze 接口的白名单配置)。
### 步骤 6新建菜品图片缓存表
```sql
CREATE TABLE `v2_dish_image_cache` (
`id` BIGINT NOT NULL AUTO_INCREMENT,
`dish_name` VARCHAR(100) NOT NULL COMMENT '菜品名称',
`original_url` VARCHAR(500) DEFAULT NULL COMMENT '原始图片URL',
`oss_url` VARCHAR(500) NOT NULL COMMENT 'OSS有效图片URL',
`ai_provider` VARCHAR(50) DEFAULT 'kieai' COMMENT 'AI生成来源',
`task_id` VARCHAR(100) DEFAULT NULL COMMENT 'KieAI任务ID',
`created_at` DATETIME DEFAULT CURRENT_TIMESTAMP,
`updated_at` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_dish_name` (`dish_name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='菜品图片缓存表';
```
对应新增:
- `crmeb-common/.../model/tool/V2DishImageCache.java`(实体)
- `crmeb-service/.../dao/tool/V2DishImageCacheDao.java`Mapper
- `resources/mapper/tool/V2DishImageCacheDao.xml`MyBatis XML
### 步骤 7扩展 OssService 支持 InputStream 上传
当前 `[OssServiceImpl.upload()](msh_crmeb_22/crmeb-service/src/main/java/com/zbkj/service/service/impl/OssServiceImpl.java)` 只支持 `File` 参数。新增 InputStream 重载:
```java
// OssService 接口新增
void upload(CloudVo cloudVo, String objectKey, InputStream inputStream, long contentLength);
// OssServiceImpl 实现
@Override
public void upload(CloudVo cloudVo, String objectKey, InputStream inputStream, long contentLength) {
OSS ossClient = new OSSClientBuilder().build(cloudVo.getRegion(), cloudVo.getAccessKey(), cloudVo.getSecretKey());
try {
ObjectMetadata metadata = new ObjectMetadata();
metadata.setContentLength(contentLength);
PutObjectRequest putRequest = new PutObjectRequest(cloudVo.getBucketName(), objectKey, inputStream, metadata);
ossClient.putObject(putRequest);
} finally {
ossClient.shutdown();
}
}
```
### 步骤 8实现 DishImageService
**新增文件:**
- `crmeb-service/.../service/tool/DishImageService.java`
- `crmeb-service/.../service/impl/tool/DishImageServiceImpl.java`
核心流程:
```java
@Service
public class DishImageServiceImpl implements DishImageService {
@Autowired private V2DishImageCacheDao cacheDao;
@Autowired private ToolKieAIService kieAIService;
@Autowired private OssService ossService;
@Autowired private SystemConfigService systemConfigService;
@Override
public String ensureValidImageUrl(String dishName, String originalUrl) {
// 1. 查缓存
V2DishImageCache cache = cacheDao.selectByDishName(dishName);
if (cache != null) return cache.getOssUrl();
// 2. HTTP HEAD 检查原始 URL3秒超时
if (checkUrlAccessible(originalUrl)) {
saveCache(dishName, originalUrl, originalUrl, null);
return originalUrl;
}
// 3. KieAI 文生图
try {
String prompt = "一道精美的中式菜品照片:" + dishName
+ ",高清美食摄影风格,白色餐盘,俯拍角度,自然光照";
KieAICreateTaskResponse createResp = kieAIService.createTextToImageTask(buildRequest(prompt));
KieAIQueryTaskResponse result = kieAIService.waitForTaskCompletion(createResp.getId(), 120000);
if ("completed".equals(result.getStatus()) && result.getOutput() != null) {
String imageUrl = result.getOutput().get(0);
// 4. 下载图片并上传 OSS
byte[] imageBytes = downloadImage(imageUrl);
String ossUrl = uploadToOss(imageBytes, dishName);
// 5. 写缓存
saveCache(dishName, originalUrl, ossUrl, createResp.getId());
return ossUrl;
}
} catch (Exception e) {
log.error("菜品图片AI生成失败: {}", dishName, e);
}
// 6. 降级:返回默认占位图
return getDefaultPlaceholderUrl();
}
}
```
### 步骤 9修改 ToolCalculatorServiceImpl
`[generateMealPlan()](msh_crmeb_22/crmeb-service/src/main/java/com/zbkj/service/service/impl/tool/ToolCalculatorServiceImpl.java)` 方法末尾集成 DishImageService
```java
@Autowired
private DishImageService dishImageService;
private MealPlan generateMealPlan(String ckdStage, Boolean dialysis) {
MealPlan plan = new MealPlan();
// ... 现有代码不变(创建早/午/晚餐 Dish 列表)...
// 确保所有菜品图片URL有效
ensureMealPlanImages(plan);
return plan;
}
private void ensureMealPlanImages(MealPlan plan) {
Stream.of(plan.getBreakfast(), plan.getLunch(), plan.getDinner())
.filter(Objects::nonNull)
.flatMap(List::stream)
.forEach(dish -> {
String validUrl = dishImageService.ensureValidImageUrl(dish.getName(), dish.getImage());
dish.setImage(validUrl);
});
}
```
---
## 四、异常处理与降级策略
- KieAI 生成失败 / 超时 -> 返回**默认占位图** URL预先上传到 OSS 的通用食物图片)
- HTTP URL 检测设置 **3 秒超时**,避免阻塞主流程
- 整个图片处理过程用 try-catch 包裹,**失败不影响营养计算结果的返回**
- 日志记录每次 AI 生成和上传操作,便于排查
- KieAI API Token 未配置时跳过 AI 生成,直接使用原始 URL 或占位图
---
## 五、性能优化
- **缓存优先**: DB 缓存命中后直接返回,零网络开销
- **仅 11 道固定菜品**: 缓存全部填充后,后续请求无 AI 调用
- **首次预热**: 可提供管理后台接口或启动任务,手动触发所有菜品图片预生成
- **并行处理**: 可用 `CompletableFuture` 并行检查/生成多道菜品图片(可选优化)
---
## 六、涉及文件清单
### 6.1 新增文件KieAI 接口复刻)
| 模块 | 文件路径 | 说明 |
| ------------- | --------------------------------------------- | ------------- |
| crmeb-common | `config/KieAIConfig.java` | KieAI 配置类 |
| crmeb-common | `request/kieai/KieAITextToImageInput.java` | 文生图输入 DTO |
| crmeb-common | `request/kieai/KieAIImageEditInput.java` | 图编辑输入 DTO |
| crmeb-common | `request/kieai/KieAICreateTaskRequest.java` | 创建任务请求 DTO |
| crmeb-common | `request/kieai/KieAINanoBananaRequest.java` | 前端完整请求 DTO |
| crmeb-common | `response/kieai/KieAICreateTaskResponse.java` | 创建任务响应 DTO |
| crmeb-common | `response/kieai/KieAIQueryTaskResponse.java` | 查询任务响应 DTO |
| crmeb-common | `response/kieai/KieAINanoBananaResponse.java` | 通用响应包装 |
| crmeb-service | `helper/KieAIHelper.java` | API 调用工具类 |
| crmeb-service | `service/tool/ToolKieAIService.java` | KieAI 服务接口 |
| crmeb-service | `service/impl/tool/ToolKieAIServiceImpl.java` | KieAI 服务实现 |
| crmeb-front | `controller/KieAIController.java` | KieAI REST 接口 |
### 6.2 新增文件(菜品图片缓存)
| 模块 | 文件路径 | 说明 |
| ------------- | ----------------------------------------------- | ------------------------- |
| crmeb-common | `model/tool/V2DishImageCache.java` | 缓存实体 |
| crmeb-service | `dao/tool/V2DishImageCacheDao.java` | MyBatis Mapper |
| crmeb-service | `resources/mapper/tool/V2DishImageCacheDao.xml` | MyBatis XML |
| crmeb-service | `service/tool/DishImageService.java` | 菜品图片服务接口 |
| crmeb-service | `service/impl/tool/DishImageServiceImpl.java` | 菜品图片服务实现 |
| - | SQL 迁移脚本 | 建 `v2_dish_image_cache` 表 |
### 6.3 修改文件
| 模块 | 文件路径 | 改动内容 |
| ------------- | -------------------------------------------------- | ------------------------ |
| crmeb-service | `service/OssService.java` | 新增 InputStream 上传重载方法 |
| crmeb-service | `service/impl/OssServiceImpl.java` | 实现 InputStream 上传 |
| crmeb-service | `service/impl/tool/ToolCalculatorServiceImpl.java` | 集成 DishImageService |
| crmeb-front | `resources/application-sophia.yml` | 新增 `kie-ai` 配置段 |
| crmeb-front | 安全白名单配置 | 添加 `/api/front/kieai/**` |