Compare commits

..

No commits in common. "a26021dc1c55ee2e7ff7ab18becbd4ca0a8efe96" and "3328aae4e394d3ecf2c4c3e5c035af6a8c77c70f" have entirely different histories.

30 changed files with 187 additions and 1087 deletions

View File

@ -74,6 +74,11 @@
<el-form-item> <el-form-item>
<el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button> <el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
<el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button> <el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
<el-row :gutter="10" class="mb8">
<el-col :span="1.5">
<el-button <el-button
type="warning" type="warning"
plain plain
@ -82,23 +87,23 @@
@click="handleExport" @click="handleExport"
v-hasPermi="['ai:order:export']" v-hasPermi="['ai:order:export']"
>导出</el-button> >导出</el-button>
</el-form-item> </el-col>
</el-form> <right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
<right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar> </el-row>
<el-table v-loading="loading" :data="orderList" @selection-change="handleSelectionChange"> <el-table v-loading="loading" :data="orderList" @selection-change="handleSelectionChange">
<el-table-column type="selection" width="55" align="center" /> <el-table-column type="selection" width="55" align="center" />
<el-table-column label="主键ID" align="center" prop="id" width="60" /> <el-table-column label="主键ID" align="center" prop="id" />
<!-- <el-table-column label="备注" align="center" prop="remark" /> --> <!-- <el-table-column label="备注" align="center" prop="remark" /> -->
<el-table-column label="订单编号" align="center" prop="orderNum" width="150"/> <el-table-column label="订单编号" align="center" prop="orderNum" />
<el-table-column label="用户ID" align="center" prop="uuid" width="100" /> <el-table-column label="用户ID" align="center" prop="uuid" />
<el-table-column label="操作类型" align="center" prop="type" width="90" > <el-table-column label="操作类型" align="center" prop="type">
<template slot-scope="scope"> <template slot-scope="scope">
<dict-tag :options="dict.type.ai_function_type" :value="scope.row.type" /> <dict-tag :options="dict.type.ai_function_type" :value="scope.row.type" />
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="金额" align="center" prop="amount" width="100" /> <el-table-column label="金额" align="center" prop="amount" />
<el-table-column label="生成结果" align="center" width="200"> <el-table-column label="生成结果" align="center">
<template slot-scope="scope"> <template slot-scope="scope">
<!-- 判断是否为链接 --> <!-- 判断是否为链接 -->
<template v-if="isUrl(scope.row.result)"> <template v-if="isUrl(scope.row.result)">
@ -118,43 +123,7 @@
@click="playVideo(scope.row.result)" @click="playVideo(scope.row.result)"
/> />
</template> </template>
<!-- 视频任务失败等场景result VideoTaskError JSON表格内简短悬停看全文 --> <!-- 非链接内容 -->
<template v-else-if="parseVolcTaskErrorJson(scope.row.result)">
<span
v-for="err in [parseVolcTaskErrorJson(scope.row.result)]"
:key="'oe-' + scope.row.id"
class="order-result-error-wrap"
>
<el-tooltip
placement="top-start"
effect="dark"
:open-delay="200"
popper-class="order-result-error-tooltip-popper"
>
<div slot="content" class="order-result-error-tooltip-body">
<div class="order-result-error-tooltip-row">
<span class="order-result-error-tooltip-k">错误编号</span>
<span class="order-result-error-tooltip-v">{{ err.code || '—' }}</span>
</div>
<div
v-if="volcTaskErrorHintForCode(err.code)"
class="order-result-error-tooltip-row"
>
<span class="order-result-error-tooltip-k">中文说明</span>
<span class="order-result-error-tooltip-v">{{
volcTaskErrorHintForCode(err.code)
}}</span>
</div>
<div class="order-result-error-tooltip-row order-result-error-tooltip-row--msg">
<span class="order-result-error-tooltip-k">信息</span>
<span class="order-result-error-tooltip-v">{{ err.message || '—' }}</span>
</div>
</div>
<span class="order-result-error-compact">{{ volcTaskErrorCellSummary(err) }}</span>
</el-tooltip>
</span>
</template>
<!-- 非链接内容如任务 id可点击尝试拉取视频 -->
<span v-else @click="handleOtherEvent(scope.row)">{{ scope.row.result }}</span> <span v-else @click="handleOtherEvent(scope.row)">{{ scope.row.result }}</span>
</template> </template>
</el-table-column> </el-table-column>
@ -176,7 +145,7 @@
>{{ scope.row.text}}</div> >{{ scope.row.text}}</div>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="置顶" align="center" key="status" width="60"> <el-table-column label="置顶" align="center" key="status">
<template slot-scope="scope"> <template slot-scope="scope">
<el-switch <el-switch
v-model="scope.row.isTop" v-model="scope.row.isTop"
@ -186,9 +155,9 @@
></el-switch> ></el-switch>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="创建时间" align="center" prop="createTime" width="150"/> <el-table-column label="创建时间" align="center" prop="createTime" />
<el-table-column label="状态" align="center" prop="status" :formatter="formatStatus" width="70"/> <el-table-column label="状态" align="center" prop="status" :formatter="formatStatus" />
<el-table-column label="来源" align="center" prop="source" width="70"/> <el-table-column label="来源" align="center" prop="source" />
</el-table> </el-table>
<pagination <pagination
@ -340,57 +309,6 @@ export default {
const value = String(str || "").trim(); const value = String(str || "").trim();
return /^https?:\/\//i.test(value); return /^https?:\/\//i.test(value);
}, },
<<<<<<< HEAD
=======
/** 已知错误码对应中文说明(与门户 VideoGen 一致) */
volcTaskErrorHintForCode(code) {
const c = String(code || "").trim();
const hints = {
OutputVideoSensitiveContentDetected: "输出视频可能包含敏感信息",
InvalidParameter: "请求参数无效"
};
return hints[c] || "";
},
/** 表格内摘要:已知码只显示中文,未知码显示 code */
volcTaskErrorCellSummary(err) {
if (!err) return "—";
const code = String(err.code || "").trim();
const hint = this.volcTaskErrorHintForCode(code);
if (hint) return hint;
if (code) return code;
const msg = String(err.message || "").trim();
return msg ? msg.slice(0, 32) + (msg.length > 32 ? "…" : "") : "—";
},
/** 已知火山错误码后附中文说明(括号),导出等场景可用 */
volcFailureCodeWithHint(code) {
const c = String(code || "").trim();
if (!c) return "";
const hint = this.volcTaskErrorHintForCode(c);
return hint ? `${c}${hint}` : c;
},
/** result 为火山回调失败写入的 VideoTaskError JSON 时解析为 { code, message } */
parseVolcTaskErrorJson(str) {
const s = String(str || "").trim();
if (!s || s[0] !== "{") return null;
try {
const o = JSON.parse(s);
if (
o &&
typeof o === "object" &&
!Array.isArray(o) &&
("code" in o || "message" in o)
) {
return {
code: o.code != null ? String(o.code) : "",
message: o.message != null ? String(o.message) : ""
};
}
} catch (_) {
/* ignore */
}
return null;
},
>>>>>>> seedance_score
// portal-ui GeneratedAssets // portal-ui GeneratedAssets
isImage(url) { isImage(url) {
const value = String(url || "").trim(); const value = String(url || "").trim();
@ -415,7 +333,6 @@ export default {
}, },
// //
async handleOtherEvent(row) { async handleOtherEvent(row) {
if (this.parseVolcTaskErrorJson(row.result)) return;
// //
if (row.isDownloading) return; if (row.isDownloading) return;
const originalResult = row.result; const originalResult = row.result;
@ -592,56 +509,6 @@ export default {
height: auto; height: auto;
} }
.order-result-error-wrap {
display: inline-block;
}
.order-result-error-compact {
display: inline-block;
max-width: 200px;
margin: 0 auto;
font-size: 12px;
line-height: 1.35;
color: #c45656;
cursor: help;
text-align: center;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
vertical-align: middle;
}
.order-result-error-tooltip-body {
max-width: 420px;
text-align: left;
line-height: 1.5;
font-size: 12px;
}
.order-result-error-tooltip-row {
margin-bottom: 6px;
}
.order-result-error-tooltip-row:last-child {
margin-bottom: 0;
}
.order-result-error-tooltip-row--msg .order-result-error-tooltip-v {
display: block;
margin-top: 2px;
white-space: pre-wrap;
word-break: break-word;
}
.order-result-error-tooltip-k {
font-weight: 600;
color: #fde2e2;
}
.order-result-error-tooltip-v {
color: #fff;
}
.text-ellipsis-two-lines { .text-ellipsis-two-lines {
/* 必须设置宽度(继承表格列宽,也可手动指定) */ /* 必须设置宽度(继承表格列宽,也可手动指定) */
width: 100%; width: 100%;

View File

@ -181,7 +181,6 @@ export default {
}) })
this.username = "" this.username = ""
this.password = "" this.password = ""
this.$router.replace({ name: 'video-gen' })
this.$emit('cancel') this.$emit('cancel')
}) })
} else { } else {

View File

@ -100,7 +100,6 @@ import { constantRoutes } from '@/router/index.js'
import Login from './Login.vue' import Login from './Login.vue'
import { LOCALE_NAMES } from '@/lang/i18n' import { LOCALE_NAMES } from '@/lang/i18n'
import User from './User.vue' import User from './User.vue'
import { getToken } from '@/utils/auth'
export default { export default {
name: 'nav-bar', name: 'nav-bar',
@ -183,9 +182,6 @@ export default {
this.openLogin() this.openLogin()
} }
this.getLogo() this.getLogo()
if (getToken()) {
this.$store.dispatch('user/getInfo').catch(() => {})
}
}, },
methods: { methods: {
openLogin() { openLogin() {

View File

@ -47,7 +47,7 @@ export const constantRoutes = [{
path: '/', path: '/',
component: Layout, component: Layout,
redirect: { redirect: {
name: 'video-gen' name: 'index'
}, },
children: [{ children: [{
path: 'index', path: 'index',

View File

@ -149,20 +149,14 @@
<span class="vg-chat-inline-status" :class="chatRowInlineStatusClass(row)"> <span class="vg-chat-inline-status" :class="chatRowInlineStatusClass(row)">
{{ taskStatusText(row) }} {{ taskStatusText(row) }}
</span> </span>
<button
type="button"
class="vg-link vg-chat-inline-cancel"
v-if="row.result && row.status === 0"
@click="cancelRowTask(row)">
取消
</button>
</div> </div>
<template v-for="(fe, feIdx) in [taskRowFailureError(row)]">
<div
v-if="fe"
:key="`${row.id || 'row'}-fe-${feIdx}`"
class="vg-chat-failure-detail">
<div v-if="fe.code" class="vg-chat-failure-line">
错误编号{{ volcFailureCodeWithHint(fe.code) }}
</div>
<div v-if="fe.message" class="vg-chat-failure-line">
信息{{ fe.message }}
</div>
</div>
</template>
</div> </div>
</div> </div>
@ -610,51 +604,6 @@ export default {
el.scrollTop = el.scrollHeight el.scrollTop = el.scrollHeight
}, },
/** 解析失败订单存库的 VideoTaskError JSON兼容旧数据、非 JSON 的 result */
parseTaskResultErrorJson(result) {
const s = String(result ?? '').trim()
if (!s || s[0] !== '{') return null
try {
const o = JSON.parse(s)
if (
o &&
typeof o === 'object' &&
!Array.isArray(o) &&
('code' in o || 'message' in o)
) {
return {
code: o.code != null ? String(o.code) : '',
message: o.message != null ? String(o.message) : ''
}
}
} catch (_) {
/* ignore */
}
return null
},
/** status=2 且 result 为错误 JSON 时返回明细,否则 null */
taskRowFailureError(row) {
const st = row?.status
if (st !== 2 && st !== '2') return null
const parsed = this.parseTaskResultErrorJson(row?.result)
if (!parsed) return null
if (!parsed.code && !parsed.message) return null
return parsed
},
/** 已知火山错误码后附中文说明(括号) */
volcFailureCodeWithHint(code) {
const c = String(code || '').trim()
if (!c) return ''
const hints = {
OutputVideoSensitiveContentDetected: '输出视频可能包含敏感信息',
InvalidParameter: '请求参数无效'
}
const hint = hints[c]
return hint ? `${c}${hint}` : c
},
taskRowResultTrim(row) { taskRowResultTrim(row) {
let r = String(row?.result ?? '').trim() let r = String(row?.result ?? '').trim()
if (r) return r if (r) return r
@ -1233,7 +1182,6 @@ export default {
this.videoId = res.data.id this.videoId = res.data.id
this.showResult = true this.showResult = true
this.$refs.videoComposeRef?.clearPromptOnly?.() this.$refs.videoComposeRef?.clearPromptOnly?.()
this.$store.dispatch('user/getInfo').catch(() => {})
this.getVideo(res.data.id) this.getVideo(res.data.id)
this.refreshChatFirstPage() this.refreshChatFirstPage()
} else if (res.code == -1) { } else if (res.code == -1) {
@ -1285,7 +1233,6 @@ export default {
this.$message.warning('任务已完成但未返回视频地址,请稍后重试或联系管理员') this.$message.warning('任务已完成但未返回视频地址,请稍后重试或联系管理员')
} }
this.destroyInterval() this.destroyInterval()
this.$store.dispatch('user/getInfo').catch(() => {})
this.refreshChatFirstPage() this.refreshChatFirstPage()
return return
} }
@ -2083,26 +2030,11 @@ export default {
.vg-chat-user-row { .vg-chat-user-row {
display: flex; display: flex;
flex-wrap: wrap;
align-items: flex-start; align-items: flex-start;
justify-content: space-between; justify-content: space-between;
gap: 12px; gap: 12px;
} }
.vg-chat-failure-detail {
flex: 1 1 100%;
margin-top: 4px;
padding-top: 8px;
border-top: 1px solid rgba(255, 255, 255, 0.08);
font-size: 12px;
line-height: 1.45;
color: rgba(255, 180, 180, 0.92);
}
.vg-chat-failure-line {
word-break: break-word;
}
.vg-chat-user-col-main { .vg-chat-user-col-main {
flex: 1; flex: 1;
min-width: 0; min-width: 0;
@ -2138,6 +2070,11 @@ export default {
color: rgba(255, 107, 107, 0.95); color: rgba(255, 107, 107, 0.95);
} }
.vg-chat-inline-cancel {
font-size: 12px;
padding: 0;
}
.vg-chat-params { .vg-chat-params {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;

View File

@ -1,36 +1,29 @@
package com.ruoyi.api; package com.ruoyi.api;
import com.ruoyi.ai.domain.*; import com.ruoyi.ai.domain.*;
import com.ruoyi.ai.service.*; 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.api.request.ByteApiRequest; import com.ruoyi.api.request.ByteApiRequest;
import com.ruoyi.common.annotation.Anonymous; import com.ruoyi.common.annotation.Anonymous;
import com.ruoyi.common.core.controller.BaseController; import com.ruoyi.common.core.controller.BaseController;
import com.ruoyi.common.core.domain.AjaxResult; import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.core.request.video.dto.VideoTaskCallBackRequest; import com.ruoyi.common.core.domain.model.LoginAiUser;
import com.ruoyi.common.core.response.video.GetVideoGenerationTaskResponse;
import com.ruoyi.common.core.response.video.dto.VideoTaskError;
import com.ruoyi.common.enums.AiOrderStatusType;
import com.ruoyi.common.enums.VideoTaskStatusType;
import com.ruoyi.common.utils.JsonUtils;
import com.ruoyi.common.utils.RandomStringUtil; import com.ruoyi.common.utils.RandomStringUtil;
import com.ruoyi.common.utils.SecurityUtils;
import com.ruoyi.common.utils.StringUtils; import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.common.utils.TencentCosUtil; import com.ruoyi.common.utils.TencentCosUtil;
import com.ruoyi.common.utils.ip.IpUtils;
import io.swagger.annotations.Api; import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation; import io.swagger.annotations.ApiOperation;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
import javax.servlet.http.HttpServletRequest;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.regex.Pattern; import java.util.regex.Pattern;
/** /**
@ -41,22 +34,20 @@ import java.util.regex.Pattern;
@Api(tags = "生成内容") @Api(tags = "生成内容")
@RequiredArgsConstructor(onConstructor_ = @Autowired) @RequiredArgsConstructor(onConstructor_ = @Autowired)
public class ByteApiController extends BaseController { 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 final IByteService byteService; private final IByteService byteService;
private final TencentCosUtil tencentCosUtil; private final TencentCosUtil tencentCosUtil;
private final IAiOrderService aiOrderService; private final IAiOrderService aiOrderService;
private final IAiManagerService managerService; private final IAiManagerService managerService;
private final IAiTagService aiTagService; private final IAiTagService aiTagService;
private final RedissonClient redissonClient; @Value("${byteapi.callBackUrl}")
private String url;
// 火山引擎配置
@Value("${volcengine.ark.baseUrl}")
private String volcBaseUrl;
@Value("${volcengine.ark.callbackUrl}") @Value("${volcengine.ark.callbackUrl}")
private String volcCallbackUrl; private String volcCallbackUrl;
private final IByteDeptApiKeyService byteDeptApiKeyService;
@PostMapping("/promptToImg") @PostMapping("/promptToImg")
@ApiOperation("文生图") @ApiOperation("文生图")
@ -340,209 +331,128 @@ public class ByteApiController extends BaseController {
@PostMapping(value = "/volcCallback") @PostMapping(value = "/volcCallback")
@ApiOperation("火山引擎视频回调") @ApiOperation("火山引擎视频回调")
@Anonymous @Anonymous
public AjaxResult volcCallback(@RequestBody VideoTaskCallBackRequest request, HttpServletRequest httpRequest) throws Exception { public AjaxResult volcCallback(@RequestBody ByteBodyRes byteBodyRes) throws Exception {
logger.info("volcCallback 收到回调数据: clientIp = {}, host = {}, request = {}", IpUtils.getIpAddr(httpRequest), logger.info("volcCallback 收到回调数据: {}", byteBodyRes);
httpRequest.getHeader("Host"), request); String id = byteBodyRes.getId();
// 1基础参数校验 if (StringUtils.isEmpty(id)) {
AjaxResult result = volcCallbackBaseCheck(request); logger.warn("volcCallback 无任务 id跳过业务处理");
if (result != null) { return AjaxResult.success("callback success");
return result;
} }
// 2查询订单
String taskId = request.getId(); Integer code = byteBodyRes.getCode();
AiOrder order = aiOrderService.selectOneByThirdPartyOrderNum(taskId); boolean codeError = code != null && code != 200;
if (order == null) { String st = byteBodyRes.getStatus();
// 可能是其他环境生成的但回调地址配置成正式的
logger.warn("volcCallback aiorder is not exist! third party order num = {}", taskId); if (st != null && !st.isEmpty()) {
return AjaxResult.success(); // 执行中状态
} Integer extStatus = null;
// 3从官方获取任务数据 if ("queued".equals(st)) {
// 根据订单用户ID查询使用的Key extStatus = 0;
// 严格来讲按逻辑这块是应放在锁内但这是调外部接口如果接口超时整个服务可能会当机所以不放锁内即不做强一致 } else if ("running".equals(st)) {
String apiKey = byteDeptApiKeyService.resolveVolcApiKey(order.getUserId()); extStatus = 1;
GetVideoGenerationTaskResponse taskResp = byteService.getVideoGenerationTasks(request.getId(), apiKey);
// 4官方数据校验
result = volcCallbackByteCheck(request, taskResp);
if (result != null) {
return result;
}
// 5查询订单 taskId 串行Redisson 分布式锁步骤 13 已在锁外
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) {
order = aiOrderService.selectOneByThirdPartyOrderNum(taskId); AiOrder order = findAiOrderByVolcTaskId(id);
if (order == null) { if (order == null) {
// 可能是其他环境生成的但回调地址配置成正式的 logger.warn("volcCallback 修改执行中状态,未找到任务对应订单, id={}, {}", id, st);
logger.warn("volcCallback aiorder is not exist! third party order num = {}", taskId); return AjaxResult.success("callback success");
return AjaxResult.success();
}
// 6状态为队列中执行中只更新任务状态
result = volcCallbackRunningTaskProcess(taskResp, order);
if (result != null) {
return result;
}
// 7订单数据校验
result = volcCallbackOrderCheck(taskResp, order);
if (result != null) {
return result;
}
// 8根据状态做不同的处理加事务
String status = taskResp.getStatus().toLowerCase();
if (VideoTaskStatusType.SUCCEEDED.getName().equals(status)) {
// 成功预扣
return aiOrderService.volcCallbackSuccessProcess(request, taskResp, order);
} else {
// 前面已判断过status的合法性并处理了三种非失败的状态所以可以确定是取消失败超时
if (taskResp.getError() != null) {
order.setResult(JsonUtils.toJson(taskResp.getError()));
} }
aiOrderService.orderFailure(order); // if (order.getStatus() != 0) {
return AjaxResult.success(); // logger.warn("订单状态不为0 因此不修改ext_status, id = {}, status = {}, order status = {}", id, st, order.getStatus());
} // return AjaxResult.success("callback success");
} catch (InterruptedException e) { // }
Thread.currentThread().interrupt(); AiOrder upd = new AiOrder();
logger.error("volcCallback interrupted while waiting for lock, third party order num = {}", taskId, e); upd.setId(order.getId());
return AjaxResult.error(); upd.setExtStatus(extStatus);
} catch (Exception ex) { aiOrderService.updateAiOrder(upd);
logger.error("volcCallback error! third party order num = {}, status = {}", return AjaxResult.success("callback success");
request.getId(), request.getStatus(), ex);
return AjaxResult.error();
} finally {
if (locked && lock.isHeldByCurrentThread()) {
lock.unlock();
} }
} }
}
private AjaxResult volcCallbackOrderCheck(GetVideoGenerationTaskResponse taskResp, AiOrder order) { if (codeError) {
// 订单状态如果不为执行中则不做处理 markVolcCallbackOrderClearResultFailed(id, "code=" + code);
if (order.getStatus() != null && order.getStatus() != AiOrderStatusType.RUNNING.ordinal()) { return AjaxResult.success("callback success");
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) { if ("succeeded".equals(st)) {
String requestStatus = request.getStatus().toLowerCase(); content contentObj = byteBodyRes.getContent();
String responseStatus = taskResp.getStatus().toLowerCase(); if (contentObj != null && StringUtils.isNotEmpty(contentObj.getVideo_url())) {
// 请求的状态与字节的状态是否一致 String videoUrl = contentObj.getVideo_url();
if (!requestStatus.equals(responseStatus)) { videoUrl = tencentCosUtil.uploadFileByUrl(videoUrl);
// 如果推送的是队列中执行中但官方任务可能已进到下一步的状态时间差此种情况不处理等待后续推送 contentObj.setVideo_url(videoUrl);
if (requestStatus.equals(VideoTaskStatusType.QUEUED.getName())
|| requestStatus.equals(VideoTaskStatusType.RUNNING.getName())) { AiOrder aiOrderByResult = findAiOrderByVolcTaskId(id);
logger.warn("volcCallback request's status != official status, no process! order third party order num = {}, request's status = {}, official status = {}", if (aiOrderByResult != null) {
request.getId(), requestStatus, responseStatus); AiOrder aiOrder = new AiOrder();
// 防止再次推送 aiOrder.setId(aiOrderByResult.getId());
return AjaxResult.success(); aiOrder.setResult(videoUrl);
} else { aiOrder.setStatus(1);
logger.error("volcCallback request's status != official status! order third party order num = {}, request's status = {}, official status = {}", aiOrderService.updateAiOrder(aiOrder);
request.getId(), requestStatus, responseStatus); }
// 不再让对方二次推送 } else {
return AjaxResult.error(); handleVolcCallbackFailure(id, "succeeded但缺少video_url");
} }
return AjaxResult.success("callback success");
} }
return null;
}
private AjaxResult volcCallbackBaseCheck(VideoTaskCallBackRequest request) { // failedcanceled 等终态 status 与成功/进行中均不一致
// 参数校验 if (StringUtils.isNotEmpty(st) || code != null) {
String status = request.getStatus(); handleVolcCallbackFailure(id, "status=" + st);
if (StringUtils.isEmpty(request.getId()) || StringUtils.isEmpty(status)) { } else {
logger.error("volcCallbackBaseCheck id or status is null! third party order num = {}, status = {}" logger.warn("volcCallback 未携带可判定的 status/codeid={}", id);
, request.getId(), status);
return AjaxResult.error("id or status is null!");
} }
// 状态是否正确 return AjaxResult.success("callback success");
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().toLowerCase();
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不退款 * 回调体 code 200HTTP/业务状态码清空 resultstatus=2不退款
*/ */
// private void markVolcCallbackOrderClearResultFailed(String taskId, String reason) { private void markVolcCallbackOrderClearResultFailed(String taskId, String reason) {
// AiOrder order = findAiOrderByVolcTaskId(taskId); AiOrder order = findAiOrderByVolcTaskId(taskId);
// if (order == null) { if (order == null) {
// logger.warn("volcCallback code 非 200未找到任务对应订单, taskId={}, {}", taskId, reason); logger.warn("volcCallback code 非 200未找到任务对应订单, taskId={}, {}", taskId, reason);
// return; return;
// } }
// AiOrder upd = new AiOrder(); AiOrder upd = new AiOrder();
// upd.setId(order.getId()); upd.setId(order.getId());
// upd.setResult(""); upd.setResult("");
// upd.setStatus(2); upd.setStatus(2);
// aiOrderService.updateAiOrder(upd); aiOrderService.updateAiOrder(upd);
// logger.warn("volcCallback code 非 200已清空 result 并 status=2, orderId={}, {}", order.getId(), reason); logger.warn("volcCallback code 非 200已清空 result 并 status=2, orderId={}, {}", order.getId(), reason);
// } }
// private AiOrder findAiOrderByVolcTaskId(String taskId) { private AiOrder findAiOrderByVolcTaskId(String taskId) {
// AiOrder order = aiOrderService.getAiOrderByPortalVideoTask(taskId); AiOrder order = aiOrderService.getAiOrderByPortalVideoTask(taskId);
// if (order != null) { if (order != null) {
// return order; return order;
// } }
// return aiOrderService.getAiOrderByResult(taskId); return aiOrderService.getAiOrderByResult(taskId);
// } }
/** /**
* 回调判定任务失败订单仍持有火山任务 id且非已失败时标记失败并退余额orderFailure * 回调判定任务失败订单仍持有火山任务 id且非已失败时标记失败并退余额orderFailure
*/ */
// private void handleVolcCallbackFailure(String taskId, String reason) { private void handleVolcCallbackFailure(String taskId, String reason) {
// AiOrder order = aiOrderService.getAiOrderByResult(taskId); AiOrder order = aiOrderService.getAiOrderByResult(taskId);
// if (order == null) { if (order == null) {
// logger.warn("volcCallback 失败:未找到 result={} 的订单, {}", taskId, reason); logger.warn("volcCallback 失败:未找到 result={} 的订单, {}", taskId, reason);
// return; return;
// } }
// if (!taskId.equals(order.getResult())) { if (!taskId.equals(order.getResult())) {
// logger.info("volcCallback 失败处理跳过:订单结果已更新, taskId={}, {}", taskId, reason); logger.info("volcCallback 失败处理跳过:订单结果已更新, taskId={}, {}", taskId, reason);
// return; return;
// } }
// if (Integer.valueOf(2).equals(order.getStatus())) { if (Integer.valueOf(2).equals(order.getStatus())) {
// return; return;
// } }
// aiOrderService.orderFailure(order); aiOrderService.orderFailure(order);
// logger.warn("volcCallback 任务失败,已标记订单失败并退款, taskId={}, reason={}", taskId, reason); 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

@ -1,6 +1,5 @@
package com.ruoyi.api; package com.ruoyi.api;
import cn.hutool.core.util.NumberUtil;
import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
@ -13,12 +12,10 @@ import com.ruoyi.ai.domain.ContentItem;
import com.ruoyi.ai.domain.ImageUrl; import com.ruoyi.ai.domain.ImageUrl;
import com.ruoyi.ai.domain.content; import com.ruoyi.ai.domain.content;
import com.ruoyi.ai.service.IAiOrderService; import com.ruoyi.ai.service.IAiOrderService;
import com.ruoyi.ai.service.IAiUserService;
import com.ruoyi.ai.service.IByteDeptApiKeyService; import com.ruoyi.ai.service.IByteDeptApiKeyService;
import com.ruoyi.ai.service.IByteService; import com.ruoyi.ai.service.IByteService;
import com.ruoyi.api.request.PortalVideoGenRequest; import com.ruoyi.api.request.PortalVideoGenRequest;
import com.ruoyi.common.core.controller.BaseController; 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.exception.ServiceException;
import com.ruoyi.common.core.domain.AjaxResult; import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.core.page.TableDataInfo; import com.ruoyi.common.core.page.TableDataInfo;
@ -33,7 +30,6 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
import java.math.BigDecimal;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
import java.util.List; import java.util.List;
@ -55,7 +51,6 @@ public class PortalVideoController extends BaseController {
private final IAiOrderService aiOrderService; private final IAiOrderService aiOrderService;
private final TencentCosUtil tencentCosUtil; private final TencentCosUtil tencentCosUtil;
private final PortalVideoProperties portalVideoProperties; private final PortalVideoProperties portalVideoProperties;
private final IAiUserService aiUserService;
@Value("${volcengine.ark.callbackUrl:}") @Value("${volcengine.ark.callbackUrl:}")
private String volcCallbackUrl; private String volcCallbackUrl;
@ -192,9 +187,7 @@ public class PortalVideoController extends BaseController {
private void fillVideoOrderRecord(AiOrder aiOrder, PortalVideoGenRequest req, String mode, ByteBodyReq body, String functionTypeResolved) { private void fillVideoOrderRecord(AiOrder aiOrder, PortalVideoGenRequest req, String mode, ByteBodyReq body, String functionTypeResolved) {
aiOrder.setText(req.getText()); aiOrder.setText(req.getText());
aiOrder.setMode(mode); aiOrder.setMode(mode);
aiOrder.setIsBackfilled(0);
applyOrderImages(aiOrder, req); applyOrderImages(aiOrder, req);
aiOrder.setExtStatus(0);
if (req.getDuration() != null) { if (req.getDuration() != null) {
aiOrder.setDuration(req.getDuration()); aiOrder.setDuration(req.getDuration());
} else if (body.getDuration() != null) { } else if (body.getDuration() != null) {
@ -263,7 +256,6 @@ public class PortalVideoController extends BaseController {
private AjaxResult submitOrderAndCreate(PortalVideoGenRequest req, String mode, ByteBodyReq byteBodyReq) { private AjaxResult submitOrderAndCreate(PortalVideoGenRequest req, String mode, ByteBodyReq byteBodyReq) {
String functionType = resolveFunctionType(req); String functionType = resolveFunctionType(req);
// 判断余额是否足够aimanager里配置了最低限额创建订单
AiOrder aiOrder = aiOrderService.getAiOrder(functionType); AiOrder aiOrder = aiOrderService.getAiOrder(functionType);
if (aiOrder == null) { if (aiOrder == null) {
return AjaxResult.error(-1, "You have a low balance, please recharge"); return AjaxResult.error(-1, "You have a low balance, please recharge");
@ -274,45 +266,14 @@ public class PortalVideoController extends BaseController {
String key = apiKey(); String key = apiKey();
ByteBodyRes byteBodyRes = byteService.imgToVideo(byteBodyReq, key); ByteBodyRes byteBodyRes = byteService.imgToVideo(byteBodyReq, key);
String thirdPartyOrderNumId = byteBodyRes.getId(); String id = byteBodyRes.getId();
if (thirdPartyOrderNumId == null) { if (id == null) {
aiOrderService.orderFailure(aiOrder); aiOrderService.orderFailure(aiOrder);
return AjaxResult.error(-2, "generation failed, balance has been refunded"); return AjaxResult.error(-2, "generation failed, balance has been refunded");
} }
mergeVolcTaskIdIntoVideoParams(aiOrder, thirdPartyOrderNumId); mergeVolcTaskIdIntoVideoParams(aiOrder, id);
aiOrder.setResult(thirdPartyOrderNumId); aiOrder.setResult(id);
// 字节订单号与请求ID aiOrderService.orderSuccess(aiOrder);
aiOrder.setThirdPartyOrderNum(thirdPartyOrderNumId);
aiOrder.setVideoGenRequestId(byteBodyRes.getRequestId());
// aiOrderService.orderSuccess(aiOrder);
aiOrderService.updateAiOrder(aiOrder);
// 逻辑暂时停用先不预扣 start !!!!!
// 查询任务详情按字节返回的预扣数量扣减
// 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");
// }
// if (task.getUsage().getTotalTokens() <= 0) {
// return AjaxResult.error(-2, "generation failed, byte task's totalTokens <= 0");
// }
// BigDecimal totalTokens = new BigDecimal(task.getUsage().getTotalTokens());
// // 同步设置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));
// 逻辑暂时停用先不预扣 start !!!!!
return AjaxResult.success(byteBodyRes); return AjaxResult.success(byteBodyRes);
} catch (Exception e) { } catch (Exception e) {
aiOrderService.orderFailure(aiOrder); aiOrderService.orderFailure(aiOrder);
@ -637,19 +598,19 @@ public class PortalVideoController extends BaseController {
return AjaxResult.success(byteBodyRes); return AjaxResult.success(byteBodyRes);
} }
// @DeleteMapping("/tasks/{taskId}") @DeleteMapping("/tasks/{taskId}")
// @ApiOperation("删除或取消视频生成任务") @ApiOperation("删除或取消视频生成任务")
// public AjaxResult deleteOrCancelTask(@PathVariable String taskId) throws Exception { public AjaxResult deleteOrCancelTask(@PathVariable String taskId) throws Exception {
// Long uid = SecurityUtils.getAiUserId(); Long uid = SecurityUtils.getAiUserId();
// AiOrder owned = aiOrderService.getAiOrderByPortalVideoTask(taskId); AiOrder owned = aiOrderService.getAiOrderByPortalVideoTask(taskId);
// if (owned == null || !uid.equals(owned.getUserId())) { if (owned == null || !uid.equals(owned.getUserId())) {
// return AjaxResult.error("无权操作该任务"); return AjaxResult.error("无权操作该任务");
// } }
// String key = apiKey(); String key = apiKey();
// AjaxResult cancelRes = byteService.cancelVideoTask(taskId, key); AjaxResult cancelRes = byteService.cancelVideoTask(taskId, key);
// if (cancelRes.isSuccess() && owned.getStatus() != null && owned.getStatus() == 0) { if (cancelRes.isSuccess() && owned.getStatus() != null && owned.getStatus() == 0) {
// aiOrderService.orderFailure(owned); aiOrderService.orderFailure(owned);
// } }
// return cancelRes; return cancelRes;
// } }
} }

View File

@ -242,6 +242,9 @@ volcengine:
ak: AKLTNmYyN2VhZTcyMDcxNDNlNzg3OGVlMDVmZjRhNWQwY2M ak: AKLTNmYyN2VhZTcyMDcxNDNlNzg3OGVlMDVmZjRhNWQwY2M
sk: Tm1ZeU1UTmlORFk1WmpKa05HUmpaRGcxTWpjMFpqUmpOVE01TUdJME5URQ== sk: Tm1ZeU1UTmlORFk1WmpKa05HUmpaRGcxTWpjMFpqUmpOVE01TUdJME5URQ==
projectAesKeyBase64: "gJajABVfQJ9xA94Q9IvQi68fqqhSIkfcKlG7pjGFt2U=" projectAesKeyBase64: "gJajABVfQJ9xA94Q9IvQi68fqqhSIkfcKlG7pjGFt2U="
url: https://ark.ap-southeast.bytepluses.com/api/v3
apiKey: 3e33e034-7e25-4228-8864-b51b2a7a8f97
callBackUrl: http://47.86.170.114:5173/
# 门户视频生成页:模型 / 比例 / 时长 / 分辨率均由此处维护,前后端不写死业务枚举 # 门户视频生成页:模型 / 比例 / 时长 / 分辨率均由此处维护,前后端不写死业务枚举
portal: portal:
@ -278,8 +281,8 @@ portal:
- 14 - 14
- 15 - 15
resolutions: resolutions:
- "480p"
- "720p" - "720p"
- "1080p"
jinsha: jinsha:
url: https://api.jinshapay.xyz url: https://api.jinshapay.xyz

View File

@ -36,8 +36,3 @@ no.update.permission=您没有修改数据的权限,请联系管理员添加
no.delete.permission=您没有删除数据的权限,请联系管理员添加权限 [{0}] no.delete.permission=您没有删除数据的权限,请联系管理员添加权限 [{0}]
no.export.permission=您没有导出数据的权限,请联系管理员添加权限 [{0}] no.export.permission=您没有导出数据的权限,请联系管理员添加权限 [{0}]
no.view.permission=您没有查看数据的权限,请联系管理员添加权限 [{0}] no.view.permission=您没有查看数据的权限,请联系管理员添加权限 [{0}]
# video generation
order.number.generation.failed=订单号 {0} 生成失败,请稍后重试。
order.number.generation.submit=订单号 {0} 生成任务已提交!
order.number.generation.successbackfill=订单号 {0} 生成成功!金额已回补!

View File

@ -30,8 +30,4 @@ email.verification.code.error=Verification code is incorrect, please try again.
# User not found # User not found
user.not.found=User not found. user.not.found=User not found.
user.password.incorrect=Password is incorrect, please try again. user.password.incorrect=Password is incorrect, please try again.
# video generation
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.submit=Order number {0} generation task submitted!
order.number.generation.successbackfill=Order number {0} sucessed! Amount is back filled!

View File

@ -29,7 +29,4 @@ email.verification.code.error=驗證碼錯誤,請重新輸入。
user.not.found=用戶不存在。 user.not.found=用戶不存在。
user.password.incorrect=密碼錯誤,請重新輸入。 user.password.incorrect=密碼錯誤,請重新輸入。
# video generation
order.number.generation.failed=訂單號 {0} 生成失敗,請稍後重試。 order.number.generation.failed=訂單號 {0} 生成失敗,請稍後重試。
order.number.generation.submit=訂單號 {0} 生成任務已提交!
order.number.generation.successbackfill=訂單號 {0} 生成成功!金額已回補!

View File

@ -1,91 +0,0 @@
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

@ -1,91 +0,0 @@
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;
}

View File

@ -1,18 +0,0 @@
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;
}

View File

@ -1,26 +0,0 @@
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;
}

View File

@ -1,22 +0,0 @@
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;
}

View File

@ -1,19 +0,0 @@
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;
}

View File

@ -1,16 +0,0 @@
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;
}

View File

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

View File

@ -1,46 +0,0 @@
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

@ -23,7 +23,7 @@ import java.util.UUID;
/** /**
* AWS S3 文件上传工具类 * AWS S3 文件上传工具类
*/ */
//@Component @Component
public class AwsS3Util { public class AwsS3Util {
// -------------------------- 配置参数需根据实际环境修改-------------------------- // -------------------------- 配置参数需根据实际环境修改--------------------------

View File

@ -59,13 +59,6 @@
<artifactId>ruoyi-system</artifactId> <artifactId>ruoyi-system</artifactId>
</dependency> </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> </dependencies>
</project> </project>

View File

@ -36,10 +36,6 @@ public class AiOrder extends BaseEntity {
@Excel(name = "订单编号") @Excel(name = "订单编号")
private String orderNum; private String orderNum;
/** 第三方单号 */
@Excel(name = "第三方单号")
private String thirdPartyOrderNum;
/** 用户ID */ /** 用户ID */
@Excel(name = "用户ID") @Excel(name = "用户ID")
private Long userId; private Long userId;
@ -48,25 +44,10 @@ public class AiOrder extends BaseEntity {
@Excel(name = "AI类型") @Excel(name = "AI类型")
private String type; private String type;
/** 预扣金额 */
@Excel(name = "预扣金额")
private BigDecimal preDeductAmount;
/** 金额 */ /** 金额 */
@Excel(name = "金额") @Excel(name = "金额")
private BigDecimal amount; private BigDecimal amount;
/** 模型Tokens用量 */
@Excel(name = "模型Tokens用量")
private BigDecimal totalUsage;
@Excel(name = "是否回补处理过: 0-否 1-是")
private Integer isBackfilled;
/** 生成视频时的请求ID */
@Excel(name = "生成视频时的请求ID")
private String videoGenRequestId;
/** 生成结果 */ /** 生成结果 */
@Excel(name = "生成结果") @Excel(name = "生成结果")
private String result; private String result;

View File

@ -34,6 +34,6 @@ public class ByteBodyRes {
private Integer duration; private Integer duration;
private Integer framespersecond; private Integer framespersecond;
private boolean draft; private boolean draft;
// 火山请求ID在head里需手动设置
private String requestId;
} }

View File

@ -6,7 +6,6 @@ import java.util.List;
import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.ruoyi.ai.domain.AiOrder; import com.ruoyi.ai.domain.AiOrder;
import org.apache.ibatis.annotations.Param; import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
/** /**
* 订单管理Mapper接口 * 订单管理Mapper接口
@ -27,7 +26,4 @@ public interface AiOrderMapper extends BaseMapper<AiOrder> {
AiOrder getAiOrderByPortalVideoTask(@Param("taskId") String taskId); AiOrder getAiOrderByPortalVideoTask(@Param("taskId") String taskId);
BigDecimal getSumAmountByUserId(@Param("userId") String userId); 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,10 +6,6 @@ import java.util.List;
import com.baomidou.mybatisplus.core.metadata.IPage; import com.baomidou.mybatisplus.core.metadata.IPage;
import com.ruoyi.ai.domain.AiOrder; import com.ruoyi.ai.domain.AiOrder;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page; 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;
/** /**
* 订单管理Service接口 * 订单管理Service接口
@ -75,29 +71,15 @@ public interface IAiOrderService {
*/ */
int deleteAiOrderById(Long id); int deleteAiOrderById(Long id);
AiOrder getAiOrder(String aiType, boolean isReduceBalance);
AiOrder getAiOrder(String aiType); AiOrder getAiOrder(String aiType);
void orderFailure(AiOrder aiOrder); void orderFailure(AiOrder aiOrder);
@Transactional
void orderFailure(AiOrder aiOrder, BigDecimal amount);
void orderSuccess(AiOrder aiOrder); void orderSuccess(AiOrder aiOrder);
AiOrder getAiOrderByResult(String result); AiOrder getAiOrderByResult(String result);
AiOrder getAiOrderByPortalVideoTask(String taskId); AiOrder getAiOrderByPortalVideoTask(String taskId);
int getChangerType(String aiType);
BigDecimal getSumAmountByUserId(String userId); BigDecimal getSumAmountByUserId(String userId);
AiOrder selectOneByThirdPartyOrderNum(String id);
/**
* 火山回调 - 任务成功时处理流程
*/
AjaxResult volcCallbackSuccessProcess(VideoTaskCallBackRequest request, GetVideoGenerationTaskResponse taskResp, AiOrder order);
} }

View File

@ -1,11 +1,8 @@
package com.ruoyi.ai.service; package com.ruoyi.ai.service;
import com.ruoyi.ai.domain.AiOrder;
import com.ruoyi.ai.domain.ByteBodyReq; import com.ruoyi.ai.domain.ByteBodyReq;
import com.ruoyi.ai.domain.ByteBodyRes; import com.ruoyi.ai.domain.ByteBodyRes;
import com.ruoyi.common.core.domain.AjaxResult; 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 { public interface IByteService {
@ -47,15 +44,4 @@ public interface IByteService {
* GET 查询视频生成任务列表火山 list 文档返回原始 JSON 字符串 * GET 查询视频生成任务列表火山 list 文档返回原始 JSON 字符串
*/ */
String listVideoGenerationTasks(int pageNum, int pageSize, String arkApiKey) throws Exception; String listVideoGenerationTasks(int pageNum, int pageSize, String arkApiKey) throws Exception;
/**
* 获取当前用户所使用的api key
* @return
*/
String resolveCurrentAiUserApiKey();
/**
* GET 查询视频生成任务(单个)
*/
GetVideoGenerationTaskResponse getVideoGenerationTasks(String thirdPartyOrderNumId, String arkApiKey) throws Exception;
} }

View File

@ -15,17 +15,11 @@ import com.ruoyi.ai.service.IAiStatisticsService;
import com.ruoyi.ai.service.IAiUserService; import com.ruoyi.ai.service.IAiUserService;
import com.ruoyi.common.constant.BalanceChangerConstants; import com.ruoyi.common.constant.BalanceChangerConstants;
import com.ruoyi.common.constant.HttpStatus; 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.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.exception.ServiceException;
import com.ruoyi.common.utils.DateUtils; import com.ruoyi.common.utils.DateUtils;
import com.ruoyi.common.utils.MessageUtils; import com.ruoyi.common.utils.MessageUtils;
import com.ruoyi.common.utils.SecurityUtils; 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.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
@ -42,7 +36,6 @@ import java.util.UUID;
* @author shi * @author shi
* @date 2025-11-13 * @date 2025-11-13
*/ */
@Slf4j
@Service @Service
public class AiOrderServiceImpl implements IAiOrderService { public class AiOrderServiceImpl implements IAiOrderService {
@ -58,11 +51,6 @@ public class AiOrderServiceImpl implements IAiOrderService {
@Autowired @Autowired
private IAiStatisticsService aiStatisticsService; private IAiStatisticsService aiStatisticsService;
// 流水表任务成功时回补
private static final String TASK_SUCCESS_BACK_FILL_REMARK = "order.number.generation.successbackfill";
// 流水表提交任务时预扣
private static final String TASK_SUBMIT_REMARK = "order.number.generation.submit";
/** /**
* 查询订单管理 * 查询订单管理
@ -148,24 +136,15 @@ public class AiOrderServiceImpl implements IAiOrderService {
return aiOrderMapper.deleteAiOrderById(id); return aiOrderMapper.deleteAiOrderById(id);
} }
/**
* 生成订单
* @param aiType 对应的AI类型
*/
public AiOrder getAiOrder(String aiType) {
return getAiOrder(aiType, true);
}
/** /**
* 生成订单 * 生成订单
* *
* @param aiType 对应的AI类型 * @param aiType 对应的AI类型
* @param isReduceBalance 是否扣减账户 * @return
*/ */
@Override @Override
@Transactional @Transactional
public AiOrder getAiOrder(String aiType, boolean isReduceBalance) { public AiOrder getAiOrder(String aiType) {
AiManager aiManager = aiManagerService.selectAiManagerByType(aiType); AiManager aiManager = aiManagerService.selectAiManagerByType(aiType);
if (aiManager == null) { if (aiManager == null) {
throw new ServiceException( throw new ServiceException(
@ -185,49 +164,24 @@ public class AiOrderServiceImpl implements IAiOrderService {
aiOrder.setUserId(SecurityUtils.getAiUserId()); aiOrder.setUserId(SecurityUtils.getAiUserId());
aiOrder.setType(aiType); aiOrder.setType(aiType);
aiOrder.setResult(null); aiOrder.setResult(null);
if (isReduceBalance) { aiOrder.setAmount(aiManager.getPrice());
aiOrder.setPreDeductAmount(aiManager.getPrice());
aiOrder.setAmount(aiManager.getPrice());
} else {
// 不按aimanager扣减的等提交任务再按实扣减
aiOrder.setAmount(new BigDecimal(0));
}
aiOrder.setStatus(0); aiOrder.setStatus(0);
aiOrder.setSource(aiUser.getSource()); aiOrder.setSource(aiUser.getSource());
aiOrderMapper.insert(aiOrder); aiOrderMapper.insert(aiOrder);
// 执行余额变更 // 执行余额变更
if (isReduceBalance) { aiUserService.addUserBalance(orderno, SecurityUtils.getAiUserId(), NumberUtil.mul(-1, aiManager.getPrice()), getChangerType(aiType));
String remark = MessageUtils.message(TASK_SUBMIT_REMARK, aiOrder.getOrderNum());
aiUserService.addUserBalance(orderno, SecurityUtils.getAiUserId()
, NumberUtil.mul(-1, aiManager.getPrice()), getChangerType(aiType), remark);
}
return aiOrder; return aiOrder;
} }
/**
* 订单失败时返回订单amount对应数量的用量
*/
@Override @Override
public void orderFailure(AiOrder aiOrder) {
orderFailure(aiOrder, aiOrder.getAmount());
}
/**
* 订单失败时返回指定用量的用量
*/
@Transactional @Transactional
@Override public void orderFailure(AiOrder aiOrder) {
public void orderFailure(AiOrder aiOrder, BigDecimal amount) {
aiOrder.setIsBackfilled(1);
aiOrder.setStatus(2); aiOrder.setStatus(2);
String remark = MessageUtils.message("order.number.generation.failed", aiOrder.getOrderNum()); String remark = MessageUtils.message("order.number.generation.failed", aiOrder.getOrderNum());
aiOrder.setRemark(remark); aiOrder.setRemark(remark);
aiOrderMapper.updateById(aiOrder); aiOrderMapper.updateById(aiOrder);
Long userId = aiOrder.getUserId() != null ? aiOrder.getUserId() : SecurityUtils.getAiUserId(); Long userId = aiOrder.getUserId() != null ? aiOrder.getUserId() : SecurityUtils.getAiUserId();
// 变更值为0则不改余额没有流水 aiUserService.addUserBalance(aiOrder.getOrderNum(), userId, aiOrder.getAmount(), BalanceChangerConstants.REFUND, remark);
if (aiOrder.getAmount() != null && !aiOrder.getAmount().equals(new BigDecimal(0))) {
aiUserService.addUserBalance(aiOrder.getOrderNum(), userId, amount, BalanceChangerConstants.REFUND, remark);
}
} }
@Override @Override
@ -255,7 +209,6 @@ public class AiOrderServiceImpl implements IAiOrderService {
return aiOrderMapper.getAiOrderByPortalVideoTask(taskId); return aiOrderMapper.getAiOrderByPortalVideoTask(taskId);
} }
@Override
public int getChangerType(String aiType) { public int getChangerType(String aiType) {
switch (aiType) { switch (aiType) {
case "11": case "11":
@ -277,67 +230,4 @@ public class AiOrderServiceImpl implements IAiOrderService {
public BigDecimal getSumAmountByUserId(String userId) { public BigDecimal getSumAmountByUserId(String userId) {
return aiOrderMapper.getSumAmountByUserId(userId); return aiOrderMapper.getSumAmountByUserId(userId);
} }
@Override
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);
// tokens用量
order.setTotalUsage(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) {
// 回补
String remark = MessageUtils.message(TASK_SUCCESS_BACK_FILL_REMARK, order.getOrderNum());
aiUserService.addUserBalance(order.getOrderNum(), order.getUserId(), addAmount,
BalanceChangerConstants.REFUND, remark);
}
}
// 按实际用量扣减
// BigDecimal reduceAmount = NumberUtil.mul(-1, realAmount);
// aiUserService.addUserBalance(order.getOrderNum(), order.getUserId(), reduceAmount,
// BalanceChangerConstants.QUICK_VIDEO_GENERATION, TASK_SUCCESS_BALANCE_REMARK);
return AjaxResult.success("callback success");
}
} }

View File

@ -3,17 +3,11 @@ package com.ruoyi.ai.service.impl;
import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import com.ruoyi.ai.domain.AiOrder;
import com.ruoyi.ai.domain.ByteBodyReq; import com.ruoyi.ai.domain.ByteBodyReq;
import com.ruoyi.ai.domain.ByteBodyRes; import com.ruoyi.ai.domain.ByteBodyRes;
import com.ruoyi.ai.service.IAiUserService;
import com.ruoyi.ai.service.IByteDeptApiKeyService; import com.ruoyi.ai.service.IByteDeptApiKeyService;
import com.ruoyi.ai.service.IByteService; import com.ruoyi.ai.service.IByteService;
import com.ruoyi.common.constant.BalanceChangerConstants;
import com.ruoyi.common.core.domain.AjaxResult; 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.SecurityUtils;
import com.ruoyi.common.utils.StringUtils; import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.common.utils.http.OkHttpUtils; import com.ruoyi.common.utils.http.OkHttpUtils;
@ -23,9 +17,6 @@ import okhttp3.*;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
@Slf4j @Slf4j
@Service @Service
@ -130,12 +121,8 @@ public class ByteService implements IByteService {
} }
String responseBody = response.body().string(); String responseBody = response.body().string();
String requestId = response.header("x-request-id"); log.info("调用火山接口, response = {}", responseBody);
log.info("调用火山接口, requestId = {}, response = {}", requestId, responseBody); return objectMapper.readValue(responseBody, ByteBodyRes.class);
ByteBodyRes resp = objectMapper.readValue(responseBody, ByteBodyRes.class);
// 从headder拿到requestId便于联调
resp.setRequestId(requestId);
return resp;
} }
@Override @Override
@ -244,36 +231,7 @@ public class ByteService implements IByteService {
return body; return body;
} }
public String resolveCurrentAiUserApiKey() { private String resolveCurrentAiUserApiKey() {
return byteDeptApiKeyService.resolveVolcApiKey(SecurityUtils.getAiUserId()); return byteDeptApiKeyService.resolveVolcApiKey(SecurityUtils.getAiUserId());
} }
@Override
public GetVideoGenerationTaskResponse getVideoGenerationTasks(String thirdPartyOrderNumId, String arkApiKey) throws Exception {
if (StringUtils.isBlank(arkApiKey)) {
throw new Exception("getVideoGenerationTasks errorapiKey is null");
}
if (StringUtils.isBlank(thirdPartyOrderNumId)) {
throw new Exception("getVideoGenerationTasks errorthirdPartyOrderNumId is null");
}
HttpUrl parsed = HttpUrl.parse(volcBaseUrl + "/api/v3/contents/generations/tasks/" + thirdPartyOrderNumId);
if (parsed == null) {
throw new Exception("listVideoGenerationTasks errorinvalid 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);
}
} }

View File

@ -12,13 +12,10 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<result property="updateBy" column="update_by" /> <result property="updateBy" column="update_by" />
<result property="updateTime" column="update_time" /> <result property="updateTime" column="update_time" />
<result property="remark" column="remark" /> <result property="remark" column="remark" />
<result property="thirdPartyOrderNum" column="third_party_order_num" />
<result property="orderNum" column="order_num" /> <result property="orderNum" column="order_num" />
<result property="userId" column="user_id" /> <result property="userId" column="user_id" />
<result property="type" column="type" /> <result property="type" column="type" />
<result property="preDeductAmount" column="pre_deduct_amount" />
<result property="amount" column="amount" /> <result property="amount" column="amount" />
<result property="totalUsage" column="total_usage" />
<result property="result" column="result" /> <result property="result" column="result" />
<result property="status" column="status" /> <result property="status" column="status" />
<result property="source" column="source" /> <result property="source" column="source" />
@ -32,19 +29,17 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<result property="ratio" column="ratio" /> <result property="ratio" column="ratio" />
<result property="model" column="model" /> <result property="model" column="model" />
<result property="videoParams" column="video_params" /> <result property="videoParams" column="video_params" />
<result property="isBackfilled" column="is_backfilled" />
<result property="videoGenRequestId" column="video_gen_request_id" />
</resultMap> </resultMap>
<sql id="selectAiOrderVo"> <sql id="selectAiOrderVo">
select ao.id, ao.del_flag, ao.create_by, ao.create_time, ao.update_by, ao.update_time, ao.remark, ao.order_num, ao.third_party_order_num, ao.user_id, ao.type, ao.pre_deduct_amount, ao.amount, ao.total_usage, ao.result, ao.status, ao.source, ao.text, ao.is_top, ao.img1, ao.img2, ao.mode, ao.duration, ao.resolution, ao.ratio, ao.model, ao.video_params, au.user_id uuid, ao.ext_status, ao.is_backfilled, ao.video_gen_request_id from ai_order ao select ao.id, ao.del_flag, ao.create_by, ao.create_time, ao.update_by, ao.update_time, ao.remark, ao.order_num, ao.user_id, ao.type, ao.amount, ao.result, ao.status, ao.source, ao.text, ao.is_top, ao.img1, ao.img2, ao.mode, ao.duration, ao.resolution, ao.ratio, ao.model, ao.video_params, au.user_id uuid, ao.ext_status from ai_order ao
left join ai_user au on au.id = ao.user_id left join ai_user au on au.id = ao.user_id
</sql> </sql>
<select id="selectAiOrderList" parameterType="AiOrder" resultMap="AiOrderResult"> <select id="selectAiOrderList" parameterType="AiOrder" resultMap="AiOrderResult">
<include refid="selectAiOrderVo"/> <include refid="selectAiOrderVo"/>
<where> <where>
<if test="orderNum != null and orderNum != ''"> and ao.order_num like concat('%', #{orderNum}, '%')</if> <if test="orderNum != null and orderNum != ''"> and ao.orderNum like concat('%', #{orderNum}, '%')</if>
<if test="text != null and text != ''"> and ao.text like concat('%', #{text}, '%')</if> <if test="text != null and text != ''"> and ao.text like concat('%', #{text}, '%')</if>
<if test="userId != null "> and ao.user_id = #{userId}</if> <if test="userId != null "> and ao.user_id = #{userId}</if>
<if test="uuid != null "> and au.user_id = #{uuid}</if> <if test="uuid != null "> and au.user_id = #{uuid}</if>