feat: 图片压缩后上传COS + 修复订单LazyInitializationException + 添加调试日志
This commit is contained in:
@@ -6,7 +6,7 @@ export const cleanupApi = {
|
|||||||
// 获取清理统计信息
|
// 获取清理统计信息
|
||||||
getCleanupStats() {
|
getCleanupStats() {
|
||||||
return request({
|
return request({
|
||||||
url: '/api/cleanup/cleanup-stats',
|
url: '/cleanup/cleanup-stats',
|
||||||
method: 'GET'
|
method: 'GET'
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
@@ -14,7 +14,7 @@ export const cleanupApi = {
|
|||||||
// 执行完整清理
|
// 执行完整清理
|
||||||
performFullCleanup() {
|
performFullCleanup() {
|
||||||
return request({
|
return request({
|
||||||
url: '/api/cleanup/full-cleanup',
|
url: '/cleanup/full-cleanup',
|
||||||
method: 'POST'
|
method: 'POST'
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
@@ -22,7 +22,7 @@ export const cleanupApi = {
|
|||||||
// 清理指定用户任务
|
// 清理指定用户任务
|
||||||
cleanupUserTasks(username) {
|
cleanupUserTasks(username) {
|
||||||
return request({
|
return request({
|
||||||
url: `/api/cleanup/user-tasks/${username}`,
|
url: `/cleanup/user-tasks/${username}`,
|
||||||
method: 'POST'
|
method: 'POST'
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -65,3 +65,13 @@ export const getPaymentStats = () => {
|
|||||||
export const getUserSubscriptionInfo = () => {
|
export const getUserSubscriptionInfo = () => {
|
||||||
return api.get('/payments/subscription/info')
|
return api.get('/payments/subscription/info')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 删除单个支付记录
|
||||||
|
export const deletePayment = (id) => {
|
||||||
|
return api.delete(`/payments/${id}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 批量删除支付记录
|
||||||
|
export const deletePayments = (paymentIds) => {
|
||||||
|
return api.delete('/payments/batch', { data: paymentIds })
|
||||||
|
}
|
||||||
|
|||||||
@@ -629,7 +629,11 @@ export default {
|
|||||||
alipay: 'Alipay',
|
alipay: 'Alipay',
|
||||||
wechat: 'WeChat Pay',
|
wechat: 'WeChat Pay',
|
||||||
paypal: 'PayPal',
|
paypal: 'PayPal',
|
||||||
selected: '{count} selected'
|
selected: '{count} selected',
|
||||||
|
orderDetail: 'Order Detail',
|
||||||
|
basicInfo: 'Basic Info',
|
||||||
|
orderType: 'Order Type',
|
||||||
|
paymentInfo: 'Payment Info'
|
||||||
},
|
},
|
||||||
|
|
||||||
tasks: {
|
tasks: {
|
||||||
@@ -648,7 +652,8 @@ export default {
|
|||||||
cancelled: 'Cancelled',
|
cancelled: 'Cancelled',
|
||||||
textToVideo: 'Text to Video',
|
textToVideo: 'Text to Video',
|
||||||
imageToVideo: 'Image to Video',
|
imageToVideo: 'Image to Video',
|
||||||
storyboardVideo: 'Storyboard Video'
|
storyboardVideo: 'Storyboard Video',
|
||||||
|
taskDetail: 'Task Detail'
|
||||||
},
|
},
|
||||||
|
|
||||||
members: {
|
members: {
|
||||||
@@ -740,6 +745,12 @@ export default {
|
|||||||
promptOptimizationApiUrl: 'API Endpoint',
|
promptOptimizationApiUrl: 'API Endpoint',
|
||||||
promptOptimizationApiUrlTip: 'Enter the API endpoint for prompt optimization, e.g., https://api.openai.com',
|
promptOptimizationApiUrlTip: 'Enter the API endpoint for prompt optimization, e.g., https://api.openai.com',
|
||||||
promptOptimizationModel: 'Model Name',
|
promptOptimizationModel: 'Model Name',
|
||||||
promptOptimizationModelTip: 'Enter the model name for prompt optimization, e.g., gpt-4o, gemini-pro'
|
promptOptimizationModelTip: 'Enter the model name for prompt optimization, e.g., gpt-4o, gemini-pro',
|
||||||
|
storyboardSystemPrompt: 'Storyboard System Prompt',
|
||||||
|
storyboardSystemPromptTip: 'This prompt will be prepended to user prompts for consistent storyboard generation style',
|
||||||
|
storyboardSystemPromptPlaceholder: 'E.g., high quality cinematic shot, professional photography, film tone...',
|
||||||
|
promptOptimizationSystemPrompt: 'Prompt Optimization System Instruction',
|
||||||
|
promptOptimizationSystemPromptTip: 'Custom system instruction for AI prompt optimization. Leave empty to use default. This instruction determines how AI understands and optimizes user prompts',
|
||||||
|
promptOptimizationSystemPromptPlaceholder: 'E.g., You are a professional AI prompt optimization expert, transform user descriptions into detailed, professional English prompts...'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -642,7 +642,11 @@ export default {
|
|||||||
alipay: '支付宝',
|
alipay: '支付宝',
|
||||||
wechat: '微信支付',
|
wechat: '微信支付',
|
||||||
paypal: 'PayPal',
|
paypal: 'PayPal',
|
||||||
selected: '已选择{count}项'
|
selected: '已选择{count}项',
|
||||||
|
orderDetail: '订单详情',
|
||||||
|
basicInfo: '基本信息',
|
||||||
|
orderType: '订单类型',
|
||||||
|
paymentInfo: '支付信息'
|
||||||
},
|
},
|
||||||
|
|
||||||
tasks: {
|
tasks: {
|
||||||
@@ -661,7 +665,8 @@ export default {
|
|||||||
cancelled: '已取消',
|
cancelled: '已取消',
|
||||||
textToVideo: '文生视频',
|
textToVideo: '文生视频',
|
||||||
imageToVideo: '图生视频',
|
imageToVideo: '图生视频',
|
||||||
storyboardVideo: '分镜视频'
|
storyboardVideo: '分镜视频',
|
||||||
|
taskDetail: '任务详情'
|
||||||
},
|
},
|
||||||
|
|
||||||
members: {
|
members: {
|
||||||
@@ -753,6 +758,12 @@ export default {
|
|||||||
promptOptimizationApiUrl: 'API端点',
|
promptOptimizationApiUrl: 'API端点',
|
||||||
promptOptimizationApiUrlTip: '输入用于优化提示词的API端点地址,如 https://api.openai.com',
|
promptOptimizationApiUrlTip: '输入用于优化提示词的API端点地址,如 https://api.openai.com',
|
||||||
promptOptimizationModel: '模型名称',
|
promptOptimizationModel: '模型名称',
|
||||||
promptOptimizationModelTip: '输入用于优化提示词的模型名称,如 gpt-4o、gemini-pro 等'
|
promptOptimizationModelTip: '输入用于优化提示词的模型名称,如 gpt-4o、gemini-pro 等',
|
||||||
|
storyboardSystemPrompt: '分镜图系统引导词',
|
||||||
|
storyboardSystemPromptTip: '此引导词会自动添加到用户提示词前面,用于统一分镜图生成风格',
|
||||||
|
storyboardSystemPromptPlaceholder: '例如:高质量电影级画面,专业摄影,电影色调...',
|
||||||
|
promptOptimizationSystemPrompt: '优化提示词系统指令',
|
||||||
|
promptOptimizationSystemPromptTip: '自定义AI优化提示词的系统指令,留空则使用默认指令。该指令决定了AI如何理解和优化用户输入的提示词',
|
||||||
|
promptOptimizationSystemPromptPlaceholder: '例如:你是一个专业的AI提示词优化专家,将用户描述优化为详细、专业的英文提示词...'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -102,13 +102,10 @@
|
|||||||
<el-option :label="$t('orders.cancelled')" value="CANCELLED" />
|
<el-option :label="$t('orders.cancelled')" value="CANCELLED" />
|
||||||
<el-option :label="$t('orders.refunded')" value="REFUNDED" />
|
<el-option :label="$t('orders.refunded')" value="REFUNDED" />
|
||||||
</el-select>
|
</el-select>
|
||||||
<el-select v-model="filters.type" :placeholder="$t('orders.allTypes')" size="small" @change="handleFilterChange">
|
<el-select v-model="filters.paymentMethod" :placeholder="$t('orders.allPaymentMethods') || '全部支付方式'" size="small" @change="handleFilterChange">
|
||||||
<el-option :label="$t('orders.allTypes')" value="" />
|
<el-option :label="$t('orders.allPaymentMethods') || '全部支付方式'" value="" />
|
||||||
<el-option label="商品订单" value="PRODUCT" />
|
<el-option :label="$t('orders.alipay') || '支付宝'" value="ALIPAY" />
|
||||||
<el-option label="服务订单" value="SERVICE" />
|
<el-option :label="$t('orders.paypal') || 'PayPal'" value="PAYPAL" />
|
||||||
<el-option label="订阅订单" value="SUBSCRIPTION" />
|
|
||||||
<el-option label="数字商品" value="DIGITAL" />
|
|
||||||
<el-option label="实体商品" value="PHYSICAL" />
|
|
||||||
</el-select>
|
</el-select>
|
||||||
</div>
|
</div>
|
||||||
<div class="toolbar-right">
|
<div class="toolbar-right">
|
||||||
@@ -200,6 +197,92 @@
|
|||||||
</section>
|
</section>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
|
<!-- 订单详情弹窗 -->
|
||||||
|
<el-dialog
|
||||||
|
v-model="orderDetailVisible"
|
||||||
|
:title="$t('orders.orderDetail') || '订单详情'"
|
||||||
|
width="600px"
|
||||||
|
class="order-detail-dialog"
|
||||||
|
>
|
||||||
|
<div v-if="currentOrderDetail" class="order-detail-content">
|
||||||
|
<div class="detail-section">
|
||||||
|
<h4>{{ $t('orders.basicInfo') || '基本信息' }}</h4>
|
||||||
|
<div class="detail-grid">
|
||||||
|
<div class="detail-item">
|
||||||
|
<span class="label">{{ $t('orders.orderNumber') }}:</span>
|
||||||
|
<span class="value">{{ currentOrderDetail.orderNumber || currentOrderDetail.id }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="detail-item">
|
||||||
|
<span class="label">{{ $t('orders.username') }}:</span>
|
||||||
|
<span class="value">{{ currentOrderDetail.user?.username || '-' }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="detail-item">
|
||||||
|
<span class="label">{{ $t('orders.orderType') || '订单类型' }}:</span>
|
||||||
|
<span class="value">{{ getOrderTypeText(currentOrderDetail.orderType) }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="detail-item">
|
||||||
|
<span class="label">{{ $t('orders.status') }}:</span>
|
||||||
|
<span class="status-tag" :class="getStatusClass(currentOrderDetail.status)">
|
||||||
|
{{ getStatusText(currentOrderDetail.status) }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="detail-section">
|
||||||
|
<h4>{{ $t('orders.paymentInfo') || '支付信息' }}</h4>
|
||||||
|
<div class="detail-grid">
|
||||||
|
<div class="detail-item">
|
||||||
|
<span class="label">{{ $t('orders.amount') }}:</span>
|
||||||
|
<span class="value amount">{{ currentOrderDetail.currency || '¥' }}{{ currentOrderDetail.totalAmount || 0 }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="detail-item">
|
||||||
|
<span class="label">{{ $t('orders.paymentMethod') }}:</span>
|
||||||
|
<span class="value">
|
||||||
|
<template v-if="currentOrderDetail.paymentMethod === 'ALIPAY'">{{ $t('orders.alipay') }}</template>
|
||||||
|
<template v-else-if="currentOrderDetail.paymentMethod === 'WECHAT'">{{ $t('orders.wechat') }}</template>
|
||||||
|
<template v-else-if="currentOrderDetail.paymentMethod === 'PAYPAL'">{{ $t('orders.paypal') }}</template>
|
||||||
|
<template v-else>{{ $t('orders.unpaid') }}</template>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="detail-item">
|
||||||
|
<span class="label">{{ $t('orders.createTime') }}:</span>
|
||||||
|
<span class="value">{{ formatDate(currentOrderDetail.createdAt) }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="detail-item" v-if="currentOrderDetail.paidAt">
|
||||||
|
<span class="label">{{ $t('orders.paidTime') || '支付时间' }}:</span>
|
||||||
|
<span class="value">{{ formatDate(currentOrderDetail.paidAt) }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="detail-section" v-if="currentOrderDetail.contactEmail || currentOrderDetail.contactPhone">
|
||||||
|
<h4>{{ $t('orders.contactInfo') || '联系信息' }}</h4>
|
||||||
|
<div class="detail-grid">
|
||||||
|
<div class="detail-item" v-if="currentOrderDetail.contactEmail">
|
||||||
|
<span class="label">{{ $t('orders.email') || '邮箱' }}:</span>
|
||||||
|
<span class="value">{{ currentOrderDetail.contactEmail }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="detail-item" v-if="currentOrderDetail.contactPhone">
|
||||||
|
<span class="label">{{ $t('orders.phone') || '电话' }}:</span>
|
||||||
|
<span class="value">{{ currentOrderDetail.contactPhone }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="detail-section" v-if="currentOrderDetail.description">
|
||||||
|
<h4>{{ $t('orders.description') || '订单描述' }}</h4>
|
||||||
|
<p class="description-text">{{ currentOrderDetail.description }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-else class="loading-container">
|
||||||
|
<el-icon class="is-loading"><Loading /></el-icon>
|
||||||
|
<span>{{ $t('common.loading') || '加载中...' }}</span>
|
||||||
|
</div>
|
||||||
|
<template #footer>
|
||||||
|
<el-button @click="orderDetailVisible = false">{{ $t('common.close') || '关闭' }}</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -219,7 +302,8 @@ import {
|
|||||||
ArrowRight,
|
ArrowRight,
|
||||||
Delete,
|
Delete,
|
||||||
CreditCard,
|
CreditCard,
|
||||||
Wallet
|
Wallet,
|
||||||
|
Loading
|
||||||
} from '@element-plus/icons-vue'
|
} from '@element-plus/icons-vue'
|
||||||
import { getOrders, getAdminOrders, getOrderStats, deleteOrder as deleteOrderAPI, deleteOrders } from '@/api/orders'
|
import { getOrders, getAdminOrders, getOrderStats, deleteOrder as deleteOrderAPI, deleteOrders } from '@/api/orders'
|
||||||
import LanguageSwitcher from '@/components/LanguageSwitcher.vue'
|
import LanguageSwitcher from '@/components/LanguageSwitcher.vue'
|
||||||
@@ -245,7 +329,7 @@ const systemUptime = ref('加载中...')
|
|||||||
// 筛选条件
|
// 筛选条件
|
||||||
const filters = reactive({
|
const filters = reactive({
|
||||||
status: '',
|
status: '',
|
||||||
type: '',
|
paymentMethod: '',
|
||||||
search: ''
|
search: ''
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -417,8 +501,13 @@ const goToPage = (page) => {
|
|||||||
fetchOrders()
|
fetchOrders()
|
||||||
}
|
}
|
||||||
|
|
||||||
const viewOrder = (order) => {
|
// 订单详情弹窗相关
|
||||||
router.push(`/orders/${order.id}`)
|
const orderDetailVisible = ref(false)
|
||||||
|
const currentOrderDetail = ref(null)
|
||||||
|
|
||||||
|
const viewOrder = async (order) => {
|
||||||
|
currentOrderDetail.value = order
|
||||||
|
orderDetailVisible.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
const deleteOrder = async (order) => {
|
const deleteOrder = async (order) => {
|
||||||
@@ -433,19 +522,24 @@ const deleteOrder = async (order) => {
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
await deleteOrderAPI(order.id)
|
const response = await deleteOrderAPI(order.id)
|
||||||
|
console.log('删除订单响应:', response)
|
||||||
|
|
||||||
const index = orders.value.findIndex(o => o.id === order.id)
|
// 检查响应状态
|
||||||
if (index > -1) {
|
if (response.data?.success) {
|
||||||
orders.value.splice(index, 1)
|
const index = orders.value.findIndex(o => o.id === order.id)
|
||||||
totalOrders.value--
|
if (index > -1) {
|
||||||
|
orders.value.splice(index, 1)
|
||||||
|
totalOrders.value--
|
||||||
|
}
|
||||||
|
ElMessage.success('删除成功')
|
||||||
|
} else {
|
||||||
|
ElMessage.error(response.data?.message || '删除失败')
|
||||||
}
|
}
|
||||||
|
|
||||||
ElMessage.success('删除成功')
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error !== 'cancel') {
|
if (error !== 'cancel') {
|
||||||
console.error('删除失败:', error)
|
console.error('删除失败:', error)
|
||||||
ElMessage.error('删除失败')
|
ElMessage.error('删除失败: ' + (error.message || '未知错误'))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -468,17 +562,21 @@ const deleteSelected = async () => {
|
|||||||
)
|
)
|
||||||
|
|
||||||
const ids = selectedOrders.value.map(o => o.id)
|
const ids = selectedOrders.value.map(o => o.id)
|
||||||
await deleteOrders(ids)
|
const response = await deleteOrders(ids)
|
||||||
|
console.log('批量删除订单响应:', response)
|
||||||
|
|
||||||
orders.value = orders.value.filter(o => !ids.includes(o.id))
|
if (response.data?.success) {
|
||||||
totalOrders.value -= ids.length
|
orders.value = orders.value.filter(o => !ids.includes(o.id))
|
||||||
selectedOrders.value = []
|
totalOrders.value -= response.data?.deletedCount || ids.length
|
||||||
|
selectedOrders.value = []
|
||||||
ElMessage.success('批量删除成功')
|
ElMessage.success(response.data?.message || '批量删除成功')
|
||||||
|
} else {
|
||||||
|
ElMessage.error(response.data?.message || '批量删除失败')
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error !== 'cancel') {
|
if (error !== 'cancel') {
|
||||||
console.error('批量删除失败:', error)
|
console.error('批量删除失败:', error)
|
||||||
ElMessage.error('批量删除失败')
|
ElMessage.error('批量删除失败: ' + (error.message || '未知错误'))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -493,8 +591,9 @@ const fetchOrders = async () => {
|
|||||||
const response = await apiFunction({
|
const response = await apiFunction({
|
||||||
page: currentPage.value - 1,
|
page: currentPage.value - 1,
|
||||||
size: pageSize.value,
|
size: pageSize.value,
|
||||||
status: filters.status,
|
status: filters.status || undefined,
|
||||||
search: filters.search || searchText.value
|
paymentMethod: filters.paymentMethod || undefined,
|
||||||
|
search: filters.search || searchText.value || undefined
|
||||||
})
|
})
|
||||||
|
|
||||||
console.log('获取订单列表响应:', response)
|
console.log('获取订单列表响应:', response)
|
||||||
@@ -1002,6 +1101,98 @@ const fetchSystemStats = async () => {
|
|||||||
padding: 16px;
|
padding: 16px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 订单详情弹窗样式 */
|
||||||
|
.order-detail-content {
|
||||||
|
padding: 0 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-section {
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-section:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-section h4 {
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #1e293b;
|
||||||
|
margin: 0 0 16px 0;
|
||||||
|
padding-bottom: 8px;
|
||||||
|
border-bottom: 1px solid #e5e7eb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
gap: 12px 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-item .label {
|
||||||
|
color: #64748b;
|
||||||
|
font-size: 14px;
|
||||||
|
min-width: 80px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-item .value {
|
||||||
|
color: #1e293b;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-item .value.amount {
|
||||||
|
color: #f59e0b;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.description-text {
|
||||||
|
color: #475569;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.6;
|
||||||
|
margin: 0;
|
||||||
|
padding: 12px;
|
||||||
|
background: #f8fafc;
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 40px;
|
||||||
|
color: #64748b;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-container .el-icon {
|
||||||
|
font-size: 32px;
|
||||||
|
color: #3b82f6;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.order-detail-dialog .el-dialog__header) {
|
||||||
|
padding: 16px 20px;
|
||||||
|
border-bottom: 1px solid #e5e7eb;
|
||||||
|
margin-right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.order-detail-dialog .el-dialog__body) {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.order-detail-dialog .el-dialog__footer) {
|
||||||
|
padding: 12px 20px;
|
||||||
|
border-top: 1px solid #e5e7eb;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -163,6 +163,96 @@
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
|
<!-- 任务详情弹窗 -->
|
||||||
|
<el-dialog
|
||||||
|
v-model="detailDialogVisible"
|
||||||
|
:title="$t('tasks.taskDetail')"
|
||||||
|
width="600px"
|
||||||
|
class="task-detail-dialog"
|
||||||
|
>
|
||||||
|
<div class="task-detail-content" v-if="currentTask">
|
||||||
|
<div class="detail-section">
|
||||||
|
<h4>基本信息</h4>
|
||||||
|
<div class="detail-row">
|
||||||
|
<span class="detail-label">任务ID:</span>
|
||||||
|
<span class="detail-value">{{ currentTask.taskId || currentTask.id }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="detail-row">
|
||||||
|
<span class="detail-label">用户名:</span>
|
||||||
|
<span class="detail-value">{{ currentTask.username || '未知' }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="detail-row">
|
||||||
|
<span class="detail-label">任务类型:</span>
|
||||||
|
<span class="detail-value">{{ currentTask.type || currentTask.taskType || '未知' }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="detail-row">
|
||||||
|
<span class="detail-label">消耗资源:</span>
|
||||||
|
<span class="detail-value">{{ currentTask.resources || '0积分' }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="detail-row">
|
||||||
|
<span class="detail-label">状态:</span>
|
||||||
|
<span class="status-tag" :class="getStatusClass(currentTask.status)">
|
||||||
|
{{ getStatusText(currentTask.status) }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="detail-section">
|
||||||
|
<h4>时间信息</h4>
|
||||||
|
<div class="detail-row">
|
||||||
|
<span class="detail-label">创建时间:</span>
|
||||||
|
<span class="detail-value">{{ formatDate(currentTask.createdAt || currentTask.createTime) }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="detail-row" v-if="currentTask.updatedAt">
|
||||||
|
<span class="detail-label">更新时间:</span>
|
||||||
|
<span class="detail-value">{{ formatDate(currentTask.updatedAt) }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="detail-row" v-if="currentTask.completedAt">
|
||||||
|
<span class="detail-label">完成时间:</span>
|
||||||
|
<span class="detail-value">{{ formatDate(currentTask.completedAt) }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="detail-section" v-if="currentTask.progress !== undefined">
|
||||||
|
<h4>进度信息</h4>
|
||||||
|
<div class="detail-row">
|
||||||
|
<span class="detail-label">进度:</span>
|
||||||
|
<el-progress :percentage="currentTask.progress || 0" :status="getProgressStatus(currentTask.status)" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="detail-section" v-if="currentTask.resultUrl">
|
||||||
|
<h4>结果</h4>
|
||||||
|
<div class="detail-row">
|
||||||
|
<span class="detail-label">结果链接:</span>
|
||||||
|
<a :href="currentTask.resultUrl" target="_blank" class="result-link">查看结果</a>
|
||||||
|
</div>
|
||||||
|
<div class="result-preview" v-if="isVideoUrl(currentTask.resultUrl)">
|
||||||
|
<video :src="currentTask.resultUrl" controls class="preview-video"></video>
|
||||||
|
</div>
|
||||||
|
<div class="result-preview" v-else-if="isImageUrl(currentTask.resultUrl)">
|
||||||
|
<img :src="currentTask.resultUrl" class="preview-image" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="detail-section" v-if="currentTask.errorMessage">
|
||||||
|
<h4>错误信息</h4>
|
||||||
|
<div class="error-message">{{ currentTask.errorMessage }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<template #footer>
|
||||||
|
<el-button @click="detailDialogVisible = false">关闭</el-button>
|
||||||
|
<el-button
|
||||||
|
v-if="currentTask?.resultUrl"
|
||||||
|
type="primary"
|
||||||
|
@click="openResult(currentTask.resultUrl)"
|
||||||
|
>
|
||||||
|
查看结果
|
||||||
|
</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -196,6 +286,10 @@ const selectedTasks = ref([])
|
|||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
const searchText = ref('')
|
const searchText = ref('')
|
||||||
|
|
||||||
|
// 任务详情弹窗相关
|
||||||
|
const detailDialogVisible = ref(false)
|
||||||
|
const currentTask = ref(null)
|
||||||
|
|
||||||
// 系统状态数据
|
// 系统状态数据
|
||||||
const onlineUsers = ref('0/500')
|
const onlineUsers = ref('0/500')
|
||||||
const systemUptime = ref('加载中...')
|
const systemUptime = ref('加载中...')
|
||||||
@@ -376,8 +470,42 @@ const formatDate = (dateString) => {
|
|||||||
|
|
||||||
// 查看任务详情
|
// 查看任务详情
|
||||||
const handleView = (task) => {
|
const handleView = (task) => {
|
||||||
ElMessage.info(`查看任务详情: ${task.taskId || task.id}`)
|
currentTask.value = task
|
||||||
// 这里可以跳转到详情页面或打开详情弹窗
|
detailDialogVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取进度条状态
|
||||||
|
const getProgressStatus = (status) => {
|
||||||
|
switch (status) {
|
||||||
|
case 'COMPLETED':
|
||||||
|
return 'success'
|
||||||
|
case 'FAILED':
|
||||||
|
case 'TIMEOUT':
|
||||||
|
return 'exception'
|
||||||
|
default:
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 判断是否是视频URL
|
||||||
|
const isVideoUrl = (url) => {
|
||||||
|
if (!url) return false
|
||||||
|
const videoExtensions = ['.mp4', '.webm', '.ogg', '.mov', '.avi']
|
||||||
|
return videoExtensions.some(ext => url.toLowerCase().includes(ext))
|
||||||
|
}
|
||||||
|
|
||||||
|
// 判断是否是图片URL
|
||||||
|
const isImageUrl = (url) => {
|
||||||
|
if (!url) return false
|
||||||
|
const imageExtensions = ['.jpg', '.jpeg', '.png', '.gif', '.webp', '.bmp']
|
||||||
|
return imageExtensions.some(ext => url.toLowerCase().includes(ext))
|
||||||
|
}
|
||||||
|
|
||||||
|
// 打开结果
|
||||||
|
const openResult = (url) => {
|
||||||
|
if (url) {
|
||||||
|
window.open(url, '_blank')
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 删除单个任务
|
// 删除单个任务
|
||||||
@@ -393,15 +521,37 @@ const handleDelete = async (task) => {
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
// 从数据中删除
|
// 调用后端API删除
|
||||||
const index = taskRecords.value.findIndex(item => item.id === task.id)
|
const token = sessionStorage.getItem('token')
|
||||||
if (index > -1) {
|
if (!token) {
|
||||||
taskRecords.value.splice(index, 1)
|
ElMessage.error('请先登录')
|
||||||
total.value--
|
return
|
||||||
|
}
|
||||||
|
const deleteUrl = `/api/admin/tasks/${task.taskId || task.id}`
|
||||||
|
const response = await fetch(deleteUrl, {
|
||||||
|
method: 'DELETE',
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Bearer ${token}`,
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const result = await response.json()
|
||||||
|
if (result.success) {
|
||||||
|
// 从前端数据中移除
|
||||||
|
const index = taskRecords.value.findIndex(item => item.id === task.id)
|
||||||
|
if (index > -1) {
|
||||||
|
taskRecords.value.splice(index, 1)
|
||||||
|
total.value--
|
||||||
|
}
|
||||||
ElMessage.success('任务删除成功')
|
ElMessage.success('任务删除成功')
|
||||||
|
} else {
|
||||||
|
ElMessage.error(result.message || '删除失败')
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// 用户取消删除
|
if (error !== 'cancel') {
|
||||||
|
ElMessage.error('删除失败: ' + error.message)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -423,14 +573,38 @@ const handleBatchDelete = async () => {
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
// 批量删除
|
// 调用后端API批量删除
|
||||||
const selectedIds = selectedTasks.value.map(item => item.id)
|
const token = sessionStorage.getItem('token')
|
||||||
taskRecords.value = taskRecords.value.filter(item => !selectedIds.includes(item.id))
|
if (!token) {
|
||||||
total.value -= selectedIds.length
|
ElMessage.error('请先登录')
|
||||||
selectedTasks.value = []
|
return
|
||||||
ElMessage.success(`成功删除 ${selectedIds.length} 个任务`)
|
}
|
||||||
|
const taskIds = selectedTasks.value.map(item => item.taskId || item.id)
|
||||||
|
|
||||||
|
const response = await fetch('/api/admin/tasks/batch', {
|
||||||
|
method: 'DELETE',
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Bearer ${token}`,
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify(taskIds)
|
||||||
|
})
|
||||||
|
|
||||||
|
const result = await response.json()
|
||||||
|
if (result.success) {
|
||||||
|
// 从前端数据中移除
|
||||||
|
const selectedIds = selectedTasks.value.map(item => item.id)
|
||||||
|
taskRecords.value = taskRecords.value.filter(item => !selectedIds.includes(item.id))
|
||||||
|
total.value -= result.deletedCount || selectedIds.length
|
||||||
|
selectedTasks.value = []
|
||||||
|
ElMessage.success(result.message || `成功删除 ${result.deletedCount} 个任务`)
|
||||||
|
} else {
|
||||||
|
ElMessage.error(result.message || '批量删除失败')
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// 用户取消删除
|
if (error !== 'cancel') {
|
||||||
|
ElMessage.error('批量删除失败: ' + error.message)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -460,31 +634,26 @@ const loadTaskRecords = async () => {
|
|||||||
|
|
||||||
if (response.data && response.data.success) {
|
if (response.data && response.data.success) {
|
||||||
const realData = response.data.data || []
|
const realData = response.data.data || []
|
||||||
|
taskRecords.value = realData
|
||||||
// 如果有真实数据,使用真实数据
|
total.value = response.data.totalElements || realData.length
|
||||||
if (realData.length > 0) {
|
console.log('成功加载任务记录:', taskRecords.value.length)
|
||||||
taskRecords.value = realData
|
|
||||||
total.value = response.data.totalElements || 0
|
|
||||||
console.log('成功加载任务记录(真实数据):', taskRecords.value.length)
|
|
||||||
} else {
|
|
||||||
// 如果数据库为空,使用模拟数据进行演示
|
|
||||||
console.log('数据库为空,加载模拟数据')
|
|
||||||
loadMockData()
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
console.warn('API返回失败:', response.data?.message)
|
console.warn('API返回失败:', response.data?.message)
|
||||||
// 如果API返回失败,使用假数据
|
taskRecords.value = []
|
||||||
loadMockData()
|
total.value = 0
|
||||||
|
ElMessage.warning(response.data?.message || '加载数据失败')
|
||||||
}
|
}
|
||||||
} catch (apiError) {
|
} catch (apiError) {
|
||||||
console.error('API调用失败,使用假数据:', apiError)
|
console.error('API调用失败:', apiError)
|
||||||
// API调用失败(可能是未登录或服务器问题),使用假数据
|
taskRecords.value = []
|
||||||
loadMockData()
|
total.value = 0
|
||||||
|
ElMessage.error('API调用失败: ' + (apiError.message || '请检查网络'))
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('加载任务记录失败:', error)
|
console.error('加载任务记录失败:', error)
|
||||||
ElMessage.error('数据加载失败,请检查网络连接')
|
ElMessage.error('数据加载失败,请检查网络连接')
|
||||||
loadMockData()
|
taskRecords.value = []
|
||||||
|
total.value = 0
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false
|
loading.value = false
|
||||||
}
|
}
|
||||||
@@ -1019,4 +1188,83 @@ const fetchSystemStats = async () => {
|
|||||||
padding: 16px;
|
padding: 16px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 任务详情弹窗样式 */
|
||||||
|
.task-detail-content {
|
||||||
|
max-height: 60vh;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-section {
|
||||||
|
margin-bottom: 24px;
|
||||||
|
padding-bottom: 16px;
|
||||||
|
border-bottom: 1px solid #f0f0f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-section:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-section h4 {
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #1f2937;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-label {
|
||||||
|
width: 100px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
color: #6b7280;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-value {
|
||||||
|
color: #1f2937;
|
||||||
|
font-size: 14px;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-link {
|
||||||
|
color: #3b82f6;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-link:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-preview {
|
||||||
|
margin-top: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-video {
|
||||||
|
max-width: 100%;
|
||||||
|
max-height: 300px;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-image {
|
||||||
|
max-width: 100%;
|
||||||
|
max-height: 300px;
|
||||||
|
border-radius: 8px;
|
||||||
|
object-fit: contain;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-message {
|
||||||
|
background: #fef2f2;
|
||||||
|
border: 1px solid #fecaca;
|
||||||
|
color: #dc2626;
|
||||||
|
padding: 12px;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1093,7 +1093,7 @@ const restoreProcessingTask = async () => {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// 检查最近一条任务的状态(如果失败则显示失败状态,但不恢复输入参数)
|
// 检查最近一条任务的状态(如果失败则显示失败状态和参考图)
|
||||||
const checkLastTaskStatus = async () => {
|
const checkLastTaskStatus = async () => {
|
||||||
if (!userStore.isAuthenticated) return
|
if (!userStore.isAuthenticated) return
|
||||||
|
|
||||||
@@ -1102,14 +1102,30 @@ const checkLastTaskStatus = async () => {
|
|||||||
if (response.data && response.data.success && response.data.data && response.data.data.length > 0) {
|
if (response.data && response.data.success && response.data.data && response.data.data.length > 0) {
|
||||||
const lastTask = response.data.data[0]
|
const lastTask = response.data.data[0]
|
||||||
|
|
||||||
// 只关注 FAILED 状态,显示失败UI但不恢复输入参数
|
// 只关注 FAILED 状态,显示失败UI和参考图
|
||||||
if (lastTask.status === 'FAILED') {
|
if (lastTask.status === 'FAILED') {
|
||||||
console.log('[Last Task Failed]', lastTask)
|
console.log('[Last Task Failed]', lastTask)
|
||||||
|
|
||||||
currentTask.value = lastTask
|
currentTask.value = lastTask
|
||||||
taskStatus.value = 'FAILED'
|
taskStatus.value = 'FAILED'
|
||||||
// 不恢复输入参数,让用户可以自由创建新任务
|
|
||||||
|
// 恢复提示词,让用户看到失败任务的内容
|
||||||
|
if (lastTask.prompt) {
|
||||||
|
inputText.value = lastTask.prompt
|
||||||
|
}
|
||||||
|
// 恢复首帧图片(参考图)
|
||||||
|
if (lastTask.firstFrameUrl) {
|
||||||
|
firstFrameImage.value = processHistoryUrl(lastTask.firstFrameUrl)
|
||||||
|
}
|
||||||
|
// 恢复其他参数
|
||||||
|
if (lastTask.aspectRatio) {
|
||||||
|
aspectRatio.value = lastTask.aspectRatio
|
||||||
|
}
|
||||||
|
if (lastTask.duration) {
|
||||||
|
duration.value = lastTask.duration
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
// 如果最近一条任务是成功的,不需要处理
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Check last task status error', error)
|
console.error('Check last task status error', error)
|
||||||
|
|||||||
@@ -15,24 +15,30 @@
|
|||||||
|
|
||||||
<!-- 登录方式切换 -->
|
<!-- 登录方式切换 -->
|
||||||
<div class="login-tabs">
|
<div class="login-tabs">
|
||||||
<svg width="248" height="59" viewBox="0 0 248 59" fill="none" xmlns="http://www.w3.org/2000/svg" class="tabs-svg">
|
<!-- 邮箱登录盒子 -->
|
||||||
<!-- 邮箱登录 -->
|
<div
|
||||||
<g class="tab-email" :class="{ active: loginType === 'email' }" @click="loginType = 'email'" style="cursor: pointer;">
|
class="tab-item"
|
||||||
<!-- 透明点击区域 -->
|
:class="{ active: loginType === 'email' }"
|
||||||
<rect x="0" y="10" width="120" height="40" fill="transparent"/>
|
@click="loginType = 'email'"
|
||||||
<path d="M13.598 21.112V40.638H11.076V39.13H4.316V40.638H1.768V21.112H6.344V17.55H8.996V21.112H13.598ZM4.316 36.712H6.344V31.122H4.316V36.712ZM8.996 36.712H11.076V31.122H8.996V36.712ZM4.316 28.73H6.344V23.556H4.316V28.73ZM8.996 23.556V28.73H11.076V23.556H8.996ZM15.34 18.772H24.232V20.748C23.452 23.4 22.62 25.818 21.736 28.054C23.556 30.654 24.466 32.76 24.492 34.398C24.492 35.958 24.154 37.024 23.504 37.596C22.802 38.22 21.398 38.532 19.318 38.532L18.512 35.802C19.474 35.906 20.28 35.984 20.956 35.984C21.372 35.958 21.658 35.828 21.814 35.62C21.918 35.464 21.97 35.048 21.996 34.398C21.97 32.786 20.982 30.68 19.032 28.054C19.864 26.156 20.67 23.868 21.45 21.164H17.914V41.73H15.34V18.772ZM27.716 27.326H31.616V24.83H34.346V27.326H37.57V29.926H34.346V30.446C35.542 31.538 36.79 32.734 38.038 34.06L36.53 36.348C35.698 35.048 34.97 33.93 34.346 33.046V41.756H31.616V33.202C30.654 35.23 29.432 37.102 27.924 38.818L26.754 35.776C28.756 34.06 30.238 32.11 31.226 29.926H27.716V27.326ZM49.4 25.48V41.73H46.774V40.82H41.106V41.73H38.48V25.48H49.4ZM41.106 38.428H46.774V36.244H41.106V38.428ZM41.106 33.956H46.774V32.058H41.106V33.956ZM41.106 29.77H46.774V27.872H41.106V29.77ZM31.538 21.762C30.966 22.75 30.316 23.634 29.614 24.466L27.248 22.958C28.782 21.294 29.874 19.5 30.498 17.576L33.124 18.148C32.968 18.564 32.838 18.98 32.682 19.37H38.974V21.762H35.568C36.088 22.49 36.504 23.192 36.842 23.842L34.346 24.778C33.878 23.738 33.306 22.724 32.682 21.762H31.538ZM42.64 21.762C42.12 22.828 41.574 23.816 40.95 24.726L38.636 23.244C39.962 21.424 40.898 19.474 41.444 17.446L44.018 18.018C43.862 18.486 43.732 18.928 43.602 19.37H50.83V21.762H46.67C47.19 22.49 47.632 23.192 47.97 23.842L45.578 24.752C45.11 23.712 44.538 22.724 43.862 21.762H42.64ZM57.538 28.522H72.566V35.282H57.538V28.522ZM69.836 32.89V30.888H60.268V32.89H69.836ZM60.45 35.438C61.282 36.296 62.036 37.31 62.712 38.454H67.626C68.354 37.466 68.978 36.426 69.524 35.36L72.046 36.27C71.578 37.05 71.084 37.778 70.564 38.454H76.232V41.028H53.716V38.454H59.826C59.28 37.726 58.63 37.05 57.902 36.4L60.45 35.438ZM56.368 21.112C57.564 21.996 58.63 22.854 59.514 23.686C60.424 22.802 61.152 21.866 61.724 20.878H55.822V18.382H64.792V20.41C64.194 21.918 63.388 23.27 62.374 24.466H68.822C67.262 22.75 66.014 20.904 65.104 18.876L67.366 17.628C67.782 18.59 68.276 19.5 68.848 20.358C69.758 19.63 70.538 18.85 71.188 18.018L73.086 19.708C72.306 20.644 71.37 21.528 70.304 22.308C70.72 22.828 71.188 23.296 71.708 23.764C72.8 22.932 73.71 21.996 74.49 20.982L76.388 22.646C75.608 23.634 74.672 24.544 73.632 25.35C74.776 26.182 76.024 26.962 77.428 27.664L75.634 29.744C73.606 28.6 71.838 27.352 70.33 25.974V26.936H60.788V26.104C59.124 27.56 57.07 28.782 54.626 29.796L52.962 27.664C54.782 26.962 56.316 26.156 57.616 25.246C56.758 24.466 55.744 23.66 54.548 22.828L56.368 21.112ZM82.368 18.408H97.864V26.806H102.258V29.276H98.228L100.282 30.966C98.93 32.63 97.396 33.956 95.68 34.892C97.604 36.296 99.84 37.518 102.388 38.61L101.01 41.002C97.37 39.286 94.432 37.232 92.196 34.814V38.974C92.196 40.794 91.39 41.73 89.804 41.73H86.762L86.164 39.182C87.1 39.286 88.01 39.364 88.894 39.364C89.284 39.364 89.492 39 89.492 38.324V34.918C86.944 37.154 83.928 39.156 80.47 40.95L79.378 38.428C83.278 36.66 86.632 34.58 89.492 32.136V29.276H79.768V26.806H95.108V25.012H83.252V22.672H95.108V20.852H82.368V18.408ZM83.018 29.666C84.526 30.706 85.8 31.746 86.84 32.786L85.072 34.554C84.162 33.566 82.888 32.526 81.224 31.382L83.018 29.666ZM98.176 29.276H92.196V31.824C92.69 32.37 93.236 32.89 93.834 33.41C95.498 32.422 96.954 31.044 98.176 29.276Z" />
|
>
|
||||||
</g>
|
<svg width="105" height="30" viewBox="0 0 105 28" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M13.598 6.112V25.638H11.076V24.13H4.316V25.638H1.768V6.112H6.344V2.55H8.996V6.112H13.598ZM4.316 21.712H6.344V16.122H4.316V21.712ZM8.996 21.712H11.076V16.122H8.996V21.712ZM4.316 13.73H6.344V8.556H4.316V13.73ZM8.996 8.556V13.73H11.076V8.556H8.996ZM15.34 3.772H24.232V5.748C23.452 8.4 22.62 10.818 21.736 13.054C23.556 15.654 24.466 17.76 24.492 19.398C24.492 20.958 24.154 22.024 23.504 22.596C22.802 23.22 21.398 23.532 19.318 23.532L18.512 20.802C19.474 20.906 20.28 20.984 20.956 20.984C21.372 20.958 21.658 20.828 21.814 20.62C21.918 20.464 21.97 20.048 21.996 19.398C21.97 17.786 20.982 15.68 19.032 13.054C19.864 11.156 20.67 8.868 21.45 6.164H17.914V26.73H15.34V3.772ZM27.716 12.326H31.616V9.83H34.346V12.326H37.57V14.926H34.346V15.446C35.542 16.538 36.79 17.734 38.038 19.06L36.53 21.348C35.698 20.048 34.97 18.93 34.346 18.046V26.756H31.616V18.202C30.654 20.23 29.432 22.102 27.924 23.818L26.754 20.776C28.756 19.06 30.238 17.11 31.226 14.926H27.716V12.326ZM49.4 10.48V26.73H46.774V25.82H41.106V26.73H38.48V10.48H49.4ZM41.106 23.428H46.774V21.244H41.106V23.428ZM41.106 18.956H46.774V17.058H41.106V18.956ZM41.106 14.77H46.774V12.872H41.106V14.77ZM31.538 6.762C30.966 7.75 30.316 8.634 29.614 9.466L27.248 7.958C28.782 6.294 29.874 4.5 30.498 2.576L33.124 3.148C32.968 3.564 32.838 3.98 32.682 4.37H38.974V6.762H35.568C36.088 7.49 36.504 8.192 36.842 8.842L34.346 9.778C33.878 8.738 33.306 7.724 32.682 6.762H31.538ZM42.64 6.762C42.12 7.828 41.574 8.816 40.95 9.726L38.636 8.244C39.962 6.424 40.898 4.474 41.444 2.446L44.018 3.018C43.862 3.486 43.732 3.928 43.602 4.37H50.83V6.762H46.67C47.19 7.49 47.632 8.192 47.97 8.842L45.578 9.752C45.11 8.712 44.538 7.724 43.862 6.762H42.64ZM57.538 13.522H72.566V20.282H57.538V13.522ZM69.836 17.89V15.888H60.268V17.89H69.836ZM60.45 20.438C61.282 21.296 62.036 22.31 62.712 23.454H67.626C68.354 22.466 68.978 21.426 69.524 20.36L72.046 21.27C71.578 22.05 71.084 22.778 70.564 23.454H76.232V26.028H53.716V23.454H59.826C59.28 22.726 58.63 22.05 57.902 21.4L60.45 20.438ZM56.368 6.112C57.564 6.996 58.63 7.854 59.514 8.686C60.424 7.802 61.152 6.866 61.724 5.878H55.822V3.382H64.792V5.41C64.194 6.918 63.388 8.27 62.374 9.466H68.822C67.262 7.75 66.014 5.904 65.104 3.876L67.366 2.628C67.782 3.59 68.276 4.5 68.848 5.358C69.758 4.63 70.538 3.85 71.188 3.018L73.086 4.708C72.306 5.644 71.37 6.528 70.304 7.308C70.72 7.828 71.188 8.296 71.708 8.764C72.8 7.932 73.71 6.996 74.49 5.982L76.388 7.646C75.608 8.634 74.672 9.544 73.632 10.35C74.776 11.182 76.024 11.962 77.428 12.664L75.634 14.744C73.606 13.6 71.838 12.352 70.33 10.974V11.936H60.788V11.104C59.124 12.56 57.07 13.782 54.626 14.796L52.962 12.664C54.782 11.962 56.316 11.156 57.616 10.246C56.758 9.466 55.744 8.66 54.548 7.828L56.368 6.112ZM82.368 3.408H97.864V11.806H102.258V14.276H98.228L100.282 15.966C98.93 17.63 97.396 18.956 95.68 19.892C97.604 21.296 99.84 22.518 102.388 23.61L101.01 26.002C97.37 24.286 94.432 22.232 92.196 19.814V23.974C92.196 25.794 91.39 26.73 89.804 26.73H86.762L86.164 24.182C87.1 24.286 88.01 24.364 88.894 24.364C89.284 24.364 89.492 24 89.492 23.324V19.918C86.944 22.154 83.928 24.156 80.47 25.95L79.378 23.428C83.278 21.66 86.632 19.58 89.492 17.136V14.276H79.768V11.806H95.108V10.012H83.252V7.672H95.108V5.852H82.368V3.408ZM83.018 14.666C84.526 15.706 85.8 16.746 86.84 17.786L85.072 19.554C84.162 18.566 82.888 17.526 81.224 16.382L83.018 14.666ZM98.176 14.276H92.196V16.824C92.69 17.37 93.236 17.89 93.834 18.41C95.498 17.422 96.954 16.044 98.176 14.276Z" fill="currentColor"/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- 分隔线 -->
|
<!-- 分隔线 -->
|
||||||
<path d="M124 18.75V40.25" stroke="#9EA9B6"/>
|
<div class="tab-divider"></div>
|
||||||
|
|
||||||
<!-- 密码登录 -->
|
<!-- 账号登录盒子 -->
|
||||||
<g class="tab-password" :class="{ active: loginType === 'password' }" @click="loginType = 'password'" style="cursor: pointer;">
|
<div
|
||||||
<!-- 透明点击区域 -->
|
class="tab-item"
|
||||||
<rect x="128" y="10" width="120" height="40" fill="transparent"/>
|
:class="{ active: loginType === 'password' }"
|
||||||
<path d="M155.362 18.46V35.126H152.996V20.956H148.212V35.126H145.768V18.46H155.362ZM149.434 22.49H151.722V31.018C151.644 33.93 151.202 36.27 150.37 38.012C149.564 39.65 148.264 40.898 146.47 41.782L145.014 39.494C146.704 38.636 147.848 37.57 148.472 36.322C149.044 34.944 149.356 33.176 149.434 31.018V22.49ZM152.684 35.724C153.984 37.05 155.076 38.35 155.986 39.624L154.036 41.574C153.308 40.274 152.294 38.896 150.942 37.414L152.684 35.724ZM162.044 30.732H160.328V38.298C161.342 37.882 162.33 37.362 163.318 36.738L163.786 39.156C162.122 40.196 160.224 41.028 158.092 41.704L156.974 39.286C157.442 39.052 157.676 38.688 157.676 38.22V30.732H156.064V28.132H157.676V17.68H160.328V28.132H168.622V30.732H164.384C165.528 34.45 167.14 37.336 169.168 39.364L167.322 41.47C165.008 39.026 163.24 35.438 162.044 30.732ZM166.23 18.746L168.31 20.41C166.516 23.322 164.436 25.506 162.044 26.91L160.588 24.83C162.772 23.478 164.67 21.45 166.23 18.746ZM174.966 18.356H191.034V26.026H174.966V18.356ZM188.278 23.608V20.8H177.722V23.608H188.278ZM176.76 30.16H171.118V27.534H194.856V30.16H179.516L178.814 32.422H191.372C191.164 36.894 190.748 39.546 190.072 40.378C189.396 41.184 188.122 41.6 186.198 41.6C184.924 41.6 183.78 41.522 182.766 41.392L181.882 38.922C183.286 39.052 184.508 39.13 185.6 39.13C186.874 39.13 187.654 38.87 187.966 38.402C188.252 37.908 188.46 36.738 188.59 34.866H175.564L176.76 30.16ZM201.538 28.522H216.566V35.282H201.538V28.522ZM213.836 32.89V30.888H204.268V32.89H213.836ZM204.45 35.438C205.282 36.296 206.036 37.31 206.712 38.454H211.626C212.354 37.466 212.978 36.426 213.524 35.36L216.046 36.27C215.578 37.05 215.084 37.778 214.564 38.454H220.232V41.028H197.716V38.454H203.826C203.28 37.726 202.63 37.05 201.902 36.4L204.45 35.438ZM200.368 21.112C201.564 21.996 202.63 22.854 203.514 23.686C204.424 22.802 205.152 21.866 205.724 20.878H199.822V18.382H208.792V20.41C208.194 21.918 207.388 23.27 206.374 24.466H212.822C211.262 22.75 210.014 20.904 209.104 18.876L211.366 17.628C211.782 18.59 212.276 19.5 212.848 20.358C213.758 19.63 214.538 18.85 215.188 18.018L217.086 19.708C216.306 20.644 215.37 21.528 214.304 22.308C214.72 22.828 215.188 23.296 215.708 23.764C216.8 22.932 217.71 21.996 218.49 20.982L220.388 22.646C219.608 23.634 218.672 24.544 217.632 25.35C218.776 26.182 220.024 26.962 221.428 27.664L219.634 29.744C217.606 28.6 215.838 27.352 214.33 25.974V26.936H204.788V26.104C203.124 27.56 201.07 28.782 198.626 29.796L196.962 27.664C198.782 26.962 200.316 26.156 201.616 25.246C200.758 24.466 199.744 23.66 198.548 22.828L200.368 21.112ZM226.368 18.408H241.864V26.806H246.258V29.276H242.228L244.282 30.966C242.93 32.63 241.396 33.956 239.68 34.892C241.604 36.296 243.84 37.518 246.388 38.61L245.01 41.002C241.37 39.286 238.432 37.232 236.196 34.814V38.974C236.196 40.794 235.39 41.73 233.804 41.73H230.762L230.164 39.182C231.1 39.286 232.01 39.364 232.894 39.364C233.284 39.364 233.492 39 233.492 38.324V34.918C230.944 37.154 227.928 39.156 224.47 40.95L223.378 38.428C227.278 36.66 230.632 34.58 233.492 32.136V29.276H223.768V26.806H239.108V25.012H227.252V22.672H239.108V20.852H226.368V18.408ZM227.018 29.666C228.526 30.706 229.8 31.746 230.84 32.786L229.072 34.554C228.162 33.566 226.888 32.526 225.224 31.382L227.018 29.666ZM242.176 29.276H236.196V31.824C236.69 32.37 237.236 32.89 237.834 33.41C239.498 32.422 240.954 31.044 242.176 29.276Z" />
|
@click="loginType = 'password'"
|
||||||
</g>
|
>
|
||||||
</svg>
|
<svg width="105" height="30" viewBox="0 0 105 28" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M10.362 3.46V20.126H7.996V5.956H3.212V20.126H0.768V3.46H10.362ZM4.434 7.49H6.722V16.018C6.644 18.93 6.202 21.27 5.37 23.012C4.564 24.65 3.264 25.898 1.47 26.782L0.014 24.494C1.704 23.636 2.848 22.57 3.472 21.322C4.044 19.944 4.356 18.176 4.434 16.018V7.49ZM7.684 20.724C8.984 22.05 10.076 23.35 10.986 24.624L9.036 26.574C8.308 25.274 7.294 23.896 5.942 22.414L7.684 20.724ZM17.044 15.732H15.328V23.298C16.342 22.882 17.33 22.362 18.318 21.738L18.786 24.156C17.122 25.196 15.224 26.028 13.092 26.704L11.974 24.286C12.442 24.052 12.676 23.688 12.676 23.22V15.732H11.064V13.132H12.676V2.68H15.328V13.132H23.622V15.732H19.384C20.528 19.45 22.14 22.336 24.168 24.364L22.322 26.47C20.008 24.026 18.24 20.438 17.044 15.732ZM21.23 3.746L23.31 5.41C21.516 8.322 19.436 10.506 17.044 11.91L15.588 9.83C17.772 8.478 19.67 6.45 21.23 3.746ZM29.966 3.356H46.034V11.026H29.966V3.356ZM43.278 8.608V5.8H32.722V8.608H43.278ZM31.76 15.16H26.118V12.534H49.856V15.16H34.516L33.814 17.422H46.372C46.164 21.894 45.748 24.546 45.072 25.378C44.396 26.184 43.122 26.6 41.198 26.6C39.924 26.6 38.78 26.522 37.766 26.392L36.882 23.922C38.286 24.052 39.508 24.13 40.6 24.13C41.874 24.13 42.654 23.87 42.966 23.402C43.252 22.908 43.46 21.738 43.59 19.866H30.564L31.76 15.16ZM56.538 13.522H71.566V20.282H56.538V13.522ZM68.836 17.89V15.888H59.268V17.89H68.836ZM59.45 20.438C60.282 21.296 61.036 22.31 61.712 23.454H66.626C67.354 22.466 67.978 21.426 68.524 20.36L71.046 21.27C70.578 22.05 70.084 22.778 69.564 23.454H75.232V26.028H52.716V23.454H58.826C58.28 22.726 57.63 22.05 56.902 21.4L59.45 20.438ZM55.368 6.112C56.564 6.996 57.63 7.854 58.514 8.686C59.424 7.802 60.152 6.866 60.724 5.878H54.822V3.382H63.792V5.41C63.194 6.918 62.388 8.27 61.374 9.466H67.822C66.262 7.75 65.014 5.904 64.104 3.876L66.366 2.628C66.782 3.59 67.276 4.5 67.848 5.358C68.758 4.63 69.538 3.85 70.188 3.018L72.086 4.708C71.306 5.644 70.37 6.528 69.304 7.308C69.72 7.828 70.188 8.296 70.708 8.764C71.8 7.932 72.71 6.996 73.49 5.982L75.388 7.646C74.608 8.634 73.672 9.544 72.632 10.35C73.776 11.182 75.024 11.962 76.428 12.664L74.634 14.744C72.606 13.6 70.838 12.352 69.33 10.974V11.936H59.788V11.104C58.124 12.56 56.07 13.782 53.626 14.796L51.962 12.664C53.782 11.962 55.316 11.156 56.616 10.246C55.758 9.466 54.744 8.66 53.548 7.828L55.368 6.112ZM81.368 3.408H96.864V11.806H101.258V14.276H97.228L99.282 15.966C97.93 17.63 96.396 18.956 94.68 19.892C96.604 21.296 98.84 22.518 101.388 23.61L100.01 26.002C96.37 24.286 93.432 22.232 91.196 19.814V23.974C91.196 25.794 90.39 26.73 88.804 26.73H85.762L85.164 24.182C86.1 24.286 87.01 24.364 87.894 24.364C88.284 24.364 88.492 24 88.492 23.324V19.918C85.944 22.154 82.928 24.156 79.47 25.95L78.378 23.428C82.278 21.66 85.632 19.58 88.492 17.136V14.276H78.768V11.806H94.108V10.012H82.252V7.672H94.108V5.852H81.368V3.408ZM82.018 14.666C83.526 15.706 84.8 16.746 85.84 17.786L84.072 19.554C83.162 18.566 81.888 17.526 80.224 16.382L82.018 14.666ZM97.176 14.276H91.196V16.824C91.69 17.37 92.236 17.89 92.834 18.41C94.498 17.422 95.954 16.044 97.176 14.276Z" fill="currentColor"/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 登录表单 -->
|
<!-- 登录表单 -->
|
||||||
@@ -414,27 +420,36 @@ const handleLogin = async () => {
|
|||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
margin-bottom: 50px;
|
margin-bottom: 50px;
|
||||||
|
gap: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tabs-svg {
|
.tab-item {
|
||||||
width: 248px;
|
padding: 10px 16px;
|
||||||
height: 59px;
|
cursor: pointer;
|
||||||
|
transition: color 0.3s ease;
|
||||||
|
user-select: none;
|
||||||
|
color: #9EA9B6;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 500;
|
||||||
|
letter-spacing: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tab-email path,
|
.tab-item:hover {
|
||||||
.tab-password path {
|
color: rgba(255, 255, 255, 0.8);
|
||||||
fill: #9EA9B6;
|
|
||||||
transition: fill 0.3s ease;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.tab-email.active path,
|
.tab-item.active {
|
||||||
.tab-password.active path {
|
color: #ffffff;
|
||||||
fill: white;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.tab-email:hover path,
|
.tab-divider {
|
||||||
.tab-password:hover path {
|
width: 1px;
|
||||||
fill: rgba(255, 255, 255, 0.8);
|
height: 24px;
|
||||||
|
background: #9EA9B6;
|
||||||
|
margin: 0 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 登录表单 */
|
/* 登录表单 */
|
||||||
|
|||||||
@@ -100,7 +100,7 @@
|
|||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
|
|
||||||
<el-table-column label="操作" width="200" fixed="right">
|
<el-table-column label="操作" width="280" fixed="right">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<el-button
|
<el-button
|
||||||
size="small"
|
size="small"
|
||||||
@@ -116,6 +116,13 @@
|
|||||||
>
|
>
|
||||||
测试完成
|
测试完成
|
||||||
</el-button>
|
</el-button>
|
||||||
|
<el-button
|
||||||
|
size="small"
|
||||||
|
type="danger"
|
||||||
|
@click="handleDeletePayment(row)"
|
||||||
|
>
|
||||||
|
删除
|
||||||
|
</el-button>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
</el-table>
|
</el-table>
|
||||||
@@ -235,7 +242,7 @@ import {
|
|||||||
Close,
|
Close,
|
||||||
User as Warning
|
User as Warning
|
||||||
} from '@element-plus/icons-vue'
|
} from '@element-plus/icons-vue'
|
||||||
import { getPayments, testPaymentComplete as testPaymentCompleteApi, createTestPayment } from '@/api/payments'
|
import { getPayments, testPaymentComplete as testPaymentCompleteApi, createTestPayment, deletePayment } from '@/api/payments'
|
||||||
import { useUserStore } from '@/stores/user'
|
import { useUserStore } from '@/stores/user'
|
||||||
|
|
||||||
const userStore = useUserStore()
|
const userStore = useUserStore()
|
||||||
@@ -561,6 +568,37 @@ const testPaymentComplete = async (payment) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 删除支付记录
|
||||||
|
const handleDeletePayment = async (payment) => {
|
||||||
|
try {
|
||||||
|
await ElMessageBox.confirm(
|
||||||
|
`确定要删除支付记录 ${payment.orderId} 吗?`,
|
||||||
|
'确认删除',
|
||||||
|
{
|
||||||
|
confirmButtonText: '确定',
|
||||||
|
cancelButtonText: '取消',
|
||||||
|
type: 'warning'
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const response = await deletePayment(payment.id)
|
||||||
|
|
||||||
|
if (response.data?.success) {
|
||||||
|
ElMessage.success('删除成功')
|
||||||
|
// 刷新支付记录列表
|
||||||
|
fetchPayments()
|
||||||
|
} else {
|
||||||
|
ElMessage.error(response.data?.message || '删除失败')
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
if (error !== 'cancel') {
|
||||||
|
console.error('Delete payment error:', error)
|
||||||
|
ElMessage.error('删除失败')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
fetchPayments()
|
fetchPayments()
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1808,7 +1808,7 @@ const restoreProcessingTask = async () => {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// 检查最近一条任务的状态(如果失败则显示失败状态,但不恢复输入参数)
|
// 检查最近一条任务的状态(如果失败则显示失败状态和参考图)
|
||||||
const checkLastTaskStatus = async () => {
|
const checkLastTaskStatus = async () => {
|
||||||
if (!userStore.isAuthenticated) return
|
if (!userStore.isAuthenticated) return
|
||||||
|
|
||||||
@@ -1817,14 +1817,30 @@ const checkLastTaskStatus = async () => {
|
|||||||
if (response.data && response.data.success && response.data.data && response.data.data.length > 0) {
|
if (response.data && response.data.success && response.data.data && response.data.data.length > 0) {
|
||||||
const lastTask = response.data.data[0]
|
const lastTask = response.data.data[0]
|
||||||
|
|
||||||
// 只关注 FAILED 状态,显示失败UI但不恢复输入参数
|
// 只关注 FAILED 状态,显示失败UI和参考图
|
||||||
if (lastTask.status === 'FAILED') {
|
if (lastTask.status === 'FAILED') {
|
||||||
console.log('[Last Task Failed]', lastTask)
|
console.log('[Last Task Failed]', lastTask)
|
||||||
|
|
||||||
currentTask.value = lastTask
|
currentTask.value = lastTask
|
||||||
taskStatus.value = 'FAILED'
|
taskStatus.value = 'FAILED'
|
||||||
// 不恢复输入参数,让用户可以自由创建新任务
|
|
||||||
|
// 恢复提示词,让用户看到失败任务的内容
|
||||||
|
if (lastTask.prompt) {
|
||||||
|
inputText.value = lastTask.prompt
|
||||||
|
}
|
||||||
|
// 恢复参考图
|
||||||
|
if (lastTask.imageUrl) {
|
||||||
|
generatedImageUrl.value = processHistoryUrl(lastTask.imageUrl)
|
||||||
|
}
|
||||||
|
// 恢复其他参数
|
||||||
|
if (lastTask.aspectRatio) {
|
||||||
|
aspectRatio.value = lastTask.aspectRatio
|
||||||
|
}
|
||||||
|
if (lastTask.hdMode !== undefined) {
|
||||||
|
hdMode.value = lastTask.hdMode
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
// 如果最近一条任务是成功的,不需要处理
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Check last task status error', error)
|
console.error('Check last task status error', error)
|
||||||
|
|||||||
@@ -78,7 +78,9 @@
|
|||||||
<el-icon><User /></el-icon>
|
<el-icon><User /></el-icon>
|
||||||
<span>{{ $t('systemSettings.membership') }}</span>
|
<span>{{ $t('systemSettings.membership') }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
<!-- 任务清理管理标签暂时隐藏 -->
|
||||||
<div
|
<div
|
||||||
|
v-if="false"
|
||||||
class="tab-item"
|
class="tab-item"
|
||||||
:class="{ active: activeTab === 'cleanup' }"
|
:class="{ active: activeTab === 'cleanup' }"
|
||||||
@click="activeTab = 'cleanup'"
|
@click="activeTab = 'cleanup'"
|
||||||
@@ -256,8 +258,34 @@
|
|||||||
<el-input v-model="promptOptimizationModel" style="width: 400px;" placeholder="gpt-5.1-thinking"></el-input>
|
<el-input v-model="promptOptimizationModel" style="width: 400px;" placeholder="gpt-5.1-thinking"></el-input>
|
||||||
<div class="model-tip">{{ $t('systemSettings.promptOptimizationModelTip') }}</div>
|
<div class="model-tip">{{ $t('systemSettings.promptOptimizationModelTip') }}</div>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
<el-form-item :label="$t('systemSettings.storyboardSystemPrompt')">
|
||||||
|
<el-input
|
||||||
|
v-model="storyboardSystemPrompt"
|
||||||
|
type="textarea"
|
||||||
|
:rows="4"
|
||||||
|
style="width: 500px;"
|
||||||
|
:placeholder="$t('systemSettings.storyboardSystemPromptPlaceholder')">
|
||||||
|
</el-input>
|
||||||
|
<div class="model-tip">{{ $t('systemSettings.storyboardSystemPromptTip') }}</div>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item :label="$t('systemSettings.promptOptimizationSystemPrompt')">
|
||||||
|
<el-input
|
||||||
|
v-model="promptOptimizationSystemPrompt"
|
||||||
|
type="textarea"
|
||||||
|
:rows="6"
|
||||||
|
style="width: 500px;"
|
||||||
|
:placeholder="$t('systemSettings.promptOptimizationSystemPromptPlaceholder')">
|
||||||
|
</el-input>
|
||||||
|
<div class="model-tip">{{ $t('systemSettings.promptOptimizationSystemPromptTip') }}</div>
|
||||||
|
</el-form-item>
|
||||||
<el-form-item>
|
<el-form-item>
|
||||||
<el-button type="primary" @click="saveAiModelSettings" :loading="savingAiModel">
|
<el-button
|
||||||
|
type="primary"
|
||||||
|
@click="saveAiModelSettings"
|
||||||
|
:loading="savingAiModel"
|
||||||
|
class="ai-save-btn"
|
||||||
|
>
|
||||||
|
<el-icon v-if="!savingAiModel"><Check /></el-icon>
|
||||||
{{ $t('common.save') }}
|
{{ $t('common.save') }}
|
||||||
</el-button>
|
</el-button>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
@@ -431,7 +459,8 @@ import {
|
|||||||
User as Search,
|
User as Search,
|
||||||
User as ArrowDown,
|
User as ArrowDown,
|
||||||
Delete,
|
Delete,
|
||||||
Refresh
|
Refresh,
|
||||||
|
Check
|
||||||
} from '@element-plus/icons-vue'
|
} from '@element-plus/icons-vue'
|
||||||
import cleanupApi from '@/api/cleanup'
|
import cleanupApi from '@/api/cleanup'
|
||||||
import { getMembershipLevels, updateMembershipLevel } from '@/api/members'
|
import { getMembershipLevels, updateMembershipLevel } from '@/api/members'
|
||||||
@@ -498,6 +527,8 @@ const cleanupConfig = reactive({
|
|||||||
// AI模型设置相关
|
// AI模型设置相关
|
||||||
const promptOptimizationModel = ref('gpt-5.1-thinking')
|
const promptOptimizationModel = ref('gpt-5.1-thinking')
|
||||||
const promptOptimizationApiUrl = ref('https://ai.comfly.chat')
|
const promptOptimizationApiUrl = ref('https://ai.comfly.chat')
|
||||||
|
const storyboardSystemPrompt = ref('')
|
||||||
|
const promptOptimizationSystemPrompt = ref('')
|
||||||
const savingAiModel = ref(false)
|
const savingAiModel = ref(false)
|
||||||
|
|
||||||
const goToDashboard = () => {
|
const goToDashboard = () => {
|
||||||
@@ -773,6 +804,12 @@ const loadAiModelSettings = async () => {
|
|||||||
if (data.promptOptimizationApiUrl) {
|
if (data.promptOptimizationApiUrl) {
|
||||||
promptOptimizationApiUrl.value = data.promptOptimizationApiUrl
|
promptOptimizationApiUrl.value = data.promptOptimizationApiUrl
|
||||||
}
|
}
|
||||||
|
if (data.storyboardSystemPrompt !== undefined) {
|
||||||
|
storyboardSystemPrompt.value = data.storyboardSystemPrompt
|
||||||
|
}
|
||||||
|
if (data.promptOptimizationSystemPrompt !== undefined) {
|
||||||
|
promptOptimizationSystemPrompt.value = data.promptOptimizationSystemPrompt
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('加载AI模型设置失败:', error)
|
console.error('加载AI模型设置失败:', error)
|
||||||
@@ -790,7 +827,9 @@ const saveAiModelSettings = async () => {
|
|||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
promptOptimizationModel: promptOptimizationModel.value,
|
promptOptimizationModel: promptOptimizationModel.value,
|
||||||
promptOptimizationApiUrl: promptOptimizationApiUrl.value
|
promptOptimizationApiUrl: promptOptimizationApiUrl.value,
|
||||||
|
storyboardSystemPrompt: storyboardSystemPrompt.value,
|
||||||
|
promptOptimizationSystemPrompt: promptOptimizationSystemPrompt.value
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
@@ -1419,6 +1458,38 @@ const fetchSystemStats = async () => {
|
|||||||
border-color: #40a9ff;
|
border-color: #40a9ff;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* AI模型设置保存按钮样式 */
|
||||||
|
.ai-save-btn {
|
||||||
|
width: auto !important;
|
||||||
|
min-width: 140px;
|
||||||
|
padding: 12px 32px !important;
|
||||||
|
font-size: 15px !important;
|
||||||
|
font-weight: 500;
|
||||||
|
border-radius: 8px !important;
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%) !important;
|
||||||
|
border: none !important;
|
||||||
|
box-shadow: 0 4px 15px rgba(102, 126, 234, 0.4);
|
||||||
|
transition: all 0.3s ease !important;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-save-btn:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 6px 20px rgba(102, 126, 234, 0.5);
|
||||||
|
background: linear-gradient(135deg, #5a6fd6 0%, #6a4190 100%) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-save-btn:active {
|
||||||
|
transform: translateY(0);
|
||||||
|
box-shadow: 0 2px 10px rgba(102, 126, 234, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-save-btn .el-icon {
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
/* 响应式调整 */
|
/* 响应式调整 */
|
||||||
@media (max-width: 480px) {
|
@media (max-width: 480px) {
|
||||||
.membership-modal {
|
.membership-modal {
|
||||||
|
|||||||
@@ -930,7 +930,7 @@ const restoreProcessingTask = async () => {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// 检查最近一条任务的状态(如果失败则显示失败状态,但不恢复输入参数)
|
// 检查最近一条任务的状态(如果失败则显示失败状态和提示词)
|
||||||
const checkLastTaskStatus = async () => {
|
const checkLastTaskStatus = async () => {
|
||||||
if (!userStore.isAuthenticated) return
|
if (!userStore.isAuthenticated) return
|
||||||
|
|
||||||
@@ -939,14 +939,29 @@ const checkLastTaskStatus = async () => {
|
|||||||
if (response.data && response.data.success && response.data.data && response.data.data.length > 0) {
|
if (response.data && response.data.success && response.data.data && response.data.data.length > 0) {
|
||||||
const lastTask = response.data.data[0]
|
const lastTask = response.data.data[0]
|
||||||
|
|
||||||
// 只关注 FAILED 状态,显示失败UI但不恢复输入参数
|
// 只关注 FAILED 状态,显示失败UI和提示词
|
||||||
if (lastTask.status === 'FAILED') {
|
if (lastTask.status === 'FAILED') {
|
||||||
console.log('[Last Task Failed]', lastTask)
|
console.log('[Last Task Failed]', lastTask)
|
||||||
|
|
||||||
currentTask.value = lastTask
|
currentTask.value = lastTask
|
||||||
taskStatus.value = 'FAILED'
|
taskStatus.value = 'FAILED'
|
||||||
// 不恢复输入参数,让用户可以自由创建新任务
|
|
||||||
|
// 恢复提示词,让用户看到失败任务的内容
|
||||||
|
if (lastTask.prompt) {
|
||||||
|
inputText.value = lastTask.prompt
|
||||||
|
}
|
||||||
|
// 恢复其他参数
|
||||||
|
if (lastTask.aspectRatio) {
|
||||||
|
aspectRatio.value = lastTask.aspectRatio
|
||||||
|
}
|
||||||
|
if (lastTask.duration) {
|
||||||
|
duration.value = lastTask.duration
|
||||||
|
}
|
||||||
|
if (lastTask.hdMode !== undefined) {
|
||||||
|
hdMode.value = lastTask.hdMode
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
// 如果最近一条任务是成功的,不需要处理
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Check last task status error', error)
|
console.error('Check last task status error', error)
|
||||||
|
|||||||
@@ -28,6 +28,7 @@
|
|||||||
</scm>
|
</scm>
|
||||||
<properties>
|
<properties>
|
||||||
<java.version>21</java.version>
|
<java.version>21</java.version>
|
||||||
|
<tomcat.version>10.1.34</tomcat.version>
|
||||||
</properties>
|
</properties>
|
||||||
<dependencies>
|
<dependencies>
|
||||||
<dependency>
|
<dependency>
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import org.springframework.web.bind.annotation.RestController;
|
|||||||
|
|
||||||
import com.example.demo.model.User;
|
import com.example.demo.model.User;
|
||||||
import com.example.demo.model.SystemSettings;
|
import com.example.demo.model.SystemSettings;
|
||||||
|
import com.example.demo.repository.TaskStatusRepository;
|
||||||
import com.example.demo.service.UserService;
|
import com.example.demo.service.UserService;
|
||||||
import com.example.demo.service.SystemSettingsService;
|
import com.example.demo.service.SystemSettingsService;
|
||||||
import com.example.demo.util.JwtUtils;
|
import com.example.demo.util.JwtUtils;
|
||||||
@@ -45,6 +46,9 @@ public class AdminController {
|
|||||||
@Autowired
|
@Autowired
|
||||||
private SystemSettingsService systemSettingsService;
|
private SystemSettingsService systemSettingsService;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private TaskStatusRepository taskStatusRepository;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 给用户增加积分
|
* 给用户增加积分
|
||||||
*/
|
*/
|
||||||
@@ -396,6 +400,8 @@ public class AdminController {
|
|||||||
|
|
||||||
response.put("promptOptimizationModel", settings.getPromptOptimizationModel());
|
response.put("promptOptimizationModel", settings.getPromptOptimizationModel());
|
||||||
response.put("promptOptimizationApiUrl", settings.getPromptOptimizationApiUrl());
|
response.put("promptOptimizationApiUrl", settings.getPromptOptimizationApiUrl());
|
||||||
|
response.put("promptOptimizationSystemPrompt", settings.getPromptOptimizationSystemPrompt());
|
||||||
|
response.put("storyboardSystemPrompt", settings.getStoryboardSystemPrompt());
|
||||||
response.put("siteName", settings.getSiteName());
|
response.put("siteName", settings.getSiteName());
|
||||||
response.put("siteSubtitle", settings.getSiteSubtitle());
|
response.put("siteSubtitle", settings.getSiteSubtitle());
|
||||||
response.put("registrationOpen", settings.getRegistrationOpen());
|
response.put("registrationOpen", settings.getRegistrationOpen());
|
||||||
@@ -437,6 +443,20 @@ public class AdminController {
|
|||||||
logger.info("更新优化提示词API端点为: {}", apiUrl);
|
logger.info("更新优化提示词API端点为: {}", apiUrl);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 更新分镜图系统引导词
|
||||||
|
if (settingsData.containsKey("storyboardSystemPrompt")) {
|
||||||
|
String prompt = (String) settingsData.get("storyboardSystemPrompt");
|
||||||
|
settings.setStoryboardSystemPrompt(prompt);
|
||||||
|
logger.info("更新分镜图系统引导词");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新优化提示词系统提示词
|
||||||
|
if (settingsData.containsKey("promptOptimizationSystemPrompt")) {
|
||||||
|
String prompt = (String) settingsData.get("promptOptimizationSystemPrompt");
|
||||||
|
settings.setPromptOptimizationSystemPrompt(prompt);
|
||||||
|
logger.info("更新优化提示词系统提示词");
|
||||||
|
}
|
||||||
|
|
||||||
systemSettingsService.update(settings);
|
systemSettingsService.update(settings);
|
||||||
|
|
||||||
response.put("success", true);
|
response.put("success", true);
|
||||||
@@ -450,5 +470,117 @@ public class AdminController {
|
|||||||
return ResponseEntity.status(500).body(response);
|
return ResponseEntity.status(500).body(response);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除单个任务记录
|
||||||
|
*/
|
||||||
|
@DeleteMapping("/tasks/{taskId}")
|
||||||
|
public ResponseEntity<Map<String, Object>> deleteTask(
|
||||||
|
@PathVariable String taskId,
|
||||||
|
@RequestHeader("Authorization") String token) {
|
||||||
|
|
||||||
|
Map<String, Object> response = new HashMap<>();
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 验证管理员权限
|
||||||
|
String actualToken = jwtUtils.extractTokenFromHeader(token);
|
||||||
|
if (actualToken == null) {
|
||||||
|
response.put("success", false);
|
||||||
|
response.put("message", "未授权访问");
|
||||||
|
return ResponseEntity.status(401).body(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
String username = jwtUtils.getUsernameFromToken(actualToken);
|
||||||
|
if (username == null || jwtUtils.isTokenExpired(actualToken)) {
|
||||||
|
response.put("success", false);
|
||||||
|
response.put("message", "Token无效或已过期");
|
||||||
|
return ResponseEntity.status(401).body(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
User admin = userService.findByUsername(username);
|
||||||
|
if (admin == null || !"ROLE_ADMIN".equals(admin.getRole())) {
|
||||||
|
response.put("success", false);
|
||||||
|
response.put("message", "需要管理员权限");
|
||||||
|
return ResponseEntity.status(403).body(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查找并删除任务
|
||||||
|
var taskOpt = taskStatusRepository.findByTaskId(taskId);
|
||||||
|
if (taskOpt.isPresent()) {
|
||||||
|
taskStatusRepository.delete(taskOpt.get());
|
||||||
|
logger.info("管理员 {} 删除了任务: {}", username, taskId);
|
||||||
|
response.put("success", true);
|
||||||
|
response.put("message", "任务删除成功");
|
||||||
|
return ResponseEntity.ok(response);
|
||||||
|
} else {
|
||||||
|
response.put("success", false);
|
||||||
|
response.put("message", "任务不存在");
|
||||||
|
return ResponseEntity.status(404).body(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.error("删除任务失败: {}", taskId, e);
|
||||||
|
response.put("success", false);
|
||||||
|
response.put("message", "删除任务失败: " + e.getMessage());
|
||||||
|
return ResponseEntity.status(500).body(response);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量删除任务记录
|
||||||
|
*/
|
||||||
|
@DeleteMapping("/tasks/batch")
|
||||||
|
public ResponseEntity<Map<String, Object>> batchDeleteTasks(
|
||||||
|
@RequestBody List<String> taskIds,
|
||||||
|
@RequestHeader("Authorization") String token) {
|
||||||
|
|
||||||
|
Map<String, Object> response = new HashMap<>();
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 验证管理员权限
|
||||||
|
String actualToken = jwtUtils.extractTokenFromHeader(token);
|
||||||
|
if (actualToken == null) {
|
||||||
|
response.put("success", false);
|
||||||
|
response.put("message", "未授权访问");
|
||||||
|
return ResponseEntity.status(401).body(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
String username = jwtUtils.getUsernameFromToken(actualToken);
|
||||||
|
if (username == null || jwtUtils.isTokenExpired(actualToken)) {
|
||||||
|
response.put("success", false);
|
||||||
|
response.put("message", "Token无效或已过期");
|
||||||
|
return ResponseEntity.status(401).body(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
User admin = userService.findByUsername(username);
|
||||||
|
if (admin == null || !"ROLE_ADMIN".equals(admin.getRole())) {
|
||||||
|
response.put("success", false);
|
||||||
|
response.put("message", "需要管理员权限");
|
||||||
|
return ResponseEntity.status(403).body(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 批量删除任务
|
||||||
|
int deletedCount = 0;
|
||||||
|
for (String taskId : taskIds) {
|
||||||
|
var taskOpt = taskStatusRepository.findByTaskId(taskId);
|
||||||
|
if (taskOpt.isPresent()) {
|
||||||
|
taskStatusRepository.delete(taskOpt.get());
|
||||||
|
deletedCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info("管理员 {} 批量删除了 {} 个任务", username, deletedCount);
|
||||||
|
response.put("success", true);
|
||||||
|
response.put("message", "成功删除 " + deletedCount + " 个任务");
|
||||||
|
response.put("deletedCount", deletedCount);
|
||||||
|
return ResponseEntity.ok(response);
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.error("批量删除任务失败", e);
|
||||||
|
response.put("success", false);
|
||||||
|
response.put("message", "批量删除任务失败: " + e.getMessage());
|
||||||
|
return ResponseEntity.status(500).body(response);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package com.example.demo.controller;
|
package com.example.demo.controller;
|
||||||
|
|
||||||
import com.example.demo.service.ImageGridService;
|
import com.example.demo.service.ImageGridService;
|
||||||
|
import com.example.demo.service.CosService;
|
||||||
import com.example.demo.util.JwtUtils;
|
import com.example.demo.util.JwtUtils;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
@@ -25,6 +26,9 @@ public class ImageGridApiController {
|
|||||||
@Autowired
|
@Autowired
|
||||||
private ImageGridService imageGridService;
|
private ImageGridService imageGridService;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private CosService cosService;
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
private JwtUtils jwtUtils;
|
private JwtUtils jwtUtils;
|
||||||
|
|
||||||
@@ -105,13 +109,44 @@ public class ImageGridApiController {
|
|||||||
|
|
||||||
logger.info("图片拼接成功,返回Base64长度: {}", mergedImage.length());
|
logger.info("图片拼接成功,返回Base64长度: {}", mergedImage.length());
|
||||||
|
|
||||||
|
// 上传到COS对象存储
|
||||||
|
String cosUrl = null;
|
||||||
|
if (cosService.isEnabled()) {
|
||||||
|
logger.debug("======== 开始上传图片到COS ========");
|
||||||
|
logger.debug("COS服务已启用,准备上传压缩后的图片");
|
||||||
|
try {
|
||||||
|
long startTime = System.currentTimeMillis();
|
||||||
|
cosUrl = cosService.uploadBase64Image(mergedImage, null);
|
||||||
|
long endTime = System.currentTimeMillis();
|
||||||
|
|
||||||
|
if (cosUrl != null) {
|
||||||
|
logger.info("======== COS上传成功 ========");
|
||||||
|
logger.info("公网访问链接: {}", cosUrl);
|
||||||
|
logger.info("上传耗时: {} ms", (endTime - startTime));
|
||||||
|
logger.info("================================");
|
||||||
|
} else {
|
||||||
|
logger.warn("COS上传返回空URL");
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.error("上传图片到COS失败: {}", e.getMessage(), e);
|
||||||
|
logger.warn("继续返回Base64数据");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
logger.debug("COS服务未启用,跳过上传");
|
||||||
|
}
|
||||||
|
|
||||||
response.put("success", true);
|
response.put("success", true);
|
||||||
response.put("message", "图片拼接成功");
|
response.put("message", "图片拼接成功");
|
||||||
response.put("data", Map.of(
|
|
||||||
"mergedImage", mergedImage,
|
// 构建返回数据
|
||||||
"imageCount", imageBase64List.size(),
|
Map<String, Object> data = new HashMap<>();
|
||||||
"cols", cols
|
data.put("mergedImage", mergedImage);
|
||||||
));
|
data.put("imageCount", imageBase64List.size());
|
||||||
|
data.put("cols", cols);
|
||||||
|
if (cosUrl != null) {
|
||||||
|
data.put("cosUrl", cosUrl); // 返回COS链接
|
||||||
|
}
|
||||||
|
response.put("data", data);
|
||||||
|
|
||||||
return ResponseEntity.ok(response);
|
return ResponseEntity.ok(response);
|
||||||
|
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ import com.example.demo.service.OrderService;
|
|||||||
import com.example.demo.service.UserService;
|
import com.example.demo.service.UserService;
|
||||||
|
|
||||||
import jakarta.validation.Valid;
|
import jakarta.validation.Valid;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/api/orders")
|
@RequestMapping("/api/orders")
|
||||||
@@ -47,12 +48,14 @@ public class OrderApiController {
|
|||||||
* 获取订单列表
|
* 获取订单列表
|
||||||
*/
|
*/
|
||||||
@GetMapping
|
@GetMapping
|
||||||
|
@Transactional(readOnly = true)
|
||||||
public ResponseEntity<Map<String, Object>> getOrders(
|
public ResponseEntity<Map<String, Object>> getOrders(
|
||||||
@RequestParam(defaultValue = "0") int page,
|
@RequestParam(defaultValue = "0") int page,
|
||||||
@RequestParam(defaultValue = "10") int size,
|
@RequestParam(defaultValue = "10") int size,
|
||||||
@RequestParam(defaultValue = "createdAt") String sortBy,
|
@RequestParam(defaultValue = "createdAt") String sortBy,
|
||||||
@RequestParam(defaultValue = "desc") String sortDir,
|
@RequestParam(defaultValue = "desc") String sortDir,
|
||||||
@RequestParam(required = false) OrderStatus status,
|
@RequestParam(required = false) OrderStatus status,
|
||||||
|
@RequestParam(required = false) String type,
|
||||||
@RequestParam(required = false) String search,
|
@RequestParam(required = false) String search,
|
||||||
Authentication authentication) {
|
Authentication authentication) {
|
||||||
try {
|
try {
|
||||||
@@ -81,10 +84,10 @@ public class OrderApiController {
|
|||||||
Page<Order> orderPage;
|
Page<Order> orderPage;
|
||||||
if (user.getRole().equals("ROLE_ADMIN")) {
|
if (user.getRole().equals("ROLE_ADMIN")) {
|
||||||
// 管理员可以查看所有订单
|
// 管理员可以查看所有订单
|
||||||
orderPage = orderService.findAllOrders(pageable, status, search);
|
orderPage = orderService.findAllOrders(pageable, status, type, search);
|
||||||
} else {
|
} else {
|
||||||
// 普通用户只能查看自己的订单
|
// 普通用户只能查看自己的订单
|
||||||
orderPage = orderService.findOrdersByUser(user, pageable, status, search);
|
orderPage = orderService.findOrdersByUser(user, pageable, status, type, search);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 转换订单数据,添加支付方式信息
|
// 转换订单数据,添加支付方式信息
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import org.slf4j.LoggerFactory;
|
|||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.security.core.Authentication;
|
import org.springframework.security.core.Authentication;
|
||||||
|
import org.springframework.web.bind.annotation.DeleteMapping;
|
||||||
import org.springframework.web.bind.annotation.GetMapping;
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
import org.springframework.web.bind.annotation.PathVariable;
|
import org.springframework.web.bind.annotation.PathVariable;
|
||||||
import org.springframework.web.bind.annotation.PostMapping;
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
@@ -644,6 +645,78 @@ public class PaymentApiController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除支付记录(仅管理员)
|
||||||
|
*/
|
||||||
|
@DeleteMapping("/{paymentId}")
|
||||||
|
public ResponseEntity<Map<String, Object>> deletePayment(
|
||||||
|
@PathVariable Long paymentId,
|
||||||
|
Authentication authentication) {
|
||||||
|
try {
|
||||||
|
String username = authentication.getName();
|
||||||
|
User user = userService.findByUsername(username);
|
||||||
|
|
||||||
|
// 只有管理员可以删除
|
||||||
|
if (!"ROLE_ADMIN".equals(user.getRole())) {
|
||||||
|
return ResponseEntity.status(403)
|
||||||
|
.body(createErrorResponse("无权限删除支付记录"));
|
||||||
|
}
|
||||||
|
|
||||||
|
paymentRepository.deleteById(paymentId);
|
||||||
|
logger.info("管理员 {} 删除了支付记录: {}", username, paymentId);
|
||||||
|
|
||||||
|
Map<String, Object> response = new HashMap<>();
|
||||||
|
response.put("success", true);
|
||||||
|
response.put("message", "支付记录删除成功");
|
||||||
|
return ResponseEntity.ok(response);
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.error("删除支付记录失败: {}", paymentId, e);
|
||||||
|
return ResponseEntity.badRequest()
|
||||||
|
.body(createErrorResponse("删除支付记录失败: " + e.getMessage()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量删除支付记录(仅管理员)
|
||||||
|
*/
|
||||||
|
@DeleteMapping("/batch")
|
||||||
|
public ResponseEntity<Map<String, Object>> deletePayments(
|
||||||
|
@RequestBody List<Long> paymentIds,
|
||||||
|
Authentication authentication) {
|
||||||
|
try {
|
||||||
|
String username = authentication.getName();
|
||||||
|
User user = userService.findByUsername(username);
|
||||||
|
|
||||||
|
// 只有管理员可以删除
|
||||||
|
if (!"ROLE_ADMIN".equals(user.getRole())) {
|
||||||
|
return ResponseEntity.status(403)
|
||||||
|
.body(createErrorResponse("无权限批量删除支付记录"));
|
||||||
|
}
|
||||||
|
|
||||||
|
int deletedCount = 0;
|
||||||
|
for (Long paymentId : paymentIds) {
|
||||||
|
try {
|
||||||
|
paymentRepository.deleteById(paymentId);
|
||||||
|
deletedCount++;
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.warn("删除支付记录 {} 失败: {}", paymentId, e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info("管理员 {} 批量删除了 {} 个支付记录", username, deletedCount);
|
||||||
|
|
||||||
|
Map<String, Object> response = new HashMap<>();
|
||||||
|
response.put("success", true);
|
||||||
|
response.put("message", "成功删除 " + deletedCount + " 个支付记录");
|
||||||
|
response.put("deletedCount", deletedCount);
|
||||||
|
return ResponseEntity.ok(response);
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.error("批量删除支付记录失败", e);
|
||||||
|
return ResponseEntity.badRequest()
|
||||||
|
.body(createErrorResponse("批量删除支付记录失败: " + e.getMessage()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private Map<String, Object> createErrorResponse(String message) {
|
private Map<String, Object> createErrorResponse(String message) {
|
||||||
Map<String, Object> response = new HashMap<>();
|
Map<String, Object> response = new HashMap<>();
|
||||||
response.put("success", false);
|
response.put("success", false);
|
||||||
|
|||||||
@@ -22,7 +22,9 @@ import org.springframework.web.bind.annotation.RequestParam;
|
|||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
import com.example.demo.model.TaskStatus;
|
import com.example.demo.model.TaskStatus;
|
||||||
|
import com.example.demo.model.User;
|
||||||
import com.example.demo.repository.TaskStatusRepository;
|
import com.example.demo.repository.TaskStatusRepository;
|
||||||
|
import com.example.demo.repository.UserRepository;
|
||||||
import com.example.demo.service.TaskStatusPollingService;
|
import com.example.demo.service.TaskStatusPollingService;
|
||||||
import com.example.demo.util.JwtUtils;
|
import com.example.demo.util.JwtUtils;
|
||||||
|
|
||||||
@@ -42,6 +44,9 @@ public class TaskStatusApiController {
|
|||||||
@Autowired
|
@Autowired
|
||||||
private JwtUtils jwtUtils;
|
private JwtUtils jwtUtils;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private UserRepository userRepository;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取任务状态
|
* 获取任务状态
|
||||||
*/
|
*/
|
||||||
@@ -237,7 +242,24 @@ public class TaskStatusApiController {
|
|||||||
Map<String, Object> record = new HashMap<>();
|
Map<String, Object> record = new HashMap<>();
|
||||||
record.put("id", task.getId());
|
record.put("id", task.getId());
|
||||||
record.put("taskId", task.getTaskId());
|
record.put("taskId", task.getTaskId());
|
||||||
record.put("username", task.getUsername());
|
|
||||||
|
// 通过存储的用户名查询真实的系统用户名
|
||||||
|
// 先尝试按 username 查找,如果找不到再按 nickname 查找
|
||||||
|
String storedValue = task.getUsername();
|
||||||
|
String displayUsername = storedValue;
|
||||||
|
try {
|
||||||
|
User user = userRepository.findByUsername(storedValue).orElse(null);
|
||||||
|
if (user == null) {
|
||||||
|
// 尝试按昵称查找
|
||||||
|
user = userRepository.findByNickname(storedValue).orElse(null);
|
||||||
|
}
|
||||||
|
if (user != null) {
|
||||||
|
displayUsername = user.getUsername();
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.debug("查询用户失败: {}", storedValue);
|
||||||
|
}
|
||||||
|
record.put("username", displayUsername);
|
||||||
record.put("type", task.getTaskType() != null ? task.getTaskType().getDescription() : "未知");
|
record.put("type", task.getTaskType() != null ? task.getTaskType().getDescription() : "未知");
|
||||||
record.put("taskType", task.getTaskType() != null ? task.getTaskType().name() : null);
|
record.put("taskType", task.getTaskType() != null ? task.getTaskType().name() : null);
|
||||||
record.put("status", task.getStatus().name());
|
record.put("status", task.getStatus().name());
|
||||||
|
|||||||
@@ -74,6 +74,14 @@ public class SystemSettings {
|
|||||||
@Column(length = 200)
|
@Column(length = 200)
|
||||||
private String promptOptimizationApiUrl = "https://ai.comfly.chat";
|
private String promptOptimizationApiUrl = "https://ai.comfly.chat";
|
||||||
|
|
||||||
|
/** 分镜图生成系统引导词 */
|
||||||
|
@Column(length = 2000)
|
||||||
|
private String storyboardSystemPrompt = "";
|
||||||
|
|
||||||
|
/** 优化提示词功能的系统提示词(指导AI如何优化) */
|
||||||
|
@Column(length = 4000)
|
||||||
|
private String promptOptimizationSystemPrompt = "";
|
||||||
|
|
||||||
public Long getId() {
|
public Long getId() {
|
||||||
return id;
|
return id;
|
||||||
}
|
}
|
||||||
@@ -177,6 +185,22 @@ public class SystemSettings {
|
|||||||
public void setPromptOptimizationApiUrl(String promptOptimizationApiUrl) {
|
public void setPromptOptimizationApiUrl(String promptOptimizationApiUrl) {
|
||||||
this.promptOptimizationApiUrl = promptOptimizationApiUrl;
|
this.promptOptimizationApiUrl = promptOptimizationApiUrl;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String getStoryboardSystemPrompt() {
|
||||||
|
return storyboardSystemPrompt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setStoryboardSystemPrompt(String storyboardSystemPrompt) {
|
||||||
|
this.storyboardSystemPrompt = storyboardSystemPrompt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getPromptOptimizationSystemPrompt() {
|
||||||
|
return promptOptimizationSystemPrompt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setPromptOptimizationSystemPrompt(String promptOptimizationSystemPrompt) {
|
||||||
|
this.promptOptimizationSystemPrompt = promptOptimizationSystemPrompt;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -248,7 +248,7 @@ public class TaskStatus {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void markAsTimeout() {
|
public void markAsTimeout() {
|
||||||
this.status = Status.TIMEOUT;
|
this.status = Status.FAILED; // 超时也标记为 FAILED,便于前端统一处理
|
||||||
this.errorMessage = "任务超时,超过最大轮询次数";
|
this.errorMessage = "任务超时,超过最大轮询次数";
|
||||||
this.updatedAt = LocalDateTime.now();
|
this.updatedAt = LocalDateTime.now();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ public class UserWork {
|
|||||||
@Column(name = "username", nullable = false, length = 100)
|
@Column(name = "username", nullable = false, length = 100)
|
||||||
private String username;
|
private String username;
|
||||||
|
|
||||||
@Column(name = "task_id", nullable = false, length = 50)
|
@Column(name = "task_id", nullable = false, length = 50, unique = true)
|
||||||
private String taskId;
|
private String taskId;
|
||||||
|
|
||||||
@Enumerated(EnumType.STRING)
|
@Enumerated(EnumType.STRING)
|
||||||
|
|||||||
@@ -202,4 +202,62 @@ public interface OrderRepository extends JpaRepository<Order, Long> {
|
|||||||
*/
|
*/
|
||||||
@Query("SELECT COUNT(DISTINCT o.user.id) FROM Order o WHERE o.createdAt BETWEEN :startTime AND :endTime")
|
@Query("SELECT COUNT(DISTINCT o.user.id) FROM Order o WHERE o.createdAt BETWEEN :startTime AND :endTime")
|
||||||
long countDistinctUsersByCreatedAtBetween(@Param("startTime") LocalDateTime startTime, @Param("endTime") LocalDateTime endTime);
|
long countDistinctUsersByCreatedAtBetween(@Param("startTime") LocalDateTime startTime, @Param("endTime") LocalDateTime endTime);
|
||||||
|
|
||||||
|
// ============ 使用 JOIN FETCH 预加载 User 的查询方法 ============
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分页查找所有订单(预加载User)
|
||||||
|
*/
|
||||||
|
@Query(value = "SELECT o FROM Order o LEFT JOIN FETCH o.user",
|
||||||
|
countQuery = "SELECT COUNT(o) FROM Order o")
|
||||||
|
Page<Order> findAllWithUser(Pageable pageable);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据状态分页查找订单(预加载User)
|
||||||
|
*/
|
||||||
|
@Query(value = "SELECT o FROM Order o LEFT JOIN FETCH o.user WHERE o.status = :status",
|
||||||
|
countQuery = "SELECT COUNT(o) FROM Order o WHERE o.status = :status")
|
||||||
|
Page<Order> findByStatusWithUser(@Param("status") OrderStatus status, Pageable pageable);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据订单号模糊查询(分页,预加载User)
|
||||||
|
*/
|
||||||
|
@Query(value = "SELECT o FROM Order o LEFT JOIN FETCH o.user WHERE LOWER(o.orderNumber) LIKE LOWER(CONCAT('%', :orderNumber, '%'))",
|
||||||
|
countQuery = "SELECT COUNT(o) FROM Order o WHERE LOWER(o.orderNumber) LIKE LOWER(CONCAT('%', :orderNumber, '%'))")
|
||||||
|
Page<Order> findByOrderNumberContainingIgnoreCaseWithUser(@Param("orderNumber") String orderNumber, Pageable pageable);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据状态和订单号模糊查询(分页,预加载User)
|
||||||
|
*/
|
||||||
|
@Query(value = "SELECT o FROM Order o LEFT JOIN FETCH o.user WHERE o.status = :status AND LOWER(o.orderNumber) LIKE LOWER(CONCAT('%', :orderNumber, '%'))",
|
||||||
|
countQuery = "SELECT COUNT(o) FROM Order o WHERE o.status = :status AND LOWER(o.orderNumber) LIKE LOWER(CONCAT('%', :orderNumber, '%'))")
|
||||||
|
Page<Order> findByStatusAndOrderNumberContainingIgnoreCaseWithUser(@Param("status") OrderStatus status, @Param("orderNumber") String orderNumber, Pageable pageable);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据用户查找订单(分页,预加载User)
|
||||||
|
*/
|
||||||
|
@Query(value = "SELECT o FROM Order o LEFT JOIN FETCH o.user WHERE o.user = :user",
|
||||||
|
countQuery = "SELECT COUNT(o) FROM Order o WHERE o.user = :user")
|
||||||
|
Page<Order> findByUserWithUser(@Param("user") User user, Pageable pageable);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据用户和状态查找订单(分页,预加载User)
|
||||||
|
*/
|
||||||
|
@Query(value = "SELECT o FROM Order o LEFT JOIN FETCH o.user WHERE o.user = :user AND o.status = :status",
|
||||||
|
countQuery = "SELECT COUNT(o) FROM Order o WHERE o.user = :user AND o.status = :status")
|
||||||
|
Page<Order> findByUserAndStatusWithUser(@Param("user") User user, @Param("status") OrderStatus status, Pageable pageable);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据用户和订单号模糊查询(分页,预加载User)
|
||||||
|
*/
|
||||||
|
@Query(value = "SELECT o FROM Order o LEFT JOIN FETCH o.user WHERE o.user = :user AND LOWER(o.orderNumber) LIKE LOWER(CONCAT('%', :orderNumber, '%'))",
|
||||||
|
countQuery = "SELECT COUNT(o) FROM Order o WHERE o.user = :user AND LOWER(o.orderNumber) LIKE LOWER(CONCAT('%', :orderNumber, '%'))")
|
||||||
|
Page<Order> findByUserAndOrderNumberContainingIgnoreCaseWithUser(@Param("user") User user, @Param("orderNumber") String orderNumber, Pageable pageable);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据用户、状态和订单号模糊查询(分页,预加载User)
|
||||||
|
*/
|
||||||
|
@Query(value = "SELECT o FROM Order o LEFT JOIN FETCH o.user WHERE o.user = :user AND o.status = :status AND LOWER(o.orderNumber) LIKE LOWER(CONCAT('%', :orderNumber, '%'))",
|
||||||
|
countQuery = "SELECT COUNT(o) FROM Order o WHERE o.user = :user AND o.status = :status AND LOWER(o.orderNumber) LIKE LOWER(CONCAT('%', :orderNumber, '%'))")
|
||||||
|
Page<Order> findByUserAndStatusAndOrderNumberContainingIgnoreCaseWithUser(@Param("user") User user, @Param("status") OrderStatus status, @Param("orderNumber") String orderNumber, Pageable pageable);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -61,6 +61,11 @@ public interface TaskStatusRepository extends JpaRepository<TaskStatus, Long> {
|
|||||||
*/
|
*/
|
||||||
long countByStatus(TaskStatus.Status status);
|
long countByStatus(TaskStatus.Status status);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据状态查找所有任务(不分页)
|
||||||
|
*/
|
||||||
|
List<TaskStatus> findAllByStatus(TaskStatus.Status status);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 统计用户指定状态的任务数量
|
* 统计用户指定状态的任务数量
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import com.example.demo.model.User;
|
|||||||
|
|
||||||
public interface UserRepository extends JpaRepository<User, Long> {
|
public interface UserRepository extends JpaRepository<User, Long> {
|
||||||
Optional<User> findByUsername(String username);
|
Optional<User> findByUsername(String username);
|
||||||
|
Optional<User> findByNickname(String nickname);
|
||||||
Optional<User> findByEmail(String email);
|
Optional<User> findByEmail(String email);
|
||||||
Optional<User> findByPhone(String phone);
|
Optional<User> findByPhone(String phone);
|
||||||
Optional<User> findByUserId(String userId);
|
Optional<User> findByUserId(String userId);
|
||||||
|
|||||||
@@ -71,19 +71,25 @@ public class TaskQueueScheduler {
|
|||||||
try {
|
try {
|
||||||
// 新策略:仅在任务队列中存在待处理任务时才进行轮询查询
|
// 新策略:仅在任务队列中存在待处理任务时才进行轮询查询
|
||||||
boolean hasQueueTasks = taskQueueService.hasTasksToCheck();
|
boolean hasQueueTasks = taskQueueService.hasTasksToCheck();
|
||||||
if (!hasQueueTasks) {
|
long processingStatusCount = taskStatusRepository.countByStatus(com.example.demo.model.TaskStatus.Status.PROCESSING);
|
||||||
// 没有待处理任务,静默跳过轮询,不输出日志以减少噪音
|
|
||||||
|
logger.info("[轮询调度] TaskQueue待检查任务: {}, TaskStatus处理中任务: {}",
|
||||||
|
hasQueueTasks, processingStatusCount);
|
||||||
|
|
||||||
|
if (!hasQueueTasks && processingStatusCount == 0) {
|
||||||
|
// 没有待处理任务,静默跳过轮询
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.debug("发现待处理任务,开始轮询查询");
|
// 队列中有任务:检查队列内任务状态
|
||||||
|
if (hasQueueTasks) {
|
||||||
|
logger.info("[轮询调度] 开始检查TaskQueue任务状态");
|
||||||
|
taskQueueService.checkTaskStatuses();
|
||||||
|
}
|
||||||
|
|
||||||
// 队列中有任务:检查队列内任务状态,并在必要时调用状态轮询(如果存在正在PROCESSING的任务)
|
// TaskStatus表中有处理中任务:调用轮询服务
|
||||||
taskQueueService.checkTaskStatuses();
|
|
||||||
|
|
||||||
long processingStatusCount = taskStatusRepository.countByStatus(com.example.demo.model.TaskStatus.Status.PROCESSING);
|
|
||||||
if (processingStatusCount > 0) {
|
if (processingStatusCount > 0) {
|
||||||
// TaskStatusPollingService 的 @Scheduled 已被禁用,统一由此处调用
|
logger.info("[轮询调度] 开始轮询TaskStatus任务");
|
||||||
taskStatusPollingService.pollTaskStatuses();
|
taskStatusPollingService.pollTaskStatuses();
|
||||||
}
|
}
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
|
|||||||
@@ -97,6 +97,104 @@ public class CosService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 上传Base64图片到COS
|
||||||
|
*
|
||||||
|
* @param base64Data Base64编码的图片数据(可以带或不带data URI前缀)
|
||||||
|
* @param filename 文件名(可选,如果为null则自动生成)
|
||||||
|
* @return COS文件URL,如果失败则返回null
|
||||||
|
*/
|
||||||
|
public String uploadBase64Image(String base64Data, String filename) {
|
||||||
|
if (!isEnabled()) {
|
||||||
|
logger.warn("COS未启用,跳过上传");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 解析Base64数据
|
||||||
|
String actualBase64 = base64Data;
|
||||||
|
String contentType = "image/png"; // 默认类型
|
||||||
|
|
||||||
|
// 如果有data URI前缀,解析它
|
||||||
|
if (base64Data.startsWith("data:")) {
|
||||||
|
int commaIndex = base64Data.indexOf(',');
|
||||||
|
if (commaIndex > 0) {
|
||||||
|
String header = base64Data.substring(0, commaIndex);
|
||||||
|
actualBase64 = base64Data.substring(commaIndex + 1);
|
||||||
|
// 解析内容类型
|
||||||
|
if (header.contains("image/jpeg") || header.contains("image/jpg")) {
|
||||||
|
contentType = "image/jpeg";
|
||||||
|
} else if (header.contains("image/png")) {
|
||||||
|
contentType = "image/png";
|
||||||
|
} else if (header.contains("image/gif")) {
|
||||||
|
contentType = "image/gif";
|
||||||
|
} else if (header.contains("image/webp")) {
|
||||||
|
contentType = "image/webp";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 解码Base64
|
||||||
|
byte[] imageBytes = java.util.Base64.getDecoder().decode(actualBase64);
|
||||||
|
logger.info("解码Base64图片成功,大小: {} KB", imageBytes.length / 1024);
|
||||||
|
|
||||||
|
// 生成文件名
|
||||||
|
if (filename == null || filename.isEmpty()) {
|
||||||
|
String extension = contentType.equals("image/jpeg") ? ".jpg" :
|
||||||
|
contentType.equals("image/png") ? ".png" :
|
||||||
|
contentType.equals("image/gif") ? ".gif" : ".png";
|
||||||
|
filename = generateFilename(extension);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 上传到COS(使用images目录)
|
||||||
|
return uploadImageBytes(imageBytes, filename, contentType);
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.error("上传Base64图片到COS失败", e);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 上传图片字节数组到COS(存储在images目录下)
|
||||||
|
*/
|
||||||
|
private String uploadImageBytes(byte[] bytes, String filename, String contentType) {
|
||||||
|
try {
|
||||||
|
// 构建对象键(图片使用 images 目录)
|
||||||
|
LocalDate now = LocalDate.now();
|
||||||
|
String datePath = now.format(DateTimeFormatter.ofPattern("yyyy/MM/dd"));
|
||||||
|
String key = "images/" + datePath + "/" + filename;
|
||||||
|
|
||||||
|
// 设置元数据
|
||||||
|
ObjectMetadata metadata = new ObjectMetadata();
|
||||||
|
metadata.setContentLength(bytes.length);
|
||||||
|
metadata.setContentType(contentType);
|
||||||
|
|
||||||
|
// 创建上传请求
|
||||||
|
PutObjectRequest putObjectRequest = new PutObjectRequest(
|
||||||
|
cosConfig.getBucketName(),
|
||||||
|
key,
|
||||||
|
new ByteArrayInputStream(bytes),
|
||||||
|
metadata
|
||||||
|
);
|
||||||
|
|
||||||
|
// 执行上传
|
||||||
|
logger.info("开始上传图片到COS: bucket={}, key={}", cosConfig.getBucketName(), key);
|
||||||
|
PutObjectResult result = cosClient.putObject(putObjectRequest);
|
||||||
|
logger.info("图片上传成功,ETag: {}", result.getETag());
|
||||||
|
|
||||||
|
// 生成访问URL
|
||||||
|
String fileUrl = getPublicUrl(key);
|
||||||
|
logger.info("图片URL: {}", fileUrl);
|
||||||
|
|
||||||
|
return fileUrl;
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.error("上传图片到COS失败: {}", filename, e);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 上传字节数组到COS
|
* 上传字节数组到COS
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -49,6 +49,9 @@ public class ImageToVideoService {
|
|||||||
@Autowired
|
@Autowired
|
||||||
private UserService userService;
|
private UserService userService;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private CosService cosService;
|
||||||
|
|
||||||
@Value("${app.upload.path:/uploads}")
|
@Value("${app.upload.path:/uploads}")
|
||||||
private String uploadPath;
|
private String uploadPath;
|
||||||
|
|
||||||
@@ -415,6 +418,7 @@ public class ImageToVideoService {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 保存图片文件
|
* 保存图片文件
|
||||||
|
* 如果COS启用,会同时上传到COS并返回COS URL
|
||||||
*/
|
*/
|
||||||
private String saveImage(MultipartFile file, String taskId, String type) throws IOException {
|
private String saveImage(MultipartFile file, String taskId, String type) throws IOException {
|
||||||
// 解析上传目录:如果配置的是相对路径,则相对于应用当前工作目录
|
// 解析上传目录:如果配置的是相对路径,则相对于应用当前工作目录
|
||||||
@@ -439,7 +443,7 @@ public class ImageToVideoService {
|
|||||||
String extension = getFileExtension(originalFilename);
|
String extension = getFileExtension(originalFilename);
|
||||||
String filename = type + "_" + System.currentTimeMillis() + extension;
|
String filename = type + "_" + System.currentTimeMillis() + extension;
|
||||||
|
|
||||||
// 保存文件(覆盖同名文件的行为由 Files.copy 决定,此处不启用 REPLACE_EXISTING)
|
// 保存文件到本地(覆盖同名文件的行为由 Files.copy 决定,此处不启用 REPLACE_EXISTING)
|
||||||
Path filePath = taskDir.resolve(filename);
|
Path filePath = taskDir.resolve(filename);
|
||||||
try {
|
try {
|
||||||
Files.copy(file.getInputStream(), filePath);
|
Files.copy(file.getInputStream(), filePath);
|
||||||
@@ -448,10 +452,42 @@ public class ImageToVideoService {
|
|||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 返回前端可访问的相对URL(由 WebMvcConfig 映射 /uploads/** -> upload 目录)
|
// 本地URL
|
||||||
// 确保使用统一的URL前缀 /uploads
|
String localUrlPath = "/uploads/" + taskId + "/" + filename;
|
||||||
String urlPath = "/uploads/" + taskId + "/" + filename;
|
|
||||||
return urlPath;
|
// 检查COS状态并记录日志
|
||||||
|
logger.info("检查COS状态: enabled={}, taskId={}", cosService.isEnabled(), taskId);
|
||||||
|
|
||||||
|
// 如果COS启用,上传到COS并返回COS URL
|
||||||
|
if (cosService.isEnabled()) {
|
||||||
|
try {
|
||||||
|
// 读取文件字节
|
||||||
|
byte[] imageBytes = file.getBytes();
|
||||||
|
String contentType = file.getContentType();
|
||||||
|
if (contentType == null || contentType.isEmpty()) {
|
||||||
|
contentType = "image/jpeg";
|
||||||
|
}
|
||||||
|
|
||||||
|
// 上传到COS
|
||||||
|
String cosFilename = taskId + "_" + filename;
|
||||||
|
String cosUrl = cosService.uploadBase64Image(
|
||||||
|
"data:" + contentType + ";base64," + java.util.Base64.getEncoder().encodeToString(imageBytes),
|
||||||
|
cosFilename
|
||||||
|
);
|
||||||
|
|
||||||
|
if (cosUrl != null && !cosUrl.isEmpty()) {
|
||||||
|
logger.info("首帧图片上传COS成功: taskId={}, cosUrl={}", taskId, cosUrl);
|
||||||
|
return cosUrl;
|
||||||
|
} else {
|
||||||
|
logger.warn("上传首帧图片到COS失败,使用本地URL: taskId={}", taskId);
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.error("上传首帧图片到COS异常,使用本地URL: taskId={}", taskId, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 返回本地URL(COS未启用或上传失败时)
|
||||||
|
return localUrlPath;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -444,34 +444,41 @@ public class OrderService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 分页查找所有订单(支持状态和搜索筛选)
|
* 分页查找所有订单(支持状态、类型和搜索筛选,预加载User)
|
||||||
*/
|
*/
|
||||||
@Transactional(readOnly = true)
|
@Transactional(readOnly = true)
|
||||||
public Page<Order> findAllOrders(Pageable pageable, OrderStatus status, String search) {
|
public Page<Order> findAllOrders(Pageable pageable, OrderStatus status, String type, String search) {
|
||||||
|
// 基础查询
|
||||||
|
Page<Order> result;
|
||||||
if (status != null && search != null && !search.trim().isEmpty()) {
|
if (status != null && search != null && !search.trim().isEmpty()) {
|
||||||
return orderRepository.findByStatusAndOrderNumberContainingIgnoreCase(status, search, pageable);
|
result = orderRepository.findByStatusAndOrderNumberContainingIgnoreCaseWithUser(status, search, pageable);
|
||||||
} else if (status != null) {
|
} else if (status != null) {
|
||||||
return orderRepository.findByStatus(status, pageable);
|
result = orderRepository.findByStatusWithUser(status, pageable);
|
||||||
} else if (search != null && !search.trim().isEmpty()) {
|
} else if (search != null && !search.trim().isEmpty()) {
|
||||||
return orderRepository.findByOrderNumberContainingIgnoreCase(search, pageable);
|
result = orderRepository.findByOrderNumberContainingIgnoreCaseWithUser(search, pageable);
|
||||||
} else {
|
} else {
|
||||||
return orderRepository.findAll(pageable);
|
result = orderRepository.findAllWithUser(pageable);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 类型筛选(如果指定了类型)
|
||||||
|
// 注意:这种方式在分页时可能导致每页数量不准确,但对于简单场景足够
|
||||||
|
// 如需精确分页,需要在 Repository 层添加对应查询方法
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 分页查找用户的订单(支持状态和搜索筛选)
|
* 分页查找用户的订单(支持状态、类型和搜索筛选,预加载User)
|
||||||
*/
|
*/
|
||||||
@Transactional(readOnly = true)
|
@Transactional(readOnly = true)
|
||||||
public Page<Order> findOrdersByUser(User user, Pageable pageable, OrderStatus status, String search) {
|
public Page<Order> findOrdersByUser(User user, Pageable pageable, OrderStatus status, String type, String search) {
|
||||||
if (status != null && search != null && !search.trim().isEmpty()) {
|
if (status != null && search != null && !search.trim().isEmpty()) {
|
||||||
return orderRepository.findByUserAndStatusAndOrderNumberContainingIgnoreCase(user, status, search, pageable);
|
return orderRepository.findByUserAndStatusAndOrderNumberContainingIgnoreCaseWithUser(user, status, search, pageable);
|
||||||
} else if (status != null) {
|
} else if (status != null) {
|
||||||
return orderRepository.findByUserAndStatus(user, status, pageable);
|
return orderRepository.findByUserAndStatusWithUser(user, status, pageable);
|
||||||
} else if (search != null && !search.trim().isEmpty()) {
|
} else if (search != null && !search.trim().isEmpty()) {
|
||||||
return orderRepository.findByUserAndOrderNumberContainingIgnoreCase(user, search, pageable);
|
return orderRepository.findByUserAndOrderNumberContainingIgnoreCaseWithUser(user, search, pageable);
|
||||||
} else {
|
} else {
|
||||||
return orderRepository.findByUser(user, pageable);
|
return orderRepository.findByUserWithUser(user, pageable);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import org.springframework.beans.factory.annotation.Value;
|
|||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
import com.example.demo.config.DynamicApiConfig;
|
import com.example.demo.config.DynamicApiConfig;
|
||||||
|
import com.example.demo.model.SystemSettings;
|
||||||
|
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
|
||||||
@@ -881,6 +882,19 @@ public class RealAIService {
|
|||||||
logger.info("提交文生图任务: prompt={}, aspectRatio={}, hdMode={}, imageModel={}",
|
logger.info("提交文生图任务: prompt={}, aspectRatio={}, hdMode={}, imageModel={}",
|
||||||
prompt, aspectRatio, hdMode, imageModel);
|
prompt, aspectRatio, hdMode, imageModel);
|
||||||
|
|
||||||
|
// 获取系统引导词并拼接到用户提示词
|
||||||
|
String finalPrompt = prompt;
|
||||||
|
try {
|
||||||
|
SystemSettings settings = systemSettingsService.getOrCreate();
|
||||||
|
String systemPrompt = settings.getStoryboardSystemPrompt();
|
||||||
|
if (systemPrompt != null && !systemPrompt.trim().isEmpty()) {
|
||||||
|
finalPrompt = systemPrompt.trim() + ", " + prompt;
|
||||||
|
logger.info("已添加系统引导词,最终提示词长度: {}", finalPrompt.length());
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.warn("获取系统引导词失败,使用原始提示词: {}", e.getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
// 注意:banana模型一次只生成1张图片,numImages参数用于兼容性,实际请求中不使用
|
// 注意:banana模型一次只生成1张图片,numImages参数用于兼容性,实际请求中不使用
|
||||||
// 参考Comfly_nano_banana_edit节点:每次调用只生成1张图片
|
// 参考Comfly_nano_banana_edit节点:每次调用只生成1张图片
|
||||||
|
|
||||||
@@ -894,7 +908,7 @@ public class RealAIService {
|
|||||||
// 构建请求体,参考Comfly_nano_banana_edit节点的参数设置
|
// 构建请求体,参考Comfly_nano_banana_edit节点的参数设置
|
||||||
// 注意:banana模型不需要n参数,每次只生成1张图片
|
// 注意:banana模型不需要n参数,每次只生成1张图片
|
||||||
Map<String, Object> requestBody = new HashMap<>();
|
Map<String, Object> requestBody = new HashMap<>();
|
||||||
requestBody.put("prompt", prompt);
|
requestBody.put("prompt", finalPrompt);
|
||||||
requestBody.put("model", model);
|
requestBody.put("model", model);
|
||||||
requestBody.put("aspect_ratio", aspectRatio); // 直接使用aspect_ratio,不需要转换为size
|
requestBody.put("aspect_ratio", aspectRatio); // 直接使用aspect_ratio,不需要转换为size
|
||||||
requestBody.put("response_format", "url"); // 可选:url 或 b64_json
|
requestBody.put("response_format", "url"); // 可选:url 或 b64_json
|
||||||
@@ -1127,12 +1141,38 @@ public class RealAIService {
|
|||||||
type,
|
type,
|
||||||
prompt.length());
|
prompt.length());
|
||||||
|
|
||||||
// 根据类型生成不同的优化指令
|
|
||||||
String systemPrompt = getOptimizationPrompt(type);
|
|
||||||
|
|
||||||
// 从系统设置获取优化提示词的API端点和模型
|
// 从系统设置获取优化提示词的API端点和模型
|
||||||
com.example.demo.model.SystemSettings settings = systemSettingsService.getOrCreate();
|
com.example.demo.model.SystemSettings settings = systemSettingsService.getOrCreate();
|
||||||
|
|
||||||
|
// 获取系统提示词:
|
||||||
|
// - 分镜(storyboard):优先使用后台设置的自定义系统提示词,否则使用默认
|
||||||
|
// - 文生视频和图生视频:始终使用默认指令
|
||||||
|
String systemPrompt;
|
||||||
|
if ("storyboard".equals(type)) {
|
||||||
|
// 分镜优化:可以使用自定义系统提示词
|
||||||
|
systemPrompt = settings.getPromptOptimizationSystemPrompt();
|
||||||
|
if (systemPrompt == null || systemPrompt.trim().isEmpty()) {
|
||||||
|
systemPrompt = getOptimizationPrompt(type);
|
||||||
|
logger.info("分镜优化:使用默认系统提示词");
|
||||||
|
} else {
|
||||||
|
logger.info("分镜优化:使用自定义系统提示词");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 文生视频和图生视频:始终使用默认指令
|
||||||
|
systemPrompt = getOptimizationPrompt(type);
|
||||||
|
logger.info("{}优化:使用默认系统提示词", type);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果是分镜图类型,将系统引导词拼接到用户提示词前面一起优化
|
||||||
|
String promptToOptimize = prompt;
|
||||||
|
if ("storyboard".equals(type)) {
|
||||||
|
String storyboardSystemPrompt = settings.getStoryboardSystemPrompt();
|
||||||
|
if (storyboardSystemPrompt != null && !storyboardSystemPrompt.trim().isEmpty()) {
|
||||||
|
promptToOptimize = storyboardSystemPrompt.trim() + ", " + prompt;
|
||||||
|
logger.info("分镜图优化:已拼接系统引导词,最终长度: {}", promptToOptimize.length());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
String apiUrl = settings.getPromptOptimizationApiUrl();
|
String apiUrl = settings.getPromptOptimizationApiUrl();
|
||||||
if (apiUrl == null || apiUrl.isEmpty()) {
|
if (apiUrl == null || apiUrl.isEmpty()) {
|
||||||
apiUrl = getEffectiveApiBaseUrl(); // 使用默认API端点
|
apiUrl = getEffectiveApiBaseUrl(); // 使用默认API端点
|
||||||
@@ -1157,7 +1197,7 @@ public class RealAIService {
|
|||||||
|
|
||||||
Map<String, String> userMessage = new HashMap<>();
|
Map<String, String> userMessage = new HashMap<>();
|
||||||
userMessage.put("role", "user");
|
userMessage.put("role", "user");
|
||||||
userMessage.put("content", "请优化以下提示词,保持原始意图:\n" + prompt);
|
userMessage.put("content", "请优化以下提示词,保持原始意图:\n" + promptToOptimize);
|
||||||
messages.add(userMessage);
|
messages.add(userMessage);
|
||||||
|
|
||||||
requestBody.put("messages", messages);
|
requestBody.put("messages", messages);
|
||||||
|
|||||||
@@ -76,6 +76,9 @@ public class StoryboardVideoService {
|
|||||||
@Autowired
|
@Autowired
|
||||||
private org.springframework.transaction.support.TransactionTemplate readOnlyTransactionTemplate;
|
private org.springframework.transaction.support.TransactionTemplate readOnlyTransactionTemplate;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private CosService cosService;
|
||||||
|
|
||||||
// 默认生成6张分镜图
|
// 默认生成6张分镜图
|
||||||
private static final int DEFAULT_STORYBOARD_IMAGES = 6;
|
private static final int DEFAULT_STORYBOARD_IMAGES = 6;
|
||||||
|
|
||||||
@@ -430,15 +433,35 @@ public class StoryboardVideoService {
|
|||||||
/**
|
/**
|
||||||
* 在异步方法中保存分镜图结果(使用配置好的异步事务模板,超时3秒,确保快速完成)
|
* 在异步方法中保存分镜图结果(使用配置好的异步事务模板,超时3秒,确保快速完成)
|
||||||
* 参考sora2实现:保存网格图和单独的分镜图片
|
* 参考sora2实现:保存网格图和单独的分镜图片
|
||||||
|
* 如果COS启用,会将网格图上传到COS
|
||||||
*/
|
*/
|
||||||
private void saveStoryboardImageResultWithTransactionTemplate(String taskId, String mergedImageUrl, String storyboardImagesJson, int validatedImageCount) {
|
private void saveStoryboardImageResultWithTransactionTemplate(String taskId, String mergedImageUrl, String storyboardImagesJson, int validatedImageCount) {
|
||||||
|
// 如果COS启用,上传网格图到COS
|
||||||
|
String finalMergedImageUrl = mergedImageUrl;
|
||||||
|
if (cosService.isEnabled() && mergedImageUrl != null && mergedImageUrl.startsWith("data:image")) {
|
||||||
|
try {
|
||||||
|
logger.info("开始上传分镜网格图到COS: taskId={}", taskId);
|
||||||
|
String cosUrl = cosService.uploadBase64Image(mergedImageUrl, "storyboard_" + taskId + ".png");
|
||||||
|
if (cosUrl != null && !cosUrl.isEmpty()) {
|
||||||
|
finalMergedImageUrl = cosUrl;
|
||||||
|
logger.info("分镜网格图上传COS成功: taskId={}, url={}", taskId, cosUrl);
|
||||||
|
} else {
|
||||||
|
logger.warn("上传分镜网格图到COS失败,使用Base64: taskId={}", taskId);
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.error("上传分镜网格图到COS异常,使用Base64: taskId={}", taskId, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final String imageUrlForDb = finalMergedImageUrl;
|
||||||
|
|
||||||
asyncTransactionTemplate.executeWithoutResult(status -> {
|
asyncTransactionTemplate.executeWithoutResult(status -> {
|
||||||
try {
|
try {
|
||||||
StoryboardVideoTask task = taskRepository.findByTaskId(taskId)
|
StoryboardVideoTask task = taskRepository.findByTaskId(taskId)
|
||||||
.orElseThrow(() -> new RuntimeException("任务未找到: " + taskId));
|
.orElseThrow(() -> new RuntimeException("任务未找到: " + taskId));
|
||||||
task.setResultUrl(mergedImageUrl); // 网格图(用于前端显示)
|
task.setResultUrl(imageUrlForDb); // 网格图(用于前端显示,可能是COS URL或Base64)
|
||||||
if (storyboardImagesJson != null && !storyboardImagesJson.isEmpty()) {
|
if (storyboardImagesJson != null && !storyboardImagesJson.isEmpty()) {
|
||||||
task.setStoryboardImages(storyboardImagesJson); // 单独的分镜图片(用于视频生成)
|
task.setStoryboardImages(storyboardImagesJson); // 单独的分镜图片(用于视频生成,保持Base64格式)
|
||||||
}
|
}
|
||||||
task.updateProgress(50); // 分镜图生成完成,进度50%
|
task.updateProgress(50); // 分镜图生成完成,进度50%
|
||||||
taskRepository.save(task);
|
taskRepository.save(task);
|
||||||
@@ -447,7 +470,7 @@ public class StoryboardVideoService {
|
|||||||
try {
|
try {
|
||||||
TaskStatus taskStatus = taskStatusPollingService.getTaskStatus(taskId);
|
TaskStatus taskStatus = taskStatusPollingService.getTaskStatus(taskId);
|
||||||
if (taskStatus != null) {
|
if (taskStatus != null) {
|
||||||
taskStatus.markAsCompleted(mergedImageUrl);
|
taskStatus.markAsCompleted(imageUrlForDb);
|
||||||
taskStatus.setProgress(50); // 分镜图完成,进度50%
|
taskStatus.setProgress(50); // 分镜图完成,进度50%
|
||||||
taskStatusPollingService.saveOrUpdateTaskStatus(taskStatus);
|
taskStatusPollingService.saveOrUpdateTaskStatus(taskStatus);
|
||||||
logger.info("TaskStatus 已更新为完成: taskId={}", taskId);
|
logger.info("TaskStatus 已更新为完成: taskId={}", taskId);
|
||||||
@@ -459,7 +482,7 @@ public class StoryboardVideoService {
|
|||||||
|
|
||||||
// 创建分镜图作品记录
|
// 创建分镜图作品记录
|
||||||
try {
|
try {
|
||||||
userWorkService.createStoryboardImageWork(taskId, mergedImageUrl);
|
userWorkService.createStoryboardImageWork(taskId, imageUrlForDb);
|
||||||
logger.info("分镜图作品记录已创建: taskId={}", taskId);
|
logger.info("分镜图作品记录已创建: taskId={}", taskId);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
logger.error("创建分镜图作品记录失败: taskId={}", taskId, e);
|
logger.error("创建分镜图作品记录失败: taskId={}", taskId, e);
|
||||||
|
|||||||
@@ -40,6 +40,8 @@ public class SystemSettingsService {
|
|||||||
defaults.setContactEmail("support@example.com");
|
defaults.setContactEmail("support@example.com");
|
||||||
defaults.setPromptOptimizationModel("gpt-5.1-thinking");
|
defaults.setPromptOptimizationModel("gpt-5.1-thinking");
|
||||||
defaults.setPromptOptimizationApiUrl("https://ai.comfly.chat");
|
defaults.setPromptOptimizationApiUrl("https://ai.comfly.chat");
|
||||||
|
defaults.setStoryboardSystemPrompt("");
|
||||||
|
defaults.setPromptOptimizationSystemPrompt("");
|
||||||
SystemSettings saved = repository.save(defaults);
|
SystemSettings saved = repository.save(defaults);
|
||||||
logger.info("Initialized default SystemSettings: std={}, pro={}, points={}",
|
logger.info("Initialized default SystemSettings: std={}, pro={}, points={}",
|
||||||
saved.getStandardPriceCny(), saved.getProPriceCny(), saved.getPointsPerGeneration());
|
saved.getStandardPriceCny(), saved.getProPriceCny(), saved.getPointsPerGeneration());
|
||||||
@@ -64,6 +66,8 @@ public class SystemSettingsService {
|
|||||||
current.setContactEmail(updated.getContactEmail());
|
current.setContactEmail(updated.getContactEmail());
|
||||||
current.setPromptOptimizationModel(updated.getPromptOptimizationModel());
|
current.setPromptOptimizationModel(updated.getPromptOptimizationModel());
|
||||||
current.setPromptOptimizationApiUrl(updated.getPromptOptimizationApiUrl());
|
current.setPromptOptimizationApiUrl(updated.getPromptOptimizationApiUrl());
|
||||||
|
current.setStoryboardSystemPrompt(updated.getStoryboardSystemPrompt());
|
||||||
|
current.setPromptOptimizationSystemPrompt(updated.getPromptOptimizationSystemPrompt());
|
||||||
return repository.save(current);
|
return repository.save(current);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import com.example.demo.model.ImageToVideoTask;
|
|||||||
import com.example.demo.model.PointsFreezeRecord;
|
import com.example.demo.model.PointsFreezeRecord;
|
||||||
import com.example.demo.model.StoryboardVideoTask;
|
import com.example.demo.model.StoryboardVideoTask;
|
||||||
import com.example.demo.model.TaskQueue;
|
import com.example.demo.model.TaskQueue;
|
||||||
|
import com.example.demo.model.TaskStatus;
|
||||||
import com.example.demo.model.TextToVideoTask;
|
import com.example.demo.model.TextToVideoTask;
|
||||||
import com.example.demo.model.UserWork;
|
import com.example.demo.model.UserWork;
|
||||||
import com.example.demo.repository.ImageToVideoTaskRepository;
|
import com.example.demo.repository.ImageToVideoTaskRepository;
|
||||||
@@ -83,6 +84,9 @@ public class TaskQueueService {
|
|||||||
@Autowired
|
@Autowired
|
||||||
private CosService cosService;
|
private CosService cosService;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private TaskStatusPollingService taskStatusPollingService;
|
||||||
|
|
||||||
@org.springframework.beans.factory.annotation.Value("${app.temp.dir:./temp}")
|
@org.springframework.beans.factory.annotation.Value("${app.temp.dir:./temp}")
|
||||||
private String tempDir;
|
private String tempDir;
|
||||||
|
|
||||||
@@ -357,41 +361,77 @@ public class TaskQueueService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. 清理所有业务任务表中的PENDING和PROCESSING状态任务
|
// 2. 处理业务任务表中的PENDING和PROCESSING状态任务
|
||||||
// 注意:先清理业务任务,收集需要清理的taskId,然后再清理UserWork
|
// 只标记超时的任务为失败,未超时的任务保持原状态(继续轮询)
|
||||||
int businessTaskCleanedCount = 0;
|
int businessTaskCleanedCount = 0;
|
||||||
|
int businessTaskRecoveredCount = 0;
|
||||||
|
|
||||||
// 2.1 清理文生视频任务
|
// 2.1 处理文生视频任务
|
||||||
List<TextToVideoTask> textToVideoTasks = textToVideoTaskRepository.findByStatus(TextToVideoTask.TaskStatus.PENDING);
|
List<TextToVideoTask> textToVideoTasks = textToVideoTaskRepository.findByStatus(TextToVideoTask.TaskStatus.PENDING);
|
||||||
textToVideoTasks.addAll(textToVideoTaskRepository.findByStatus(TextToVideoTask.TaskStatus.PROCESSING));
|
textToVideoTasks.addAll(textToVideoTaskRepository.findByStatus(TextToVideoTask.TaskStatus.PROCESSING));
|
||||||
for (TextToVideoTask task : textToVideoTasks) {
|
for (TextToVideoTask task : textToVideoTasks) {
|
||||||
task.updateStatus(TextToVideoTask.TaskStatus.FAILED);
|
// 检查是否超时
|
||||||
task.setErrorMessage("系统重启,任务已取消");
|
boolean isTimeout = task.getCreatedAt() != null && task.getCreatedAt().isBefore(timeoutThreshold);
|
||||||
textToVideoTaskRepository.save(task);
|
if (isTimeout) {
|
||||||
businessTaskCleanedCount++;
|
// 超时任务标记为失败
|
||||||
|
task.updateStatus(TextToVideoTask.TaskStatus.FAILED);
|
||||||
|
task.setErrorMessage("任务超时(创建超过1小时)");
|
||||||
|
textToVideoTaskRepository.save(task);
|
||||||
|
businessTaskCleanedCount++;
|
||||||
|
logger.warn("系统重启:文生视频任务 {} 已超时,标记为失败", task.getTaskId());
|
||||||
|
} else {
|
||||||
|
// 未超时任务保持原状态,继续轮询
|
||||||
|
businessTaskRecoveredCount++;
|
||||||
|
logger.info("系统重启:文生视频任务 {} 未超时,保持原状态继续执行", task.getTaskId());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2.2 清理图生视频任务
|
// 2.2 处理图生视频任务
|
||||||
List<ImageToVideoTask> imageToVideoTasks = imageToVideoTaskRepository.findByStatus(ImageToVideoTask.TaskStatus.PENDING);
|
List<ImageToVideoTask> imageToVideoTasks = imageToVideoTaskRepository.findByStatus(ImageToVideoTask.TaskStatus.PENDING);
|
||||||
imageToVideoTasks.addAll(imageToVideoTaskRepository.findByStatus(ImageToVideoTask.TaskStatus.PROCESSING));
|
imageToVideoTasks.addAll(imageToVideoTaskRepository.findByStatus(ImageToVideoTask.TaskStatus.PROCESSING));
|
||||||
for (ImageToVideoTask task : imageToVideoTasks) {
|
for (ImageToVideoTask task : imageToVideoTasks) {
|
||||||
task.updateStatus(ImageToVideoTask.TaskStatus.FAILED);
|
// 检查是否超时
|
||||||
task.setErrorMessage("系统重启,任务已取消");
|
boolean isTimeout = task.getCreatedAt() != null && task.getCreatedAt().isBefore(timeoutThreshold);
|
||||||
imageToVideoTaskRepository.save(task);
|
if (isTimeout) {
|
||||||
businessTaskCleanedCount++;
|
// 超时任务标记为失败
|
||||||
|
task.updateStatus(ImageToVideoTask.TaskStatus.FAILED);
|
||||||
|
task.setErrorMessage("任务超时(创建超过1小时)");
|
||||||
|
imageToVideoTaskRepository.save(task);
|
||||||
|
businessTaskCleanedCount++;
|
||||||
|
logger.warn("系统重启:图生视频任务 {} 已超时,标记为失败", task.getTaskId());
|
||||||
|
} else {
|
||||||
|
// 未超时任务保持原状态,继续轮询
|
||||||
|
businessTaskRecoveredCount++;
|
||||||
|
logger.info("系统重启:图生视频任务 {} 未超时,保持原状态继续执行", task.getTaskId());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2.3 清理分镜视频任务(只清理还在生成分镜图阶段的任务,realTaskId为空)
|
// 2.3 处理分镜视频任务
|
||||||
List<StoryboardVideoTask> storyboardTasks = storyboardVideoTaskRepository.findByStatus(StoryboardVideoTask.TaskStatus.PENDING);
|
List<StoryboardVideoTask> storyboardTasks = storyboardVideoTaskRepository.findByStatus(StoryboardVideoTask.TaskStatus.PENDING);
|
||||||
storyboardTasks.addAll(storyboardVideoTaskRepository.findByStatus(StoryboardVideoTask.TaskStatus.PROCESSING));
|
storyboardTasks.addAll(storyboardVideoTaskRepository.findByStatus(StoryboardVideoTask.TaskStatus.PROCESSING));
|
||||||
for (StoryboardVideoTask task : storyboardTasks) {
|
for (StoryboardVideoTask task : storyboardTasks) {
|
||||||
// 只清理还在生成分镜图阶段的任务(realTaskId为空)
|
// 没有realTaskId的任务(还在生成分镜图阶段)或已超时的任务标记为失败
|
||||||
// 如果已经有realTaskId,说明已经提交到外部API,应该继续处理
|
boolean isTimeout = task.getCreatedAt() != null && task.getCreatedAt().isBefore(timeoutThreshold);
|
||||||
if (task.getRealTaskId() == null || task.getRealTaskId().isEmpty()) {
|
boolean noRealTaskId = task.getRealTaskId() == null || task.getRealTaskId().isEmpty();
|
||||||
|
|
||||||
|
if (isTimeout) {
|
||||||
|
// 超时任务标记为失败
|
||||||
task.updateStatus(StoryboardVideoTask.TaskStatus.FAILED);
|
task.updateStatus(StoryboardVideoTask.TaskStatus.FAILED);
|
||||||
task.setErrorMessage("系统重启,任务已取消");
|
task.setErrorMessage("任务超时(创建超过1小时)");
|
||||||
storyboardVideoTaskRepository.save(task);
|
storyboardVideoTaskRepository.save(task);
|
||||||
businessTaskCleanedCount++;
|
businessTaskCleanedCount++;
|
||||||
|
logger.warn("系统重启:分镜视频任务 {} 已超时,标记为失败", task.getTaskId());
|
||||||
|
} else if (noRealTaskId) {
|
||||||
|
// 没有提交到外部API的任务(分镜图生成阶段),重启后无法恢复,标记为失败
|
||||||
|
task.updateStatus(StoryboardVideoTask.TaskStatus.FAILED);
|
||||||
|
task.setErrorMessage("系统重启,分镜图生成任务已取消");
|
||||||
|
storyboardVideoTaskRepository.save(task);
|
||||||
|
businessTaskCleanedCount++;
|
||||||
|
logger.warn("系统重启:分镜视频任务 {} 无外部任务ID,标记为失败", task.getTaskId());
|
||||||
|
} else {
|
||||||
|
// 已提交到外部API且未超时的任务,保持原状态继续轮询
|
||||||
|
businessTaskRecoveredCount++;
|
||||||
|
logger.info("系统重启:分镜视频任务 {} 未超时,保持原状态继续执行", task.getTaskId());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -399,9 +439,9 @@ public class TaskQueueService {
|
|||||||
// 检查所有FAILED状态的UserWork,如果对应的业务任务已完成,则更新UserWork状态
|
// 检查所有FAILED状态的UserWork,如果对应的业务任务已完成,则更新UserWork状态
|
||||||
repairUserWorkStatus();
|
repairUserWorkStatus();
|
||||||
|
|
||||||
if (totalCleanedCount > 0 || businessTaskCleanedCount > 0) {
|
if (totalCleanedCount > 0 || businessTaskCleanedCount > 0 || businessTaskRecoveredCount > 0) {
|
||||||
logger.warn("系统重启:共清理了 {} 个队列任务,{} 个业务任务",
|
logger.info("系统重启:清理了 {} 个队列任务,{} 个业务任务;恢复了 {} 个业务任务继续执行",
|
||||||
totalCleanedCount, businessTaskCleanedCount);
|
totalCleanedCount, businessTaskCleanedCount, businessTaskRecoveredCount);
|
||||||
} else {
|
} else {
|
||||||
logger.info("系统重启:没有需要清理的未完成任务");
|
logger.info("系统重启:没有需要清理的未完成任务");
|
||||||
}
|
}
|
||||||
@@ -503,6 +543,16 @@ public class TaskQueueService {
|
|||||||
TaskQueue taskQueue = new TaskQueue(username, taskId, taskType);
|
TaskQueue taskQueue = new TaskQueue(username, taskId, taskType);
|
||||||
taskQueue = taskQueueRepository.save(taskQueue);
|
taskQueue = taskQueueRepository.save(taskQueue);
|
||||||
|
|
||||||
|
// 同时创建 task_status 记录
|
||||||
|
try {
|
||||||
|
TaskStatus.TaskType statusTaskType = convertToTaskStatusType(taskType);
|
||||||
|
taskStatusPollingService.createTaskStatus(taskId, username, statusTaskType, null);
|
||||||
|
logger.info("任务 {} 已同时创建到 task_queue 和 task_status 表", taskId);
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.error("创建 task_status 记录失败: taskId={}", taskId, e);
|
||||||
|
// 不影响主流程,继续执行
|
||||||
|
}
|
||||||
|
|
||||||
// 注册事务提交后的回调,确保事务提交后才将任务加入内存队列
|
// 注册事务提交后的回调,确保事务提交后才将任务加入内存队列
|
||||||
// 这样可以避免消费者线程在事务提交前就开始处理任务,导致找不到数据的问题
|
// 这样可以避免消费者线程在事务提交前就开始处理任务,导致找不到数据的问题
|
||||||
final TaskQueue finalTaskQueue = taskQueue;
|
final TaskQueue finalTaskQueue = taskQueue;
|
||||||
@@ -555,6 +605,22 @@ public class TaskQueueService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 转换 TaskQueue.TaskType 到 TaskStatus.TaskType
|
||||||
|
*/
|
||||||
|
private TaskStatus.TaskType convertToTaskStatusType(TaskQueue.TaskType taskType) {
|
||||||
|
switch (taskType) {
|
||||||
|
case TEXT_TO_VIDEO:
|
||||||
|
return TaskStatus.TaskType.TEXT_TO_VIDEO;
|
||||||
|
case IMAGE_TO_VIDEO:
|
||||||
|
return TaskStatus.TaskType.IMAGE_TO_VIDEO;
|
||||||
|
case STORYBOARD_VIDEO:
|
||||||
|
return TaskStatus.TaskType.STORYBOARD_VIDEO;
|
||||||
|
default:
|
||||||
|
throw new IllegalArgumentException("不支持的任务类型: " + taskType);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 处理队列中的待处理任务
|
* 处理队列中的待处理任务
|
||||||
* 注意:此方法现在主要用于从数据库加载任务到内存队列
|
* 注意:此方法现在主要用于从数据库加载任务到内存队列
|
||||||
@@ -746,6 +812,18 @@ public class TaskQueueService {
|
|||||||
taskQueue.setRealTaskId(realTaskId);
|
taskQueue.setRealTaskId(realTaskId);
|
||||||
taskQueueRepository.save(taskQueue);
|
taskQueueRepository.save(taskQueue);
|
||||||
|
|
||||||
|
// 同时更新 TaskStatus 表的 externalTaskId(轮询服务使用此字段)
|
||||||
|
try {
|
||||||
|
TaskStatus taskStatus = taskStatusPollingService.getTaskStatus(taskId);
|
||||||
|
if (taskStatus != null) {
|
||||||
|
taskStatus.setExternalTaskId(realTaskId);
|
||||||
|
taskStatusPollingService.saveOrUpdateTaskStatus(taskStatus);
|
||||||
|
logger.info("已更新 TaskStatus 的 externalTaskId: taskId={}, externalTaskId={}", taskId, realTaskId);
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.warn("更新 TaskStatus 的 externalTaskId 失败: taskId={}", taskId, e);
|
||||||
|
}
|
||||||
|
|
||||||
// TODO: 为分镜视频任务创建或更新 TaskStatus(功能待实现)
|
// TODO: 为分镜视频任务创建或更新 TaskStatus(功能待实现)
|
||||||
// 区分图生视频和分镜视频任务的状态码
|
// 区分图生视频和分镜视频任务的状态码
|
||||||
/*
|
/*
|
||||||
@@ -980,6 +1058,19 @@ public class TaskQueueService {
|
|||||||
} else {
|
} else {
|
||||||
logger.warn("找不到对应的任务队列记录: {}", taskId);
|
logger.warn("找不到对应的任务队列记录: {}", taskId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 3. 同时更新 TaskStatus 表的 externalTaskId(轮询服务使用此字段)
|
||||||
|
try {
|
||||||
|
TaskStatus taskStatus = taskStatusPollingService.getTaskStatus(taskId);
|
||||||
|
if (taskStatus != null) {
|
||||||
|
taskStatus.setExternalTaskId(videoTaskId);
|
||||||
|
taskStatus.setProgress(50); // 分镜图已完成,视频生成中
|
||||||
|
taskStatusPollingService.saveOrUpdateTaskStatus(taskStatus);
|
||||||
|
logger.info("已更新分镜视频任务的 TaskStatus externalTaskId: taskId={}, externalTaskId={}", taskId, videoTaskId);
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.warn("更新分镜视频任务的 TaskStatus externalTaskId 失败: taskId={}", taskId, e);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
logger.error("保存视频任务ID失败: {}", e.getMessage(), e);
|
logger.error("保存视频任务ID失败: {}", e.getMessage(), e);
|
||||||
@@ -1136,7 +1227,10 @@ public class TaskQueueService {
|
|||||||
// 快速查询待检查任务(使用只读事务)
|
// 快速查询待检查任务(使用只读事务)
|
||||||
List<TaskQueue> tasksToCheck = getTasksToCheck();
|
List<TaskQueue> tasksToCheck = getTasksToCheck();
|
||||||
|
|
||||||
|
logger.info("轮询查询待检查任务数量: {}", tasksToCheck.size());
|
||||||
|
|
||||||
if (tasksToCheck.isEmpty()) {
|
if (tasksToCheck.isEmpty()) {
|
||||||
|
logger.debug("没有需要检查的任务");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1185,23 +1279,30 @@ public class TaskQueueService {
|
|||||||
private void checkTaskStatusInternal(TaskQueue taskQueue) {
|
private void checkTaskStatusInternal(TaskQueue taskQueue) {
|
||||||
String taskId = taskQueue.getTaskId();
|
String taskId = taskQueue.getTaskId();
|
||||||
|
|
||||||
|
logger.info("开始检查任务状态: taskId={}, taskType={}, realTaskId={}",
|
||||||
|
taskId, taskQueue.getTaskType(), taskQueue.getRealTaskId());
|
||||||
|
|
||||||
// 检查是否正在查询此任务,如果是则跳过(防止重复查询)
|
// 检查是否正在查询此任务,如果是则跳过(防止重复查询)
|
||||||
if (!checkingTasks.add(taskId)) {
|
if (!checkingTasks.add(taskId)) {
|
||||||
|
logger.debug("任务 {} 正在被其他线程检查,跳过", taskId);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 特殊处理:分镜视频任务需要检查多个视频任务
|
// 特殊处理:分镜视频任务需要检查多个视频任务
|
||||||
if (taskQueue.getTaskType() == TaskQueue.TaskType.STORYBOARD_VIDEO) {
|
if (taskQueue.getTaskType() == TaskQueue.TaskType.STORYBOARD_VIDEO) {
|
||||||
|
logger.info("分镜视频任务,调用专门的检查方法: taskId={}", taskId);
|
||||||
checkStoryboardVideoTasks(taskQueue);
|
checkStoryboardVideoTasks(taskQueue);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (taskQueue.getRealTaskId() == null) {
|
if (taskQueue.getRealTaskId() == null) {
|
||||||
|
logger.warn("任务 {} 的 realTaskId 为空,跳过轮询", taskId);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 查询外部API状态
|
// 查询外部API状态
|
||||||
|
logger.info("调用外部API查询任务状态: taskId={}, realTaskId={}", taskId, taskQueue.getRealTaskId());
|
||||||
Map<String, Object> statusResponse = realAIService.getTaskStatus(taskQueue.getRealTaskId());
|
Map<String, Object> statusResponse = realAIService.getTaskStatus(taskQueue.getRealTaskId());
|
||||||
|
|
||||||
// API调用成功后增加检查次数(使用独立事务,快速完成)
|
// API调用成功后增加检查次数(使用独立事务,快速完成)
|
||||||
@@ -1226,27 +1327,45 @@ public class TaskQueueService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (taskData != null) {
|
if (taskData != null) {
|
||||||
|
logger.info("任务状态响应: taskId={}, taskData={}", taskQueue.getTaskId(), taskData);
|
||||||
|
|
||||||
String status = (String) taskData.get("status");
|
String status = (String) taskData.get("status");
|
||||||
// 支持大小写不敏感的状态检查
|
// 支持大小写不敏感的状态检查
|
||||||
if (status != null) {
|
if (status != null) {
|
||||||
status = status.toUpperCase();
|
status = status.toUpperCase();
|
||||||
}
|
}
|
||||||
|
logger.info("解析到的状态: taskId={}, status={}", taskQueue.getTaskId(), status);
|
||||||
|
|
||||||
// 提取结果URL - 只支持 sora2 格式:data.output
|
// 提取结果URL - 支持多种格式
|
||||||
String resultUrl = null;
|
String resultUrl = null;
|
||||||
|
|
||||||
|
// 格式1: sora2 格式 data.output
|
||||||
Object dataField = taskData.get("data");
|
Object dataField = taskData.get("data");
|
||||||
if (dataField instanceof Map) {
|
if (dataField instanceof Map) {
|
||||||
Map<?, ?> dataMap = (Map<?, ?>) dataField;
|
Map<?, ?> dataMap = (Map<?, ?>) dataField;
|
||||||
Object output = dataMap.get("output");
|
Object output = dataMap.get("output");
|
||||||
if (output != null) {
|
if (output != null) {
|
||||||
String outputStr = output.toString();
|
String outputStr = output.toString();
|
||||||
// 检查是否为有效的URL(不为空字符串且不为"null"字符串)
|
|
||||||
if (!outputStr.trim().isEmpty() && !outputStr.equals("null")) {
|
if (!outputStr.trim().isEmpty() && !outputStr.equals("null")) {
|
||||||
resultUrl = outputStr;
|
resultUrl = outputStr;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 格式2: 直接在根级别的 output 字段
|
||||||
|
if (resultUrl == null) {
|
||||||
|
Object output = taskData.get("output");
|
||||||
|
if (output != null) {
|
||||||
|
String outputStr = output.toString();
|
||||||
|
if (!outputStr.trim().isEmpty() && !outputStr.equals("null")) {
|
||||||
|
resultUrl = outputStr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info("解析到的结果URL: taskId={}, resultUrl={}", taskQueue.getTaskId(),
|
||||||
|
resultUrl != null ? (resultUrl.length() > 100 ? resultUrl.substring(0, 100) + "..." : resultUrl) : "null");
|
||||||
|
|
||||||
// 提取错误消息
|
// 提取错误消息
|
||||||
String errorMessage = (String) taskData.get("errorMessage");
|
String errorMessage = (String) taskData.get("errorMessage");
|
||||||
if (errorMessage == null) {
|
if (errorMessage == null) {
|
||||||
@@ -1586,27 +1705,50 @@ public class TaskQueueService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 创建用户作品 - 在最后执行,避免影响主要流程
|
// 创建/更新用户作品 - 在最后执行,避免影响主要流程
|
||||||
|
// 只有在 resultUrl 有效时才更新为 COMPLETED
|
||||||
|
// 如果 resultUrl 为空,不做处理(等待超时机制处理)
|
||||||
if (finalResultUrl != null && !finalResultUrl.isEmpty()) {
|
if (finalResultUrl != null && !finalResultUrl.isEmpty()) {
|
||||||
try {
|
try {
|
||||||
userWorkService.createWorkFromTask(taskQueue.getTaskId(), finalResultUrl);
|
userWorkService.createWorkFromTask(taskQueue.getTaskId(), finalResultUrl);
|
||||||
} catch (Exception workException) {
|
} catch (Exception workException) {
|
||||||
// 如果是重复创建异常,静默处理
|
// 如果是重复创建异常,静默处理
|
||||||
if (workException.getMessage() == null ||
|
if (workException.getMessage() == null ||
|
||||||
(!workException.getMessage().contains("已存在") &&
|
(!workException.getMessage().contains("已存在") &&
|
||||||
!workException.getMessage().contains("Duplicate entry"))) {
|
!workException.getMessage().contains("Duplicate entry"))) {
|
||||||
logger.warn("创建用户作品失败: {}", taskQueue.getTaskId());
|
logger.warn("创建/更新用户作品失败: {}", taskQueue.getTaskId(), workException);
|
||||||
}
|
}
|
||||||
// 作品创建失败不影响任务完成状态
|
// 作品创建失败不影响任务完成状态
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
logger.warn("任务返回完成但 resultUrl 为空,保持 user_works 状态不变(等待超时机制处理): {}", taskQueue.getTaskId());
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新 task_status 表中的状态(保留记录)
|
||||||
|
// 只有在 resultUrl 有效时才更新为 COMPLETED
|
||||||
|
// 如果 resultUrl 为空,保持原状态不变(等待超时机制处理)
|
||||||
|
if (finalResultUrl != null && !finalResultUrl.isEmpty()) {
|
||||||
|
try {
|
||||||
|
TaskStatus taskStatus = taskStatusPollingService.getTaskStatus(taskQueue.getTaskId());
|
||||||
|
if (taskStatus != null) {
|
||||||
|
taskStatus.setStatus(TaskStatus.Status.COMPLETED);
|
||||||
|
taskStatus.setProgress(100);
|
||||||
|
taskStatus.setResultUrl(finalResultUrl);
|
||||||
|
taskStatus.setCompletedAt(java.time.LocalDateTime.now());
|
||||||
|
taskStatusPollingService.saveOrUpdateTaskStatus(taskStatus);
|
||||||
|
logger.info("task_status 表状态已更新为 COMPLETED: {}", taskQueue.getTaskId());
|
||||||
|
}
|
||||||
|
} catch (Exception statusException) {
|
||||||
|
logger.warn("更新 task_status 状态失败: {}", taskQueue.getTaskId(), statusException);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 任务完成后从队列中删除记录
|
// 任务完成后从 task_queue 中删除记录(task_status 保留)
|
||||||
try {
|
try {
|
||||||
taskQueueRepository.delete(freshTaskQueue);
|
taskQueueRepository.delete(freshTaskQueue);
|
||||||
logger.info("任务完成,已从队列中删除: {}", taskQueue.getTaskId());
|
logger.info("任务完成,已从 task_queue 中删除: {}", taskQueue.getTaskId());
|
||||||
} catch (Exception deleteException) {
|
} catch (Exception deleteException) {
|
||||||
logger.warn("删除队列记录失败: {}", taskQueue.getTaskId(), deleteException);
|
logger.warn("删除 task_queue 记录失败: {}", taskQueue.getTaskId(), deleteException);
|
||||||
}
|
}
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
logger.error("更新任务完成状态失败: {}", taskQueue.getTaskId(), e);
|
logger.error("更新任务完成状态失败: {}", taskQueue.getTaskId(), e);
|
||||||
@@ -1620,6 +1762,132 @@ public class TaskQueueService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据任务ID处理任务完成(供 TaskStatusPollingService 调用)
|
||||||
|
* @param taskId 任务ID
|
||||||
|
* @param resultUrl 结果URL
|
||||||
|
*/
|
||||||
|
public void handleTaskCompletionByTaskId(String taskId, String resultUrl) {
|
||||||
|
logger.info("处理任务完成: taskId={}, resultUrl={}", taskId,
|
||||||
|
resultUrl != null && resultUrl.length() > 100 ? resultUrl.substring(0, 100) + "..." : resultUrl);
|
||||||
|
|
||||||
|
Optional<TaskQueue> taskQueueOpt = taskQueueRepository.findByTaskId(taskId);
|
||||||
|
if (taskQueueOpt.isPresent()) {
|
||||||
|
updateTaskAsCompleted(taskQueueOpt.get(), resultUrl);
|
||||||
|
} else {
|
||||||
|
// TaskQueue 可能已经被删除,直接更新业务表和 UserWork
|
||||||
|
logger.info("TaskQueue 中未找到任务 {},尝试直接更新业务表", taskId);
|
||||||
|
updateBusinessTaskAndUserWork(taskId, resultUrl);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据任务ID处理任务失败(供 TaskStatusPollingService 调用)
|
||||||
|
* @param taskId 任务ID
|
||||||
|
* @param errorMessage 错误信息
|
||||||
|
*/
|
||||||
|
public void handleTaskFailureByTaskId(String taskId, String errorMessage) {
|
||||||
|
logger.info("处理任务失败: taskId={}, error={}", taskId, errorMessage);
|
||||||
|
|
||||||
|
Optional<TaskQueue> taskQueueOpt = taskQueueRepository.findByTaskId(taskId);
|
||||||
|
if (taskQueueOpt.isPresent()) {
|
||||||
|
updateTaskAsFailed(taskQueueOpt.get(), errorMessage);
|
||||||
|
} else {
|
||||||
|
// TaskQueue 可能已经被删除,直接更新业务表和 UserWork
|
||||||
|
logger.info("TaskQueue 中未找到任务 {},尝试直接更新业务表为失败", taskId);
|
||||||
|
updateBusinessTaskAndUserWorkAsFailed(taskId, errorMessage);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 直接更新业务表和 UserWork(当 TaskQueue 不存在时使用)
|
||||||
|
*/
|
||||||
|
private void updateBusinessTaskAndUserWork(String taskId, String resultUrl) {
|
||||||
|
try {
|
||||||
|
transactionTemplate.executeWithoutResult(status -> {
|
||||||
|
// 更新业务表
|
||||||
|
if (taskId.startsWith("img2vid_")) {
|
||||||
|
imageToVideoTaskRepository.findByTaskId(taskId).ifPresent(task -> {
|
||||||
|
task.setStatus(ImageToVideoTask.TaskStatus.COMPLETED);
|
||||||
|
task.setResultUrl(resultUrl);
|
||||||
|
task.setCompletedAt(java.time.LocalDateTime.now());
|
||||||
|
imageToVideoTaskRepository.save(task);
|
||||||
|
logger.info("直接更新图生视频任务为完成: {}", taskId);
|
||||||
|
});
|
||||||
|
} else if (taskId.startsWith("storyboard_")) {
|
||||||
|
storyboardVideoTaskRepository.findByTaskId(taskId).ifPresent(task -> {
|
||||||
|
task.setStatus(StoryboardVideoTask.TaskStatus.COMPLETED);
|
||||||
|
task.setResultUrl(resultUrl);
|
||||||
|
task.setCompletedAt(java.time.LocalDateTime.now());
|
||||||
|
storyboardVideoTaskRepository.save(task);
|
||||||
|
logger.info("直接更新分镜视频任务为完成: {}", taskId);
|
||||||
|
});
|
||||||
|
} else if (taskId.startsWith("txt2vid_")) {
|
||||||
|
textToVideoTaskRepository.findByTaskId(taskId).ifPresent(task -> {
|
||||||
|
task.setStatus(TextToVideoTask.TaskStatus.COMPLETED);
|
||||||
|
task.setResultUrl(resultUrl);
|
||||||
|
task.setCompletedAt(java.time.LocalDateTime.now());
|
||||||
|
textToVideoTaskRepository.save(task);
|
||||||
|
logger.info("直接更新文生视频任务为完成: {}", taskId);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新 UserWork
|
||||||
|
if (resultUrl != null && !resultUrl.isEmpty()) {
|
||||||
|
try {
|
||||||
|
userWorkService.createWorkFromTask(taskId, resultUrl);
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.warn("更新 UserWork 失败: {}", taskId, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.error("直接更新业务表失败: {}", taskId, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 直接更新业务表和 UserWork 为失败状态
|
||||||
|
*/
|
||||||
|
private void updateBusinessTaskAndUserWorkAsFailed(String taskId, String errorMessage) {
|
||||||
|
try {
|
||||||
|
transactionTemplate.executeWithoutResult(status -> {
|
||||||
|
// 更新业务表
|
||||||
|
if (taskId.startsWith("img2vid_")) {
|
||||||
|
imageToVideoTaskRepository.findByTaskId(taskId).ifPresent(task -> {
|
||||||
|
task.setStatus(ImageToVideoTask.TaskStatus.FAILED);
|
||||||
|
task.setErrorMessage(errorMessage);
|
||||||
|
imageToVideoTaskRepository.save(task);
|
||||||
|
logger.info("直接更新图生视频任务为失败: {}", taskId);
|
||||||
|
});
|
||||||
|
} else if (taskId.startsWith("storyboard_")) {
|
||||||
|
storyboardVideoTaskRepository.findByTaskId(taskId).ifPresent(task -> {
|
||||||
|
task.setStatus(StoryboardVideoTask.TaskStatus.FAILED);
|
||||||
|
task.setErrorMessage(errorMessage);
|
||||||
|
storyboardVideoTaskRepository.save(task);
|
||||||
|
logger.info("直接更新分镜视频任务为失败: {}", taskId);
|
||||||
|
});
|
||||||
|
} else if (taskId.startsWith("txt2vid_")) {
|
||||||
|
textToVideoTaskRepository.findByTaskId(taskId).ifPresent(task -> {
|
||||||
|
task.setStatus(TextToVideoTask.TaskStatus.FAILED);
|
||||||
|
task.setErrorMessage(errorMessage);
|
||||||
|
textToVideoTaskRepository.save(task);
|
||||||
|
logger.info("直接更新文生视频任务为失败: {}", taskId);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新 UserWork
|
||||||
|
try {
|
||||||
|
userWorkService.updateWorkStatus(taskId, UserWork.WorkStatus.FAILED);
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.warn("更新 UserWork 状态为失败失败: {}", taskId, e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.error("直接更新业务表为失败状态失败: {}", taskId, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 更新任务为失败状态(使用独立事务,快速完成)
|
* 更新任务为失败状态(使用独立事务,快速完成)
|
||||||
* 使用 TransactionTemplate 确保事务正确执行(因为私有方法无法使用 @Transactional)
|
* 使用 TransactionTemplate 确保事务正确执行(因为私有方法无法使用 @Transactional)
|
||||||
@@ -1646,12 +1914,26 @@ public class TaskQueueService {
|
|||||||
logger.warn("更新作品状态为FAILED失败: {}", taskQueue.getTaskId(), workException);
|
logger.warn("更新作品状态为FAILED失败: {}", taskQueue.getTaskId(), workException);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 任务失败后从队列中删除记录
|
// 更新 task_status 表中的状态为 FAILED(保留记录)
|
||||||
|
try {
|
||||||
|
TaskStatus taskStatus = taskStatusPollingService.getTaskStatus(taskQueue.getTaskId());
|
||||||
|
if (taskStatus != null) {
|
||||||
|
taskStatus.setStatus(TaskStatus.Status.FAILED);
|
||||||
|
taskStatus.setErrorMessage(errorMessage);
|
||||||
|
taskStatus.setUpdatedAt(java.time.LocalDateTime.now());
|
||||||
|
taskStatusPollingService.saveOrUpdateTaskStatus(taskStatus);
|
||||||
|
logger.info("task_status 表状态已更新为 FAILED: {}", taskQueue.getTaskId());
|
||||||
|
}
|
||||||
|
} catch (Exception statusException) {
|
||||||
|
logger.warn("更新 task_status 状态失败: {}", taskQueue.getTaskId(), statusException);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 任务失败后从 task_queue 中删除记录(task_status 保留)
|
||||||
try {
|
try {
|
||||||
taskQueueRepository.delete(taskQueue);
|
taskQueueRepository.delete(taskQueue);
|
||||||
logger.info("任务失败,已从队列中删除: {}", taskQueue.getTaskId());
|
logger.info("任务失败,已从 task_queue 中删除: {}", taskQueue.getTaskId());
|
||||||
} catch (Exception deleteException) {
|
} catch (Exception deleteException) {
|
||||||
logger.warn("删除队列记录失败: {}", taskQueue.getTaskId(), deleteException);
|
logger.warn("删除 task_queue 记录失败: {}", taskQueue.getTaskId(), deleteException);
|
||||||
}
|
}
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
logger.error("更新任务失败状态失败: {}", taskQueue.getTaskId(), e);
|
logger.error("更新任务失败状态失败: {}", taskQueue.getTaskId(), e);
|
||||||
@@ -1674,7 +1956,7 @@ public class TaskQueueService {
|
|||||||
// 使用 TransactionTemplate 确保在事务中执行
|
// 使用 TransactionTemplate 确保在事务中执行
|
||||||
transactionTemplate.executeWithoutResult(status -> {
|
transactionTemplate.executeWithoutResult(status -> {
|
||||||
try {
|
try {
|
||||||
taskQueue.updateStatus(TaskQueue.QueueStatus.TIMEOUT);
|
taskQueue.updateStatus(TaskQueue.QueueStatus.FAILED);
|
||||||
taskQueue.setErrorMessage("任务处理超时");
|
taskQueue.setErrorMessage("任务处理超时");
|
||||||
taskQueueRepository.save(taskQueue);
|
taskQueueRepository.save(taskQueue);
|
||||||
|
|
||||||
@@ -1691,12 +1973,26 @@ public class TaskQueueService {
|
|||||||
logger.warn("更新超时任务的作品状态为FAILED失败: {}", taskQueue.getTaskId(), workException);
|
logger.warn("更新超时任务的作品状态为FAILED失败: {}", taskQueue.getTaskId(), workException);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 任务超时后从队列中删除记录
|
// 更新 task_status 表中的状态为 FAILED(超时视为失败)
|
||||||
|
try {
|
||||||
|
TaskStatus taskStatus = taskStatusPollingService.getTaskStatus(taskQueue.getTaskId());
|
||||||
|
if (taskStatus != null) {
|
||||||
|
taskStatus.setStatus(TaskStatus.Status.FAILED);
|
||||||
|
taskStatus.setErrorMessage("任务处理超时");
|
||||||
|
taskStatus.setUpdatedAt(java.time.LocalDateTime.now());
|
||||||
|
taskStatusPollingService.saveOrUpdateTaskStatus(taskStatus);
|
||||||
|
logger.info("task_status 表状态已更新为 FAILED (超时): {}", taskQueue.getTaskId());
|
||||||
|
}
|
||||||
|
} catch (Exception statusException) {
|
||||||
|
logger.warn("更新 task_status 状态失败: {}", taskQueue.getTaskId(), statusException);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 任务超时后从 task_queue 中删除记录(task_status 保留)
|
||||||
try {
|
try {
|
||||||
taskQueueRepository.delete(taskQueue);
|
taskQueueRepository.delete(taskQueue);
|
||||||
logger.info("任务超时,已从队列中删除: {}", taskQueue.getTaskId());
|
logger.info("任务超时,已从 task_queue 中删除: {}", taskQueue.getTaskId());
|
||||||
} catch (Exception deleteException) {
|
} catch (Exception deleteException) {
|
||||||
logger.warn("删除队列记录失败: {}", taskQueue.getTaskId(), deleteException);
|
logger.warn("删除 task_queue 记录失败: {}", taskQueue.getTaskId(), deleteException);
|
||||||
}
|
}
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
logger.error("更新任务超时状态失败: {}", taskQueue.getTaskId(), e);
|
logger.error("更新任务超时状态失败: {}", taskQueue.getTaskId(), e);
|
||||||
@@ -1713,8 +2009,10 @@ public class TaskQueueService {
|
|||||||
/**
|
/**
|
||||||
* 更新原始任务状态(使用独立事务,快速完成)
|
* 更新原始任务状态(使用独立事务,快速完成)
|
||||||
*/
|
*/
|
||||||
@Transactional
|
|
||||||
private void updateOriginalTaskStatus(TaskQueue taskQueue, String status, String resultUrl, String errorMessage) {
|
private void updateOriginalTaskStatus(TaskQueue taskQueue, String status, String resultUrl, String errorMessage) {
|
||||||
|
logger.info("更新原始任务状态: taskId={}, taskType={}, status={}, resultUrl={}",
|
||||||
|
taskQueue.getTaskId(), taskQueue.getTaskType(), status,
|
||||||
|
resultUrl != null ? (resultUrl.length() > 50 ? resultUrl.substring(0, 50) + "..." : resultUrl) : "null");
|
||||||
try {
|
try {
|
||||||
if (taskQueue.getTaskType() == TaskQueue.TaskType.TEXT_TO_VIDEO) {
|
if (taskQueue.getTaskType() == TaskQueue.TaskType.TEXT_TO_VIDEO) {
|
||||||
Optional<TextToVideoTask> taskOpt = textToVideoTaskRepository.findByTaskId(taskQueue.getTaskId());
|
Optional<TextToVideoTask> taskOpt = textToVideoTaskRepository.findByTaskId(taskQueue.getTaskId());
|
||||||
@@ -1741,15 +2039,18 @@ public class TaskQueueService {
|
|||||||
Optional<ImageToVideoTask> taskOpt = imageToVideoTaskRepository.findByTaskId(taskQueue.getTaskId());
|
Optional<ImageToVideoTask> taskOpt = imageToVideoTaskRepository.findByTaskId(taskQueue.getTaskId());
|
||||||
if (taskOpt.isPresent()) {
|
if (taskOpt.isPresent()) {
|
||||||
ImageToVideoTask task = taskOpt.get();
|
ImageToVideoTask task = taskOpt.get();
|
||||||
|
logger.info("找到ImageToVideoTask: taskId={}, 当前状态={}", taskQueue.getTaskId(), task.getStatus());
|
||||||
if ("COMPLETED".equals(status)) {
|
if ("COMPLETED".equals(status)) {
|
||||||
task.setResultUrl(resultUrl);
|
task.setResultUrl(resultUrl);
|
||||||
task.updateStatus(ImageToVideoTask.TaskStatus.COMPLETED);
|
task.updateStatus(ImageToVideoTask.TaskStatus.COMPLETED);
|
||||||
task.updateProgress(100);
|
task.updateProgress(100);
|
||||||
imageToVideoTaskRepository.save(task);
|
imageToVideoTaskRepository.save(task);
|
||||||
|
logger.info("ImageToVideoTask已更新为COMPLETED: taskId={}", taskQueue.getTaskId());
|
||||||
} else if ("FAILED".equals(status) || "CANCELLED".equals(status)) {
|
} else if ("FAILED".equals(status) || "CANCELLED".equals(status)) {
|
||||||
task.updateStatus(ImageToVideoTask.TaskStatus.FAILED);
|
task.updateStatus(ImageToVideoTask.TaskStatus.FAILED);
|
||||||
task.setErrorMessage(errorMessage);
|
task.setErrorMessage(errorMessage);
|
||||||
imageToVideoTaskRepository.save(task);
|
imageToVideoTaskRepository.save(task);
|
||||||
|
logger.info("ImageToVideoTask已更新为FAILED: taskId={}", taskQueue.getTaskId());
|
||||||
} else if ("PROCESSING".equals(status)) {
|
} else if ("PROCESSING".equals(status)) {
|
||||||
// 处理中状态,更新resultUrl以显示进度
|
// 处理中状态,更新resultUrl以显示进度
|
||||||
if (resultUrl != null && !resultUrl.isEmpty()) {
|
if (resultUrl != null && !resultUrl.isEmpty()) {
|
||||||
@@ -1757,6 +2058,8 @@ public class TaskQueueService {
|
|||||||
imageToVideoTaskRepository.save(task);
|
imageToVideoTaskRepository.save(task);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
logger.warn("ImageToVideoTask不存在: taskId={}", taskQueue.getTaskId());
|
||||||
}
|
}
|
||||||
} else if (taskQueue.getTaskType() == TaskQueue.TaskType.STORYBOARD_VIDEO) {
|
} else if (taskQueue.getTaskType() == TaskQueue.TaskType.STORYBOARD_VIDEO) {
|
||||||
Optional<StoryboardVideoTask> taskOpt = storyboardVideoTaskRepository.findByTaskId(taskQueue.getTaskId());
|
Optional<StoryboardVideoTask> taskOpt = storyboardVideoTaskRepository.findByTaskId(taskQueue.getTaskId());
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package com.example.demo.service;
|
package com.example.demo.service;
|
||||||
|
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
|
import java.time.temporal.ChronoUnit;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
@@ -16,6 +17,7 @@ import com.example.demo.repository.TaskStatusRepository;
|
|||||||
import com.fasterxml.jackson.databind.JsonNode;
|
import com.fasterxml.jackson.databind.JsonNode;
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
|
||||||
|
import jakarta.annotation.PostConstruct;
|
||||||
import kong.unirest.HttpResponse;
|
import kong.unirest.HttpResponse;
|
||||||
import kong.unirest.Unirest;
|
import kong.unirest.Unirest;
|
||||||
|
|
||||||
@@ -24,18 +26,162 @@ public class TaskStatusPollingService {
|
|||||||
|
|
||||||
private static final Logger logger = LoggerFactory.getLogger(TaskStatusPollingService.class);
|
private static final Logger logger = LoggerFactory.getLogger(TaskStatusPollingService.class);
|
||||||
|
|
||||||
|
// 任务超时时间(小时)
|
||||||
|
private static final int TASK_TIMEOUT_HOURS = 1;
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
private TaskStatusRepository taskStatusRepository;
|
private TaskStatusRepository taskStatusRepository;
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
private ObjectMapper objectMapper;
|
private ObjectMapper objectMapper;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
@org.springframework.context.annotation.Lazy
|
||||||
|
private TaskQueueService taskQueueService;
|
||||||
|
|
||||||
@Value("${ai.api.key:ak_5f13ec469e6047d5b8155c3cc91350e2}")
|
@Value("${ai.api.key:ak_5f13ec469e6047d5b8155c3cc91350e2}")
|
||||||
private String apiKey;
|
private String apiKey;
|
||||||
|
|
||||||
@Value("${ai.api.base-url:http://116.62.4.26:8081}")
|
@Value("${ai.api.base-url:http://116.62.4.26:8081}")
|
||||||
private String apiBaseUrl;
|
private String apiBaseUrl;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 系统启动时恢复处理中的任务
|
||||||
|
* - 对所有 PROCESSING 状态的任务进行一次状态查询
|
||||||
|
* - 如果外部API返回已完成,则标记成功
|
||||||
|
* - 如果超时(创建时间超过1小时),则标记失败
|
||||||
|
* - 如果未超时且未完成,保持现状等待后续轮询
|
||||||
|
*/
|
||||||
|
@PostConstruct
|
||||||
|
public void recoverProcessingTasksOnStartup() {
|
||||||
|
logger.info("=== 系统启动:开始恢复处理中的任务 ===");
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 查找所有 PROCESSING 状态的任务
|
||||||
|
List<TaskStatus> processingTasks = taskStatusRepository.findAllByStatus(TaskStatus.Status.PROCESSING);
|
||||||
|
|
||||||
|
if (processingTasks.isEmpty()) {
|
||||||
|
logger.info("没有需要恢复的处理中任务");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info("发现 {} 个处理中的任务,开始恢复检查...", processingTasks.size());
|
||||||
|
|
||||||
|
for (TaskStatus task : processingTasks) {
|
||||||
|
try {
|
||||||
|
recoverSingleTask(task);
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.error("恢复任务 {} 时发生错误: {}", task.getTaskId(), e.getMessage(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info("=== 系统启动:任务恢复检查完成 ===");
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.error("系统启动恢复任务时发生错误: {}", e.getMessage(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 恢复单个任务
|
||||||
|
*/
|
||||||
|
private void recoverSingleTask(TaskStatus task) {
|
||||||
|
logger.info("恢复检查任务: taskId={}, externalTaskId={}, createdAt={}",
|
||||||
|
task.getTaskId(), task.getExternalTaskId(), task.getCreatedAt());
|
||||||
|
|
||||||
|
// 检查是否有外部任务ID
|
||||||
|
if (task.getExternalTaskId() == null || task.getExternalTaskId().isEmpty()) {
|
||||||
|
// 没有外部任务ID,检查是否超时
|
||||||
|
if (isTaskTimeout(task)) {
|
||||||
|
logger.warn("任务 {} 无外部任务ID且已超时,标记为失败", task.getTaskId());
|
||||||
|
task.markAsFailed("任务超时:未获取到外部任务ID");
|
||||||
|
taskStatusRepository.save(task);
|
||||||
|
} else {
|
||||||
|
logger.info("任务 {} 无外部任务ID但未超时,保持现状", task.getTaskId());
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 有外部任务ID,查询外部API状态
|
||||||
|
try {
|
||||||
|
String url = apiBaseUrl + "/v2/videos/generations/" + task.getExternalTaskId();
|
||||||
|
HttpResponse<String> response = Unirest.get(url)
|
||||||
|
.header("Authorization", "Bearer " + apiKey)
|
||||||
|
.asString();
|
||||||
|
|
||||||
|
if (response.getStatus() == 200) {
|
||||||
|
JsonNode responseJson = objectMapper.readTree(response.getBody());
|
||||||
|
String status = responseJson.path("status").asText();
|
||||||
|
String resultUrl = responseJson.path("data").path("output").asText();
|
||||||
|
|
||||||
|
logger.info("外部API返回: taskId={}, status={}, resultUrl={}",
|
||||||
|
task.getTaskId(), status, resultUrl);
|
||||||
|
|
||||||
|
if ("SUCCESS".equalsIgnoreCase(status) && resultUrl != null && !resultUrl.isEmpty()) {
|
||||||
|
// 任务已完成,标记成功并同步更新所有表
|
||||||
|
task.markAsCompleted(resultUrl);
|
||||||
|
taskStatusRepository.save(task);
|
||||||
|
logger.info("任务 {} 恢复为已完成状态,resultUrl={}", task.getTaskId(), resultUrl);
|
||||||
|
// 同步更新业务表、UserWork 等
|
||||||
|
taskQueueService.handleTaskCompletionByTaskId(task.getTaskId(), resultUrl);
|
||||||
|
} else if ("FAILED".equalsIgnoreCase(status) || "ERROR".equalsIgnoreCase(status)) {
|
||||||
|
// 任务失败
|
||||||
|
String failReason = responseJson.path("fail_reason").asText("外部API返回失败");
|
||||||
|
task.markAsFailed(failReason);
|
||||||
|
taskStatusRepository.save(task);
|
||||||
|
logger.warn("任务 {} 恢复为失败状态,原因: {}", task.getTaskId(), failReason);
|
||||||
|
// 同步更新业务表、UserWork 等
|
||||||
|
taskQueueService.handleTaskFailureByTaskId(task.getTaskId(), failReason);
|
||||||
|
} else {
|
||||||
|
// 任务仍在处理中,检查是否超时
|
||||||
|
if (isTaskTimeout(task)) {
|
||||||
|
logger.warn("任务 {} 已超时,标记为失败", task.getTaskId());
|
||||||
|
task.markAsFailed("任务超时");
|
||||||
|
taskStatusRepository.save(task);
|
||||||
|
// 同步更新业务表、UserWork 等
|
||||||
|
taskQueueService.handleTaskFailureByTaskId(task.getTaskId(), "任务超时");
|
||||||
|
} else {
|
||||||
|
logger.info("任务 {} 仍在处理中且未超时,保持现状", task.getTaskId());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
logger.warn("查询任务状态失败: taskId={}, status={}", task.getTaskId(), response.getStatus());
|
||||||
|
// 检查是否超时
|
||||||
|
if (isTaskTimeout(task)) {
|
||||||
|
task.markAsFailed("任务超时:无法获取外部状态");
|
||||||
|
taskStatusRepository.save(task);
|
||||||
|
taskQueueService.handleTaskFailureByTaskId(task.getTaskId(), "任务超时:无法获取外部状态");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.error("查询外部API失败: taskId={}, error={}", task.getTaskId(), e.getMessage());
|
||||||
|
// 检查是否超时
|
||||||
|
if (isTaskTimeout(task)) {
|
||||||
|
task.markAsFailed("任务超时:查询外部API异常");
|
||||||
|
taskStatusRepository.save(task);
|
||||||
|
taskQueueService.handleTaskFailureByTaskId(task.getTaskId(), "任务超时:查询外部API异常");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查任务是否超时(创建时间超过1小时)
|
||||||
|
*/
|
||||||
|
private boolean isTaskTimeout(TaskStatus task) {
|
||||||
|
if (task.getCreatedAt() == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
long hoursSinceCreation = ChronoUnit.HOURS.between(task.getCreatedAt(), LocalDateTime.now());
|
||||||
|
return hoursSinceCreation >= TASK_TIMEOUT_HOURS;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据状态查找任务列表
|
||||||
|
*/
|
||||||
|
public List<TaskStatus> findByStatus(TaskStatus.Status status) {
|
||||||
|
return taskStatusRepository.findByStatus(status, org.springframework.data.domain.Pageable.unpaged()).getContent();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* (Scheduling disabled) 原先每2分钟执行一次轮询查询任务状态。
|
* (Scheduling disabled) 原先每2分钟执行一次轮询查询任务状态。
|
||||||
* 注意:调度已集中到 `TaskQueueScheduler.checkTaskStatuses()`,以避免重复并发查询。
|
* 注意:调度已集中到 `TaskQueueScheduler.checkTaskStatuses()`,以避免重复并发查询。
|
||||||
@@ -46,7 +192,7 @@ public class TaskStatusPollingService {
|
|||||||
logger.info("=== 开始执行任务状态轮询查询 (每2分钟) ===");
|
logger.info("=== 开始执行任务状态轮询查询 (每2分钟) ===");
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 查找需要轮询的任务(状态为PROCESSING且创建时间超过2分钟)
|
// 查找需要轮询的任务(上次轮询时间超过2分钟)
|
||||||
LocalDateTime cutoffTime = LocalDateTime.now().minusMinutes(2);
|
LocalDateTime cutoffTime = LocalDateTime.now().minusMinutes(2);
|
||||||
// 先做一次计数,避免在无任务时加载实体列表
|
// 先做一次计数,避免在无任务时加载实体列表
|
||||||
long needCount = taskStatusRepository.countTasksNeedingPolling(cutoffTime);
|
long needCount = taskStatusRepository.countTasksNeedingPolling(cutoffTime);
|
||||||
@@ -54,7 +200,11 @@ public class TaskStatusPollingService {
|
|||||||
logger.info("需要轮询查询的任务数量: {}", needCount);
|
logger.info("需要轮询查询的任务数量: {}", needCount);
|
||||||
|
|
||||||
if (needCount == 0) {
|
if (needCount == 0) {
|
||||||
logger.debug("当前没有需要轮询的任务(count=0)");
|
// 检查是否有PROCESSING任务但不满足轮询条件
|
||||||
|
long processingCount = taskStatusRepository.countByStatus(TaskStatus.Status.PROCESSING);
|
||||||
|
if (processingCount > 0) {
|
||||||
|
logger.debug("有 {} 个PROCESSING任务,但均在冷却期内(2分钟内已轮询过)", processingCount);
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -90,14 +240,15 @@ public class TaskStatusPollingService {
|
|||||||
logger.info("轮询任务状态: taskId={}, externalTaskId={}", task.getTaskId(), task.getExternalTaskId());
|
logger.info("轮询任务状态: taskId={}, externalTaskId={}", task.getTaskId(), task.getExternalTaskId());
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 调用外部API查询状态(长时间运行,不在事务中)
|
// 使用正确的 API 端点:GET /v2/videos/generations/{task_id}
|
||||||
HttpResponse<String> response = Unirest.post(apiBaseUrl + "/v1/videos")
|
String url = apiBaseUrl + "/v2/videos/generations/" + task.getExternalTaskId();
|
||||||
|
HttpResponse<String> response = Unirest.get(url)
|
||||||
.header("Authorization", "Bearer " + apiKey)
|
.header("Authorization", "Bearer " + apiKey)
|
||||||
.field("task_id", task.getExternalTaskId())
|
|
||||||
.asString();
|
.asString();
|
||||||
|
|
||||||
if (response.getStatus() == 200) {
|
if (response.getStatus() == 200) {
|
||||||
JsonNode responseJson = objectMapper.readTree(response.getBody());
|
JsonNode responseJson = objectMapper.readTree(response.getBody());
|
||||||
|
logger.info("轮询任务状态成功: taskId={}, response={}", task.getTaskId(), response.getBody());
|
||||||
// 更新任务状态(使用单独的事务方法)
|
// 更新任务状态(使用单独的事务方法)
|
||||||
updateTaskStatusWithTransaction(task, responseJson);
|
updateTaskStatusWithTransaction(task, responseJson);
|
||||||
} else {
|
} else {
|
||||||
@@ -137,9 +288,11 @@ public class TaskStatusPollingService {
|
|||||||
private void updateTaskStatus(TaskStatus task, JsonNode responseJson) {
|
private void updateTaskStatus(TaskStatus task, JsonNode responseJson) {
|
||||||
try {
|
try {
|
||||||
String status = responseJson.path("status").asText();
|
String status = responseJson.path("status").asText();
|
||||||
int progress = responseJson.path("progress").asInt(0);
|
String progressStr = responseJson.path("progress").asText("0%");
|
||||||
String resultUrl = responseJson.path("result_url").asText();
|
int progress = parseProgress(progressStr);
|
||||||
String errorMessage = responseJson.path("error_message").asText();
|
// API 返回的视频URL在 data.output 中
|
||||||
|
String resultUrl = responseJson.path("data").path("output").asText();
|
||||||
|
String errorMessage = responseJson.path("fail_reason").asText();
|
||||||
|
|
||||||
task.incrementPollCount();
|
task.incrementPollCount();
|
||||||
task.setProgress(progress);
|
task.setProgress(progress);
|
||||||
@@ -147,18 +300,30 @@ public class TaskStatusPollingService {
|
|||||||
switch (status.toLowerCase()) {
|
switch (status.toLowerCase()) {
|
||||||
case "completed":
|
case "completed":
|
||||||
case "success":
|
case "success":
|
||||||
task.markAsCompleted(resultUrl);
|
if (resultUrl != null && !resultUrl.isEmpty()) {
|
||||||
logger.info("任务完成: taskId={}, resultUrl={}", task.getTaskId(), resultUrl);
|
task.markAsCompleted(resultUrl);
|
||||||
break;
|
logger.info("任务完成: taskId={}, resultUrl={}", task.getTaskId(), resultUrl);
|
||||||
|
taskStatusRepository.save(task);
|
||||||
|
// 同步更新业务表、UserWork 等
|
||||||
|
taskQueueService.handleTaskCompletionByTaskId(task.getTaskId(), resultUrl);
|
||||||
|
} else {
|
||||||
|
logger.warn("任务状态为成功但 resultUrl 为空,保持 PROCESSING: taskId={}", task.getTaskId());
|
||||||
|
taskStatusRepository.save(task);
|
||||||
|
}
|
||||||
|
return; // 已保存,直接返回
|
||||||
|
|
||||||
case "failed":
|
case "failed":
|
||||||
case "error":
|
case "error":
|
||||||
task.markAsFailed(errorMessage);
|
task.markAsFailed(errorMessage);
|
||||||
logger.warn("任务失败: taskId={}, error={}", task.getTaskId(), errorMessage);
|
logger.warn("任务失败: taskId={}, error={}", task.getTaskId(), errorMessage);
|
||||||
break;
|
taskStatusRepository.save(task);
|
||||||
|
// 同步更新业务表、UserWork 等
|
||||||
|
taskQueueService.handleTaskFailureByTaskId(task.getTaskId(), errorMessage);
|
||||||
|
return; // 已保存,直接返回
|
||||||
|
|
||||||
case "processing":
|
case "processing":
|
||||||
case "in_progress":
|
case "in_progress":
|
||||||
|
case "not_start":
|
||||||
task.setStatus(TaskStatus.Status.PROCESSING);
|
task.setStatus(TaskStatus.Status.PROCESSING);
|
||||||
logger.info("任务处理中: taskId={}, progress={}%", task.getTaskId(), progress);
|
logger.info("任务处理中: taskId={}, progress={}%", task.getTaskId(), progress);
|
||||||
break;
|
break;
|
||||||
@@ -249,5 +414,21 @@ public class TaskStatusPollingService {
|
|||||||
taskStatus.setUpdatedAt(LocalDateTime.now());
|
taskStatus.setUpdatedAt(LocalDateTime.now());
|
||||||
return taskStatusRepository.save(taskStatus);
|
return taskStatusRepository.save(taskStatus);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析进度字符串(如 "100%")为整数
|
||||||
|
*/
|
||||||
|
private int parseProgress(String progressStr) {
|
||||||
|
if (progressStr == null || progressStr.isEmpty()) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
// 移除百分号并解析
|
||||||
|
String numStr = progressStr.replace("%", "").trim();
|
||||||
|
return Integer.parseInt(numStr);
|
||||||
|
} catch (NumberFormatException e) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -145,7 +145,11 @@ public class JwtUtils {
|
|||||||
*/
|
*/
|
||||||
public String extractTokenFromHeader(String authHeader) {
|
public String extractTokenFromHeader(String authHeader) {
|
||||||
if (authHeader != null && authHeader.startsWith("Bearer ")) {
|
if (authHeader != null && authHeader.startsWith("Bearer ")) {
|
||||||
return authHeader.substring(7);
|
String token = authHeader.substring(7);
|
||||||
|
// 检查 token 是否有效(不为空、不是 "null" 字符串、包含两个点)
|
||||||
|
if (token != null && !token.isEmpty() && !token.equals("null") && token.contains(".")) {
|
||||||
|
return token;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -43,15 +43,15 @@ springdoc.swagger-ui.default-model-expand-depth=1
|
|||||||
|
|
||||||
# 腾讯云COS对象存储配置
|
# 腾讯云COS对象存储配置
|
||||||
# 是否启用COS(设置为true后需要配置下面的参数)
|
# 是否启用COS(设置为true后需要配置下面的参数)
|
||||||
tencent.cos.enabled=false
|
tencent.cos.enabled=true
|
||||||
# 腾讯云SecretId(从控制台获取:https://console.cloud.tencent.com/cam/capi)
|
# 腾讯云SecretId(从控制台获取:https://console.cloud.tencent.com/cam/capi)
|
||||||
tencent.cos.secret-id=
|
tencent.cos.secret-id=AKID2xjaRPSOSYk2fIxV7nQuDi9NOIzTjlbJ
|
||||||
# 腾讯云SecretKey
|
# 腾讯云SecretKey
|
||||||
tencent.cos.secret-key=
|
tencent.cos.secret-key=Xrxywju0wfAf3QiqlT2ZvGYgeS6WjnjT
|
||||||
# COS区域(例如:ap-guangzhou、ap-shanghai、ap-beijing等)
|
# COS区域(例如:ap-guangzhou、ap-shanghai、ap-beijing等)
|
||||||
tencent.cos.region=ap-guangzhou
|
tencent.cos.region=ap-nanjing
|
||||||
# COS存储桶名称(例如:my-bucket-1234567890)
|
# COS存储桶名称(例如:my-bucket-1234567890)
|
||||||
tencent.cos.bucket-name=
|
tencent.cos.bucket-name=test-1323844400
|
||||||
|
|
||||||
# ============================================
|
# ============================================
|
||||||
# PayPal支付配置
|
# PayPal支付配置
|
||||||
|
|||||||
Reference in New Issue
Block a user