Compare commits
20 Commits
3328aae4e3
...
a26021dc1c
| Author | SHA1 | Date |
|---|---|---|
|
|
a26021dc1c | |
|
|
e93fa6baa9 | |
|
|
1bedde7fa7 | |
|
|
f31e4c38c6 | |
|
|
fedaef2ff7 | |
|
|
b9e08938e0 | |
|
|
85dc61b07d | |
|
|
f1e09eac1a | |
|
|
f6408400ee | |
|
|
7068fad278 | |
|
|
dda43f2de6 | |
|
|
4d428b9d94 | |
|
|
982ce1f179 | |
|
|
a67a8e202a | |
|
|
dbc292b058 | |
|
|
bb5daf02be | |
|
|
992d95992a | |
|
|
f53f849ec8 | |
|
|
80c136ad18 | |
|
|
38b26f9ffb |
|
|
@ -74,11 +74,6 @@
|
|||
<el-form-item>
|
||||
<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-form-item>
|
||||
</el-form>
|
||||
|
||||
<el-row :gutter="10" class="mb8">
|
||||
<el-col :span="1.5">
|
||||
<el-button
|
||||
type="warning"
|
||||
plain
|
||||
|
|
@ -87,23 +82,23 @@
|
|||
@click="handleExport"
|
||||
v-hasPermi="['ai:order:export']"
|
||||
>导出</el-button>
|
||||
</el-col>
|
||||
<right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
|
||||
</el-row>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
|
||||
|
||||
<el-table v-loading="loading" :data="orderList" @selection-change="handleSelectionChange">
|
||||
<el-table-column type="selection" width="55" align="center" />
|
||||
<el-table-column label="主键ID" align="center" prop="id" />
|
||||
<el-table-column label="主键ID" align="center" prop="id" width="60" />
|
||||
<!-- <el-table-column label="备注" align="center" prop="remark" /> -->
|
||||
<el-table-column label="订单编号" align="center" prop="orderNum" />
|
||||
<el-table-column label="用户ID" align="center" prop="uuid" />
|
||||
<el-table-column label="操作类型" align="center" prop="type">
|
||||
<el-table-column label="订单编号" align="center" prop="orderNum" width="150"/>
|
||||
<el-table-column label="用户ID" align="center" prop="uuid" width="100" />
|
||||
<el-table-column label="操作类型" align="center" prop="type" width="90" >
|
||||
<template slot-scope="scope">
|
||||
<dict-tag :options="dict.type.ai_function_type" :value="scope.row.type" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="金额" align="center" prop="amount" />
|
||||
<el-table-column label="生成结果" align="center">
|
||||
<el-table-column label="金额" align="center" prop="amount" width="100" />
|
||||
<el-table-column label="生成结果" align="center" width="200">
|
||||
<template slot-scope="scope">
|
||||
<!-- 判断是否为链接 -->
|
||||
<template v-if="isUrl(scope.row.result)">
|
||||
|
|
@ -123,7 +118,43 @@
|
|||
@click="playVideo(scope.row.result)"
|
||||
/>
|
||||
</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>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
|
@ -145,7 +176,7 @@
|
|||
>{{ scope.row.text}}</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="置顶" align="center" key="status">
|
||||
<el-table-column label="置顶" align="center" key="status" width="60">
|
||||
<template slot-scope="scope">
|
||||
<el-switch
|
||||
v-model="scope.row.isTop"
|
||||
|
|
@ -155,9 +186,9 @@
|
|||
></el-switch>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="创建时间" align="center" prop="createTime" />
|
||||
<el-table-column label="状态" align="center" prop="status" :formatter="formatStatus" />
|
||||
<el-table-column label="来源" align="center" prop="source" />
|
||||
<el-table-column label="创建时间" align="center" prop="createTime" width="150"/>
|
||||
<el-table-column label="状态" align="center" prop="status" :formatter="formatStatus" width="70"/>
|
||||
<el-table-column label="来源" align="center" prop="source" width="70"/>
|
||||
</el-table>
|
||||
|
||||
<pagination
|
||||
|
|
@ -309,6 +340,57 @@ export default {
|
|||
const value = String(str || "").trim();
|
||||
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 一致)
|
||||
isImage(url) {
|
||||
const value = String(url || "").trim();
|
||||
|
|
@ -333,6 +415,7 @@ export default {
|
|||
},
|
||||
// 非链接内容点击事件
|
||||
async handleOtherEvent(row) {
|
||||
if (this.parseVolcTaskErrorJson(row.result)) return;
|
||||
// 防止重复点击
|
||||
if (row.isDownloading) return;
|
||||
const originalResult = row.result;
|
||||
|
|
@ -509,6 +592,56 @@ export default {
|
|||
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 {
|
||||
/* 必须设置宽度(继承表格列宽,也可手动指定) */
|
||||
width: 100%;
|
||||
|
|
|
|||
|
|
@ -181,6 +181,7 @@ export default {
|
|||
})
|
||||
this.username = ""
|
||||
this.password = ""
|
||||
this.$router.replace({ name: 'video-gen' })
|
||||
this.$emit('cancel')
|
||||
})
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -100,6 +100,7 @@ import { constantRoutes } from '@/router/index.js'
|
|||
import Login from './Login.vue'
|
||||
import { LOCALE_NAMES } from '@/lang/i18n'
|
||||
import User from './User.vue'
|
||||
import { getToken } from '@/utils/auth'
|
||||
|
||||
export default {
|
||||
name: 'nav-bar',
|
||||
|
|
@ -182,6 +183,9 @@ export default {
|
|||
this.openLogin()
|
||||
}
|
||||
this.getLogo()
|
||||
if (getToken()) {
|
||||
this.$store.dispatch('user/getInfo').catch(() => {})
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
openLogin() {
|
||||
|
|
|
|||
|
|
@ -47,7 +47,7 @@ export const constantRoutes = [{
|
|||
path: '/',
|
||||
component: Layout,
|
||||
redirect: {
|
||||
name: 'index'
|
||||
name: 'video-gen'
|
||||
},
|
||||
children: [{
|
||||
path: 'index',
|
||||
|
|
|
|||
|
|
@ -149,14 +149,20 @@
|
|||
<span class="vg-chat-inline-status" :class="chatRowInlineStatusClass(row)">
|
||||
{{ taskStatusText(row) }}
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
class="vg-link vg-chat-inline-cancel"
|
||||
v-if="row.result && row.status === 0"
|
||||
@click="cancelRowTask(row)">
|
||||
取消
|
||||
</button>
|
||||
</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>
|
||||
|
||||
|
|
@ -604,6 +610,51 @@ export default {
|
|||
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) {
|
||||
let r = String(row?.result ?? '').trim()
|
||||
if (r) return r
|
||||
|
|
@ -1182,6 +1233,7 @@ export default {
|
|||
this.videoId = res.data.id
|
||||
this.showResult = true
|
||||
this.$refs.videoComposeRef?.clearPromptOnly?.()
|
||||
this.$store.dispatch('user/getInfo').catch(() => {})
|
||||
this.getVideo(res.data.id)
|
||||
this.refreshChatFirstPage()
|
||||
} else if (res.code == -1) {
|
||||
|
|
@ -1233,6 +1285,7 @@ export default {
|
|||
this.$message.warning('任务已完成但未返回视频地址,请稍后重试或联系管理员')
|
||||
}
|
||||
this.destroyInterval()
|
||||
this.$store.dispatch('user/getInfo').catch(() => {})
|
||||
this.refreshChatFirstPage()
|
||||
return
|
||||
}
|
||||
|
|
@ -2030,11 +2083,26 @@ export default {
|
|||
|
||||
.vg-chat-user-row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
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 {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
|
|
@ -2070,11 +2138,6 @@ export default {
|
|||
color: rgba(255, 107, 107, 0.95);
|
||||
}
|
||||
|
||||
.vg-chat-inline-cancel {
|
||||
font-size: 12px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.vg-chat-params {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
|
|
|
|||
|
|
@ -1,29 +1,36 @@
|
|||
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.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.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.SecurityUtils;
|
||||
import com.ruoyi.common.utils.StringUtils;
|
||||
import com.ruoyi.common.utils.TencentCosUtil;
|
||||
import com.ruoyi.common.utils.ip.IpUtils;
|
||||
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.BeanUtils;
|
||||
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 javax.servlet.http.HttpServletRequest;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
|
|
@ -34,20 +41,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 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 RedissonClient redissonClient;
|
||||
|
||||
// 火山引擎配置
|
||||
@Value("${volcengine.ark.baseUrl}")
|
||||
private String volcBaseUrl;
|
||||
@Value("${volcengine.ark.callbackUrl}")
|
||||
private String volcCallbackUrl;
|
||||
private final IByteDeptApiKeyService byteDeptApiKeyService;
|
||||
|
||||
@PostMapping("/promptToImg")
|
||||
@ApiOperation("文生图")
|
||||
|
|
@ -331,128 +340,209 @@ 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, HttpServletRequest httpRequest) throws Exception {
|
||||
logger.info("volcCallback 收到回调数据: clientIp = {}, host = {}, request = {}", IpUtils.getIpAddr(httpRequest),
|
||||
httpRequest.getHeader("Host"), 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 taskId = request.getId();
|
||||
AiOrder order = aiOrderService.selectOneByThirdPartyOrderNum(taskId);
|
||||
if (order == null) {
|
||||
// 可能是其他环境生成的但回调地址配置成正式的
|
||||
logger.warn("volcCallback aiorder is not exist! third party order num = {}", taskId);
|
||||
return AjaxResult.success();
|
||||
}
|
||||
// 3、从官方获取任务数据
|
||||
// 根据订单用户ID查询使用的Key
|
||||
// 严格来讲,按逻辑这块是应放在锁内,但这是调外部接口,如果接口超时整个服务可能会当机,所以不放锁内,即不做强一致
|
||||
String apiKey = byteDeptApiKeyService.resolveVolcApiKey(order.getUserId());
|
||||
GetVideoGenerationTaskResponse taskResp = byteService.getVideoGenerationTasks(request.getId(), apiKey);
|
||||
// 4、官方数据校验
|
||||
result = volcCallbackByteCheck(request, taskResp);
|
||||
if (result != null) {
|
||||
return result;
|
||||
}
|
||||
// 5、查询订单(同 taskId 串行:Redisson 分布式锁;步骤 1~3 已在锁外)
|
||||
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");
|
||||
// 锁内二次查询,防止并发时状态变更
|
||||
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);
|
||||
}
|
||||
// 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 {
|
||||
handleVolcCallbackFailure(id, "succeeded但缺少video_url");
|
||||
// 前面已判断过status的合法性,并处理了三种非失败的状态,所以可以确定是取消、失败、超时
|
||||
if (taskResp.getError() != null) {
|
||||
order.setResult(JsonUtils.toJson(taskResp.getError()));
|
||||
}
|
||||
aiOrderService.orderFailure(order);
|
||||
return AjaxResult.success();
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
logger.error("volcCallback interrupted while waiting for lock, third party order num = {}", taskId, e);
|
||||
return AjaxResult.error();
|
||||
} catch (Exception ex) {
|
||||
logger.error("volcCallback error! third party order num = {}, status = {}",
|
||||
request.getId(), request.getStatus(), ex);
|
||||
return AjaxResult.error();
|
||||
} finally {
|
||||
if (locked && lock.isHeldByCurrentThread()) {
|
||||
lock.unlock();
|
||||
}
|
||||
return AjaxResult.success("callback success");
|
||||
}
|
||||
}
|
||||
|
||||
// failed、canceled 等终态,或 status 与成功/进行中均不一致
|
||||
if (StringUtils.isNotEmpty(st) || code != null) {
|
||||
handleVolcCallbackFailure(id, "status=" + st);
|
||||
} else {
|
||||
logger.warn("volcCallback 未携带可判定的 status/code,id={}", id);
|
||||
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();
|
||||
}
|
||||
return AjaxResult.success("callback 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 requestStatus = request.getStatus().toLowerCase();
|
||||
String responseStatus = taskResp.getStatus().toLowerCase();
|
||||
// 请求的状态与字节的状态是否一致
|
||||
if (!requestStatus.equals(responseStatus)) {
|
||||
// 如果推送的是队列中、执行中,但官方任务可能已进到下一步的状态,(时间差)此种情况不处理,等待后续推送
|
||||
if (requestStatus.equals(VideoTaskStatusType.QUEUED.getName())
|
||||
|| requestStatus.equals(VideoTaskStatusType.RUNNING.getName())) {
|
||||
logger.warn("volcCallback request's status != official status, no process! order third party order num = {}, request's status = {}, official status = {}",
|
||||
request.getId(), requestStatus, responseStatus);
|
||||
// 防止再次推送
|
||||
return AjaxResult.success();
|
||||
} else {
|
||||
logger.error("volcCallback request's status != official status! order third party order num = {}, request's status = {}, official status = {}",
|
||||
request.getId(), requestStatus, responseStatus);
|
||||
// 不再让对方二次推送
|
||||
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().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 非 200(HTTP/业务状态码):清空 result,status=2,不退款。
|
||||
*/
|
||||
private void markVolcCallbackOrderClearResultFailed(String taskId, String reason) {
|
||||
AiOrder order = findAiOrderByVolcTaskId(taskId);
|
||||
if (order == null) {
|
||||
logger.warn("volcCallback code 非 200:未找到任务对应订单, taskId={}, {}", taskId, reason);
|
||||
return;
|
||||
}
|
||||
AiOrder upd = new AiOrder();
|
||||
upd.setId(order.getId());
|
||||
upd.setResult("");
|
||||
upd.setStatus(2);
|
||||
aiOrderService.updateAiOrder(upd);
|
||||
logger.warn("volcCallback code 非 200,已清空 result 并 status=2, orderId={}, {}", order.getId(), reason);
|
||||
}
|
||||
// private void markVolcCallbackOrderClearResultFailed(String taskId, String reason) {
|
||||
// AiOrder order = findAiOrderByVolcTaskId(taskId);
|
||||
// if (order == null) {
|
||||
// logger.warn("volcCallback code 非 200:未找到任务对应订单, taskId={}, {}", taskId, reason);
|
||||
// return;
|
||||
// }
|
||||
// AiOrder upd = new AiOrder();
|
||||
// upd.setId(order.getId());
|
||||
// upd.setResult("");
|
||||
// upd.setStatus(2);
|
||||
// aiOrderService.updateAiOrder(upd);
|
||||
// logger.warn("volcCallback code 非 200,已清空 result 并 status=2, orderId={}, {}", order.getId(), reason);
|
||||
// }
|
||||
|
||||
private AiOrder findAiOrderByVolcTaskId(String taskId) {
|
||||
AiOrder order = aiOrderService.getAiOrderByPortalVideoTask(taskId);
|
||||
if (order != null) {
|
||||
return order;
|
||||
}
|
||||
return aiOrderService.getAiOrderByResult(taskId);
|
||||
}
|
||||
// private AiOrder findAiOrderByVolcTaskId(String taskId) {
|
||||
// AiOrder order = aiOrderService.getAiOrderByPortalVideoTask(taskId);
|
||||
// if (order != null) {
|
||||
// return order;
|
||||
// }
|
||||
// return aiOrderService.getAiOrderByResult(taskId);
|
||||
// }
|
||||
|
||||
/**
|
||||
* 回调判定任务失败:订单仍持有火山任务 id、且非已失败时,标记失败并退余额(orderFailure)。
|
||||
*/
|
||||
private void handleVolcCallbackFailure(String taskId, String reason) {
|
||||
AiOrder order = aiOrderService.getAiOrderByResult(taskId);
|
||||
if (order == null) {
|
||||
logger.warn("volcCallback 失败:未找到 result={} 的订单, {}", taskId, reason);
|
||||
return;
|
||||
}
|
||||
if (!taskId.equals(order.getResult())) {
|
||||
logger.info("volcCallback 失败处理跳过:订单结果已更新, taskId={}, {}", taskId, reason);
|
||||
return;
|
||||
}
|
||||
if (Integer.valueOf(2).equals(order.getStatus())) {
|
||||
return;
|
||||
}
|
||||
aiOrderService.orderFailure(order);
|
||||
logger.warn("volcCallback 任务失败,已标记订单失败并退款, taskId={}, reason={}", taskId, reason);
|
||||
}
|
||||
|
||||
@PostMapping(value = "/{id}/cancel")
|
||||
@ApiOperation("取消视频生成任务")
|
||||
public AjaxResult cancelTask(@PathVariable("id") String id) throws Exception {
|
||||
return byteService.cancelVideoTask(id);
|
||||
}
|
||||
// private void handleVolcCallbackFailure(String taskId, String reason) {
|
||||
// AiOrder order = aiOrderService.getAiOrderByResult(taskId);
|
||||
// if (order == null) {
|
||||
// logger.warn("volcCallback 失败:未找到 result={} 的订单, {}", taskId, reason);
|
||||
// return;
|
||||
// }
|
||||
// if (!taskId.equals(order.getResult())) {
|
||||
// logger.info("volcCallback 失败处理跳过:订单结果已更新, taskId={}, {}", taskId, reason);
|
||||
// return;
|
||||
// }
|
||||
// if (Integer.valueOf(2).equals(order.getStatus())) {
|
||||
// return;
|
||||
// }
|
||||
// aiOrderService.orderFailure(order);
|
||||
// logger.warn("volcCallback 任务失败,已标记订单失败并退款, taskId={}, reason={}", taskId, reason);
|
||||
// }
|
||||
|
||||
// @PostMapping(value = "/{id}/cancel")
|
||||
// @ApiOperation("取消视频生成任务")
|
||||
// public AjaxResult cancelTask(@PathVariable("id") String id) throws Exception {
|
||||
// return byteService.cancelVideoTask(id);
|
||||
// }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -187,7 +192,9 @@ public class PortalVideoController extends BaseController {
|
|||
private void fillVideoOrderRecord(AiOrder aiOrder, PortalVideoGenRequest req, String mode, ByteBodyReq body, String functionTypeResolved) {
|
||||
aiOrder.setText(req.getText());
|
||||
aiOrder.setMode(mode);
|
||||
aiOrder.setIsBackfilled(0);
|
||||
applyOrderImages(aiOrder, req);
|
||||
aiOrder.setExtStatus(0);
|
||||
if (req.getDuration() != null) {
|
||||
aiOrder.setDuration(req.getDuration());
|
||||
} else if (body.getDuration() != null) {
|
||||
|
|
@ -256,6 +263,7 @@ public class PortalVideoController extends BaseController {
|
|||
|
||||
private AjaxResult submitOrderAndCreate(PortalVideoGenRequest req, String mode, ByteBodyReq byteBodyReq) {
|
||||
String functionType = resolveFunctionType(req);
|
||||
// 判断余额是否足够,aimanager里配置了最低限额。创建订单
|
||||
AiOrder aiOrder = aiOrderService.getAiOrder(functionType);
|
||||
if (aiOrder == null) {
|
||||
return AjaxResult.error(-1, "You have a low balance, please recharge");
|
||||
|
|
@ -266,14 +274,45 @@ public class PortalVideoController extends BaseController {
|
|||
|
||||
String key = apiKey();
|
||||
ByteBodyRes byteBodyRes = byteService.imgToVideo(byteBodyReq, key);
|
||||
String id = byteBodyRes.getId();
|
||||
if (id == null) {
|
||||
String thirdPartyOrderNumId = byteBodyRes.getId();
|
||||
if (thirdPartyOrderNumId == null) {
|
||||
aiOrderService.orderFailure(aiOrder);
|
||||
return AjaxResult.error(-2, "generation failed, balance has been refunded");
|
||||
}
|
||||
mergeVolcTaskIdIntoVideoParams(aiOrder, id);
|
||||
aiOrder.setResult(id);
|
||||
aiOrderService.orderSuccess(aiOrder);
|
||||
mergeVolcTaskIdIntoVideoParams(aiOrder, thirdPartyOrderNumId);
|
||||
aiOrder.setResult(thirdPartyOrderNumId);
|
||||
// 字节订单号与请求ID
|
||||
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);
|
||||
} catch (Exception e) {
|
||||
aiOrderService.orderFailure(aiOrder);
|
||||
|
|
@ -598,19 +637,19 @@ public class PortalVideoController extends BaseController {
|
|||
return AjaxResult.success(byteBodyRes);
|
||||
}
|
||||
|
||||
@DeleteMapping("/tasks/{taskId}")
|
||||
@ApiOperation("删除或取消视频生成任务")
|
||||
public AjaxResult deleteOrCancelTask(@PathVariable String taskId) throws Exception {
|
||||
Long uid = SecurityUtils.getAiUserId();
|
||||
AiOrder owned = aiOrderService.getAiOrderByPortalVideoTask(taskId);
|
||||
if (owned == null || !uid.equals(owned.getUserId())) {
|
||||
return AjaxResult.error("无权操作该任务");
|
||||
}
|
||||
String key = apiKey();
|
||||
AjaxResult cancelRes = byteService.cancelVideoTask(taskId, key);
|
||||
if (cancelRes.isSuccess() && owned.getStatus() != null && owned.getStatus() == 0) {
|
||||
aiOrderService.orderFailure(owned);
|
||||
}
|
||||
return cancelRes;
|
||||
}
|
||||
// @DeleteMapping("/tasks/{taskId}")
|
||||
// @ApiOperation("删除或取消视频生成任务")
|
||||
// public AjaxResult deleteOrCancelTask(@PathVariable String taskId) throws Exception {
|
||||
// Long uid = SecurityUtils.getAiUserId();
|
||||
// AiOrder owned = aiOrderService.getAiOrderByPortalVideoTask(taskId);
|
||||
// if (owned == null || !uid.equals(owned.getUserId())) {
|
||||
// return AjaxResult.error("无权操作该任务");
|
||||
// }
|
||||
// String key = apiKey();
|
||||
// AjaxResult cancelRes = byteService.cancelVideoTask(taskId, key);
|
||||
// if (cancelRes.isSuccess() && owned.getStatus() != null && owned.getStatus() == 0) {
|
||||
// aiOrderService.orderFailure(owned);
|
||||
// }
|
||||
// return cancelRes;
|
||||
// }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -242,9 +242,6 @@ volcengine:
|
|||
ak: AKLTNmYyN2VhZTcyMDcxNDNlNzg3OGVlMDVmZjRhNWQwY2M
|
||||
sk: Tm1ZeU1UTmlORFk1WmpKa05HUmpaRGcxTWpjMFpqUmpOVE01TUdJME5URQ==
|
||||
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:
|
||||
|
|
@ -281,8 +278,8 @@ portal:
|
|||
- 14
|
||||
- 15
|
||||
resolutions:
|
||||
- "480p"
|
||||
- "720p"
|
||||
- "1080p"
|
||||
|
||||
jinsha:
|
||||
url: https://api.jinshapay.xyz
|
||||
|
|
|
|||
|
|
@ -36,3 +36,8 @@ no.update.permission=您没有修改数据的权限,请联系管理员添加
|
|||
no.delete.permission=您没有删除数据的权限,请联系管理员添加权限 [{0}]
|
||||
no.export.permission=您没有导出数据的权限,请联系管理员添加权限 [{0}]
|
||||
no.view.permission=您没有查看数据的权限,请联系管理员添加权限 [{0}]
|
||||
|
||||
# video generation
|
||||
order.number.generation.failed=订单号 {0} 生成失败,请稍后重试。
|
||||
order.number.generation.submit=订单号 {0} 生成任务已提交!
|
||||
order.number.generation.successbackfill=订单号 {0} 生成成功!金额已回补!
|
||||
|
|
@ -30,4 +30,8 @@ email.verification.code.error=Verification code is incorrect, please try again.
|
|||
# User not found
|
||||
user.not.found=User not found.
|
||||
user.password.incorrect=Password is incorrect, please try again.
|
||||
|
||||
# video generation
|
||||
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!
|
||||
|
|
@ -29,4 +29,7 @@ email.verification.code.error=驗證碼錯誤,請重新輸入。
|
|||
user.not.found=用戶不存在。
|
||||
user.password.incorrect=密碼錯誤,請重新輸入。
|
||||
|
||||
# video generation
|
||||
order.number.generation.failed=訂單號 {0} 生成失敗,請稍後重試。
|
||||
order.number.generation.submit=訂單號 {0} 生成任務已提交!
|
||||
order.number.generation.successbackfill=訂單號 {0} 生成成功!金額已回補!
|
||||
|
|
@ -0,0 +1,91 @@
|
|||
package com.ruoyi.common.core.request.video.dto;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import com.ruoyi.common.core.response.video.dto.VideoGenerationUsage;
|
||||
import com.ruoyi.common.core.response.video.dto.VideoTaskContent;
|
||||
import com.ruoyi.common.core.response.video.dto.VideoTaskError;
|
||||
import com.ruoyi.common.core.response.video.dto.VideoTaskTool;
|
||||
import io.swagger.annotations.ApiModel;
|
||||
import io.swagger.annotations.ApiModelProperty;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Data
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
@ApiModel(description = "生成视频任务的回调接口")
|
||||
public class VideoTaskCallBackRequest {
|
||||
@ApiModelProperty(value = "视频生成任务 ID。")
|
||||
private String id;
|
||||
|
||||
@ApiModelProperty(value = "任务使用的模型名称和版本,模型名称-版本。")
|
||||
private String model;
|
||||
|
||||
@ApiModelProperty(value = "任务状态:queued 排队中;running 运行中;cancelled 已取消;succeeded 成功;failed 失败;expired 超时。")
|
||||
private String status;
|
||||
|
||||
@ApiModelProperty(value = "错误提示信息,任务成功时为 null。")
|
||||
private VideoTaskError error;
|
||||
|
||||
@JsonProperty("created_at")
|
||||
@ApiModelProperty(value = "任务创建时间的 Unix 时间戳(秒)。")
|
||||
private Integer createdAt;
|
||||
|
||||
@JsonProperty("updated_at")
|
||||
@ApiModelProperty(value = "任务当前状态更新时间的 Unix 时间戳(秒)。")
|
||||
private Integer updatedAt;
|
||||
|
||||
@ApiModelProperty(value = "视频生成任务的输出内容。")
|
||||
private VideoTaskContent content;
|
||||
|
||||
@ApiModelProperty(value = "本次请求使用的种子整数值。")
|
||||
private Integer seed;
|
||||
|
||||
@ApiModelProperty(value = "生成视频的分辨率。")
|
||||
private String resolution;
|
||||
|
||||
@ApiModelProperty(value = "生成视频的宽高比。")
|
||||
private String ratio;
|
||||
|
||||
@ApiModelProperty(value = "生成视频的时长,单位:秒。与 frames 只会返回其一。")
|
||||
private Integer duration;
|
||||
|
||||
@ApiModelProperty(value = "生成视频的帧数。与 duration 只会返回其一。")
|
||||
private Integer frames;
|
||||
|
||||
@JsonProperty("framespersecond")
|
||||
@ApiModelProperty(value = "生成视频的帧率。")
|
||||
private Integer framesPerSecond;
|
||||
|
||||
@JsonProperty("generate_audio")
|
||||
@ApiModelProperty(value = "生成的视频是否包含与画面同步的声音。仅 Seedance 1.5 pro 返回。")
|
||||
private Boolean generateAudio;
|
||||
|
||||
@ApiModelProperty(value = "本次请求模型实际使用的工具,未使用工具时不返回。")
|
||||
private List<VideoTaskTool> tools;
|
||||
|
||||
@JsonProperty("safety_identifier")
|
||||
@ApiModelProperty(value = "终端用户的唯一标识符,创建任务时传入则原样返回。")
|
||||
private String safetyIdentifier;
|
||||
|
||||
@ApiModelProperty(value = "是否为 Draft 视频。仅 Seedance 1.5 pro 返回。")
|
||||
private Boolean draft;
|
||||
|
||||
@JsonProperty("draft_task_id")
|
||||
@ApiModelProperty(value = "Draft 视频任务 ID,基于 Draft 生成正式视频时返回。")
|
||||
private String draftTaskId;
|
||||
|
||||
@JsonProperty("service_tier")
|
||||
@ApiModelProperty(value = "实际处理任务使用的服务等级。")
|
||||
private String serviceTier;
|
||||
|
||||
@JsonProperty("execution_expires_after")
|
||||
@ApiModelProperty(value = "任务超时阈值,单位:秒。")
|
||||
private Integer executionExpiresAfter;
|
||||
|
||||
@ApiModelProperty(value = "本次请求的 token 用量。")
|
||||
private VideoGenerationUsage usage;
|
||||
}
|
||||
|
|
@ -0,0 +1,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;
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
package com.ruoyi.common.enums;
|
||||
|
||||
/**
|
||||
* AiOrder表中status的类型
|
||||
*/
|
||||
public enum AiOrderStatusType {
|
||||
// 0-进行中
|
||||
RUNNING,
|
||||
// 1-已完成
|
||||
FINISH,
|
||||
// 2-失败(余额退回)
|
||||
FAIL;
|
||||
}
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
package com.ruoyi.common.enums;
|
||||
|
||||
import lombok.Getter;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 生成视频任务中,状态的枚举
|
||||
*/
|
||||
@Getter
|
||||
public enum VideoTaskStatusType {
|
||||
QUEUED("queued"),
|
||||
|
||||
RUNNING("running"),
|
||||
|
||||
CANCELLED("cancelled"),
|
||||
|
||||
SUCCEEDED("succeeded"),
|
||||
|
||||
FAILED("failed"),
|
||||
|
||||
EXPIRED("expired");
|
||||
|
||||
private final String name;
|
||||
|
||||
private static final Set<String> VALID_NAMES = Arrays.stream(VideoTaskStatusType.values())
|
||||
.map(status -> status.name)
|
||||
.collect(Collectors.toSet());
|
||||
/**
|
||||
* 判断名称是否正确, 不区分大小写
|
||||
*/
|
||||
public static boolean isValidName(String name) {
|
||||
if (name == null) {
|
||||
return false;
|
||||
}
|
||||
String nameLowerCase = name.toLowerCase();
|
||||
// 直接判断小写后的入参是否在静态Set中
|
||||
return VALID_NAMES.contains(nameLowerCase);
|
||||
}
|
||||
|
||||
VideoTaskStatusType(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
}
|
||||
|
|
@ -23,7 +23,7 @@ import java.util.UUID;
|
|||
/**
|
||||
* AWS S3 文件上传工具类
|
||||
*/
|
||||
@Component
|
||||
//@Component
|
||||
public class AwsS3Util {
|
||||
|
||||
// -------------------------- 配置参数(需根据实际环境修改)--------------------------
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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,25 @@ public class AiOrder extends BaseEntity {
|
|||
@Excel(name = "AI类型")
|
||||
private String type;
|
||||
|
||||
/** 预扣金额 */
|
||||
@Excel(name = "预扣金额")
|
||||
private BigDecimal preDeductAmount;
|
||||
|
||||
/** 金额 */
|
||||
@Excel(name = "金额")
|
||||
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 = "生成结果")
|
||||
private String result;
|
||||
|
|
|
|||
|
|
@ -34,6 +34,6 @@ public class ByteBodyRes {
|
|||
private Integer duration;
|
||||
private Integer framespersecond;
|
||||
private boolean draft;
|
||||
|
||||
|
||||
// 火山请求ID,在head里,需手动设置
|
||||
private String requestId;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import java.util.List;
|
|||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.ruoyi.ai.domain.AiOrder;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import org.apache.ibatis.annotations.Select;
|
||||
|
||||
/**
|
||||
* 订单管理Mapper接口
|
||||
|
|
@ -26,4 +27,7 @@ public interface AiOrderMapper extends BaseMapper<AiOrder> {
|
|||
AiOrder getAiOrderByPortalVideoTask(@Param("taskId") String taskId);
|
||||
|
||||
BigDecimal getSumAmountByUserId(@Param("userId") String userId);
|
||||
|
||||
@Select("SELECT * FROM ai_order WHERE third_party_order_num = #{id} LIMIT 1")
|
||||
AiOrder selectOneByThirdPartyOrderNum(@Param("id") String id);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,10 @@ import java.util.List;
|
|||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.ruoyi.ai.domain.AiOrder;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.ruoyi.common.core.domain.AjaxResult;
|
||||
import com.ruoyi.common.core.request.video.dto.VideoTaskCallBackRequest;
|
||||
import com.ruoyi.common.core.response.video.GetVideoGenerationTaskResponse;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
/**
|
||||
* 订单管理Service接口
|
||||
|
|
@ -71,15 +75,29 @@ public interface IAiOrderService {
|
|||
*/
|
||||
int deleteAiOrderById(Long id);
|
||||
|
||||
AiOrder getAiOrder(String aiType, boolean isReduceBalance);
|
||||
|
||||
AiOrder getAiOrder(String aiType);
|
||||
|
||||
void orderFailure(AiOrder aiOrder);
|
||||
|
||||
@Transactional
|
||||
void orderFailure(AiOrder aiOrder, BigDecimal amount);
|
||||
|
||||
void orderSuccess(AiOrder aiOrder);
|
||||
|
||||
AiOrder getAiOrderByResult(String result);
|
||||
|
||||
AiOrder getAiOrderByPortalVideoTask(String taskId);
|
||||
|
||||
int getChangerType(String aiType);
|
||||
|
||||
BigDecimal getSumAmountByUserId(String userId);
|
||||
|
||||
AiOrder selectOneByThirdPartyOrderNum(String id);
|
||||
|
||||
/**
|
||||
* 火山回调 - 任务成功时处理流程
|
||||
*/
|
||||
AjaxResult volcCallbackSuccessProcess(VideoTaskCallBackRequest request, GetVideoGenerationTaskResponse taskResp, AiOrder order);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,11 @@
|
|||
package com.ruoyi.ai.service;
|
||||
|
||||
import com.ruoyi.ai.domain.AiOrder;
|
||||
import com.ruoyi.ai.domain.ByteBodyReq;
|
||||
import com.ruoyi.ai.domain.ByteBodyRes;
|
||||
import com.ruoyi.common.core.domain.AjaxResult;
|
||||
import com.ruoyi.common.core.request.video.dto.VideoTaskCallBackRequest;
|
||||
import com.ruoyi.common.core.response.video.GetVideoGenerationTaskResponse;
|
||||
|
||||
public interface IByteService {
|
||||
|
||||
|
|
@ -44,4 +47,15 @@ public interface IByteService {
|
|||
* GET 查询视频生成任务列表(火山 list 文档),返回原始 JSON 字符串
|
||||
*/
|
||||
String listVideoGenerationTasks(int pageNum, int pageSize, String arkApiKey) throws Exception;
|
||||
|
||||
/**
|
||||
* 获取当前用户所使用的api key
|
||||
* @return
|
||||
*/
|
||||
String resolveCurrentAiUserApiKey();
|
||||
|
||||
/**
|
||||
* GET 查询视频生成任务(单个)
|
||||
*/
|
||||
GetVideoGenerationTaskResponse getVideoGenerationTasks(String thirdPartyOrderNumId, String arkApiKey) throws Exception;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,11 +15,17 @@ import com.ruoyi.ai.service.IAiStatisticsService;
|
|||
import com.ruoyi.ai.service.IAiUserService;
|
||||
import com.ruoyi.common.constant.BalanceChangerConstants;
|
||||
import com.ruoyi.common.constant.HttpStatus;
|
||||
import com.ruoyi.common.core.domain.AjaxResult;
|
||||
import com.ruoyi.common.core.domain.entity.AiUser;
|
||||
import com.ruoyi.common.core.request.video.dto.VideoTaskCallBackRequest;
|
||||
import com.ruoyi.common.core.response.video.GetVideoGenerationTaskResponse;
|
||||
import com.ruoyi.common.enums.AiOrderStatusType;
|
||||
import com.ruoyi.common.exception.ServiceException;
|
||||
import com.ruoyi.common.utils.DateUtils;
|
||||
import com.ruoyi.common.utils.MessageUtils;
|
||||
import com.ruoyi.common.utils.SecurityUtils;
|
||||
import com.ruoyi.common.utils.StringUtils;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
|
@ -36,6 +42,7 @@ import java.util.UUID;
|
|||
* @author shi
|
||||
* @date 2025-11-13
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
public class AiOrderServiceImpl implements IAiOrderService {
|
||||
|
||||
|
|
@ -51,6 +58,11 @@ public class AiOrderServiceImpl implements IAiOrderService {
|
|||
@Autowired
|
||||
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";
|
||||
|
||||
/**
|
||||
* 查询订单管理
|
||||
|
|
@ -136,15 +148,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,24 +185,49 @@ public class AiOrderServiceImpl implements IAiOrderService {
|
|||
aiOrder.setUserId(SecurityUtils.getAiUserId());
|
||||
aiOrder.setType(aiType);
|
||||
aiOrder.setResult(null);
|
||||
aiOrder.setAmount(aiManager.getPrice());
|
||||
if (isReduceBalance) {
|
||||
aiOrder.setPreDeductAmount(aiManager.getPrice());
|
||||
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) {
|
||||
String remark = MessageUtils.message(TASK_SUBMIT_REMARK, aiOrder.getOrderNum());
|
||||
aiUserService.addUserBalance(orderno, SecurityUtils.getAiUserId()
|
||||
, NumberUtil.mul(-1, aiManager.getPrice()), getChangerType(aiType), remark);
|
||||
}
|
||||
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);
|
||||
// 变更值为0则不改余额,没有流水
|
||||
if (aiOrder.getAmount() != null && !aiOrder.getAmount().equals(new BigDecimal(0))) {
|
||||
aiUserService.addUserBalance(aiOrder.getOrderNum(), userId, amount, BalanceChangerConstants.REFUND, remark);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -209,6 +255,7 @@ public class AiOrderServiceImpl implements IAiOrderService {
|
|||
return aiOrderMapper.getAiOrderByPortalVideoTask(taskId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getChangerType(String aiType) {
|
||||
switch (aiType) {
|
||||
case "11":
|
||||
|
|
@ -230,4 +277,67 @@ public class AiOrderServiceImpl implements IAiOrderService {
|
|||
public BigDecimal getSumAmountByUserId(String 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");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,11 +3,17 @@ package com.ruoyi.ai.service.impl;
|
|||
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
import com.fasterxml.jackson.databind.DeserializationFeature;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.ruoyi.ai.domain.AiOrder;
|
||||
import com.ruoyi.ai.domain.ByteBodyReq;
|
||||
import com.ruoyi.ai.domain.ByteBodyRes;
|
||||
import com.ruoyi.ai.service.IAiUserService;
|
||||
import com.ruoyi.ai.service.IByteDeptApiKeyService;
|
||||
import com.ruoyi.ai.service.IByteService;
|
||||
import com.ruoyi.common.constant.BalanceChangerConstants;
|
||||
import com.ruoyi.common.core.domain.AjaxResult;
|
||||
import com.ruoyi.common.core.request.video.dto.VideoTaskCallBackRequest;
|
||||
import com.ruoyi.common.core.response.video.GetVideoGenerationTaskResponse;
|
||||
import com.ruoyi.common.enums.AiOrderStatusType;
|
||||
import com.ruoyi.common.utils.SecurityUtils;
|
||||
import com.ruoyi.common.utils.StringUtils;
|
||||
import com.ruoyi.common.utils.http.OkHttpUtils;
|
||||
|
|
@ -17,6 +23,9 @@ import okhttp3.*;
|
|||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
|
|
@ -121,8 +130,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
|
||||
|
|
@ -231,7 +244,36 @@ public class ByteService implements IByteService {
|
|||
return body;
|
||||
}
|
||||
|
||||
private String resolveCurrentAiUserApiKey() {
|
||||
public String resolveCurrentAiUserApiKey() {
|
||||
return byteDeptApiKeyService.resolveVolcApiKey(SecurityUtils.getAiUserId());
|
||||
}
|
||||
|
||||
@Override
|
||||
public GetVideoGenerationTaskResponse getVideoGenerationTasks(String thirdPartyOrderNumId, String arkApiKey) throws Exception {
|
||||
if (StringUtils.isBlank(arkApiKey)) {
|
||||
throw new Exception("getVideoGenerationTasks error:apiKey is null");
|
||||
}
|
||||
if (StringUtils.isBlank(thirdPartyOrderNumId)) {
|
||||
throw new Exception("getVideoGenerationTasks error:thirdPartyOrderNumId is null");
|
||||
}
|
||||
|
||||
HttpUrl parsed = HttpUrl.parse(volcBaseUrl + "/api/v3/contents/generations/tasks/" + thirdPartyOrderNumId);
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,10 +12,13 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
|||
<result property="updateBy" column="update_by" />
|
||||
<result property="updateTime" column="update_time" />
|
||||
<result property="remark" column="remark" />
|
||||
<result property="thirdPartyOrderNum" column="third_party_order_num" />
|
||||
<result property="orderNum" column="order_num" />
|
||||
<result property="userId" column="user_id" />
|
||||
<result property="type" column="type" />
|
||||
<result property="preDeductAmount" column="pre_deduct_amount" />
|
||||
<result property="amount" column="amount" />
|
||||
<result property="totalUsage" column="total_usage" />
|
||||
<result property="result" column="result" />
|
||||
<result property="status" column="status" />
|
||||
<result property="source" column="source" />
|
||||
|
|
@ -29,17 +32,19 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
|||
<result property="ratio" column="ratio" />
|
||||
<result property="model" column="model" />
|
||||
<result property="videoParams" column="video_params" />
|
||||
<result property="isBackfilled" column="is_backfilled" />
|
||||
<result property="videoGenRequestId" column="video_gen_request_id" />
|
||||
</resultMap>
|
||||
|
||||
<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.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
|
||||
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
|
||||
left join ai_user au on au.id = ao.user_id
|
||||
</sql>
|
||||
|
||||
<select id="selectAiOrderList" parameterType="AiOrder" resultMap="AiOrderResult">
|
||||
<include refid="selectAiOrderVo"/>
|
||||
<where>
|
||||
<if test="orderNum != null and orderNum != ''"> and ao.orderNum like concat('%', #{orderNum}, '%')</if>
|
||||
<if test="orderNum != null and orderNum != ''"> and ao.order_num like concat('%', #{orderNum}, '%')</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="uuid != null "> and au.user_id = #{uuid}</if>
|
||||
|
|
|
|||
Loading…
Reference in New Issue