fix: 生成视频、任务回调 - 检查代码并修改相关逻辑问题

This commit is contained in:
yys 2026-04-10 10:03:28 +08:00
parent 80c136ad18
commit f53f849ec8
7 changed files with 163 additions and 120 deletions

View File

@ -12,6 +12,7 @@ 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;
@ -43,19 +44,17 @@ public class ByteApiController extends BaseController {
// 锁参数
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;
private final IAiUserService aiUserService;
private final RedissonClient redissonClient;
@Value("${volcengine.ark.callbackUrl}")
private String volcCallbackUrl;
private final IByteDeptApiKeyService byteDeptApiKeyService;
@PostMapping("/promptToImg")
@ApiOperation("文生图")
@ -346,16 +345,25 @@ public class ByteApiController extends BaseController {
if (result != null) {
return result;
}
// 2从官方获取任务数据
String apiKey = byteService.resolveCurrentAiUserApiKey();
// 2查询订单
String taskId = request.getId();
AiOrder order = aiOrderService.selectOneByThirdPartyOrderNum(taskId);
if (order == null) {
// 可能是其他环境生成的但回调地址配置成正式的
logger.warn("volcCallback aiorder is not exist! third party order num = {}", taskId);
return AjaxResult.success();
}
// 3从官方获取任务数据
// 根据订单用户ID查询使用的Key
// 严格来讲按逻辑这块是应放在锁内但这是调外部接口如果接口超时整个服务可能会当机所以不放锁内即不做强一致
String apiKey = byteDeptApiKeyService.resolveVolcApiKey(order.getUserId());
GetVideoGenerationTaskResponse taskResp = byteService.getVideoGenerationTasks(request.getId(), apiKey);
// 3官方数据校验
// 4官方数据校验
result = volcCallbackByteCheck(request, taskResp);
if (result != null) {
return result;
}
// 4查询订单 taskId 串行Redisson 分布式锁步骤 13 已在锁外
String taskId = request.getId();
// 5查询订单 taskId 串行Redisson 分布式锁步骤 13 已在锁外
String lockKey = VOLC_CALLBACK_LOCK_KEY_PREFIX + taskId;
RLock lock = redissonClient.getLock(lockKey);
boolean locked = false;
@ -365,30 +373,32 @@ public class ByteApiController extends BaseController {
logger.warn("volcCallback skip: concurrent handling for same task, third party order num = {}", taskId);
return AjaxResult.success();
}
AiOrder order = aiOrderService.selectOneByThirdPartyOrderNum(taskId);
// 锁内二次查询防止并发时状态变更
order = aiOrderService.selectOneByThirdPartyOrderNum(taskId);
if (order == null) {
// 可能是其他环境生成的但回调地址配置成正式的
logger.warn("volcCallback aiorder is not exist! third party order num = {}", taskId);
return AjaxResult.success();
}
// 5状态为队列中执行中只更新任务状态
// 6状态为队列中执行中只更新任务状态
result = volcCallbackRunningTaskProcess(taskResp, order);
if (result != null) {
return result;
}
// 6订单数据校验
// 7订单数据校验
result = volcCallbackOrderCheck(taskResp, order);
if (result != null) {
return result;
}
// 7根据状态做不同的处理加事务
String status = taskResp.getStatus();
// 8根据状态做不同的处理加事务
String status = taskResp.getStatus().toLowerCase();
if (VideoTaskStatusType.SUCCEEDED.getName().equals(status)) {
// 成功预扣
return volcCallbackSuccessProcess(request, taskResp, order);
return aiOrderService.volcCallbackSuccessProcess(request, taskResp, order);
} else {
// 前面已判断过status的合法性并处理了三种非失败的状态所以可以确定是取消失败超时
return volcCallbackFailProcess(taskResp, order);
aiOrderService.orderFailure(order);
return AjaxResult.success();
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
@ -405,55 +415,6 @@ public class ByteApiController extends BaseController {
}
}
@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()) {
@ -471,7 +432,7 @@ public class ByteApiController extends BaseController {
}
private AjaxResult volcCallbackByteCheck(VideoTaskCallBackRequest request, GetVideoGenerationTaskResponse taskResp) {
String status = taskResp.getStatus();
String status = request.getStatus().toLowerCase();
// 请求的状态与字节的状态是否一致
if (!status.equals(taskResp.getStatus().toLowerCase())) {
logger.error("volcCallback request's status != official status! order third party order num = {}, request's status = {}, official status = {}",
@ -501,7 +462,7 @@ public class ByteApiController extends BaseController {
private AjaxResult volcCallbackRunningTaskProcess(GetVideoGenerationTaskResponse taskResp, AiOrder order) {
// 执行中状态 更新到ext_status字段
Integer extStatus = null;
String status = taskResp.getStatus();
String status = taskResp.getStatus().toLowerCase();
if (VideoTaskStatusType.QUEUED.getName().equals(status)) {
extStatus = 0;
} else if (VideoTaskStatusType.RUNNING.getName().equals(status)) {
@ -534,33 +495,33 @@ public class ByteApiController extends BaseController {
// logger.warn("volcCallback code 非 200已清空 result 并 status=2, orderId={}, {}", order.getId(), reason);
// }
private AiOrder findAiOrderByVolcTaskId(String taskId) {
AiOrder order = aiOrderService.getAiOrderByPortalVideoTask(taskId);
if (order != null) {
return order;
}
return aiOrderService.getAiOrderByResult(taskId);
}
// private AiOrder findAiOrderByVolcTaskId(String taskId) {
// AiOrder order = aiOrderService.getAiOrderByPortalVideoTask(taskId);
// if (order != null) {
// return order;
// }
// return aiOrderService.getAiOrderByResult(taskId);
// }
/**
* 回调判定任务失败订单仍持有火山任务 id且非已失败时标记失败并退余额orderFailure
*/
private void handleVolcCallbackFailure(String taskId, String reason) {
AiOrder order = aiOrderService.getAiOrderByResult(taskId);
if (order == null) {
logger.warn("volcCallback 失败:未找到 result={} 的订单, {}", taskId, reason);
return;
}
if (!taskId.equals(order.getResult())) {
logger.info("volcCallback 失败处理跳过:订单结果已更新, taskId={}, {}", taskId, reason);
return;
}
if (Integer.valueOf(2).equals(order.getStatus())) {
return;
}
aiOrderService.orderFailure(order);
logger.warn("volcCallback 任务失败,已标记订单失败并退款, taskId={}, reason={}", taskId, reason);
}
// private void handleVolcCallbackFailure(String taskId, String reason) {
// AiOrder order = aiOrderService.getAiOrderByResult(taskId);
// if (order == null) {
// logger.warn("volcCallback 失败:未找到 result={} 的订单, {}", taskId, reason);
// return;
// }
// if (!taskId.equals(order.getResult())) {
// logger.info("volcCallback 失败处理跳过:订单结果已更新, taskId={}, {}", taskId, reason);
// return;
// }
// if (Integer.valueOf(2).equals(order.getStatus())) {
// return;
// }
// aiOrderService.orderFailure(order);
// logger.warn("volcCallback 任务失败,已标记订单失败并退款, taskId={}, reason={}", taskId, reason);
// }
// @PostMapping(value = "/{id}/cancel")
// @ApiOperation("取消视频生成任务")

View File

@ -272,19 +272,20 @@ public class PortalVideoController extends BaseController {
String key = apiKey();
ByteBodyRes byteBodyRes = byteService.imgToVideo(byteBodyReq, key);
String id = byteBodyRes.getId();
if (id == null) {
String thirdPartyOrderNumId = byteBodyRes.getId();
if (thirdPartyOrderNumId == null) {
aiOrderService.orderFailure(aiOrder);
return AjaxResult.error(-2, "generation failed, balance has been refunded");
}
mergeVolcTaskIdIntoVideoParams(aiOrder, id);
aiOrder.setResult(id);
mergeVolcTaskIdIntoVideoParams(aiOrder, thirdPartyOrderNumId);
aiOrder.setResult(thirdPartyOrderNumId);
// 字节订单号与请求ID
aiOrder.setThirdPartyOrderNum(id);
aiOrder.setThirdPartyOrderNum(thirdPartyOrderNumId);
aiOrder.setVideoGenRequestId(byteBodyRes.getRequestId());
aiOrderService.orderSuccess(aiOrder);
// aiOrderService.orderSuccess(aiOrder);
aiOrderService.updateAiOrder(aiOrder);
// 查询任务详情按字节返回的预扣数量扣减
GetVideoGenerationTaskResponse task = byteService.getVideoGenerationTasks(id, key);
GetVideoGenerationTaskResponse task = byteService.getVideoGenerationTasks(thirdPartyOrderNumId, key);
if (task == null || task.getUsage() == null || task.getUsage().getTotalTokens() == null) {
return AjaxResult.error(-2, "generation failed, byte task's usage is null");
}
@ -292,16 +293,21 @@ public class PortalVideoController extends BaseController {
return AjaxResult.error(-2, "generation failed, byte task's totalTokens <= 0");
}
BigDecimal totalTokens = new BigDecimal(task.getUsage().getTotalTokens());
// 扣减余额
aiUserService.addUserBalance(aiOrder.getOrderNum(), SecurityUtils.getAiUserId()
, NumberUtil.mul(-1, totalTokens), aiOrderService.getChangerType(functionType));
// 同步设置aiOrder以防在抛异常时数值没变
aiOrder.setPreDeductAmount(totalTokens);
aiOrder.setAmount(totalTokens);
// 设置订单信息
AiOrder updAiOrder = new AiOrder();
updAiOrder.setId(aiOrder.getId());
updAiOrder.setOrderNum(aiOrder.getOrderNum());
updAiOrder.setPreDeductAmount(totalTokens);
// 先设置成预扣数量等收到回调再改过来这样后续报表会比较准确
updAiOrder.setAmount(totalTokens);
aiOrderService.updateAiOrder(updAiOrder);
// 扣减余额
aiUserService.addUserBalance(aiOrder.getOrderNum(), SecurityUtils.getAiUserId()
, NumberUtil.mul(-1, totalTokens), aiOrderService.getChangerType(functionType));
return AjaxResult.success(byteBodyRes);
} catch (Exception e) {
aiOrderService.orderFailure(aiOrder);
@ -626,19 +632,19 @@ public class PortalVideoController extends BaseController {
return AjaxResult.success(byteBodyRes);
}
@DeleteMapping("/tasks/{taskId}")
@ApiOperation("删除或取消视频生成任务")
public AjaxResult deleteOrCancelTask(@PathVariable String taskId) throws Exception {
Long uid = SecurityUtils.getAiUserId();
AiOrder owned = aiOrderService.getAiOrderByPortalVideoTask(taskId);
if (owned == null || !uid.equals(owned.getUserId())) {
return AjaxResult.error("无权操作该任务");
}
String key = apiKey();
AjaxResult cancelRes = byteService.cancelVideoTask(taskId, key);
if (cancelRes.isSuccess() && owned.getStatus() != null && owned.getStatus() == 0) {
aiOrderService.orderFailure(owned);
}
return cancelRes;
}
// @DeleteMapping("/tasks/{taskId}")
// @ApiOperation("删除或取消视频生成任务")
// public AjaxResult deleteOrCancelTask(@PathVariable String taskId) throws Exception {
// Long uid = SecurityUtils.getAiUserId();
// AiOrder owned = aiOrderService.getAiOrderByPortalVideoTask(taskId);
// if (owned == null || !uid.equals(owned.getUserId())) {
// return AjaxResult.error("无权操作该任务");
// }
// String key = apiKey();
// AjaxResult cancelRes = byteService.cancelVideoTask(taskId, key);
// if (cancelRes.isSuccess() && owned.getStatus() != null && owned.getStatus() == 0) {
// aiOrderService.orderFailure(owned);
// }
// return cancelRes;
// }
}

View File

@ -281,8 +281,8 @@ portal:
- 14
- 15
resolutions:
- "480p"
- "720p"
- "1080p"
jinsha:
url: https://api.jinshapay.xyz

View File

@ -6,6 +6,9 @@ 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 com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.core.request.video.dto.VideoTaskCallBackRequest;
import com.ruoyi.common.core.response.video.GetVideoGenerationTaskResponse;
import org.springframework.transaction.annotation.Transactional;
/**
@ -92,4 +95,9 @@ public interface IAiOrderService {
BigDecimal getSumAmountByUserId(String userId);
AiOrder selectOneByThirdPartyOrderNum(String id);
/**
* 火山回调 - 任务成功时处理流程
*/
AjaxResult volcCallbackSuccessProcess(VideoTaskCallBackRequest request, GetVideoGenerationTaskResponse taskResp, AiOrder order);
}

View File

@ -1,8 +1,10 @@
package com.ruoyi.ai.service;
import com.ruoyi.ai.domain.AiOrder;
import com.ruoyi.ai.domain.ByteBodyReq;
import com.ruoyi.ai.domain.ByteBodyRes;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.core.request.video.dto.VideoTaskCallBackRequest;
import com.ruoyi.common.core.response.video.GetVideoGenerationTaskResponse;
public interface IByteService {
@ -55,5 +57,5 @@ public interface IByteService {
/**
* GET 查询视频生成任务(单个)
*/
GetVideoGenerationTaskResponse getVideoGenerationTasks(String id, String arkApiKey) throws Exception;
GetVideoGenerationTaskResponse getVideoGenerationTasks(String thirdPartyOrderNumId, String arkApiKey) throws Exception;
}

View File

@ -15,11 +15,17 @@ import com.ruoyi.ai.service.IAiStatisticsService;
import com.ruoyi.ai.service.IAiUserService;
import com.ruoyi.common.constant.BalanceChangerConstants;
import com.ruoyi.common.constant.HttpStatus;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.core.domain.entity.AiUser;
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.exception.ServiceException;
import com.ruoyi.common.utils.DateUtils;
import com.ruoyi.common.utils.MessageUtils;
import com.ruoyi.common.utils.SecurityUtils;
import com.ruoyi.common.utils.StringUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@ -36,6 +42,7 @@ import java.util.UUID;
* @author shi
* @date 2025-11-13
*/
@Slf4j
@Service
public class AiOrderServiceImpl implements IAiOrderService {
@ -51,6 +58,8 @@ public class AiOrderServiceImpl implements IAiOrderService {
@Autowired
private IAiStatisticsService aiStatisticsService;
// 任务成功时流水表备注
private static final String TASK_SUCCESS_BALANCE_REMARK = "order.number.generation.successes";
/**
* 查询订单管理
@ -266,4 +275,53 @@ public class AiOrderServiceImpl implements IAiOrderService {
public AiOrder selectOneByThirdPartyOrderNum(String id) {
return aiOrderMapper.selectOneByThirdPartyOrderNum(id);
}
@Override
@Transactional(rollbackFor = Exception.class)
public 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();
// !!!注意
// 以下检查不成立直接返回error事务不会回退所以不要在检查前面有存库操作
// 这两个检查只是以防万一的防止恶意回调如果出现允许数据库中存在异常单后续由程序通过日志检查问题
if (!requestTotalTokens.equals(officialTotalTokens)) {
log.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) {
// 异常情况应该不会出现以防万一
log.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 (taskResp.getContent() != null && StringUtils.isNotEmpty(taskResp.getContent().getVideoUrl())) {
order.setResult(taskResp.getContent().getVideoUrl());
}
// 设置用量
order.setAmount(realAmount);
// 订单状态
order.setStatus(AiOrderStatusType.FINISH.ordinal());
order.setIsBackfilled(1);
orderSuccess(order);
// 用量回补多退少补 = 预扣量 - 实际用量
// 有预扣值才回补没有的是历史单不处理
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);
}
}
return AjaxResult.success("callback success");
}
}

View File

@ -3,12 +3,17 @@ 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.AiOrder;
import com.ruoyi.ai.domain.ByteBodyReq;
import com.ruoyi.ai.domain.ByteBodyRes;
import com.ruoyi.ai.service.IAiUserService;
import com.ruoyi.ai.service.IByteDeptApiKeyService;
import com.ruoyi.ai.service.IByteService;
import com.ruoyi.common.constant.BalanceChangerConstants;
import com.ruoyi.common.core.domain.AjaxResult;
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.utils.SecurityUtils;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.common.utils.http.OkHttpUtils;
@ -18,6 +23,9 @@ import okhttp3.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
@Slf4j
@Service
@ -241,15 +249,15 @@ public class ByteService implements IByteService {
}
@Override
public GetVideoGenerationTaskResponse getVideoGenerationTasks(String id, String arkApiKey) throws Exception {
public GetVideoGenerationTaskResponse getVideoGenerationTasks(String thirdPartyOrderNumId, String arkApiKey) throws Exception {
if (StringUtils.isBlank(arkApiKey)) {
throw new Exception("getVideoGenerationTasks errorapiKey is null");
}
if (StringUtils.isBlank(id)) {
throw new Exception("getVideoGenerationTasks errorid is null");
if (StringUtils.isBlank(thirdPartyOrderNumId)) {
throw new Exception("getVideoGenerationTasks errorthirdPartyOrderNumId is null");
}
HttpUrl parsed = HttpUrl.parse(volcBaseUrl + "/api/v3/contents/generations/tasks/" + id);
HttpUrl parsed = HttpUrl.parse(volcBaseUrl + "/api/v3/contents/generations/tasks/" + thirdPartyOrderNumId);
if (parsed == null) {
throw new Exception("listVideoGenerationTasks errorinvalid base url");
}