feat: 集成 KieAI 服务,移除 models-integration 子项目

- 添加 Gemini 2.5 Flash 对话接口(流式+非流式)
- 添加 NanoBanana 图像生成/编辑接口
- 添加 Sora2 视频生成接口(文生视频、图生视频、去水印)
- 移除 models-integration 子项目(功能已迁移至主后端)
- 新增测试文档和 Playwright E2E 配置
- 更新前端页面和 API 接口
- 更新后端配置和日志处理
This commit is contained in:
2026-03-03 15:33:50 +08:00
parent 1ddb051977
commit 4be53dcd1b
586 changed files with 21142 additions and 25130 deletions

View File

@@ -130,7 +130,9 @@ swagger:
username: #访问swagger的账号
password: #访问swagger的密码
# 行为验证码
# 行为验证码captcha.enabled: false 可关闭验证,便于测试)
captcha:
enabled: false
aj:
captcha:
type: default # 验证码类型

View File

@@ -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 = "图片 URLtype=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

View File

@@ -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" }

View File

@@ -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

View File

@@ -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);
}
/**
* 创建文本生成图像任务
*/

View File

@@ -312,6 +312,16 @@ public class ToolController {
return CommonResult.success(toolKnowledgeService.getNutrientDetail(name));
}
/**
* 为 cover_image 为空的饮食指南/科普文章生成封面图KieAI 1:1100KB 内,上传 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());
}
// ==================== 食谱相关 ====================
/**

View File

@@ -1,6 +1,6 @@
# CRMEB 相关配置
crmeb:
imagePath: /usr/local/crmeb/crmebimage/ # 服务器图片路径配置 斜杠结尾
imagePath: /www/wwwroot/crmebimage/ # 服务器图片路径配置 斜杠结尾
asyncConfig: true #是否同步config表数据到redis
server:

View File

@@ -104,7 +104,9 @@ mybatis-plus:
logic-not-delete-value: 0 # 逻辑未删除值(默认为 0)
# 行为验证码
# 行为验证码captcha.enabled: false 可关闭验证,便于测试)
captcha:
enabled: false
aj:
captcha:
type: default # 验证码类型

View File

@@ -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);
}

View File

@@ -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;
}

View File

@@ -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
*/

View File

@@ -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())

View File

@@ -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);
}

View File

@@ -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;
}
}

View File

@@ -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 方图。";
}
}

View File

@@ -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);
}

View File

@@ -35,5 +35,11 @@ public interface ToolHomeService {
* @return 健康档案状态
*/
Map<String, Object> getHealthStatus();
/**
* 获取首页展示配置(如是否显示四大功能入口,依赖 eb_system_config 中 field011=显示)
* @return 含 showFunctionEntries 等展示开关
*/
Map<String, Object> getDisplayConfig();
}

View File

@@ -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);
}

View File

@@ -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);
}