feat: 生成视频时,按字节接口扣减用量
This commit is contained in:
parent
d807f71677
commit
38b26f9ffb
|
|
@ -1,5 +1,6 @@
|
|||
package com.ruoyi.api;
|
||||
|
||||
import cn.hutool.core.util.NumberUtil;
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
|
@ -12,10 +13,12 @@ import com.ruoyi.ai.domain.ContentItem;
|
|||
import com.ruoyi.ai.domain.ImageUrl;
|
||||
import com.ruoyi.ai.domain.content;
|
||||
import com.ruoyi.ai.service.IAiOrderService;
|
||||
import com.ruoyi.ai.service.IAiUserService;
|
||||
import com.ruoyi.ai.service.IByteDeptApiKeyService;
|
||||
import com.ruoyi.ai.service.IByteService;
|
||||
import com.ruoyi.api.request.PortalVideoGenRequest;
|
||||
import com.ruoyi.common.core.controller.BaseController;
|
||||
import com.ruoyi.common.core.response.video.GetVideoGenerationTaskResponse;
|
||||
import com.ruoyi.common.exception.ServiceException;
|
||||
import com.ruoyi.common.core.domain.AjaxResult;
|
||||
import com.ruoyi.common.core.page.TableDataInfo;
|
||||
|
|
@ -30,6 +33,7 @@ import org.springframework.beans.factory.annotation.Autowired;
|
|||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
|
|
@ -51,6 +55,7 @@ public class PortalVideoController extends BaseController {
|
|||
private final IAiOrderService aiOrderService;
|
||||
private final TencentCosUtil tencentCosUtil;
|
||||
private final PortalVideoProperties portalVideoProperties;
|
||||
private final IAiUserService aiUserService;
|
||||
|
||||
@Value("${volcengine.ark.callbackUrl:}")
|
||||
private String volcCallbackUrl;
|
||||
|
|
@ -256,7 +261,8 @@ public class PortalVideoController extends BaseController {
|
|||
|
||||
private AjaxResult submitOrderAndCreate(PortalVideoGenRequest req, String mode, ByteBodyReq byteBodyReq) {
|
||||
String functionType = resolveFunctionType(req);
|
||||
AiOrder aiOrder = aiOrderService.getAiOrder(functionType);
|
||||
// 判断余额是否足够,aimanager里配置了最低限额。创建订单
|
||||
AiOrder aiOrder = aiOrderService.getAiOrder(functionType, false);
|
||||
if (aiOrder == null) {
|
||||
return AjaxResult.error(-1, "You have a low balance, please recharge");
|
||||
}
|
||||
|
|
@ -273,7 +279,29 @@ public class PortalVideoController extends BaseController {
|
|||
}
|
||||
mergeVolcTaskIdIntoVideoParams(aiOrder, id);
|
||||
aiOrder.setResult(id);
|
||||
// 字节订单号与请求ID
|
||||
aiOrder.setThirdPartyOrderNum(id);
|
||||
aiOrder.setVideoGenRequestId(byteBodyRes.getRequestId());
|
||||
aiOrderService.orderSuccess(aiOrder);
|
||||
// 查询任务详情,按字节返回的预扣数量扣减
|
||||
GetVideoGenerationTaskResponse task = byteService.getVideoGenerationTasks(id, key);
|
||||
if (task == null || task.getUsage() == null || task.getUsage().getTotalTokens() == null) {
|
||||
return AjaxResult.error(-2, "generation failed, byte task's usage is null");
|
||||
}
|
||||
if (task.getUsage().getTotalTokens() <= 0) {
|
||||
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 updAiOrder = new AiOrder();
|
||||
updAiOrder.setOrderNum(aiOrder.getOrderNum());
|
||||
updAiOrder.setPreDeductAmount(totalTokens);
|
||||
// 先设置成预扣数量,等收到回调再改过来,这样后续报表会比较准确
|
||||
updAiOrder.setAmount(totalTokens);
|
||||
aiOrderService.updateAiOrder(updAiOrder);
|
||||
return AjaxResult.success(byteBodyRes);
|
||||
} catch (Exception e) {
|
||||
aiOrderService.orderFailure(aiOrder);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,91 @@
|
|||
package com.ruoyi.common.core.response.video;
|
||||
|
||||
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 = "查询视频生成任务 API 返回参数")
|
||||
public class GetVideoGenerationTaskResponse {
|
||||
@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,18 @@
|
|||
package com.ruoyi.common.core.response.video.dto;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import io.swagger.annotations.ApiModel;
|
||||
import io.swagger.annotations.ApiModelProperty;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
@Data
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
@ApiModel(description = "使用工具的用量信息")
|
||||
public class UsageToolUsage {
|
||||
@JsonProperty("web_search")
|
||||
@ApiModelProperty(value = "实际调用联网搜索工具的次数,仅开启联网搜索时返回。")
|
||||
private Integer webSearch;
|
||||
}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
package com.ruoyi.common.core.response.video.dto;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import io.swagger.annotations.ApiModel;
|
||||
import io.swagger.annotations.ApiModelProperty;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
@Data
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
@ApiModel(description = "本次请求的 token 用量")
|
||||
public class VideoGenerationUsage {
|
||||
@JsonProperty("completion_tokens")
|
||||
@ApiModelProperty(value = "模型输出视频花费的 token 数量。")
|
||||
private Integer completionTokens;
|
||||
|
||||
@JsonProperty("total_tokens")
|
||||
@ApiModelProperty(value = "本次请求消耗的总 token 数量。视频生成不统计输入 token,故 total_tokens 与 completion_tokens 一致。")
|
||||
private Integer totalTokens;
|
||||
|
||||
@JsonProperty("tool_usage")
|
||||
@ApiModelProperty(value = "使用工具的用量信息。")
|
||||
private UsageToolUsage toolUsage;
|
||||
}
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
package com.ruoyi.common.core.response.video.dto;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import io.swagger.annotations.ApiModel;
|
||||
import io.swagger.annotations.ApiModelProperty;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
@Data
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
@ApiModel(description = "视频生成任务的输出内容")
|
||||
public class VideoTaskContent {
|
||||
@JsonProperty("video_url")
|
||||
@ApiModelProperty(value = "生成视频的 URL,格式为 mp4。为保障信息安全,生成的视频会在 24 小时后被清理,请及时转存。")
|
||||
private String videoUrl;
|
||||
|
||||
@JsonProperty("last_frame_url")
|
||||
@ApiModelProperty(value = "视频的尾帧图像 URL。有效期为 24 小时,请及时转存。创建任务时设置 return_last_frame: true 时返回。")
|
||||
private String lastFrameUrl;
|
||||
}
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
package com.ruoyi.common.core.response.video.dto;
|
||||
|
||||
import io.swagger.annotations.ApiModel;
|
||||
import io.swagger.annotations.ApiModelProperty;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
@Data
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
@ApiModel(description = "视频生成任务错误信息(任务成功时为 null)")
|
||||
public class VideoTaskError {
|
||||
@ApiModelProperty(value = "错误码。")
|
||||
private String code;
|
||||
|
||||
@ApiModelProperty(value = "错误提示信息。")
|
||||
private String message;
|
||||
}
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
package com.ruoyi.common.core.response.video.dto;
|
||||
|
||||
import io.swagger.annotations.ApiModel;
|
||||
import io.swagger.annotations.ApiModelProperty;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
@Data
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
@ApiModel(description = "本次请求模型实际使用的工具")
|
||||
public class VideoTaskTool {
|
||||
@ApiModelProperty(value = "实际使用的工具类型,例如 web_search(联网搜索)。")
|
||||
private String type;
|
||||
}
|
||||
|
|
@ -36,6 +36,10 @@ public class AiOrder extends BaseEntity {
|
|||
@Excel(name = "订单编号")
|
||||
private String orderNum;
|
||||
|
||||
/** 第三方单号 */
|
||||
@Excel(name = "第三方单号")
|
||||
private String thirdPartyOrderNum;
|
||||
|
||||
/** 用户ID */
|
||||
@Excel(name = "用户ID")
|
||||
private Long userId;
|
||||
|
|
@ -44,10 +48,18 @@ public class AiOrder extends BaseEntity {
|
|||
@Excel(name = "AI类型")
|
||||
private String type;
|
||||
|
||||
/** 预扣金额 */
|
||||
@Excel(name = "预扣金额")
|
||||
private BigDecimal preDeductAmount;
|
||||
|
||||
/** 金额 */
|
||||
@Excel(name = "金额")
|
||||
private BigDecimal amount;
|
||||
|
||||
/** 生成视频时的请求ID */
|
||||
@Excel(name = "生成视频时的请求ID")
|
||||
private String videoGenRequestId;
|
||||
|
||||
/** 生成结果 */
|
||||
@Excel(name = "生成结果")
|
||||
private String result;
|
||||
|
|
|
|||
|
|
@ -34,6 +34,6 @@ public class ByteBodyRes {
|
|||
private Integer duration;
|
||||
private Integer framespersecond;
|
||||
private boolean draft;
|
||||
|
||||
|
||||
// 火山请求ID,在head里,需手动设置
|
||||
private String requestId;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -71,6 +71,8 @@ public interface IAiOrderService {
|
|||
*/
|
||||
int deleteAiOrderById(Long id);
|
||||
|
||||
AiOrder getAiOrder(String aiType, boolean isReduceBalance);
|
||||
|
||||
AiOrder getAiOrder(String aiType);
|
||||
|
||||
void orderFailure(AiOrder aiOrder);
|
||||
|
|
@ -81,5 +83,7 @@ public interface IAiOrderService {
|
|||
|
||||
AiOrder getAiOrderByPortalVideoTask(String taskId);
|
||||
|
||||
int getChangerType(String aiType);
|
||||
|
||||
BigDecimal getSumAmountByUserId(String userId);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ package com.ruoyi.ai.service;
|
|||
import com.ruoyi.ai.domain.ByteBodyReq;
|
||||
import com.ruoyi.ai.domain.ByteBodyRes;
|
||||
import com.ruoyi.common.core.domain.AjaxResult;
|
||||
import com.ruoyi.common.core.response.video.GetVideoGenerationTaskResponse;
|
||||
|
||||
public interface IByteService {
|
||||
|
||||
|
|
@ -44,4 +45,9 @@ public interface IByteService {
|
|||
* GET 查询视频生成任务列表(火山 list 文档),返回原始 JSON 字符串
|
||||
*/
|
||||
String listVideoGenerationTasks(int pageNum, int pageSize, String arkApiKey) throws Exception;
|
||||
|
||||
/**
|
||||
* GET 查询视频生成任务(单个)
|
||||
*/
|
||||
GetVideoGenerationTaskResponse getVideoGenerationTasks(String id, String arkApiKey) throws Exception;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -136,15 +136,24 @@ public class AiOrderServiceImpl implements IAiOrderService {
|
|||
return aiOrderMapper.deleteAiOrderById(id);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 生成订单
|
||||
* @param aiType 对应的AI类型
|
||||
*/
|
||||
public AiOrder getAiOrder(String aiType) {
|
||||
return getAiOrder(aiType, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成订单
|
||||
*
|
||||
* @param aiType 对应的AI类型
|
||||
* @return
|
||||
* @param isReduceBalance 是否扣减账户
|
||||
*/
|
||||
@Override
|
||||
@Transactional
|
||||
public AiOrder getAiOrder(String aiType) {
|
||||
public AiOrder getAiOrder(String aiType, boolean isReduceBalance) {
|
||||
AiManager aiManager = aiManagerService.selectAiManagerByType(aiType);
|
||||
if (aiManager == null) {
|
||||
throw new ServiceException(
|
||||
|
|
@ -164,12 +173,20 @@ public class AiOrderServiceImpl implements IAiOrderService {
|
|||
aiOrder.setUserId(SecurityUtils.getAiUserId());
|
||||
aiOrder.setType(aiType);
|
||||
aiOrder.setResult(null);
|
||||
aiOrder.setAmount(aiManager.getPrice());
|
||||
if (isReduceBalance) {
|
||||
aiOrder.setAmount(aiManager.getPrice());
|
||||
} else {
|
||||
// 不按aimanager扣减的,等提交任务再按实扣减
|
||||
aiOrder.setAmount(new BigDecimal(0));
|
||||
}
|
||||
aiOrder.setStatus(0);
|
||||
aiOrder.setSource(aiUser.getSource());
|
||||
aiOrderMapper.insert(aiOrder);
|
||||
// 执行余额变更
|
||||
aiUserService.addUserBalance(orderno, SecurityUtils.getAiUserId(), NumberUtil.mul(-1, aiManager.getPrice()), getChangerType(aiType));
|
||||
if (isReduceBalance) {
|
||||
aiUserService.addUserBalance(orderno, SecurityUtils.getAiUserId()
|
||||
, NumberUtil.mul(-1, aiManager.getPrice()), getChangerType(aiType));
|
||||
}
|
||||
return aiOrder;
|
||||
}
|
||||
|
||||
|
|
@ -209,6 +226,7 @@ public class AiOrderServiceImpl implements IAiOrderService {
|
|||
return aiOrderMapper.getAiOrderByPortalVideoTask(taskId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getChangerType(String aiType) {
|
||||
switch (aiType) {
|
||||
case "11":
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import com.ruoyi.ai.domain.ByteBodyRes;
|
|||
import com.ruoyi.ai.service.IByteDeptApiKeyService;
|
||||
import com.ruoyi.ai.service.IByteService;
|
||||
import com.ruoyi.common.core.domain.AjaxResult;
|
||||
import com.ruoyi.common.core.response.video.GetVideoGenerationTaskResponse;
|
||||
import com.ruoyi.common.utils.SecurityUtils;
|
||||
import com.ruoyi.common.utils.StringUtils;
|
||||
import com.ruoyi.common.utils.http.OkHttpUtils;
|
||||
|
|
@ -121,8 +122,12 @@ public class ByteService implements IByteService {
|
|||
}
|
||||
|
||||
String responseBody = response.body().string();
|
||||
log.info("调用火山接口, response = {}", responseBody);
|
||||
return objectMapper.readValue(responseBody, ByteBodyRes.class);
|
||||
String requestId = response.header("x-request-id");
|
||||
log.info("调用火山接口, requestId = {}, response = {}", requestId, responseBody);
|
||||
ByteBodyRes resp = objectMapper.readValue(responseBody, ByteBodyRes.class);
|
||||
// 从headder拿到requestId,便于联调
|
||||
resp.setRequestId(requestId);
|
||||
return resp;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -234,4 +239,33 @@ public class ByteService implements IByteService {
|
|||
private String resolveCurrentAiUserApiKey() {
|
||||
return byteDeptApiKeyService.resolveVolcApiKey(SecurityUtils.getAiUserId());
|
||||
}
|
||||
|
||||
@Override
|
||||
public GetVideoGenerationTaskResponse getVideoGenerationTasks(String id, String arkApiKey) throws Exception {
|
||||
if (StringUtils.isBlank(arkApiKey)) {
|
||||
throw new Exception("getVideoGenerationTasks error:apiKey is null");
|
||||
}
|
||||
if (StringUtils.isBlank(id)) {
|
||||
throw new Exception("getVideoGenerationTasks error:id is null");
|
||||
}
|
||||
|
||||
HttpUrl parsed = HttpUrl.parse(volcBaseUrl + "/api/v3/contents/generations/tasks/" + id);
|
||||
if (parsed == null) {
|
||||
throw new Exception("listVideoGenerationTasks error:invalid base url");
|
||||
}
|
||||
Request request = new Request.Builder()
|
||||
.url(parsed)
|
||||
.header("Content-Type", "application/json")
|
||||
.header("Authorization", "Bearer " + arkApiKey)
|
||||
.get().build();
|
||||
Response response = OkHttpUtils.newCall(request).execute();
|
||||
if (response.body() == null) {
|
||||
throw new Exception("listVideoGenerationTasks response null");
|
||||
}
|
||||
String body = response.body().string();
|
||||
if (!response.isSuccessful()) {
|
||||
throw new Exception("listVideoGenerationTasks error:" + body);
|
||||
}
|
||||
return objectMapper.readValue(body, GetVideoGenerationTaskResponse.class);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue