feat: 集成 KieAI 服务,移除 models-integration 子项目
- 添加 Gemini 2.5 Flash 对话接口(流式+非流式) - 添加 NanoBanana 图像生成/编辑接口 - 添加 Sora2 视频生成接口(文生视频、图生视频、去水印) - 移除 models-integration 子项目(功能已迁移至主后端) - 新增测试文档和 Playwright E2E 配置 - 更新前端页面和 API 接口 - 更新后端配置和日志处理
This commit is contained in:
@@ -130,7 +130,9 @@ swagger:
|
||||
username: #访问swagger的账号
|
||||
password: #访问swagger的密码
|
||||
|
||||
# 行为验证码
|
||||
# 行为验证码(captcha.enabled: false 可关闭验证,便于测试)
|
||||
captcha:
|
||||
enabled: false
|
||||
aj:
|
||||
captcha:
|
||||
type: default # 验证码类型
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
package com.zbkj.common.request.kieai;
|
||||
|
||||
import io.swagger.annotations.ApiModel;
|
||||
import io.swagger.annotations.ApiModelProperty;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import javax.validation.Valid;
|
||||
import javax.validation.constraints.NotEmpty;
|
||||
import javax.validation.constraints.NotNull;
|
||||
import java.io.Serializable;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* KieAI Gemini 2.5 Flash Chat Completions 请求 DTO
|
||||
* 对应 https://docs.kie.ai/market/gemini/gemini-2.5-flash
|
||||
* +----------------------------------------------------------------------
|
||||
* | Author:ScottPan
|
||||
* +----------------------------------------------------------------------
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@ApiModel(value = "KieAIGeminiChatRequest", description = "Gemini 2.5 Flash Chat Completions 请求")
|
||||
public class KieAIGeminiChatRequest implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@Valid
|
||||
@NotEmpty(message = "messages不能为空")
|
||||
@ApiModelProperty(value = "消息列表", required = true)
|
||||
private List<Message> messages;
|
||||
|
||||
@ApiModelProperty(value = "是否流式返回", example = "false")
|
||||
private Boolean stream;
|
||||
|
||||
@ApiModelProperty(value = "工具定义(如 googleSearch 或 function calling),与 response_format 互斥")
|
||||
private List<Map<String, Object>> tools;
|
||||
|
||||
@ApiModelProperty(value = "是否在响应中包含思考过程", example = "true")
|
||||
private Boolean includeThoughts;
|
||||
|
||||
@ApiModelProperty(value = "响应格式 JSON Schema,与 function calling 互斥")
|
||||
private Map<String, Object> responseFormat;
|
||||
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@ApiModel(value = "Message", description = "单条消息")
|
||||
public static class Message implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@ApiModelProperty(value = "角色: developer, system, user, assistant, tool")
|
||||
private String role;
|
||||
|
||||
@NotNull(message = "content不能为空")
|
||||
@ApiModelProperty(value = "内容:字符串或多模态数组 [{type,text} | {type,image_url}]", required = true)
|
||||
private Object content;
|
||||
}
|
||||
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@ApiModel(value = "ContentItem", description = "多模态内容项")
|
||||
public static class ContentItem implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@ApiModelProperty(value = "类型: text | image_url")
|
||||
private String type;
|
||||
|
||||
@ApiModelProperty(value = "文本内容(type=text 时使用)")
|
||||
private String text;
|
||||
|
||||
@ApiModelProperty(value = "图片 URL(type=image_url 时使用)")
|
||||
private ImageUrl imageUrl;
|
||||
}
|
||||
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@ApiModel(value = "ImageUrl", description = "图片 URL")
|
||||
public static class ImageUrl implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@ApiModelProperty(value = "图片地址")
|
||||
private String url;
|
||||
}
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
@@ -1,4 +0,0 @@
|
||||
{ "app": "Cjava22", "timestamp":"2026-02-03 11:52:04.963", "level": "DEBUG", "thread": "SpringContextShutdownHook",
|
||||
"class": "o.s.b.w.s.c.AnnotationConfigServletWebServerApplicationContext",
|
||||
"message": "Closing org.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApplicationContext@53aac487, started on Sun Feb 01 23:11:14 CST 2026" }
|
||||
|
||||
@@ -1,153 +0,0 @@
|
||||
{
|
||||
"app": "Cjava22",
|
||||
"timestamp":"2026-02-01 23:11:40.942",
|
||||
"level": "ERROR",
|
||||
"thread": "http-nio-20822-exec-1",
|
||||
"class": "c.z.s.exception.GlobalExceptionHandler",
|
||||
"message": "捕获到异常:" }
|
||||
|
||||
org.springframework.web.util.NestedServletException: Handler dispatch failed; nested exception is java.lang.NoSuchMethodError: io.jsonwebtoken.JwtBuilder.signWith(Ljava/security/Key;Lio/jsonwebtoken/SignatureAlgorithm;)Lio/jsonwebtoken/JwtBuilder;
|
||||
at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1055)
|
||||
at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:943)
|
||||
at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1006)
|
||||
at org.springframework.web.servlet.FrameworkServlet.doPost(FrameworkServlet.java:909)
|
||||
at javax.servlet.http.HttpServlet.service(HttpServlet.java:665)
|
||||
at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:883)
|
||||
at javax.servlet.http.HttpServlet.service(HttpServlet.java:750)
|
||||
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:231)
|
||||
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
|
||||
at org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:53)
|
||||
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)
|
||||
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
|
||||
at org.springframework.web.filter.CorsFilter.doFilterInternal(CorsFilter.java:92)
|
||||
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119)
|
||||
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)
|
||||
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
|
||||
at com.zbkj.front.filter.ResponseFilter.doFilter(ResponseFilter.java:32)
|
||||
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)
|
||||
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
|
||||
at com.alibaba.druid.support.http.WebStatFilter.doFilter(WebStatFilter.java:124)
|
||||
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)
|
||||
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
|
||||
at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:320)
|
||||
at org.springframework.security.web.access.intercept.FilterSecurityInterceptor.invoke(FilterSecurityInterceptor.java:126)
|
||||
at org.springframework.security.web.access.intercept.FilterSecurityInterceptor.doFilter(FilterSecurityInterceptor.java:90)
|
||||
at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:334)
|
||||
at org.springframework.security.web.access.ExceptionTranslationFilter.doFilter(ExceptionTranslationFilter.java:118)
|
||||
at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:334)
|
||||
at org.springframework.security.web.session.SessionManagementFilter.doFilter(SessionManagementFilter.java:137)
|
||||
at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:334)
|
||||
at org.springframework.security.web.authentication.AnonymousAuthenticationFilter.doFilter(AnonymousAuthenticationFilter.java:111)
|
||||
at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:334)
|
||||
at org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter.doFilter(SecurityContextHolderAwareRequestFilter.java:158)
|
||||
at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:334)
|
||||
at org.springframework.security.web.savedrequest.RequestCacheAwareFilter.doFilter(RequestCacheAwareFilter.java:63)
|
||||
at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:334)
|
||||
at org.springframework.security.web.authentication.logout.LogoutFilter.doFilter(LogoutFilter.java:116)
|
||||
at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:334)
|
||||
at org.springframework.security.web.header.HeaderWriterFilter.doHeadersAfter(HeaderWriterFilter.java:92)
|
||||
at org.springframework.security.web.header.HeaderWriterFilter.doFilterInternal(HeaderWriterFilter.java:77)
|
||||
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119)
|
||||
at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:334)
|
||||
at org.springframework.security.web.context.SecurityContextPersistenceFilter.doFilter(SecurityContextPersistenceFilter.java:105)
|
||||
at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:334)
|
||||
at org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter.doFilterInternal(WebAsyncManagerIntegrationFilter.java:56)
|
||||
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119)
|
||||
at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:334)
|
||||
at org.springframework.security.web.FilterChainProxy.doFilterInternal(FilterChainProxy.java:215)
|
||||
at org.springframework.security.web.FilterChainProxy.doFilter(FilterChainProxy.java:178)
|
||||
at org.springframework.web.filter.DelegatingFilterProxy.invokeDelegate(DelegatingFilterProxy.java:358)
|
||||
at org.springframework.web.filter.DelegatingFilterProxy.doFilter(DelegatingFilterProxy.java:271)
|
||||
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)
|
||||
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
|
||||
at org.springframework.web.filter.RequestContextFilter.doFilterInternal(RequestContextFilter.java:100)
|
||||
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119)
|
||||
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)
|
||||
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
|
||||
at org.springframework.web.filter.FormContentFilter.doFilterInternal(FormContentFilter.java:93)
|
||||
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119)
|
||||
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)
|
||||
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
|
||||
at org.springframework.boot.actuate.metrics.web.servlet.WebMvcMetricsFilter.doFilterInternal(WebMvcMetricsFilter.java:109)
|
||||
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119)
|
||||
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)
|
||||
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
|
||||
at org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:201)
|
||||
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119)
|
||||
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)
|
||||
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
|
||||
at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:202)
|
||||
at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:96)
|
||||
at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:541)
|
||||
at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:139)
|
||||
at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:92)
|
||||
at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:74)
|
||||
at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:343)
|
||||
at org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:373)
|
||||
at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:65)
|
||||
at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:868)
|
||||
at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1594)
|
||||
at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:49)
|
||||
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
|
||||
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
|
||||
at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)
|
||||
at java.lang.Thread.run(Thread.java:750)
|
||||
Caused by: java.lang.NoSuchMethodError: io.jsonwebtoken.JwtBuilder.signWith(Ljava/security/Key;Lio/jsonwebtoken/SignatureAlgorithm;)Lio/jsonwebtoken/JwtBuilder;
|
||||
at com.coze.openapi.service.auth.JWTOAuthClient.generateJWT(JWTOAuthClient.java:104)
|
||||
at com.coze.openapi.service.auth.JWTOAuthClient.doGetAccessToken(JWTOAuthClient.java:83)
|
||||
at com.coze.openapi.service.auth.JWTOAuthClient.getAccessToken(JWTOAuthClient.java:48)
|
||||
at com.zbkj.service.service.impl.tool.ToolCozeServiceImpl.getClient(ToolCozeServiceImpl.java:69)
|
||||
at com.zbkj.service.service.impl.tool.ToolCozeServiceImpl.workflow(ToolCozeServiceImpl.java:175)
|
||||
at com.zbkj.front.controller.CozeController.runWorkflow(CozeController.java:72)
|
||||
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
|
||||
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
|
||||
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
|
||||
at java.lang.reflect.Method.invoke(Method.java:498)
|
||||
at org.springframework.web.method.support.InvocableHandlerMethod.doInvoke(InvocableHandlerMethod.java:190)
|
||||
at org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:138)
|
||||
at org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:105)
|
||||
at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:879)
|
||||
at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:793)
|
||||
at org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:87)
|
||||
at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1040)
|
||||
... 84 common frames omitted
|
||||
{
|
||||
"app": "Cjava22",
|
||||
"timestamp":"2026-02-01 23:11:47.304",
|
||||
"level": "ERROR",
|
||||
"thread": "Druid-ConnectionPool-Create-2045500291",
|
||||
"class": "com.alibaba.druid.pool.DruidDataSource",
|
||||
"message": "create connection holder error" }
|
||||
|
||||
com.mysql.cj.jdbc.exceptions.CommunicationsException: Communications link failure
|
||||
|
||||
The last packet successfully received from the server was 1,602 milliseconds ago. The last packet sent successfully to the server was 1,969 milliseconds ago.
|
||||
at com.mysql.cj.jdbc.exceptions.SQLError.createCommunicationsException(SQLError.java:175)
|
||||
at com.mysql.cj.jdbc.exceptions.SQLExceptionsMapping.translateException(SQLExceptionsMapping.java:64)
|
||||
at com.mysql.cj.jdbc.ConnectionImpl.isReadOnly(ConnectionImpl.java:1374)
|
||||
at com.mysql.cj.jdbc.ConnectionImpl.isReadOnly(ConnectionImpl.java:1359)
|
||||
at com.alibaba.druid.pool.DruidConnectionHolder.<init>(DruidConnectionHolder.java:137)
|
||||
at com.alibaba.druid.pool.DruidConnectionHolder.<init>(DruidConnectionHolder.java:77)
|
||||
at com.alibaba.druid.pool.DruidDataSource.put(DruidDataSource.java:2412)
|
||||
at com.alibaba.druid.pool.DruidDataSource$CreateConnectionThread.run(DruidDataSource.java:2757)
|
||||
Caused by: com.mysql.cj.exceptions.CJCommunicationsException: Communications link failure
|
||||
|
||||
The last packet successfully received from the server was 1,602 milliseconds ago. The last packet sent successfully to the server was 1,969 milliseconds ago.
|
||||
at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
|
||||
at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62)
|
||||
at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)
|
||||
at java.lang.reflect.Constructor.newInstance(Constructor.java:423)
|
||||
at com.mysql.cj.exceptions.ExceptionFactory.createException(ExceptionFactory.java:62)
|
||||
at com.mysql.cj.exceptions.ExceptionFactory.createException(ExceptionFactory.java:105)
|
||||
at com.mysql.cj.exceptions.ExceptionFactory.createException(ExceptionFactory.java:150)
|
||||
at com.mysql.cj.exceptions.ExceptionFactory.createCommunicationsException(ExceptionFactory.java:166)
|
||||
at com.mysql.cj.protocol.a.NativeProtocol.clearInputStream(NativeProtocol.java:870)
|
||||
at com.mysql.cj.protocol.a.NativeProtocol.sendCommand(NativeProtocol.java:682)
|
||||
at com.mysql.cj.protocol.a.NativeProtocol.sendCommand(NativeProtocol.java:156)
|
||||
at com.mysql.cj.NativeSession.queryServerVariable(NativeSession.java:589)
|
||||
at com.mysql.cj.jdbc.ConnectionImpl.isReadOnly(ConnectionImpl.java:1366)
|
||||
... 5 common frames omitted
|
||||
Caused by: java.io.IOException: Socket is closed.
|
||||
at com.mysql.cj.protocol.AbstractSocketConnection.getMysqlInput(AbstractSocketConnection.java:73)
|
||||
at com.mysql.cj.protocol.a.NativeProtocol.clearInputStream(NativeProtocol.java:866)
|
||||
... 9 common frames omitted
|
||||
@@ -2,6 +2,7 @@ package com.zbkj.front.controller;
|
||||
|
||||
import com.zbkj.common.config.KieAIConfig;
|
||||
import com.zbkj.common.request.kieai.CreateProTextToVideoRequest;
|
||||
import com.zbkj.common.request.kieai.KieAIGeminiChatRequest;
|
||||
import com.zbkj.common.request.kieai.KieAINanoBananaRequest;
|
||||
import com.zbkj.common.request.kieai.Sora2Request;
|
||||
import com.zbkj.common.response.kieai.KieAICreateTaskResponse;
|
||||
@@ -19,6 +20,7 @@ import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
|
||||
|
||||
import javax.validation.constraints.Min;
|
||||
import javax.validation.constraints.NotBlank;
|
||||
@@ -49,6 +51,36 @@ public class KieAIController {
|
||||
@Autowired
|
||||
private KieAIConfig kieAIConfig;
|
||||
|
||||
// ==================== Gemini 2.5 Flash Chat ====================
|
||||
|
||||
/**
|
||||
* Gemini 2.5 Flash 对话(根据 stream 参数返回流式或非流式)
|
||||
*/
|
||||
@PostMapping("/gemini/chat")
|
||||
@ApiOperation(value = "Gemini 2.5 Flash 对话", notes = "根据 stream 参数返回流式(SSE)或非流式(JSON)")
|
||||
public Object geminiChat(@RequestBody @Validated KieAIGeminiChatRequest request) {
|
||||
try {
|
||||
if (Boolean.TRUE.equals(request.getStream())) {
|
||||
return toolKieAIService.geminiChatStream(request);
|
||||
}
|
||||
Map<String, Object> result = toolKieAIService.geminiChat(request);
|
||||
return CommonResult.success(result);
|
||||
} catch (Exception e) {
|
||||
logger.error("Gemini chat 失败", e);
|
||||
return CommonResult.failed("Gemini 对话失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gemini 2.5 Flash 流式对话
|
||||
*/
|
||||
@PostMapping(value = "/gemini/chat/stream", produces = "text/event-stream")
|
||||
@ApiOperation(value = "Gemini 2.5 Flash 流式对话", notes = "使用 SSE 实时推送响应")
|
||||
public SseEmitter geminiChatStream(@RequestBody @Validated KieAIGeminiChatRequest request) {
|
||||
request.setStream(true);
|
||||
return toolKieAIService.geminiChatStream(request);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建文本生成图像任务
|
||||
*/
|
||||
|
||||
@@ -312,6 +312,16 @@ public class ToolController {
|
||||
return CommonResult.success(toolKnowledgeService.getNutrientDetail(name));
|
||||
}
|
||||
|
||||
/**
|
||||
* 为 cover_image 为空的饮食指南/科普文章生成封面图(KieAI 1:1,100KB 内,上传 OSS 并更新 v2_knowledge)
|
||||
*/
|
||||
@ApiOperation(value = "补全知识封面图")
|
||||
@PostMapping("/knowledge/fill-cover-images")
|
||||
public CommonResult<Integer> fillKnowledgeCoverImages(@RequestParam(defaultValue = "10") int limit) {
|
||||
int updated = toolKnowledgeService.fillMissingCoverImages(limit);
|
||||
return CommonResult.success(updated);
|
||||
}
|
||||
|
||||
// ==================== 打卡社区相关 ====================
|
||||
|
||||
/**
|
||||
@@ -486,6 +496,15 @@ public class ToolController {
|
||||
return CommonResult.success(toolHomeService.getHealthStatus());
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取首页展示配置(如四大功能入口是否显示,由 eb_system_config 中 field01 控制,1=显示)
|
||||
*/
|
||||
@ApiOperation(value = "获取首页展示配置")
|
||||
@GetMapping("/home/display-config")
|
||||
public CommonResult<Map<String, Object>> getHomeDisplayConfig() {
|
||||
return CommonResult.success(toolHomeService.getDisplayConfig());
|
||||
}
|
||||
|
||||
// ==================== 食谱相关 ====================
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# CRMEB 相关配置
|
||||
crmeb:
|
||||
imagePath: /usr/local/crmeb/crmebimage/ # 服务器图片路径配置 斜杠结尾
|
||||
imagePath: /www/wwwroot/crmebimage/ # 服务器图片路径配置 斜杠结尾
|
||||
asyncConfig: true #是否同步config表数据到redis
|
||||
|
||||
server:
|
||||
|
||||
@@ -104,7 +104,9 @@ mybatis-plus:
|
||||
logic-not-delete-value: 0 # 逻辑未删除值(默认为 0)
|
||||
|
||||
|
||||
# 行为验证码
|
||||
# 行为验证码(captcha.enabled: false 可关闭验证,便于测试)
|
||||
captcha:
|
||||
enabled: false
|
||||
aj:
|
||||
captcha:
|
||||
type: default # 验证码类型
|
||||
|
||||
@@ -6,6 +6,7 @@ import com.anji.captcha.service.CaptchaService;
|
||||
import com.anji.captcha.util.StringUtils;
|
||||
import com.zbkj.service.service.SafetyService;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
@@ -20,6 +21,9 @@ import javax.servlet.http.HttpServletRequest;
|
||||
@Service
|
||||
public class SafetyServiceImpl implements SafetyService {
|
||||
|
||||
@Value("${captcha.enabled:true}")
|
||||
private boolean captchaEnabled;
|
||||
|
||||
@Autowired
|
||||
private CaptchaService captchaService;
|
||||
|
||||
@@ -28,6 +32,9 @@ public class SafetyServiceImpl implements SafetyService {
|
||||
*/
|
||||
@Override
|
||||
public ResponseModel getSafetyCode(CaptchaVO data, HttpServletRequest request) {
|
||||
if (!captchaEnabled) {
|
||||
return ResponseModel.successMsg("captcha disabled");
|
||||
}
|
||||
assert request.getRemoteHost() != null;
|
||||
data.setBrowserInfo(getRemoteId(request));
|
||||
return captchaService.get(data);
|
||||
@@ -38,6 +45,9 @@ public class SafetyServiceImpl implements SafetyService {
|
||||
*/
|
||||
@Override
|
||||
public ResponseModel checkSafetyCode(CaptchaVO data, HttpServletRequest request) {
|
||||
if (!captchaEnabled) {
|
||||
return ResponseModel.successMsg("captcha disabled");
|
||||
}
|
||||
data.setBrowserInfo(getRemoteId(request));
|
||||
return captchaService.check(data);
|
||||
}
|
||||
@@ -47,6 +57,9 @@ public class SafetyServiceImpl implements SafetyService {
|
||||
*/
|
||||
@Override
|
||||
public ResponseModel verifySafetyCode(CaptchaVO data) {
|
||||
if (!captchaEnabled) {
|
||||
return ResponseModel.successMsg("captcha disabled");
|
||||
}
|
||||
return captchaService.verification(data);
|
||||
}
|
||||
|
||||
|
||||
@@ -204,15 +204,10 @@ public class UserSignServiceImpl extends ServiceImpl<UserSignDao, UserSign> impl
|
||||
@Override
|
||||
public HashMap<String, Object> get() {
|
||||
HashMap<String, Object> map = new HashMap<>();
|
||||
//当前积分
|
||||
User info = userService.getInfo();
|
||||
User info = userService.getInfoException();
|
||||
map.put("integral", info.getIntegral());
|
||||
//总计签到天数
|
||||
map.put("count", signCount(info.getUid()));
|
||||
//连续签到数据
|
||||
|
||||
//今日是否已经签到
|
||||
map.put("today", false);
|
||||
map.put("today", checkDaySign(info.getUid()));
|
||||
return map;
|
||||
}
|
||||
|
||||
|
||||
@@ -53,7 +53,7 @@ public class DishImageServiceImpl implements DishImageService {
|
||||
|
||||
/** 默认占位图 URL */
|
||||
private static final String DEFAULT_PLACEHOLDER_URL =
|
||||
"https://uthink2025.oss-cn-shanghai.aliyuncs.com/recipes/default-food.png";
|
||||
"https://uthink2026.oss-cn-shanghai.aliyuncs.com/recipes/default-food.png";
|
||||
|
||||
/** OSS 上传路径前缀(菜品) */
|
||||
private static final String OSS_RECIPES_PATH = "recipes/";
|
||||
@@ -61,6 +61,9 @@ public class DishImageServiceImpl implements DishImageService {
|
||||
/** OSS 上传路径前缀(食物百科) */
|
||||
private static final String OSS_FOODS_PATH = "foods/";
|
||||
|
||||
/** OSS 上传路径前缀(知识封面) */
|
||||
private static final String OSS_KNOWLEDGE_PATH = "knowledge/";
|
||||
|
||||
/** 上传前图片最大体积(字节),压缩到此值以内 */
|
||||
private static final long MAX_IMAGE_BYTES = 100 * 1024L;
|
||||
|
||||
@@ -178,43 +181,10 @@ public class DishImageServiceImpl implements DishImageService {
|
||||
}
|
||||
|
||||
/**
|
||||
* 通用流程:根据 name 与 prompt 调用 KieAI 文生图 → 下载 → 压缩至 100KB → 上传 OSS,返回 OSS 完整 URL
|
||||
*
|
||||
* @param name 名称(用于生成文件名)
|
||||
* @param prompt 文生图 prompt
|
||||
* @param pathPrefix OSS 路径前缀,如 "recipes/" 或 "foods/"
|
||||
* @param filePrefix 文件名前缀,如 "dish-" 或 "food-"
|
||||
* @return OSS 完整 URL,失败返回 null
|
||||
* 通用流程:根据 name 与 prompt 调用 KieAI 文生图 → 下载 → 压缩至 100KB → 上传 OSS,返回 OSS 完整 URL(使用默认比例)
|
||||
*/
|
||||
private String generateImageAndUploadToOss(String name, String prompt, String pathPrefix, String filePrefix) {
|
||||
if (kieAIConfig.getApiToken() == null || kieAIConfig.getApiToken().trim().isEmpty()) {
|
||||
logger.warn("KieAI API Token 未配置,跳过AI图片生成");
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
KieAINanoBananaRequest request = buildKieAIRequest(prompt);
|
||||
KieAICreateTaskResponse createResp = kieAIService.createTextToImageTask(request);
|
||||
String taskId = createResp.getTaskId();
|
||||
logger.info("KieAI文生图任务已创建, name: {}, taskId: {}", name, taskId);
|
||||
|
||||
KieAIQueryTaskResponse result = kieAIService.waitForTaskCompletion(taskId, KIEAI_WAIT_TIMEOUT);
|
||||
if (!"success".equalsIgnoreCase(result.getState()) || result.getResultJson() == null || result.getResultJson().isEmpty()) {
|
||||
logger.warn("KieAI文生图未成功, name: {}, state: {}", name, result != null ? result.getState() : "null");
|
||||
return null;
|
||||
}
|
||||
String generatedImageUrl = extractImageUrlFromResultJson(result.getResultJson());
|
||||
if (generatedImageUrl == null) {
|
||||
return null;
|
||||
}
|
||||
byte[] imageBytes = downloadImage(generatedImageUrl);
|
||||
if (imageBytes == null || imageBytes.length == 0) {
|
||||
return null;
|
||||
}
|
||||
return uploadToOss(imageBytes, name, pathPrefix, filePrefix);
|
||||
} catch (Exception e) {
|
||||
logger.error("generateImageAndUploadToOss 失败: name={}", name, e);
|
||||
return null;
|
||||
}
|
||||
return generateImageAndUploadToOss(name, prompt, pathPrefix, filePrefix, kieAIConfig.getDefaultImageSize());
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -291,15 +261,64 @@ public class DishImageServiceImpl implements DishImageService {
|
||||
* 构建 KieAI 请求
|
||||
*/
|
||||
private KieAINanoBananaRequest buildKieAIRequest(String prompt) {
|
||||
return buildKieAIRequest(prompt, kieAIConfig.getDefaultImageSize());
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建 KieAI 请求,可指定图片比例(如 1:1)
|
||||
*/
|
||||
private KieAINanoBananaRequest buildKieAIRequest(String prompt, String imageSize) {
|
||||
KieAINanoBananaRequest request = new KieAINanoBananaRequest();
|
||||
KieAINanoBananaRequest.Input input = new KieAINanoBananaRequest.Input();
|
||||
input.setPrompt(prompt);
|
||||
input.setOutput_format(kieAIConfig.getDefaultOutputFormat());
|
||||
input.setImage_size(kieAIConfig.getDefaultImageSize());
|
||||
input.setImage_size(imageSize != null ? imageSize : kieAIConfig.getDefaultImageSize());
|
||||
request.setInput(input);
|
||||
return request;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String generateImageAndUploadToOssForKnowledge(String name, String prompt) {
|
||||
if (StrUtil.isBlank(prompt)) {
|
||||
prompt = "健康营养主题的配图,简洁现代风格";
|
||||
}
|
||||
return generateImageAndUploadToOss(name, prompt, OSS_KNOWLEDGE_PATH, "knowledge-", "1:1");
|
||||
}
|
||||
|
||||
/**
|
||||
* 通用流程:根据 name 与 prompt 调用 KieAI 文生图 → 下载 → 压缩至 100KB → 上传 OSS(可指定比例)
|
||||
*/
|
||||
private String generateImageAndUploadToOss(String name, String prompt, String pathPrefix, String filePrefix, String imageSize) {
|
||||
if (kieAIConfig.getApiToken() == null || kieAIConfig.getApiToken().trim().isEmpty()) {
|
||||
logger.warn("KieAI API Token 未配置,跳过AI图片生成");
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
KieAINanoBananaRequest request = buildKieAIRequest(prompt, imageSize);
|
||||
KieAICreateTaskResponse createResp = kieAIService.createTextToImageTask(request);
|
||||
String taskId = createResp.getTaskId();
|
||||
logger.info("KieAI文生图任务已创建, name: {}, taskId: {}", name, taskId);
|
||||
|
||||
KieAIQueryTaskResponse result = kieAIService.waitForTaskCompletion(taskId, KIEAI_WAIT_TIMEOUT);
|
||||
if (!"success".equalsIgnoreCase(result.getState()) || result.getResultJson() == null || result.getResultJson().isEmpty()) {
|
||||
logger.warn("KieAI文生图未成功, name: {}, state: {}", name, result != null ? result.getState() : "null");
|
||||
return null;
|
||||
}
|
||||
String generatedImageUrl = extractImageUrlFromResultJson(result.getResultJson());
|
||||
if (generatedImageUrl == null) {
|
||||
return null;
|
||||
}
|
||||
byte[] imageBytes = downloadImage(generatedImageUrl);
|
||||
if (imageBytes == null || imageBytes.length == 0) {
|
||||
return null;
|
||||
}
|
||||
return uploadToOss(imageBytes, name, pathPrefix, filePrefix);
|
||||
} catch (Exception e) {
|
||||
logger.error("generateImageAndUploadToOss 失败: name={}", name, e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 下载图片(带浏览器 User-Agent 绕过 Cloudflare WAF)
|
||||
*/
|
||||
|
||||
@@ -116,6 +116,10 @@ public class ToolCozeServiceImpl implements ToolCozeService {
|
||||
try {
|
||||
CozeAPI client = getClient();
|
||||
List<Message> messages = buildMessages(request);
|
||||
if (messages == null || messages.isEmpty()) {
|
||||
logger.warn("Coze chat: no user message in request (additionalMessages/chatHistory empty or without content)");
|
||||
return CozeBaseResponse.error("请提供对话内容");
|
||||
}
|
||||
|
||||
CreateChatReq.CreateChatReqBuilder builder = CreateChatReq.builder()
|
||||
.botID(request.getBotId())
|
||||
@@ -142,6 +146,11 @@ public class ToolCozeServiceImpl implements ToolCozeService {
|
||||
try {
|
||||
CozeAPI client = getClient();
|
||||
List<Message> messages = buildMessages(request);
|
||||
if (messages == null || messages.isEmpty()) {
|
||||
logger.warn("Coze chat stream: no user message in request");
|
||||
emitter.completeWithError(new RuntimeException("请提供对话内容"));
|
||||
return emitter;
|
||||
}
|
||||
|
||||
CreateChatReq.CreateChatReqBuilder builder = CreateChatReq.builder()
|
||||
.botID(request.getBotId())
|
||||
|
||||
@@ -64,8 +64,11 @@ public class ToolFoodServiceImpl implements ToolFoodService {
|
||||
map.put("id", food.getFoodId());
|
||||
map.put("name", food.getName());
|
||||
map.put("image", food.getImage());
|
||||
map.put("category", food.getCategory());
|
||||
map.put("energy", food.getEnergy());
|
||||
map.put("protein", food.getProtein());
|
||||
map.put("potassium", food.getPotassium());
|
||||
map.put("phosphorus", food.getPhosphorus());
|
||||
map.put("suitabilityLevel", food.getSuitabilityLevel());
|
||||
result.add(map);
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import com.zbkj.common.utils.RedisUtil;
|
||||
import com.zbkj.service.dao.tool.V2KnowledgeDao;
|
||||
import com.zbkj.service.dao.tool.V2RecipeDao;
|
||||
import com.zbkj.service.dao.tool.V2UserPointsDao;
|
||||
import com.zbkj.service.service.SystemConfigService;
|
||||
import com.zbkj.service.service.tool.ToolHomeService;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
@@ -44,6 +45,9 @@ public class ToolHomeServiceImpl implements ToolHomeService {
|
||||
private static final long CACHE_SECONDS_RECOMMENDED_LIST = 600L;
|
||||
private static final long CACHE_SECONDS_HEALTH_STATUS = 300L;
|
||||
|
||||
/** 系统配置 key:首页是否显示四大功能入口,value=1 时显示 */
|
||||
private static final String CONFIG_KEY_FIELD01 = "field101";
|
||||
|
||||
@Resource
|
||||
private V2RecipeDao v2RecipeDao;
|
||||
|
||||
@@ -59,6 +63,9 @@ public class ToolHomeServiceImpl implements ToolHomeService {
|
||||
@Autowired
|
||||
private RedisUtil redisUtil;
|
||||
|
||||
@Autowired
|
||||
private SystemConfigService systemConfigService;
|
||||
|
||||
/**
|
||||
* 获取首页数据
|
||||
* @return 首页数据
|
||||
@@ -258,4 +265,17 @@ public class ToolHomeServiceImpl implements ToolHomeService {
|
||||
}
|
||||
return status;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Object> getDisplayConfig() {
|
||||
Map<String, Object> config = new HashMap<>();
|
||||
try {
|
||||
String field01 = systemConfigService.getValueByKey(CONFIG_KEY_FIELD01);
|
||||
config.put("showFunctionEntries", "1".equals(field01));
|
||||
} catch (Exception e) {
|
||||
log.warn("getDisplayConfig field01 not set, default showFunctionEntries=false", e);
|
||||
config.put("showFunctionEntries", false);
|
||||
}
|
||||
return config;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import com.zbkj.common.exception.CrmebException;
|
||||
import com.zbkj.common.model.tool.V2Knowledge;
|
||||
import com.zbkj.common.request.PageParamRequest;
|
||||
import com.zbkj.service.dao.tool.V2KnowledgeDao;
|
||||
import com.zbkj.service.service.tool.DishImageService;
|
||||
import com.zbkj.service.service.tool.ToolKnowledgeService;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
@@ -31,6 +32,9 @@ public class ToolKnowledgeServiceImpl implements ToolKnowledgeService {
|
||||
@Resource
|
||||
private V2KnowledgeDao v2KnowledgeDao;
|
||||
|
||||
@Resource
|
||||
private DishImageService dishImageService;
|
||||
|
||||
/**
|
||||
* 获取营养知识列表
|
||||
* @param pageParamRequest 分页参数
|
||||
@@ -103,4 +107,46 @@ public class ToolKnowledgeServiceImpl implements ToolKnowledgeService {
|
||||
|
||||
return BeanUtil.beanToMap(knowledge);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int fillMissingCoverImages(int limit) {
|
||||
if (limit <= 0) {
|
||||
limit = 10;
|
||||
}
|
||||
limit = Math.min(limit, 20);
|
||||
LambdaQueryWrapper<V2Knowledge> lqw = new LambdaQueryWrapper<>();
|
||||
lqw.in(V2Knowledge::getType, "guide", "article");
|
||||
lqw.eq(V2Knowledge::getStatus, "published");
|
||||
lqw.and(w -> w.isNull(V2Knowledge::getCoverImage).or().eq(V2Knowledge::getCoverImage, ""));
|
||||
lqw.orderByAsc(V2Knowledge::getKnowledgeId);
|
||||
lqw.last("LIMIT " + limit);
|
||||
List<V2Knowledge> list = v2KnowledgeDao.selectList(lqw);
|
||||
int updated = 0;
|
||||
for (V2Knowledge item : list) {
|
||||
String name = item.getTitle() != null ? item.getTitle() : "knowledge-" + item.getKnowledgeId();
|
||||
String prompt = buildKnowledgeCoverPrompt(item);
|
||||
String ossUrl = dishImageService.generateImageAndUploadToOssForKnowledge(name, prompt);
|
||||
if (StrUtil.isNotBlank(ossUrl)) {
|
||||
item.setCoverImage(ossUrl);
|
||||
v2KnowledgeDao.updateById(item);
|
||||
updated++;
|
||||
log.info("知识封面已更新: knowledgeId={}, title={}, ossUrl={}", item.getKnowledgeId(), item.getTitle(), ossUrl);
|
||||
} else {
|
||||
log.warn("知识封面生成失败: knowledgeId={}, title={}", item.getKnowledgeId(), item.getTitle());
|
||||
}
|
||||
}
|
||||
return updated;
|
||||
}
|
||||
|
||||
private String buildKnowledgeCoverPrompt(V2Knowledge item) {
|
||||
String title = StrUtil.isNotBlank(item.getTitle()) ? item.getTitle() : "营养知识";
|
||||
String summary = StrUtil.isNotBlank(item.getSummary()) ? item.getSummary() : "";
|
||||
if (StrUtil.isNotBlank(summary)) {
|
||||
if (summary.length() > 80) {
|
||||
summary = summary.substring(0, 80) + "…";
|
||||
}
|
||||
return "健康营养主题配图,标题:" + title + "。摘要:" + summary + "。简洁现代风格,1:1 方图。";
|
||||
}
|
||||
return "健康营养主题配图,标题:" + title + "。简洁现代风格,1:1 方图。";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,4 +37,13 @@ public interface DishImageService {
|
||||
* @return 更新后的图片 URL(若已是 OSS 或更新成功返回 URL,失败返回原 image 或 null)
|
||||
*/
|
||||
String ensureFoodImageAndUpdateDb(Long foodId);
|
||||
|
||||
/**
|
||||
* 为知识封面生成 1:1 图片并压缩到 100KB 以内上传到 OSS
|
||||
*
|
||||
* @param name 名称(用于生成文件名)
|
||||
* @param prompt 文生图描述
|
||||
* @return OSS 完整 URL,失败返回 null
|
||||
*/
|
||||
String generateImageAndUploadToOssForKnowledge(String name, String prompt);
|
||||
}
|
||||
|
||||
@@ -35,5 +35,11 @@ public interface ToolHomeService {
|
||||
* @return 健康档案状态
|
||||
*/
|
||||
Map<String, Object> getHealthStatus();
|
||||
|
||||
/**
|
||||
* 获取首页展示配置(如是否显示四大功能入口,依赖 eb_system_config 中 field01:1=显示)
|
||||
* @return 含 showFunctionEntries 等展示开关
|
||||
*/
|
||||
Map<String, Object> getDisplayConfig();
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
package com.zbkj.service.service.tool;
|
||||
|
||||
import com.zbkj.common.request.kieai.KieAIGeminiChatRequest;
|
||||
import com.zbkj.common.request.kieai.KieAINanoBananaRequest;
|
||||
import com.zbkj.common.response.kieai.KieAICreateTaskResponse;
|
||||
import com.zbkj.common.response.kieai.KieAIQueryTaskResponse;
|
||||
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* KieAI 服务接口
|
||||
@@ -53,4 +57,20 @@ public interface ToolKieAIService {
|
||||
* @param response 回调数据
|
||||
*/
|
||||
void handleTaskCallback(String taskId, KieAIQueryTaskResponse response);
|
||||
|
||||
/**
|
||||
* Gemini 2.5 Flash 非流式对话
|
||||
*
|
||||
* @param request 对话请求
|
||||
* @return 完整 JSON 响应(id, choices, usage 等)
|
||||
*/
|
||||
Map<String, Object> geminiChat(KieAIGeminiChatRequest request);
|
||||
|
||||
/**
|
||||
* Gemini 2.5 Flash 流式对话(SSE)
|
||||
*
|
||||
* @param request 对话请求
|
||||
* @return SseEmitter,向前端推送 data 事件
|
||||
*/
|
||||
SseEmitter geminiChatStream(KieAIGeminiChatRequest request);
|
||||
}
|
||||
|
||||
@@ -35,5 +35,13 @@ public interface ToolKnowledgeService {
|
||||
* @return 营养素详情
|
||||
*/
|
||||
Map<String, Object> getNutrientDetail(String name);
|
||||
|
||||
/**
|
||||
* 为 cover_image 为空的饮食指南/科普文章记录生成封面图(KieAI 1:1,压缩至 100KB,上传 OSS 并写回 v2_knowledge)
|
||||
*
|
||||
* @param limit 本次最多处理条数
|
||||
* @return 成功更新条数
|
||||
*/
|
||||
int fillMissingCoverImages(int limit);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user