fix: order的内容

This commit is contained in:
old burden 2026-04-10 12:04:06 +08:00
parent fa4eb95e2b
commit 91769fd2df
7 changed files with 441 additions and 100 deletions

View File

@ -43,6 +43,12 @@ public class ByteApiController extends BaseController {
@Value("${byteapi.callBackUrl}")
private String url;
@Value("${nanobanana.callbackUrl}")
private String nanoCallbackUrl;
@Value("${nanobanana.token}")
private String nanoToken;
@PostMapping("/promptToImg")
@ApiOperation("文生图")
public AjaxResult promptToImg(@RequestBody ByteApiRequest request) {
@ -84,31 +90,32 @@ public class ByteApiController extends BaseController {
return AjaxResult.error(-1, "You have a low balance, please recharge");
}
aiOrder.setText(text);
ByteBodyReq byteBodyReq = new ByteBodyReq();
byteBodyReq.setModel("ep-20251104104536-2gpgz");
byteBodyReq.setPrompt(text);
byteBodyReq.setSequential_image_generation("disabled");
byteBodyReq.setResponse_format("url");
byteBodyReq.setSize("2K");
byteBodyReq.setStream(false);
byteBodyReq.setWatermark(false);
ByteBodyRes byteBodyRes = byteService.promptToImg(byteBodyReq);
List<ByteDataRes> data = byteBodyRes.getData();
ByteDataRes byteDataRes = data.get(0);
String url = byteDataRes.getUrl();
url = awsS3Util.uploadFileByUrl(url);
if (url == null) {
// 判断生成失败退回金额逻辑
// 4. 组装NanoBanana (原火山)接口参数 - 符合用户需求
NanoBananaRequest nanoRequest = NanoBananaRequest.forTextToImage(
text,
nanoCallbackUrl,
request.getAspectRatio() != null ? request.getAspectRatio() : "auto",
request.getResolution() != null ? request.getResolution() : "1K"
);
// 5. 调用NanoBanana接口 (curl POST https://api.nanobananaapi.ai/...)
NanoBananaResponse nanoResponse = byteService.generateImage(nanoRequest);
if (nanoResponse == null || nanoResponse.getCode() != 200 || nanoResponse.getData() == null) {
aiOrderService.orderFailure(aiOrder);
return AjaxResult.error(-2, "generation failed, balance has been refunded");
}
aiOrder.setResult(url);
aiOrderService.orderSuccess(aiOrder);
return AjaxResult.success(url);
String taskId = nanoResponse.getData().getTaskId();
aiOrder.setTaskId(taskId); // 保存taskId用于回调匹配
aiOrder.setResult(taskId); // 临时用taskId作为result后续回调更新为真实URL
aiOrderService.orderSuccess(aiOrder); // 先标记为进行中回调成功后再更新
return AjaxResult.success(taskId); // 返回taskId给前端后续可轮询或等待回调
} catch (Exception e) {
// 判断生成失败退回金额逻辑
aiOrderService.orderFailure(aiOrder);
// return AjaxResult.error(-2, "generation failed, balance has been refunded");
throw new RuntimeException(e);
}
}
@ -160,30 +167,36 @@ public class ByteApiController extends BaseController {
}
aiOrder.setText(text);
aiOrder.setImg1(firstUrl.toString());
ByteBodyReq byteBodyReq = new ByteBodyReq();
byteBodyReq.setModel("ep-20251104104536-2gpgz");
byteBodyReq.setPrompt(text);
byteBodyReq.setImage(firstUrl);
byteBodyReq.setSequential_image_generation("disabled");
byteBodyReq.setResponse_format("url");
byteBodyReq.setSize("2K");
byteBodyReq.setStream(false);
byteBodyReq.setWatermark(false);
ByteBodyRes byteBodyRes = byteService.promptToImg(byteBodyReq);
List<ByteDataRes> data = byteBodyRes.getData();
ByteDataRes byteDataRes = data.get(0);
String url = byteDataRes.getUrl();
url = awsS3Util.uploadFileByUrl(url);
if (url == null) {
// 判断生成失败退回金额逻辑
// 4. 组装NanoBanana接口参数 - 图生图
List<String> imageUrls = new ArrayList<>();
if (firstUrl != null) {
imageUrls.add(firstUrl.toString());
}
NanoBananaRequest nanoRequest = NanoBananaRequest.forImageToImage(
text,
imageUrls,
nanoCallbackUrl,
request.getAspectRatio() != null ? request.getAspectRatio() : "auto",
request.getResolution() != null ? request.getResolution() : "1K"
);
// 5. 调用NanoBanana接口
NanoBananaResponse nanoResponse = byteService.generateImageWithReference(nanoRequest);
if (nanoResponse == null || nanoResponse.getCode() != 200 || nanoResponse.getData() == null) {
aiOrderService.orderFailure(aiOrder);
return AjaxResult.error(-2, "generation failed, balance has been refunded");
}
aiOrder.setResult(url);
String taskId = nanoResponse.getData().getTaskId();
aiOrder.setTaskId(taskId);
aiOrder.setResult(taskId);
aiOrderService.orderSuccess(aiOrder);
return AjaxResult.success(url);
return AjaxResult.success(taskId);
} catch (Exception e) {
aiOrderService.orderFailure(aiOrder);
throw new RuntimeException(e);
@ -311,10 +324,59 @@ public class ByteApiController extends BaseController {
return AjaxResult.success(byteBodyRes);
}
@GetMapping(value = "/callBack")
@ApiOperation("视频下载回调")
@PostMapping(value = "/nano-callback")
@ApiOperation("NanoBanana生成回调")
@Anonymous
public AjaxResult callBack(@PathVariable("id") ByteBodyRes byteBodyRes) throws Exception {
public AjaxResult nanoCallback(@RequestBody NanoBananaCallback callback) throws Exception {
if (callback == null || callback.getData() == null) {
return AjaxResult.error("回调数据为空");
}
NanoBananaCallback.NanoBananaCallbackData data = callback.getData();
String taskId = data.getTaskId();
Integer successFlag = data.getSuccessFlag();
// 根据successFlag处理不同状态
if (successFlag == 1 && data.getResponse() != null && data.getResponse().getResultImageUrl() != null) {
// 成功 - 上传到S3并更新订单
String imageUrl = data.getResponse().getResultImageUrl();
String s3Url = awsS3Util.uploadFileByUrl(imageUrl);
if (s3Url != null) {
AiOrder aiOrderByResult = aiOrderService.getAiOrderByResult(taskId);
if (aiOrderByResult != null) {
AiOrder aiOrder = new AiOrder();
aiOrder.setId(aiOrderByResult.getId());
aiOrder.setResult(s3Url);
aiOrder.setTaskId(taskId);
aiOrder.setStatus(1);
aiOrderService.updateAiOrder(aiOrder);
return AjaxResult.success("回调处理成功,图像已上传");
}
}
return AjaxResult.error("图像上传失败");
} else if (successFlag == 3 || successFlag == 2) {
// 失败 - 退款
AiOrder aiOrderByResult = aiOrderService.getAiOrderByResult(taskId);
if (aiOrderByResult != null) {
aiOrderService.orderFailure(aiOrderByResult);
return AjaxResult.success("回调处理失败,已退款");
}
} else if (successFlag == 0) {
// 生成中 - 可忽略或记录
return AjaxResult.success("任务生成中");
}
return AjaxResult.success("回调已接收");
}
/**
* 保留原有视频回调GET转POST兼容
*/
@PostMapping(value = "/callBack")
@ApiOperation("视频回调")
@Anonymous
public AjaxResult callBack(@RequestBody ByteBodyRes byteBodyRes) throws Exception {
if ("succeeded".equals(byteBodyRes.getStatus())) {
String id = byteBodyRes.getId();
content content = byteBodyRes.getContent();
@ -322,12 +384,13 @@ public class ByteApiController extends BaseController {
videoUrl = awsS3Util.uploadFileByUrl(videoUrl);
content.setVideo_url(videoUrl);
AiOrder aiOrderByResult = aiOrderService.getAiOrderByResult(id);
if (aiOrderByResult != null) {
AiOrder aiOrder = new AiOrder();
aiOrder.setId(aiOrderByResult.getId());
aiOrder.setResult(videoUrl);
// aiOrder.setUpdateBy(SecurityUtils.getLoginAiUser().getUsername());
aiOrderService.updateAiOrder(aiOrder);
}
}
return AjaxResult.success(byteBodyRes);
}

View File

@ -222,6 +222,14 @@ byteapi:
apiKey: 327d2815-2516-44c2-9e32-2dc50bf7afd7
callBackUrl: www.google.com
nanobanana:
# NanoBanana API Token (Bearer Token)
token: your_nanobanana_token_here
# 回调地址,需替换为实际部署域名 (POST接口)
callbackUrl: https://your-domain.com/api/ai/nano-callback
# 生成API地址
apiUrl: https://api.nanobananaapi.ai/api/v1/nanobanana/generate-2
jinsha:
url: https://api.jinshapay.xyz
appId: 1763617360

View File

@ -0,0 +1,101 @@
package com.ruoyi.ai.domain;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
/**
* NanoBanana API 回调请求体
* 符合阿里巴巴Java开发手册规范
*
* @author AI Assistant
* @date 2026-04-10
*/
@Data
public class NanoBananaCallback {
/**
* 顶层响应码
*/
private Integer code;
/**
* 状态消息
*/
private String msg;
/**
* 回调数据
*/
private NanoBananaCallbackData data;
@Data
public static class NanoBananaCallbackData {
/**
* 任务 ID
*/
@JsonProperty("taskId")
private String taskId;
/**
* 本次调用使用的参数 JSON 字符串
*/
@JsonProperty("paramJson")
private String paramJson;
/**
* 任务完成时间
*/
@JsonProperty("completeTime")
private String completeTime;
/**
* 响应数据包含图像URL
*/
private NanoBananaCallbackResponse response;
/**
* 生成状态标志
* 0-生成中, 1-成功, 2-创建失败, 3-生成失败
*/
@JsonProperty("successFlag")
private Integer successFlag;
/**
* 错误码 422 敏感内容
*/
@JsonProperty("errorCode")
private Integer errorCode;
/**
* 错误消息
*/
@JsonProperty("errorMessage")
private String errorMessage;
/**
* 操作类型/模型标识
*/
@JsonProperty("operationType")
private String operationType;
@JsonProperty("createTime")
private String createTime;
}
@Data
public static class NanoBananaCallbackResponse {
/**
* 原始图像URL可能为null
*/
@JsonProperty("originImageUrl")
private String originImageUrl;
/**
* 结果图像URL成功时存在
*/
@JsonProperty("resultImageUrl")
private String resultImageUrl;
}
}

View File

@ -0,0 +1,81 @@
package com.ruoyi.ai.domain;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
import java.util.List;
/**
* NanoBanana API 文生图/图生图请求参数
* 符合阿里巴巴Java开发手册规范
*
* @author AI Assistant
* @date 2026-04-10
*/
@Data
public class NanoBananaRequest {
/**
* 文本提示词必填最长 20000 字符
*/
private String prompt;
/**
* 参考图 URL 数组文生图时传空数组最多 14
*/
@JsonProperty("imageUrls")
private List<String> imageUrls = List.of();
/**
* 生成图像宽高比
* 支持 1:11:41:82:33:23:44:14:34:55:48:19:1616:921:9auto
*/
@JsonProperty("aspectRatio")
private String aspectRatio = "auto";
/**
* 分辨率质量可选 1K / 2K / 4K
*/
private String resolution = "1K";
/**
* 是否启用 Google Web Search 增强默认 false
*/
@JsonProperty("googleSearch")
private Boolean googleSearch = false;
/**
* 输出格式支持 png / jpg默认 jpg
*/
@JsonProperty("outputFormat")
private String outputFormat = "jpg";
/**
* 可选回调 URL用于接收任务完成通知
*/
@JsonProperty("callBackUrl")
private String callBackUrl;
/**
* 构造函数 - 文生图
*/
public static NanoBananaRequest forTextToImage(String prompt, String callBackUrl, String aspectRatio, String resolution) {
NanoBananaRequest req = new NanoBananaRequest();
req.setPrompt(prompt);
req.setCallBackUrl(callBackUrl);
if (aspectRatio != null) req.setAspectRatio(aspectRatio);
if (resolution != null) req.setResolution(resolution);
return req;
}
/**
* 构造函数 - 图生图
*/
public static NanoBananaRequest forImageToImage(String prompt, List<String> imageUrls, String callBackUrl, String aspectRatio, String resolution) {
NanoBananaRequest req = forTextToImage(prompt, callBackUrl, aspectRatio, resolution);
if (imageUrls != null && !imageUrls.isEmpty()) {
req.setImageUrls(imageUrls);
}
return req;
}
}

View File

@ -0,0 +1,40 @@
package com.ruoyi.ai.domain;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
/**
* NanoBanana API 提交任务响应
* 符合阿里巴巴Java开发手册规范
*
* @author AI Assistant
* @date 2026-04-10
*/
@Data
public class NanoBananaResponse {
/**
* 响应码200 表示成功
*/
private Integer code;
/**
* 响应消息
*/
private String message;
/**
* 响应数据
*/
private NanoBananaData data;
@Data
public static class NanoBananaData {
/**
* 任务ID用于后续回调匹配
*/
@JsonProperty("taskId")
private String taskId;
}
}

View File

@ -2,26 +2,50 @@ package com.ruoyi.ai.service;
import com.ruoyi.ai.domain.ByteBodyReq;
import com.ruoyi.ai.domain.ByteBodyRes;
import com.ruoyi.ai.domain.NanoBananaRequest;
import com.ruoyi.ai.domain.NanoBananaResponse;
/**
* ByteService 接口
* 现已切换为 NanoBanana API原Byte/火山引擎接口已弃用
* 符合阿里巴巴Java开发手册规范
*
* @author shi
* @date 2026-04-10
*/
public interface IByteService {
/**
* 文生图
* 文生图 - 使用 NanoBanana API 异步生成返回 taskId
*/
NanoBananaResponse generateImage(NanoBananaRequest req) throws Exception;
/**
* 图生图 - 使用 NanoBanana API 异步生成返回 taskId
*/
NanoBananaResponse generateImageWithReference(NanoBananaRequest req) throws Exception;
/**
* 旧接口兼容文生图/图生图
* @deprecated 使用 generateImage 替代
*/
@Deprecated
ByteBodyRes promptToImg(ByteBodyReq req) throws Exception;
/**
* 图生图
* 旧接口兼容图生图
* @deprecated 使用 generateImageWithReference 替代
*/
@Deprecated
ByteBodyRes imgToImg(ByteBodyReq req) throws Exception;
/**
* 首尾帧图生视频
* 首尾帧图生视频保持原有
*/
ByteBodyRes imgToVideo(ByteBodyReq req) throws Exception;
/**
* 下载视频
* 下载视频保持原有
*/
ByteBodyRes uploadVideo(String id) throws Exception;
}

View File

@ -3,8 +3,7 @@ package com.ruoyi.ai.service.impl;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.ruoyi.ai.domain.ByteBodyReq;
import com.ruoyi.ai.domain.ByteBodyRes;
import com.ruoyi.ai.domain.*;
import com.ruoyi.ai.service.IByteService;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.common.utils.http.OkHttpUtils;
@ -12,18 +11,36 @@ import okhttp3.*;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import java.util.List;
/**
* ByteService 实现类
* 已切换为 NanoBanana API (https://api.nanobananaapi.ai)
* 原火山/Byte接口保留用于视频功能
* 符合阿里巴巴Java开发手册规范使用Lombok清晰注释异常处理
*
* @author shi
* @date 2026-04-10
*/
@Service
public class ByteService implements IByteService {
// private final OkHttpClient okHttpClient = OkHttpUtils.createOkHttpClient();
// Jackson用于JSON序列化/反序列化
private final ObjectMapper objectMapper = new ObjectMapper()
.setSerializationInclusion(JsonInclude.Include.NON_NULL)
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
.configure(DeserializationFeature.ACCEPT_EMPTY_STRING_AS_NULL_OBJECT, true);
// API地址可配置在配置文件中
@Value("${nanobanana.apiUrl:https://api.nanobananaapi.ai/api/v1/nanobanana/generate-2}")
private String nanoApiUrl;
@Value("${nanobanana.token:}")
private String nanoToken;
@Value("${nanobanana.callbackUrl:}")
private String callbackUrl;
// 保留原有Byte配置用于视频功能
@Value("${byteapi.url}")
private String API_URL;
@ -31,60 +48,71 @@ public class ByteService implements IByteService {
private String apiKey;
@Override
public ByteBodyRes promptToImg(ByteBodyReq req) throws Exception {
return this.imgToImg(req);
}
@Override
public ByteBodyRes imgToImg(ByteBodyReq req) throws Exception {
// 1. 验证请求参数可选根据业务需求
public NanoBananaResponse generateImage(NanoBananaRequest req) throws Exception {
// 4. 组装NanoBanana接口参数 (替换原VolcengineImageRequest)
if (StringUtils.isBlank(req.getPrompt())) {
throw new Exception("imgToImg errorprompt is null");
throw new Exception("prompt不能为空");
}
if (StringUtils.isBlank(req.getCallBackUrl())) {
req.setCallBackUrl(callbackUrl);
}
// 2. 构建请求体JSON基于ByteBodyReq的字段
// 注意ByteBodyReq需包含与API参数对应的字段modelprompt等
// 构建JSON请求体
String jsonBody = objectMapper.writeValueAsString(req);
// 3. 构建请求
// 5. 调用NanoBanana API (使用curl对应POST请求)
Request request = new Request.Builder()
.url(API_URL + "/images/generations")
.url(nanoApiUrl)
.header("Authorization", "Bearer " + nanoToken)
.header("Content-Type", "application/json")
.header("Authorization", "Bearer " + apiKey)
// .proxy(new Proxy(Proxy.Type.HTTP, new InetSocketAddress("192.168.1.1", 8080)))
.post(RequestBody.create(
MediaType.parse("application/json"),
MediaType.parse("application/json; charset=utf-8"),
jsonBody
))
.build();
// 4. 发送同步请求因方法需要返回值使用execute而非enqueue
Response response = OkHttpUtils.newCall(request).execute();
// 5. 处理响应
if (!response.isSuccessful()) {
// 非200状态返回错误信息假设ByteBodyRes有error字段
String errorMsg = response.body() != null ? response.body().string() : "imgToImg error";
throw new Exception("imgToImg error" + errorMsg);
String errorMsg = response.body() != null ? response.body().string() : "generateImage error";
throw new Exception("NanoBanana API调用失败: " + errorMsg);
}
// 6. 解析成功响应为ByteBodyRes
if (response.body() == null) {
throw new Exception("imgToImg response null");
throw new Exception("NanoBanana API响应为空");
}
String responseBody = response.body().string();
return objectMapper.readValue(responseBody, ByteBodyRes.class);
return objectMapper.readValue(responseBody, NanoBananaResponse.class);
}
@Override
public NanoBananaResponse generateImageWithReference(NanoBananaRequest req) throws Exception {
return generateImage(req); // 复用imageUrls已在请求中设置
}
@Override
@Deprecated
public ByteBodyRes promptToImg(ByteBodyReq req) throws Exception {
// 兼容旧代码实际已切换到NanoBanana
throw new UnsupportedOperationException("promptToImg 已弃用,请使用 generateImage");
}
@Override
@Deprecated
public ByteBodyRes imgToImg(ByteBodyReq req) throws Exception {
throw new UnsupportedOperationException("imgToImg 已弃用,请使用 generateImageWithReference");
}
@Override
public ByteBodyRes imgToVideo(ByteBodyReq req) throws Exception {
// 视频功能保持原有Byte实现
if (StringUtils.isBlank(req.getPrompt())) {
throw new Exception("imgToVideo errorprompt is null");
}
// 1. 验证请求参数可选根据业务需求
// if (StringUtils.isBlank(req.getPrompt())) {
// throw new Exception("imgToVideo errorprompt is null");
// }
// 2. 构建请求体JSON基于ByteBodyReq的字段
// 注意ByteBodyReq需包含与API参数对应的字段modelprompt等
String jsonBody = objectMapper.writeValueAsString(req);
// 3. 构建请求
Request request = new Request.Builder()
.url(API_URL + "/contents/generations/tasks")
.header("Content-Type", "application/json")
@ -94,51 +122,47 @@ public class ByteService implements IByteService {
jsonBody
))
.build();
// 4. 发送同步请求因方法需要返回值使用execute而非enqueue
Response response = OkHttpUtils.newCall(request).execute();
// 5. 处理响应
if (!response.isSuccessful()) {
// 非200状态返回错误信息假设ByteBodyRes有error字段
String errorMsg = response.body() != null ? response.body().string() : "imgToVideo error";
throw new Exception("imgToVideo error" + errorMsg);
}
// 6. 解析成功响应为ByteBodyRes
if (response.body() == null) {
throw new Exception("imgToVideo response null");
}
String responseBody = response.body().string();
return objectMapper.readValue(responseBody, ByteBodyRes.class);
}
@Override
public ByteBodyRes uploadVideo(String id) throws Exception {
// 1. 验证请求参数可选根据业务需求
// 视频功能保持原有Byte实现
if (StringUtils.isBlank(id)) {
throw new Exception("uploadVideo errorid is null");
}
// 2. 构建请求体JSON基于ByteBodyReq的字段
// 注意ByteBodyReq需包含与API参数对应的字段modelprompt等
//String jsonBody = objectMapper.writeValueAsString(req);
// 3. 构建请求
Request request = new Request.Builder()
.url(API_URL + "/contents/generations/tasks/" + id)
.header("Content-Type", "application/json")
.header("Authorization", "Bearer " + apiKey)
.get()
.build();
// 4. 发送同步请求因方法需要返回值使用execute而非enqueue
Response response = OkHttpUtils.newCall(request).execute();
// 5. 处理响应
if (!response.isSuccessful()) {
// 非200状态返回错误信息假设ByteBodyRes有error字段
String errorMsg = response.body() != null ? response.body().string() : "uploadVideo error";
throw new Exception("uploadVideo error" + errorMsg);
}
// 6. 解析成功响应为ByteBodyRes
if (response.body() == null) {
throw new Exception("uploadVideo response null");
}
String responseBody = response.body().string();
return objectMapper.readValue(responseBody, ByteBodyRes.class);
}