Compare commits
2 Commits
f1e09eac1a
...
b9e08938e0
| Author | SHA1 | Date |
|---|---|---|
|
|
b9e08938e0 | |
|
|
85dc61b07d |
|
|
@ -123,7 +123,21 @@
|
|||
@click="playVideo(scope.row.result)"
|
||||
/>
|
||||
</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>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
|
@ -309,6 +323,39 @@ export default {
|
|||
const value = String(str || "").trim();
|
||||
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 一致)
|
||||
isImage(url) {
|
||||
const value = String(url || "").trim();
|
||||
|
|
@ -333,6 +380,7 @@ export default {
|
|||
},
|
||||
// 非链接内容点击事件
|
||||
async handleOtherEvent(row) {
|
||||
if (this.parseVolcTaskErrorJson(row.result)) return;
|
||||
// 防止重复点击
|
||||
if (row.isDownloading) return;
|
||||
const originalResult = row.result;
|
||||
|
|
@ -509,6 +557,19 @@ export default {
|
|||
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 {
|
||||
/* 必须设置宽度(继承表格列宽,也可手动指定) */
|
||||
width: 100%;
|
||||
|
|
|
|||
|
|
@ -126,6 +126,19 @@
|
|||
{{ taskStatusText(row) }}
|
||||
</span>
|
||||
</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>
|
||||
|
||||
|
|
@ -573,6 +586,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
|
||||
|
|
@ -1969,11 +2027,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;
|
||||
|
|
|
|||
|
|
@ -8,8 +8,10 @@ import com.ruoyi.common.core.controller.BaseController;
|
|||
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.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.StringUtils;
|
||||
import com.ruoyi.common.utils.TencentCosUtil;
|
||||
|
|
@ -398,6 +400,9 @@ public class ByteApiController extends BaseController {
|
|||
return aiOrderService.volcCallbackSuccessProcess(request, taskResp, order);
|
||||
} else {
|
||||
// 前面已判断过status的合法性,并处理了三种非失败的状态,所以可以确定是取消、失败、超时
|
||||
if (taskResp.getError() != null) {
|
||||
order.setResult(JsonUtils.toJson(taskResp.getError()));
|
||||
}
|
||||
aiOrderService.orderFailure(order);
|
||||
return AjaxResult.success();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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} 生成成功!金额已回补!
|
||||
|
|
@ -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} 生成成功!金額已回補!
|
||||
|
|
@ -56,6 +56,10 @@ public class AiOrder extends BaseEntity {
|
|||
@Excel(name = "金额")
|
||||
private BigDecimal amount;
|
||||
|
||||
/** 模型Tokens用量 */
|
||||
@Excel(name = "模型Tokens用量")
|
||||
private BigDecimal totalUsage;
|
||||
|
||||
@Excel(name = "是否回补处理过: 0-否 1-是")
|
||||
private Integer isBackfilled;
|
||||
|
||||
|
|
|
|||
|
|
@ -197,8 +197,9 @@ public class AiOrderServiceImpl implements IAiOrderService {
|
|||
aiOrderMapper.insert(aiOrder);
|
||||
// 执行余额变更
|
||||
if (isReduceBalance) {
|
||||
String remark = MessageUtils.message(TASK_SUBMIT_REMARK, aiOrder.getOrderNum());
|
||||
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;
|
||||
}
|
||||
|
|
@ -312,6 +313,8 @@ public class AiOrderServiceImpl implements IAiOrderService {
|
|||
}
|
||||
// 设置用量
|
||||
order.setAmount(realAmount);
|
||||
// tokens用量
|
||||
order.setTotalUsage(realAmount);
|
||||
// 订单状态
|
||||
order.setStatus(AiOrderStatusType.FINISH.ordinal());
|
||||
// 已回补
|
||||
|
|
@ -325,8 +328,9 @@ public class AiOrderServiceImpl implements IAiOrderService {
|
|||
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, TASK_SUCCESS_BACK_FILL_REMARK);
|
||||
BalanceChangerConstants.REFUND, remark);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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,10 +32,12 @@ 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>
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue