feat: 生成视频时,按字节接口扣减用量(代码初步完成)
This commit is contained in:
parent
38b26f9ffb
commit
80c136ad18
|
|
@ -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<ContentItem> 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);
|
||||
// }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
order.number.generation.failed=Order number {0} generation failed, please try again later.
|
||||
order.number.generation.successes=Order number {0} generation successes!
|
||||
|
|
@ -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<VideoTaskTool> 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;
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
package com.ruoyi.common.enums;
|
||||
|
||||
/**
|
||||
* AiOrder表中status的类型
|
||||
*/
|
||||
public enum AiOrderStatusType {
|
||||
// 0-进行中
|
||||
RUNNING,
|
||||
// 1-已完成
|
||||
FINISH,
|
||||
// 2-失败(余额退回)
|
||||
FAIL;
|
||||
}
|
||||
|
|
@ -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<String> 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;
|
||||
}
|
||||
}
|
||||
|
|
@ -59,6 +59,13 @@
|
|||
<artifactId>ruoyi-system</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Redisson 分布式锁(与 Spring Boot 2.5 对齐 3.17.x) -->
|
||||
<dependency>
|
||||
<groupId>org.redisson</groupId>
|
||||
<artifactId>redisson-spring-boot-starter</artifactId>
|
||||
<version>3.17.7</version>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
</project>
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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> {
|
|||
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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -46,6 +46,12 @@ public interface IByteService {
|
|||
*/
|
||||
String listVideoGenerationTasks(int pageNum, int pageSize, String arkApiKey) throws Exception;
|
||||
|
||||
/**
|
||||
* 获取当前用户所使用的api key
|
||||
* @return
|
||||
*/
|
||||
String resolveCurrentAiUserApiKey();
|
||||
|
||||
/**
|
||||
* GET 查询视频生成任务(单个)
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -236,7 +236,7 @@ public class ByteService implements IByteService {
|
|||
return body;
|
||||
}
|
||||
|
||||
private String resolveCurrentAiUserApiKey() {
|
||||
public String resolveCurrentAiUserApiKey() {
|
||||
return byteDeptApiKeyService.resolveVolcApiKey(SecurityUtils.getAiUserId());
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue