From 80c136ad18748ec6b26767603b6fbe5cffe09c0a Mon Sep 17 00:00:00 2001 From: yys <47@gamerwa.com> Date: Thu, 9 Apr 2026 16:52:50 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E7=94=9F=E6=88=90=E8=A7=86=E9=A2=91?= =?UTF-8?q?=E6=97=B6=EF=BC=8C=E6=8C=89=E5=AD=97=E8=8A=82=E6=8E=A5=E5=8F=A3?= =?UTF-8?q?=E6=89=A3=E5=87=8F=E7=94=A8=E9=87=8F(=E4=BB=A3=E7=A0=81?= =?UTF-8?q?=E5=88=9D=E6=AD=A5=E5=AE=8C=E6=88=90)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/ruoyi/api/ByteApiController.java | 302 ++++++++++++------ .../resources/i18n/messages_en_US.properties | 3 +- .../video/dto/VideoTaskCallBackRequest.java | 91 ++++++ .../ruoyi/common/enums/AiOrderStatusType.java | 13 + .../common/enums/VideoTaskStatusType.java | 46 +++ web-api/ruoyi-framework/pom.xml | 7 + .../java/com/ruoyi/ai/domain/AiOrder.java | 3 + .../com/ruoyi/ai/mapper/AiOrderMapper.java | 4 + .../com/ruoyi/ai/service/IAiOrderService.java | 6 + .../com/ruoyi/ai/service/IByteService.java | 6 + .../ai/service/impl/AiOrderServiceImpl.java | 22 +- .../ruoyi/ai/service/impl/ByteService.java | 2 +- 12 files changed, 406 insertions(+), 99 deletions(-) create mode 100644 web-api/ruoyi-common/src/main/java/com/ruoyi/common/core/request/video/dto/VideoTaskCallBackRequest.java create mode 100644 web-api/ruoyi-common/src/main/java/com/ruoyi/common/enums/AiOrderStatusType.java create mode 100644 web-api/ruoyi-common/src/main/java/com/ruoyi/common/enums/VideoTaskStatusType.java diff --git a/web-api/ruoyi-admin/src/main/java/com/ruoyi/api/ByteApiController.java b/web-api/ruoyi-admin/src/main/java/com/ruoyi/api/ByteApiController.java index 7871511..dcbbf68 100644 --- a/web-api/ruoyi-admin/src/main/java/com/ruoyi/api/ByteApiController.java +++ b/web-api/ruoyi-admin/src/main/java/com/ruoyi/api/ByteApiController.java @@ -1,29 +1,33 @@ package com.ruoyi.api; import com.ruoyi.ai.domain.*; -import com.ruoyi.ai.service.IAiManagerService; -import com.ruoyi.ai.service.IAiOrderService; -import com.ruoyi.ai.service.IAiTagService; -import com.ruoyi.ai.service.IByteService; +import com.ruoyi.ai.service.*; import com.ruoyi.api.request.ByteApiRequest; import com.ruoyi.common.annotation.Anonymous; +import com.ruoyi.common.constant.BalanceChangerConstants; import com.ruoyi.common.core.controller.BaseController; import com.ruoyi.common.core.domain.AjaxResult; -import com.ruoyi.common.core.domain.model.LoginAiUser; +import com.ruoyi.common.core.request.video.dto.VideoTaskCallBackRequest; +import com.ruoyi.common.core.response.video.GetVideoGenerationTaskResponse; +import com.ruoyi.common.enums.AiOrderStatusType; +import com.ruoyi.common.enums.VideoTaskStatusType; import com.ruoyi.common.utils.RandomStringUtil; -import com.ruoyi.common.utils.SecurityUtils; import com.ruoyi.common.utils.StringUtils; import com.ruoyi.common.utils.TencentCosUtil; import io.swagger.annotations.Api; import io.swagger.annotations.ApiOperation; import lombok.RequiredArgsConstructor; +import org.redisson.api.RLock; +import org.redisson.api.RedissonClient; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.transaction.annotation.Transactional; import org.springframework.web.bind.annotation.*; +import java.math.BigDecimal; import java.util.ArrayList; import java.util.List; +import java.util.concurrent.TimeUnit; import java.util.regex.Pattern; /** @@ -34,18 +38,22 @@ import java.util.regex.Pattern; @Api(tags = "生成内容") @RequiredArgsConstructor(onConstructor_ = @Autowired) public class ByteApiController extends BaseController { + // 回调时分布式锁的key前缀 + private static final String VOLC_CALLBACK_LOCK_KEY_PREFIX = "volc:callback:lock:"; + // 锁参数 + private static final int LOCK_WAIT_SECONDS = 10; + private static final int LOCK_LEASE_SECONDS = 20; + // 任务成功时流水表备注 + private static final String TASK_SUCCESS_BALANCE_REMARK = "order.number.generation.successes"; private final IByteService byteService; private final TencentCosUtil tencentCosUtil; private final IAiOrderService aiOrderService; private final IAiManagerService managerService; private final IAiTagService aiTagService; - @Value("${byteapi.callBackUrl}") - private String url; + private final IAiUserService aiUserService; + private final RedissonClient redissonClient; - // 火山引擎配置 - @Value("${volcengine.ark.baseUrl}") - private String volcBaseUrl; @Value("${volcengine.ark.callbackUrl}") private String volcCallbackUrl; @@ -95,7 +103,7 @@ public class ByteApiController extends BaseController { ByteBodyReq byteBodyReq = new ByteBodyReq(); // model由前端传入,默认为Seedance 2.0 - byteBodyReq.setModel(StringUtils.isNotEmpty(request.getModel()) ? + byteBodyReq.setModel(StringUtils.isNotEmpty(request.getModel()) ? request.getModel() : "ep-20260326165811-dlkth"); byteBodyReq.setPrompt(text); byteBodyReq.setSequential_image_generation("disabled"); @@ -254,13 +262,13 @@ public class ByteApiController extends BaseController { ByteBodyReq byteBodyReq = new ByteBodyReq(); // model由前端传入,默认为Seedance2.0 - byteBodyReq.setModel(StringUtils.isNotEmpty(request.getModel()) ? + byteBodyReq.setModel(StringUtils.isNotEmpty(request.getModel()) ? request.getModel() : "ep-20260326165811-dlkth"); byteBodyReq.setCallback_url(volcCallbackUrl); // 构建符合火山引擎格式的content List contentList = new ArrayList<>(); - + // 文本提示词 ContentItem textItem = new ContentItem(); textItem.setType("text"); @@ -331,95 +339,200 @@ public class ByteApiController extends BaseController { @PostMapping(value = "/volcCallback") @ApiOperation("火山引擎视频回调") @Anonymous - public AjaxResult volcCallback(@RequestBody ByteBodyRes byteBodyRes) throws Exception { - logger.info("volcCallback 收到回调数据: {}", byteBodyRes); - String id = byteBodyRes.getId(); - if (StringUtils.isEmpty(id)) { - logger.warn("volcCallback 无任务 id,跳过业务处理"); - return AjaxResult.success("callback success"); + public AjaxResult volcCallback(@RequestBody VideoTaskCallBackRequest request) throws Exception { + logger.info("volcCallback 收到回调数据: {}", request); + // 1、基础参数校验 + AjaxResult result = volcCallbackBaseCheck(request); + if (result != null) { + return result; } - - Integer code = byteBodyRes.getCode(); - boolean codeError = code != null && code != 200; - String st = byteBodyRes.getStatus(); - - if (st != null && !st.isEmpty()) { - // 执行中状态 - Integer extStatus = null; - if ("queued".equals(st)) { - extStatus = 0; - } else if ("running".equals(st)) { - extStatus = 1; + // 2、从官方获取任务数据 + String apiKey = byteService.resolveCurrentAiUserApiKey(); + GetVideoGenerationTaskResponse taskResp = byteService.getVideoGenerationTasks(request.getId(), apiKey); + // 3、官方数据校验 + result = volcCallbackByteCheck(request, taskResp); + if (result != null) { + return result; + } + // 4、查询订单(同 taskId 串行:Redisson 分布式锁;步骤 1~3 已在锁外) + String taskId = request.getId(); + String lockKey = VOLC_CALLBACK_LOCK_KEY_PREFIX + taskId; + RLock lock = redissonClient.getLock(lockKey); + boolean locked = false; + try { + locked = lock.tryLock(LOCK_WAIT_SECONDS, LOCK_LEASE_SECONDS, TimeUnit.SECONDS); + if (!locked) { + logger.warn("volcCallback skip: concurrent handling for same task, third party order num = {}", taskId); + return AjaxResult.success(); } - if (extStatus != null) { - AiOrder order = findAiOrderByVolcTaskId(id); - if (order == null) { - logger.warn("volcCallback 修改执行中状态,未找到任务对应订单, id={}, {}", id, st); - return AjaxResult.success("callback success"); - } -// if (order.getStatus() != 0) { -// logger.warn("订单状态不为0, 因此不修改ext_status, id = {}, status = {}, order status = {}", id, st, order.getStatus()); -// return AjaxResult.success("callback success"); -// } - AiOrder upd = new AiOrder(); - upd.setId(order.getId()); - upd.setExtStatus(extStatus); - aiOrderService.updateAiOrder(upd); - return AjaxResult.success("callback success"); + AiOrder order = aiOrderService.selectOneByThirdPartyOrderNum(taskId); + if (order == null) { + // 可能是其他环境生成的但回调地址配置成正式的 + logger.warn("volcCallback aiorder is not exist! third party order num = {}", taskId); + return AjaxResult.success(); } - } - - if (codeError) { - markVolcCallbackOrderClearResultFailed(id, "code=" + code); - return AjaxResult.success("callback success"); - } - - if ("succeeded".equals(st)) { - content contentObj = byteBodyRes.getContent(); - if (contentObj != null && StringUtils.isNotEmpty(contentObj.getVideo_url())) { - String videoUrl = contentObj.getVideo_url(); - videoUrl = tencentCosUtil.uploadFileByUrl(videoUrl); - contentObj.setVideo_url(videoUrl); - - AiOrder aiOrderByResult = findAiOrderByVolcTaskId(id); - if (aiOrderByResult != null) { - AiOrder aiOrder = new AiOrder(); - aiOrder.setId(aiOrderByResult.getId()); - aiOrder.setResult(videoUrl); - aiOrder.setStatus(1); - aiOrderService.updateAiOrder(aiOrder); - } + // 5、状态为队列中、执行中,只更新任务状态 + result = volcCallbackRunningTaskProcess(taskResp, order); + if (result != null) { + return result; + } + // 6、订单数据校验 + result = volcCallbackOrderCheck(taskResp, order); + if (result != null) { + return result; + } + // 7、根据状态做不同的处理(加事务) + String status = taskResp.getStatus(); + if (VideoTaskStatusType.SUCCEEDED.getName().equals(status)) { + // 成功,预扣 + return volcCallbackSuccessProcess(request, taskResp, order); } else { - handleVolcCallbackFailure(id, "succeeded但缺少video_url"); + // 前面已判断过status的合法性,并处理了三种非失败的状态,所以可以确定是取消、失败、超时 + return volcCallbackFailProcess(taskResp, order); + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + logger.error("volcCallback interrupted while waiting for lock, third party order num = {}", taskId, e); + return AjaxResult.error(); + } catch (Exception ex) { + logger.error("volcCallback error! third party order num = {}, status = {}", + request.getId(), request.getStatus(), ex); + return AjaxResult.error(); + } finally { + if (locked && lock.isHeldByCurrentThread()) { + lock.unlock(); } - return AjaxResult.success("callback success"); } + } - // failed、canceled 等终态,或 status 与成功/进行中均不一致 - if (StringUtils.isNotEmpty(st) || code != null) { - handleVolcCallbackFailure(id, "status=" + st); - } else { - logger.warn("volcCallback 未携带可判定的 status/code,id={}", id); + @Transactional(rollbackFor = Exception.class) + private AjaxResult volcCallbackFailProcess(GetVideoGenerationTaskResponse taskResp, AiOrder order) { + aiOrderService.orderFailure(order); + return null; + } + + @Transactional(rollbackFor = Exception.class) + private AjaxResult volcCallbackSuccessProcess(VideoTaskCallBackRequest request, GetVideoGenerationTaskResponse taskResp, AiOrder order) { + // 用量是否一致 + Integer requestTotalTokens = request.getUsage() == null || request.getUsage().getTotalTokens() == null + ? 0 : request.getUsage().getTotalTokens(); + Integer officialTotalTokens = taskResp.getUsage() == null || taskResp.getUsage().getTotalTokens() == null + ? 0 : taskResp.getUsage().getTotalTokens(); + if (requestTotalTokens.equals(officialTotalTokens)) { + logger.error("volcCallback request's total tokens != official tokens! third party order num = {}, request's tokens = {}, official tokens = {}", + request.getId(), requestTotalTokens, officialTotalTokens); + return AjaxResult.error(); } + if (officialTotalTokens <= 0) { + // 异常情况,应该不会出现,以防万一 + logger.error("volcCallback official tokens <= 0! third party order num = {}, request's tokens = {}, official tokens = {}", + request.getId(), requestTotalTokens, officialTotalTokens); + return AjaxResult.error(); + } + BigDecimal realAmount = new BigDecimal(officialTotalTokens); + // 用量回补、多退少补 = 预扣量 - 实际用量 + // 有预扣值才回补,没有的是历史单,不处理 + if (order.getPreDeductAmount() != null + && order.getPreDeductAmount().compareTo(new BigDecimal(0)) > 0) { + BigDecimal addAmount = order.getPreDeductAmount().subtract(realAmount); + if (addAmount.compareTo(new BigDecimal(0)) != 0) { + // 回补 + aiUserService.addUserBalance(order.getOrderNum(), order.getUserId(), addAmount, + BalanceChangerConstants.REFUND, TASK_SUCCESS_BALANCE_REMARK); + } + } + // 设置视频地址与状态 + if (taskResp.getContent() != null && StringUtils.isNotEmpty(taskResp.getContent().getVideoUrl())) { + order.setResult(taskResp.getContent().getVideoUrl()); + } + // 设置用量 + order.setAmount(realAmount); + // 订单状态 + order.setStatus(AiOrderStatusType.FINISH.ordinal()); + order.setIsBackfilled(1); + aiOrderService.orderSuccess(order); return AjaxResult.success("callback success"); } + private AjaxResult volcCallbackOrderCheck(GetVideoGenerationTaskResponse taskResp, AiOrder order) { + // 订单状态如果不为执行中则不做处理 + if (order.getStatus() != null && order.getStatus() != AiOrderStatusType.RUNNING.ordinal()) { + logger.warn("volcCallback aiorder's status is not running! third party order num = {}, order status = {}" + , taskResp.getId(), order.getStatus()); + return AjaxResult.success(); + } + if (order.getIsBackfilled() != null && order.getIsBackfilled() == 1) { + // 已回补过,不再回补,直接返回成功 + logger.warn("volcCallback is back filled! third party order num = {}, order status = {}" + , taskResp.getId(), order.getStatus()); + return AjaxResult.success(); + } + return null; + } + + private AjaxResult volcCallbackByteCheck(VideoTaskCallBackRequest request, GetVideoGenerationTaskResponse taskResp) { + String status = taskResp.getStatus(); + // 请求的状态与字节的状态是否一致 + if (!status.equals(taskResp.getStatus().toLowerCase())) { + logger.error("volcCallback request's status != official status! order third party order num = {}, request's status = {}, official status = {}", + request.getId(), status, taskResp.getStatus()); + return AjaxResult.error(); + } + return null; + } + + private AjaxResult volcCallbackBaseCheck(VideoTaskCallBackRequest request) { + // 参数校验 + String status = request.getStatus(); + if (StringUtils.isEmpty(request.getId()) || StringUtils.isEmpty(status)) { + logger.error("volcCallbackBaseCheck id or status is null! third party order num = {}, status = {}" + , request.getId(), status); + return AjaxResult.error("id or status is null!"); + } + // 状态是否正确 + if (!VideoTaskStatusType.isValidName(status)) { + logger.error("volcCallbackBaseCheck invalid status! third party order num = {}, status = {}" + , request.getId(), status); + return AjaxResult.error(); + } + return null; + } + + private AjaxResult volcCallbackRunningTaskProcess(GetVideoGenerationTaskResponse taskResp, AiOrder order) { + // 执行中状态 ,更新到ext_status字段 + Integer extStatus = null; + String status = taskResp.getStatus(); + if (VideoTaskStatusType.QUEUED.getName().equals(status)) { + extStatus = 0; + } else if (VideoTaskStatusType.RUNNING.getName().equals(status)) { + extStatus = 1; + } + if (extStatus != null) { + order.setExtStatus(extStatus); + aiOrderService.updateAiOrder(order); + logger.info("volcCallback order extStatus is updated! third party order num = {}, extStatus = {}" + , taskResp.getId(), order.getExtStatus()); + return AjaxResult.success(); + } + return null; + } + /** * 回调体 code 非 200(HTTP/业务状态码):清空 result,status=2,不退款。 */ - private void markVolcCallbackOrderClearResultFailed(String taskId, String reason) { - AiOrder order = findAiOrderByVolcTaskId(taskId); - if (order == null) { - logger.warn("volcCallback code 非 200:未找到任务对应订单, taskId={}, {}", taskId, reason); - return; - } - AiOrder upd = new AiOrder(); - upd.setId(order.getId()); - upd.setResult(""); - upd.setStatus(2); - aiOrderService.updateAiOrder(upd); - logger.warn("volcCallback code 非 200,已清空 result 并 status=2, orderId={}, {}", order.getId(), reason); - } +// private void markVolcCallbackOrderClearResultFailed(String taskId, String reason) { +// AiOrder order = findAiOrderByVolcTaskId(taskId); +// if (order == null) { +// logger.warn("volcCallback code 非 200:未找到任务对应订单, taskId={}, {}", taskId, reason); +// return; +// } +// AiOrder upd = new AiOrder(); +// upd.setId(order.getId()); +// upd.setResult(""); +// upd.setStatus(2); +// aiOrderService.updateAiOrder(upd); +// logger.warn("volcCallback code 非 200,已清空 result 并 status=2, orderId={}, {}", order.getId(), reason); +// } private AiOrder findAiOrderByVolcTaskId(String taskId) { AiOrder order = aiOrderService.getAiOrderByPortalVideoTask(taskId); @@ -449,10 +562,9 @@ public class ByteApiController extends BaseController { logger.warn("volcCallback 任务失败,已标记订单失败并退款, taskId={}, reason={}", taskId, reason); } - @PostMapping(value = "/{id}/cancel") - @ApiOperation("取消视频生成任务") - public AjaxResult cancelTask(@PathVariable("id") String id) throws Exception { - return byteService.cancelVideoTask(id); - } - +// @PostMapping(value = "/{id}/cancel") +// @ApiOperation("取消视频生成任务") +// public AjaxResult cancelTask(@PathVariable("id") String id) throws Exception { +// return byteService.cancelVideoTask(id); +// } } diff --git a/web-api/ruoyi-admin/src/main/resources/i18n/messages_en_US.properties b/web-api/ruoyi-admin/src/main/resources/i18n/messages_en_US.properties index b6cc20e..2ed9e69 100644 --- a/web-api/ruoyi-admin/src/main/resources/i18n/messages_en_US.properties +++ b/web-api/ruoyi-admin/src/main/resources/i18n/messages_en_US.properties @@ -30,4 +30,5 @@ email.verification.code.error=Verification code is incorrect, please try again. # User not found user.not.found=User not found. user.password.incorrect=Password is incorrect, please try again. -order.number.generation.failed=Order number {0} generation failed, please try again later. \ No newline at end of file +order.number.generation.failed=Order number {0} generation failed, please try again later. +order.number.generation.successes=Order number {0} generation successes! \ No newline at end of file diff --git a/web-api/ruoyi-common/src/main/java/com/ruoyi/common/core/request/video/dto/VideoTaskCallBackRequest.java b/web-api/ruoyi-common/src/main/java/com/ruoyi/common/core/request/video/dto/VideoTaskCallBackRequest.java new file mode 100644 index 0000000..94a5c71 --- /dev/null +++ b/web-api/ruoyi-common/src/main/java/com/ruoyi/common/core/request/video/dto/VideoTaskCallBackRequest.java @@ -0,0 +1,91 @@ +package com.ruoyi.common.core.request.video.dto; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.ruoyi.common.core.response.video.dto.VideoGenerationUsage; +import com.ruoyi.common.core.response.video.dto.VideoTaskContent; +import com.ruoyi.common.core.response.video.dto.VideoTaskError; +import com.ruoyi.common.core.response.video.dto.VideoTaskTool; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +@Data +@AllArgsConstructor +@NoArgsConstructor +@ApiModel(description = "生成视频任务的回调接口") +public class VideoTaskCallBackRequest { + @ApiModelProperty(value = "视频生成任务 ID。") + private String id; + + @ApiModelProperty(value = "任务使用的模型名称和版本,模型名称-版本。") + private String model; + + @ApiModelProperty(value = "任务状态:queued 排队中;running 运行中;cancelled 已取消;succeeded 成功;failed 失败;expired 超时。") + private String status; + + @ApiModelProperty(value = "错误提示信息,任务成功时为 null。") + private VideoTaskError error; + + @JsonProperty("created_at") + @ApiModelProperty(value = "任务创建时间的 Unix 时间戳(秒)。") + private Integer createdAt; + + @JsonProperty("updated_at") + @ApiModelProperty(value = "任务当前状态更新时间的 Unix 时间戳(秒)。") + private Integer updatedAt; + + @ApiModelProperty(value = "视频生成任务的输出内容。") + private VideoTaskContent content; + + @ApiModelProperty(value = "本次请求使用的种子整数值。") + private Integer seed; + + @ApiModelProperty(value = "生成视频的分辨率。") + private String resolution; + + @ApiModelProperty(value = "生成视频的宽高比。") + private String ratio; + + @ApiModelProperty(value = "生成视频的时长,单位:秒。与 frames 只会返回其一。") + private Integer duration; + + @ApiModelProperty(value = "生成视频的帧数。与 duration 只会返回其一。") + private Integer frames; + + @JsonProperty("framespersecond") + @ApiModelProperty(value = "生成视频的帧率。") + private Integer framesPerSecond; + + @JsonProperty("generate_audio") + @ApiModelProperty(value = "生成的视频是否包含与画面同步的声音。仅 Seedance 1.5 pro 返回。") + private Boolean generateAudio; + + @ApiModelProperty(value = "本次请求模型实际使用的工具,未使用工具时不返回。") + private List tools; + + @JsonProperty("safety_identifier") + @ApiModelProperty(value = "终端用户的唯一标识符,创建任务时传入则原样返回。") + private String safetyIdentifier; + + @ApiModelProperty(value = "是否为 Draft 视频。仅 Seedance 1.5 pro 返回。") + private Boolean draft; + + @JsonProperty("draft_task_id") + @ApiModelProperty(value = "Draft 视频任务 ID,基于 Draft 生成正式视频时返回。") + private String draftTaskId; + + @JsonProperty("service_tier") + @ApiModelProperty(value = "实际处理任务使用的服务等级。") + private String serviceTier; + + @JsonProperty("execution_expires_after") + @ApiModelProperty(value = "任务超时阈值,单位:秒。") + private Integer executionExpiresAfter; + + @ApiModelProperty(value = "本次请求的 token 用量。") + private VideoGenerationUsage usage; +} diff --git a/web-api/ruoyi-common/src/main/java/com/ruoyi/common/enums/AiOrderStatusType.java b/web-api/ruoyi-common/src/main/java/com/ruoyi/common/enums/AiOrderStatusType.java new file mode 100644 index 0000000..51bab74 --- /dev/null +++ b/web-api/ruoyi-common/src/main/java/com/ruoyi/common/enums/AiOrderStatusType.java @@ -0,0 +1,13 @@ +package com.ruoyi.common.enums; + +/** + * AiOrder表中status的类型 + */ +public enum AiOrderStatusType { + // 0-进行中 + RUNNING, + // 1-已完成 + FINISH, + // 2-失败(余额退回) + FAIL; +} \ No newline at end of file diff --git a/web-api/ruoyi-common/src/main/java/com/ruoyi/common/enums/VideoTaskStatusType.java b/web-api/ruoyi-common/src/main/java/com/ruoyi/common/enums/VideoTaskStatusType.java new file mode 100644 index 0000000..cc9ab14 --- /dev/null +++ b/web-api/ruoyi-common/src/main/java/com/ruoyi/common/enums/VideoTaskStatusType.java @@ -0,0 +1,46 @@ +package com.ruoyi.common.enums; + +import lombok.Getter; + +import java.util.Arrays; +import java.util.Set; +import java.util.stream.Collectors; + +/** + * 生成视频任务中,状态的枚举 + */ +@Getter +public enum VideoTaskStatusType { + QUEUED("queued"), + + RUNNING("running"), + + CANCELLED("cancelled"), + + SUCCEEDED("succeeded"), + + FAILED("failed"), + + EXPIRED("expired"); + + private final String name; + + private static final Set VALID_NAMES = Arrays.stream(VideoTaskStatusType.values()) + .map(status -> status.name) + .collect(Collectors.toSet()); + /** + * 判断名称是否正确, 不区分大小写 + */ + public static boolean isValidName(String name) { + if (name == null) { + return false; + } + String nameLowerCase = name.toLowerCase(); + // 直接判断小写后的入参是否在静态Set中 + return VALID_NAMES.contains(nameLowerCase); + } + + VideoTaskStatusType(String name) { + this.name = name; + } +} diff --git a/web-api/ruoyi-framework/pom.xml b/web-api/ruoyi-framework/pom.xml index e42a12f..e2acc00 100644 --- a/web-api/ruoyi-framework/pom.xml +++ b/web-api/ruoyi-framework/pom.xml @@ -59,6 +59,13 @@ ruoyi-system + + + org.redisson + redisson-spring-boot-starter + 3.17.7 + + \ No newline at end of file diff --git a/web-api/ruoyi-system/src/main/java/com/ruoyi/ai/domain/AiOrder.java b/web-api/ruoyi-system/src/main/java/com/ruoyi/ai/domain/AiOrder.java index 0a116e7..c0ad6f6 100644 --- a/web-api/ruoyi-system/src/main/java/com/ruoyi/ai/domain/AiOrder.java +++ b/web-api/ruoyi-system/src/main/java/com/ruoyi/ai/domain/AiOrder.java @@ -56,6 +56,9 @@ public class AiOrder extends BaseEntity { @Excel(name = "金额") private BigDecimal amount; + @Excel(name = "是否回补处理过: 0-否 1-是") + private Integer isBackfilled; + /** 生成视频时的请求ID */ @Excel(name = "生成视频时的请求ID") private String videoGenRequestId; diff --git a/web-api/ruoyi-system/src/main/java/com/ruoyi/ai/mapper/AiOrderMapper.java b/web-api/ruoyi-system/src/main/java/com/ruoyi/ai/mapper/AiOrderMapper.java index cafa00d..bf1653e 100644 --- a/web-api/ruoyi-system/src/main/java/com/ruoyi/ai/mapper/AiOrderMapper.java +++ b/web-api/ruoyi-system/src/main/java/com/ruoyi/ai/mapper/AiOrderMapper.java @@ -6,6 +6,7 @@ import java.util.List; import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.ruoyi.ai.domain.AiOrder; import org.apache.ibatis.annotations.Param; +import org.apache.ibatis.annotations.Select; /** * 订单管理Mapper接口 @@ -26,4 +27,7 @@ public interface AiOrderMapper extends BaseMapper { AiOrder getAiOrderByPortalVideoTask(@Param("taskId") String taskId); BigDecimal getSumAmountByUserId(@Param("userId") String userId); + + @Select("SELECT * FROM ai_order WHERE third_party_order_num = #{id} LIMIT 1") + AiOrder selectOneByThirdPartyOrderNum(@Param("id") String id); } diff --git a/web-api/ruoyi-system/src/main/java/com/ruoyi/ai/service/IAiOrderService.java b/web-api/ruoyi-system/src/main/java/com/ruoyi/ai/service/IAiOrderService.java index 7942212..28c3835 100644 --- a/web-api/ruoyi-system/src/main/java/com/ruoyi/ai/service/IAiOrderService.java +++ b/web-api/ruoyi-system/src/main/java/com/ruoyi/ai/service/IAiOrderService.java @@ -6,6 +6,7 @@ import java.util.List; import com.baomidou.mybatisplus.core.metadata.IPage; import com.ruoyi.ai.domain.AiOrder; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import org.springframework.transaction.annotation.Transactional; /** * 订单管理Service接口 @@ -77,6 +78,9 @@ public interface IAiOrderService { void orderFailure(AiOrder aiOrder); + @Transactional + void orderFailure(AiOrder aiOrder, BigDecimal amount); + void orderSuccess(AiOrder aiOrder); AiOrder getAiOrderByResult(String result); @@ -86,4 +90,6 @@ public interface IAiOrderService { int getChangerType(String aiType); BigDecimal getSumAmountByUserId(String userId); + + AiOrder selectOneByThirdPartyOrderNum(String id); } diff --git a/web-api/ruoyi-system/src/main/java/com/ruoyi/ai/service/IByteService.java b/web-api/ruoyi-system/src/main/java/com/ruoyi/ai/service/IByteService.java index c780de6..6f02655 100644 --- a/web-api/ruoyi-system/src/main/java/com/ruoyi/ai/service/IByteService.java +++ b/web-api/ruoyi-system/src/main/java/com/ruoyi/ai/service/IByteService.java @@ -46,6 +46,12 @@ public interface IByteService { */ String listVideoGenerationTasks(int pageNum, int pageSize, String arkApiKey) throws Exception; + /** + * 获取当前用户所使用的api key + * @return + */ + String resolveCurrentAiUserApiKey(); + /** * GET 查询视频生成任务(单个) */ diff --git a/web-api/ruoyi-system/src/main/java/com/ruoyi/ai/service/impl/AiOrderServiceImpl.java b/web-api/ruoyi-system/src/main/java/com/ruoyi/ai/service/impl/AiOrderServiceImpl.java index 813f8d1..5a4efea 100644 --- a/web-api/ruoyi-system/src/main/java/com/ruoyi/ai/service/impl/AiOrderServiceImpl.java +++ b/web-api/ruoyi-system/src/main/java/com/ruoyi/ai/service/impl/AiOrderServiceImpl.java @@ -190,15 +190,28 @@ public class AiOrderServiceImpl implements IAiOrderService { return aiOrder; } + /** + * 订单失败时,返回订单amount对应数量的用量 + */ @Override - @Transactional public void orderFailure(AiOrder aiOrder) { + orderFailure(aiOrder, aiOrder.getAmount()); + } + + /** + * 订单失败时,返回指定用量的用量 + */ + @Transactional + @Override + public void orderFailure(AiOrder aiOrder, BigDecimal amount) { + aiOrder.setIsBackfilled(1); aiOrder.setStatus(2); String remark = MessageUtils.message("order.number.generation.failed", aiOrder.getOrderNum()); aiOrder.setRemark(remark); aiOrderMapper.updateById(aiOrder); Long userId = aiOrder.getUserId() != null ? aiOrder.getUserId() : SecurityUtils.getAiUserId(); - aiUserService.addUserBalance(aiOrder.getOrderNum(), userId, aiOrder.getAmount(), BalanceChangerConstants.REFUND, remark); + aiUserService.addUserBalance(aiOrder.getOrderNum(), userId, amount, BalanceChangerConstants.REFUND, remark); + } @Override @@ -248,4 +261,9 @@ public class AiOrderServiceImpl implements IAiOrderService { public BigDecimal getSumAmountByUserId(String userId) { return aiOrderMapper.getSumAmountByUserId(userId); } + + @Override + public AiOrder selectOneByThirdPartyOrderNum(String id) { + return aiOrderMapper.selectOneByThirdPartyOrderNum(id); + } } diff --git a/web-api/ruoyi-system/src/main/java/com/ruoyi/ai/service/impl/ByteService.java b/web-api/ruoyi-system/src/main/java/com/ruoyi/ai/service/impl/ByteService.java index b1e98f7..9496616 100644 --- a/web-api/ruoyi-system/src/main/java/com/ruoyi/ai/service/impl/ByteService.java +++ b/web-api/ruoyi-system/src/main/java/com/ruoyi/ai/service/impl/ByteService.java @@ -236,7 +236,7 @@ public class ByteService implements IByteService { return body; } - private String resolveCurrentAiUserApiKey() { + public String resolveCurrentAiUserApiKey() { return byteDeptApiKeyService.resolveVolcApiKey(SecurityUtils.getAiUserId()); }