Compare commits

...

2 Commits

8 changed files with 165 additions and 5 deletions

View File

@ -123,7 +123,21 @@
@click="playVideo(scope.row.result)" @click="playVideo(scope.row.result)"
/> />
</template> </template>
<!-- 非链接内容 --> <!-- 视频任务失败等场景result VideoTaskError JSON -->
<template v-else-if="parseVolcTaskErrorJson(scope.row.result)">
<div class="order-result-error">
<div
v-if="parseVolcTaskErrorJson(scope.row.result).code"
class="order-result-error-line"
>
编号{{ volcFailureCodeWithHint(parseVolcTaskErrorJson(scope.row.result).code) }}
</div>
<div class="order-result-error-line">
信息{{ parseVolcTaskErrorJson(scope.row.result).message || '—' }}
</div>
</div>
</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>
@ -309,6 +323,39 @@ export default {
const value = String(str || "").trim(); const value = String(str || "").trim();
return /^https?:\/\//i.test(value); return /^https?:\/\//i.test(value);
}, },
/** 已知火山错误码后附中文说明(括号) */
volcFailureCodeWithHint(code) {
const c = String(code || "").trim();
if (!c) return "";
const hints = {
OutputVideoSensitiveContentDetected: "输出视频可能包含敏感信息",
InvalidParameter: "请求参数无效"
};
const hint = hints[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;
},
// portal-ui GeneratedAssets // portal-ui GeneratedAssets
isImage(url) { isImage(url) {
const value = String(url || "").trim(); const value = String(url || "").trim();
@ -333,6 +380,7 @@ 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;
@ -509,6 +557,19 @@ export default {
height: auto; height: auto;
} }
.order-result-error {
text-align: left;
max-width: 280px;
margin: 0 auto;
font-size: 12px;
line-height: 1.45;
color: #c45656;
}
.order-result-error-line {
word-break: break-word;
}
.text-ellipsis-two-lines { .text-ellipsis-two-lines {
/* 必须设置宽度(继承表格列宽,也可手动指定) */ /* 必须设置宽度(继承表格列宽,也可手动指定) */
width: 100%; width: 100%;

View File

@ -126,6 +126,19 @@
{{ taskStatusText(row) }} {{ taskStatusText(row) }}
</span> </span>
</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>
@ -573,6 +586,51 @@ 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
@ -1969,11 +2027,26 @@ 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;

View File

@ -8,8 +8,10 @@ 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.request.video.dto.VideoTaskCallBackRequest;
import com.ruoyi.common.core.response.video.GetVideoGenerationTaskResponse; 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.AiOrderStatusType;
import com.ruoyi.common.enums.VideoTaskStatusType; 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.StringUtils; import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.common.utils.TencentCosUtil; import com.ruoyi.common.utils.TencentCosUtil;
@ -398,6 +400,9 @@ public class ByteApiController extends BaseController {
return aiOrderService.volcCallbackSuccessProcess(request, taskResp, order); return aiOrderService.volcCallbackSuccessProcess(request, taskResp, order);
} else { } else {
// 前面已判断过status的合法性并处理了三种非失败的状态所以可以确定是取消失败超时 // 前面已判断过status的合法性并处理了三种非失败的状态所以可以确定是取消失败超时
if (taskResp.getError() != null) {
order.setResult(JsonUtils.toJson(taskResp.getError()));
}
aiOrderService.orderFailure(order); aiOrderService.orderFailure(order);
return AjaxResult.success(); return AjaxResult.success();
} }

View File

@ -36,3 +36,8 @@ 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

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

View File

@ -56,6 +56,10 @@ public class AiOrder extends BaseEntity {
@Excel(name = "金额") @Excel(name = "金额")
private BigDecimal amount; private BigDecimal amount;
/** 模型Tokens用量 */
@Excel(name = "模型Tokens用量")
private BigDecimal totalUsage;
@Excel(name = "是否回补处理过: 0-否 1-是") @Excel(name = "是否回补处理过: 0-否 1-是")
private Integer isBackfilled; private Integer isBackfilled;

View File

@ -197,8 +197,9 @@ public class AiOrderServiceImpl implements IAiOrderService {
aiOrderMapper.insert(aiOrder); aiOrderMapper.insert(aiOrder);
// 执行余额变更 // 执行余额变更
if (isReduceBalance) { if (isReduceBalance) {
String remark = MessageUtils.message(TASK_SUBMIT_REMARK, aiOrder.getOrderNum());
aiUserService.addUserBalance(orderno, SecurityUtils.getAiUserId() aiUserService.addUserBalance(orderno, SecurityUtils.getAiUserId()
, NumberUtil.mul(-1, aiManager.getPrice()), getChangerType(aiType), TASK_SUBMIT_REMARK); , NumberUtil.mul(-1, aiManager.getPrice()), getChangerType(aiType), remark);
} }
return aiOrder; return aiOrder;
} }
@ -312,6 +313,8 @@ public class AiOrderServiceImpl implements IAiOrderService {
} }
// 设置用量 // 设置用量
order.setAmount(realAmount); order.setAmount(realAmount);
// tokens用量
order.setTotalUsage(realAmount);
// 订单状态 // 订单状态
order.setStatus(AiOrderStatusType.FINISH.ordinal()); order.setStatus(AiOrderStatusType.FINISH.ordinal());
// 已回补 // 已回补
@ -325,8 +328,9 @@ public class AiOrderServiceImpl implements IAiOrderService {
BigDecimal addAmount = order.getPreDeductAmount().subtract(realAmount); BigDecimal addAmount = order.getPreDeductAmount().subtract(realAmount);
if (addAmount.compareTo(new BigDecimal(0)) != 0) { 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, aiUserService.addUserBalance(order.getOrderNum(), order.getUserId(), addAmount,
BalanceChangerConstants.REFUND, TASK_SUCCESS_BACK_FILL_REMARK); BalanceChangerConstants.REFUND, remark);
} }
} }

View File

@ -12,10 +12,13 @@ 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" />
@ -29,10 +32,12 @@ 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.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 left join ai_user au on au.id = ao.user_id
</sql> </sql>