20 KiB
name, overview, todos, isProject
| name | overview | todos | isProject | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| MealPlan Image AI Generation | 将 models-integration 项目中 KieAI 图像/视频生成接口复刻到 crmeb-front,并在 mealPlan 菜品图片 URL 无效时,通过 KieAI NanoBanana API 生成图片、上传 OSS、返回有效 URL。 |
|
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 接口复刻架构
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 菜品图片生成流程
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) 复刻,保留核心字段:
@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 新增配置段:
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):
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) 复刻核心方法:
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:新建菜品图片缓存表
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 重载:
// 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.javacrmeb-service/.../service/impl/tool/DishImageServiceImpl.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 检查原始 URL(3秒超时)
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:
@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/** |