feat: 生成视频时,按字节接口扣减用量(代码初步完成)

This commit is contained in:
yys 2026-04-09 16:52:50 +08:00
parent 38b26f9ffb
commit 80c136ad18
12 changed files with 406 additions and 99 deletions

View File

@ -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;
@ -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 分布式锁步骤 13 已在锁外
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");
}
}
// failedcanceled 等终态 status 与成功/进行中均不一致
if (StringUtils.isNotEmpty(st) || code != null) {
handleVolcCallbackFailure(id, "status=" + st);
} else {
logger.warn("volcCallback 未携带可判定的 status/codeid={}", 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 200HTTP/业务状态码清空 resultstatus=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);
// }
}

View File

@ -31,3 +31,4 @@ email.verification.code.error=Verification code is incorrect, please try again.
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.successes=Order number {0} generation successes!

View File

@ -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;
}

View File

@ -0,0 +1,13 @@
package com.ruoyi.common.enums;
/**
* AiOrder表中status的类型
*/
public enum AiOrderStatusType {
// 0-进行中
RUNNING,
// 1-已完成
FINISH,
// 2-失败余额退回
FAIL;
}

View File

@ -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;
}
}

View File

@ -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>

View File

@ -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;

View File

@ -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);
}

View File

@ -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);
}

View File

@ -46,6 +46,12 @@ public interface IByteService {
*/
String listVideoGenerationTasks(int pageNum, int pageSize, String arkApiKey) throws Exception;
/**
* 获取当前用户所使用的api key
* @return
*/
String resolveCurrentAiUserApiKey();
/**
* GET 查询视频生成任务(单个)
*/

View File

@ -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);
}
}

View File

@ -236,7 +236,7 @@ public class ByteService implements IByteService {
return body;
}
private String resolveCurrentAiUserApiKey() {
public String resolveCurrentAiUserApiKey() {
return byteDeptApiKeyService.resolveVolcApiKey(SecurityUtils.getAiUserId());
}