fix: 视频生成问题

This commit is contained in:
old burden 2026-04-03 12:38:46 +08:00
parent 4ea4d009a6
commit 37f3237f61
10 changed files with 221 additions and 31 deletions

View File

@ -6,3 +6,6 @@ ENV = 'production'
# 若依管理系统/生产环境 # 若依管理系统/生产环境
VUE_APP_BASE_API = 'http://111.230.37.169:10009' VUE_APP_BASE_API = 'http://111.230.37.169:10009'
# VUE_APP_BASE_API = 'http://101.96.201.225:8011'
# VUE_APP_BASE_API = 'http://47.86.170.114:8011'

View File

@ -8,5 +8,4 @@ NODE_ENV = production
# 测试环境配置 # 测试环境配置
ENV = 'staging' ENV = 'staging'
# 若依管理系统/测试环境 VUE_APP_BASE_API = 'http://101.96.201.225:8011'
VUE_APP_BASE_API = '/api'

View File

@ -67,6 +67,27 @@
</el-form> </el-form>
<el-row :gutter="10" class="mb8"> <el-row :gutter="10" class="mb8">
<el-col :span="1.5">
<el-button
type="primary"
plain
icon="el-icon-plus"
size="mini"
@click="handleAdd"
v-hasPermi="['ai:user:add']"
>新增</el-button>
</el-col>
<el-col :span="1.5">
<el-button
type="danger"
plain
icon="el-icon-delete"
size="mini"
:disabled="multiple"
@click="handleDelete()"
v-hasPermi="['ai:user:remove']"
>删除</el-button>
</el-col>
<el-col :span="1.5"> <el-col :span="1.5">
<el-button <el-button
type="warning" type="warning"
@ -114,7 +135,7 @@
</el-table-column> </el-table-column>
<el-table-column label="余额" align="center" prop="balance" /> <el-table-column label="余额" align="center" prop="balance" />
<el-table-column label="source" align="center" prop="source" /> <el-table-column label="source" align="center" prop="source" />
<el-table-column label="操作" align="center" class-name="small-padding fixed-width" width="250"> <el-table-column label="操作" align="center" class-name="small-padding fixed-width" width="310">
<template slot-scope="scope"> <template slot-scope="scope">
<el-button <el-button
size="mini" size="mini"
@ -137,6 +158,13 @@
@click="updateBalance(scope.row)" @click="updateBalance(scope.row)"
v-hasPermi="['ai:user:remove']" v-hasPermi="['ai:user:remove']"
>修改余额</el-button> >修改余额</el-button>
<el-button
size="mini"
type="text"
icon="el-icon-delete"
@click="handleDelete(scope.row)"
v-hasPermi="['ai:user:remove']"
>删除</el-button>
</template> </template>
</el-table-column> </el-table-column>
</el-table> </el-table>
@ -150,8 +178,24 @@
/> />
<!-- 添加或修改ai-用户信息对话框 --> <!-- 添加或修改ai-用户信息对话框 -->
<el-dialog :title="title" :visible.sync="open" width="500px" append-to-body> <el-dialog :title="title" :visible.sync="open" width="520px" append-to-body>
<el-form ref="form" :model="form" :rules="rules" label-width="80px"> <el-form ref="form" :model="form" :rules="rules" label-width="88px">
<el-form-item label="用户账号" prop="username">
<el-input
v-model="form.username"
placeholder="门户登录账号,必填"
:disabled="form.id != null"
/>
</el-form-item>
<el-form-item label="密码" prop="password">
<el-input
v-model="form.password"
type="password"
show-password
:placeholder="form.id != null ? '不填表示不修改密码' : '请输入登录密码'"
autocomplete="new-password"
/>
</el-form-item>
<el-form-item label="用户昵称" prop="nickname"> <el-form-item label="用户昵称" prop="nickname">
<el-input v-model="form.nickname" placeholder="请输入用户昵称" /> <el-input v-model="form.nickname" placeholder="请输入用户昵称" />
</el-form-item> </el-form-item>
@ -176,10 +220,10 @@
<el-form-item label="余额" prop="balance"> <el-form-item label="余额" prop="balance">
<el-input v-model="form.balance" placeholder="请输入余额" /> <el-input v-model="form.balance" placeholder="请输入余额" />
</el-form-item> </el-form-item>
<el-form-item label="备注" prop="remark">
<el-input v-model="form.remark" type="textarea" placeholder="请输入内容" />
</el-form-item>
</el-form> </el-form>
<el-form-item label="备注" prop="remark">
<el-input v-model="form.remark" type="textarea" placeholder="请输入内容" />
</el-form-item>
<div slot="footer" class="dialog-footer"> <div slot="footer" class="dialog-footer">
<el-button type="primary" @click="submitForm"> </el-button> <el-button type="primary" @click="submitForm"> </el-button>
<el-button @click="cancel"> </el-button> <el-button @click="cancel"> </el-button>
@ -307,11 +351,18 @@ export default {
form: {}, form: {},
// //
rules: { rules: {
delFlag: [ username: [{ required: true, message: "用户账号不能为空", trigger: "blur" }],
{ required: true, message: "删除标志不能为空", trigger: "blur" } password: [
], {
phone: [ validator: (rule, value, callback) => {
{ required: true, message: "手机号码不能为空", trigger: "blur" } if (this.form.id == null && (!value || !String(value).trim())) {
callback(new Error("密码不能为空"));
} else {
callback();
}
},
trigger: "blur"
}
] ]
} }
}; };
@ -442,6 +493,7 @@ export default {
updateBy: null, updateBy: null,
updateTime: null, updateTime: null,
remark: null, remark: null,
username: null,
nickname: null, nickname: null,
gender: null, gender: null,
avatar: null, avatar: null,
@ -496,13 +548,17 @@ export default {
this.$refs["form"].validate(valid => { this.$refs["form"].validate(valid => {
if (valid) { if (valid) {
if (this.form.id != null) { if (this.form.id != null) {
updateUser(this.form).then(response => { const payload = { ...this.form };
if (!payload.password || !String(payload.password).trim()) {
delete payload.password;
}
updateUser(payload).then(() => {
this.$modal.msgSuccess("修改成功"); this.$modal.msgSuccess("修改成功");
this.open = false; this.open = false;
this.getList(); this.getList();
}); });
} else { } else {
addUser(this.form).then(response => { addUser(this.form).then(() => {
this.$modal.msgSuccess("新增成功"); this.$modal.msgSuccess("新增成功");
this.open = false; this.open = false;
this.getList(); this.getList();
@ -523,11 +579,16 @@ export default {
}, },
/** 删除按钮操作 */ /** 删除按钮操作 */
handleDelete(row) { handleDelete(row) {
const ids = row.id || this.ids; const ids = row && row.id != null ? row.id : this.ids;
if (ids == null || (Array.isArray(ids) && !ids.length)) {
this.$modal.msgWarning("请选择要删除的数据");
return;
}
const idParam = Array.isArray(ids) ? ids.join(",") : ids;
this.$modal this.$modal
.confirm('是否确认删除ai-用户信息编号为"' + ids + '"的数据项?') .confirm('是否确认删除ai-用户信息编号为"' + idParam + '"的数据项?')
.then(function() { .then(() => {
return delUser(ids); return delUser(idParam);
}) })
.then(() => { .then(() => {
this.getList(); this.getList();

View File

@ -581,10 +581,14 @@ export default {
}, },
taskStatusText(row) { taskStatusText(row) {
if (row.status === 1 && row.result && this.isHttpOrHttpsUrl(row.result)) return '已完成' // 0 2 1+http(s) 1 http
if (row.status === 1) return '执行任务中'
if (row.status === 2) return '已失败/已取消' if (row.status === 2) return '已失败/已取消'
return '进行中' if (row.status === 0) return '执行中'
if (row.status === 1) {
if (row.result && this.isHttpOrHttpsUrl(row.result)) return '已完成'
return '任务生成失败'
}
return '执行中'
}, },
/** 是否展示完整「结果区」(视频 / 链接);其余状态仅紧凑展示在用户行右侧 */ /** 是否展示完整「结果区」(视频 / 链接);其余状态仅紧凑展示在用户行右侧 */
@ -594,6 +598,7 @@ export default {
chatRowInlineStatusClass(row) { chatRowInlineStatusClass(row) {
if (row.status === 2) return 'vg-chat-inline-status--failed' if (row.status === 2) return 'vg-chat-inline-status--failed'
if (row.status === 1 && !this.isHttpOrHttpsUrl(row.result)) return 'vg-chat-inline-status--failed'
return 'vg-chat-inline-status--running' return 'vg-chat-inline-status--running'
}, },

View File

@ -327,30 +327,105 @@ public class ByteApiController extends BaseController {
return AjaxResult.success(byteBodyRes); return AjaxResult.success(byteBodyRes);
} }
@GetMapping(value = "/volcCallback") @PostMapping(value = "/volcCallback")
@ApiOperation("火山引擎视频回调") @ApiOperation("火山引擎视频回调")
@Anonymous @Anonymous
public AjaxResult volcCallback(@RequestBody ByteBodyRes byteBodyRes) throws Exception { public AjaxResult volcCallback(@RequestBody ByteBodyRes byteBodyRes) throws Exception {
if ("succeeded".equals(byteBodyRes.getStatus())) { logger.info("volcCallback 收到回调数据: {}", byteBodyRes);
String id = byteBodyRes.getId(); String id = byteBodyRes.getId();
if (StringUtils.isEmpty(id)) {
logger.warn("volcCallback 无任务 id跳过业务处理");
return AjaxResult.success("callback success");
}
Integer code = byteBodyRes.getCode();
boolean codeError = code != null && code != 200;
String st = byteBodyRes.getStatus();
if ("running".equals(st) && !codeError) {
return AjaxResult.success("callback success");
}
if (codeError) {
markVolcCallbackOrderClearResultFailed(id, "code=" + code);
return AjaxResult.success("callback success");
}
if ("succeeded".equals(st)) {
content contentObj = byteBodyRes.getContent(); content contentObj = byteBodyRes.getContent();
if (contentObj != null && StringUtils.isNotEmpty(contentObj.getVideo_url())) { if (contentObj != null && StringUtils.isNotEmpty(contentObj.getVideo_url())) {
String videoUrl = contentObj.getVideo_url(); String videoUrl = contentObj.getVideo_url();
videoUrl = tencentCosUtil.uploadFileByUrl(videoUrl); videoUrl = tencentCosUtil.uploadFileByUrl(videoUrl);
contentObj.setVideo_url(videoUrl); contentObj.setVideo_url(videoUrl);
AiOrder aiOrderByResult = aiOrderService.getAiOrderByResult(id); AiOrder aiOrderByResult = findAiOrderByVolcTaskId(id);
if (aiOrderByResult != null) { if (aiOrderByResult != null) {
AiOrder aiOrder = new AiOrder(); AiOrder aiOrder = new AiOrder();
aiOrder.setId(aiOrderByResult.getId()); aiOrder.setId(aiOrderByResult.getId());
aiOrder.setResult(videoUrl); aiOrder.setResult(videoUrl);
aiOrder.setStatus(1);
aiOrderService.updateAiOrder(aiOrder); aiOrderService.updateAiOrder(aiOrder);
} }
} else {
handleVolcCallbackFailure(id, "succeeded但缺少video_url");
} }
return AjaxResult.success("callback success");
}
// failedcanceled 等终态 status 与成功/进行中均不一致
if (StringUtils.isNotEmpty(st) || code != null) {
handleVolcCallbackFailure(id, "status=" + st);
} else {
logger.warn("volcCallback 未携带可判定的 status/codeid={}", id);
} }
return AjaxResult.success("callback success"); return AjaxResult.success("callback success");
} }
/**
* 回调体 code 200HTTP/业务状态码清空 resultstatus=2不退款
*/
private void markVolcCallbackOrderClearResultFailed(String taskId, String reason) {
AiOrder order = findAiOrderByVolcTaskId(taskId);
if (order == null) {
logger.warn("volcCallback code 非 200未找到任务对应订单, taskId={}, {}", taskId, reason);
return;
}
AiOrder upd = new AiOrder();
upd.setId(order.getId());
upd.setResult("");
upd.setStatus(2);
aiOrderService.updateAiOrder(upd);
logger.warn("volcCallback code 非 200已清空 result 并 status=2, orderId={}, {}", order.getId(), reason);
}
private 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") @PostMapping(value = "/{id}/cancel")
@ApiOperation("取消视频生成任务") @ApiOperation("取消视频生成任务")
public AjaxResult cancelTask(@PathVariable("id") String id) throws Exception { public AjaxResult cancelTask(@PathVariable("id") String id) throws Exception {

View File

@ -484,7 +484,24 @@ public class PortalVideoController extends BaseController {
return AjaxResult.error("无权查看该任务"); return AjaxResult.error("无权查看该任务");
} }
String key = apiKey(); String key = apiKey();
ByteBodyRes byteBodyRes = byteService.uploadVideo(taskId, key); ByteBodyRes byteBodyRes;
try {
byteBodyRes = byteService.uploadVideo(taskId, key);
} catch (Exception e) {
String msg = e.getMessage();
// ByteService HTTP 2xx 时抛出以此前缀开头的异常仅此种情况清空订单避免网络抖动等误伤
if (msg != null && msg.startsWith("uploadVideo error")) {
logger.warn("查询火山任务 HTTP 非成功,已清空订单 result 并标记失败, orderId={}, taskId={}",
owned.getId(), taskId, e);
AiOrder failedOrder = new AiOrder();
failedOrder.setId(owned.getId());
failedOrder.setResult("");
failedOrder.setStatus(2);
aiOrderService.updateAiOrder(failedOrder);
return AjaxResult.error(msg);
}
throw e;
}
if ("succeeded".equals(byteBodyRes.getStatus())) { if ("succeeded".equals(byteBodyRes.getStatus())) {
content contentObj = byteBodyRes.getContent(); content contentObj = byteBodyRes.getContent();
if (contentObj != null && StringUtils.isNotEmpty(contentObj.getVideo_url())) { if (contentObj != null && StringUtils.isNotEmpty(contentObj.getVideo_url())) {

View File

@ -8,7 +8,7 @@ import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName; import com.baomidou.mybatisplus.annotation.TableName;
import com.fasterxml.jackson.annotation.JsonFormat; import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data; import lombok.Data;
import org.apache.commons.lang3.builder.ToStringBuilder; import org.apache.commons.lang3.builder.ToStringBuilder;
import org.apache.commons.lang3.builder.ToStringStyle; import org.apache.commons.lang3.builder.ToStringStyle;
@ -54,8 +54,8 @@ public class AiUser extends BaseEntity {
@Excel(name = "手机号码") @Excel(name = "手机号码")
private String phone; private String phone;
/** 密码 */ /** 密码(仅接受入参反序列化,响应 Json 不输出,避免与 @JsonIgnore 导致入参丢失) */
@JsonIgnore @JsonProperty(access = JsonProperty.Access.WRITE_ONLY)
@Excel(name = "密码") @Excel(name = "密码")
private String password; private String password;

View File

@ -12,6 +12,11 @@ public class ByteBodyRes {
private ByteUsageRes usage; private ByteUsageRes usage;
private String id; private String id;
/**
* 部分错误响应携带的业务/HTTP类状态码 200 视为失败 status 同时判定时以失败告终
*/
private Integer code;
/** /**
* running生成中 succeeded已完成 * running生成中 succeeded已完成
*/ */

View File

@ -180,7 +180,8 @@ public class AiOrderServiceImpl implements IAiOrderService {
String remark = MessageUtils.message("order.number.generation.failed", aiOrder.getOrderNum()); String remark = MessageUtils.message("order.number.generation.failed", aiOrder.getOrderNum());
aiOrder.setRemark(remark); aiOrder.setRemark(remark);
aiOrderMapper.updateById(aiOrder); aiOrderMapper.updateById(aiOrder);
aiUserService.addUserBalance(aiOrder.getOrderNum(), SecurityUtils.getAiUserId(), aiOrder.getAmount(), BalanceChangerConstants.REFUND, remark); Long userId = aiOrder.getUserId() != null ? aiOrder.getUserId() : SecurityUtils.getAiUserId();
aiUserService.addUserBalance(aiOrder.getOrderNum(), userId, aiOrder.getAmount(), BalanceChangerConstants.REFUND, remark);
} }
@Override @Override

View File

@ -114,6 +114,25 @@ public class AiUserServiceImpl implements IAiUserService {
*/ */
@Override @Override
public int insertAiUser(AiUser aiUser) { public int insertAiUser(AiUser aiUser) {
if (StringUtils.isEmpty(aiUser.getUsername())) {
throw new ServiceException("用户账号不能为空", HttpStatus.BAD_REQUEST);
}
if (StringUtils.isEmpty(aiUser.getPassword())) {
throw new ServiceException("密码不能为空", HttpStatus.BAD_REQUEST);
}
if (getUserCountByUserName(aiUser.getUsername()) > 0) {
throw new ServiceException(MessageUtils.message("user.username.exists", aiUser.getUsername()), HttpStatus.CONFLICT);
}
aiUser.setPassword(SecurityUtils.encryptPassword(aiUser.getPassword()));
if (StringUtils.isEmpty(aiUser.getUserId())) {
aiUser.setUserId(generateUuiD());
}
if (StringUtils.isEmpty(aiUser.getInvitationCode())) {
aiUser.setInvitationCode(generateUniqueString());
}
if (aiUser.getStatus() == null) {
aiUser.setStatus(0);
}
aiUser.setDelFlag("0"); aiUser.setDelFlag("0");
aiUser.setCreateBy(SecurityUtils.getUsername()); aiUser.setCreateBy(SecurityUtils.getUsername());
aiUser.setCreateTime(DateUtils.getNowDate()); aiUser.setCreateTime(DateUtils.getNowDate());
@ -129,6 +148,11 @@ public class AiUserServiceImpl implements IAiUserService {
@Override @Override
public int updateAiUser(AiUser aiUser) { public int updateAiUser(AiUser aiUser) {
aiUser.setUpdateTime(DateUtils.getNowDate()); aiUser.setUpdateTime(DateUtils.getNowDate());
if (StringUtils.isNotEmpty(aiUser.getPassword())) {
aiUser.setPassword(SecurityUtils.encryptPassword(aiUser.getPassword()));
} else {
aiUser.setPassword(null);
}
return aiUserMapper.updateById(aiUser); return aiUserMapper.updateById(aiUser);
} }