--- 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 相关 DTO(TextToImageInput, CreateTaskRequest, CreateTaskResponse, QueryTaskResponse, NanoBananaRequest, NanoBananaResponse 等) status: completed - id: replicate-kieai-helper content: 在 crmeb-service 中复刻 NanoBananaHelper(API 调用工具类) 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: 实现 DishImageService:URL 检测 + 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 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 检查原始 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: ```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/**` |