feat: 添加分页和优化会议任务处理逻辑
- 在前端 `Dashboard` 页面中添加分页功能 - 优化 `AiTaskServiceImpl` 中的 ASR 任务处理逻辑,支持任务恢复和失败处理 - 更新相关服务和组件以支持新的分页和任务处理逻辑dev_na
parent
f7480df565
commit
7d08234919
|
|
@ -214,26 +214,18 @@ public class AiTaskServiceImpl extends ServiceImpl<AiTaskMapper, AiTask> impleme
|
|||
? String.valueOf(taskRecord.getResponseData().getOrDefault("task_id", ""))
|
||||
: "";
|
||||
|
||||
if (taskId == null || taskId.isBlank()) {
|
||||
updateProgress(meeting.getId(), 5, "正在提交识别请求...", 0);
|
||||
Map<String, Object> req = buildAsrRequest(meeting, taskRecord, asrModel);
|
||||
taskRecord.setRequestData(req);
|
||||
this.updateById(taskRecord);
|
||||
|
||||
String respBody = postJson(submitUrl, req, asrModel.getApiKey());
|
||||
JsonNode submitNode = objectMapper.readTree(respBody);
|
||||
if (submitNode.path("code").asInt() != 0) {
|
||||
updateAiTaskFail(taskRecord, "提交失败: " + respBody);
|
||||
throw new RuntimeException("ASR引擎拒绝请求: " + submitNode.path("msg").asText());
|
||||
}
|
||||
taskId = submitNode.path("data").path("task_id").asText();
|
||||
taskRecord.setResponseData(Map.of("task_id", taskId));
|
||||
this.updateById(taskRecord);
|
||||
} else {
|
||||
if (taskId != null && !taskId.isBlank()) {
|
||||
updateProgress(meeting.getId(), 5, "Resuming ASR polling...", 0);
|
||||
if (!canResumeAsrTask(asrModel, meeting.getId(), taskId)) {
|
||||
clearAsrTaskId(taskRecord);
|
||||
taskId = "";
|
||||
}
|
||||
}
|
||||
|
||||
if (taskId == null || taskId.isBlank()) {
|
||||
taskId = submitAsrTask(meeting, taskRecord, asrModel, submitUrl);
|
||||
}
|
||||
this.updateById(taskRecord);
|
||||
|
||||
String queryUrl = appendPath(asrModel.getBaseUrl(), "api/v1/asr/transcriptions/" + taskId);
|
||||
|
||||
// 轮询逻辑(带防卡死防护)
|
||||
|
|
@ -245,6 +237,11 @@ public class AiTaskServiceImpl extends ServiceImpl<AiTaskMapper, AiTask> impleme
|
|||
Thread.sleep(2000);
|
||||
String queryResp = get(queryUrl, asrModel.getApiKey());
|
||||
JsonNode statusNode = objectMapper.readTree(queryResp);
|
||||
int code = statusNode.path("code").asInt(500);
|
||||
if (code!=0){
|
||||
updateAiTaskFail(taskRecord, "ASR 引擎返回失败:" + queryResp);
|
||||
throw new RuntimeException("ASR引擎处理失败: " + statusNode.get("message").asText());
|
||||
}
|
||||
JsonNode data = statusNode.path("data");
|
||||
String status = data.path("status").asText();
|
||||
|
||||
|
|
@ -325,6 +322,58 @@ public class AiTaskServiceImpl extends ServiceImpl<AiTaskMapper, AiTask> impleme
|
|||
return req;
|
||||
}
|
||||
|
||||
private boolean canResumeAsrTask(AiModelVO asrModel, Long meetingId, String taskId) {
|
||||
String queryUrl = appendPath(asrModel.getBaseUrl(), "api/v1/asr/transcriptions/" + taskId);
|
||||
try {
|
||||
String queryResp = get(queryUrl, asrModel.getApiKey());
|
||||
JsonNode statusNode = objectMapper.readTree(queryResp);
|
||||
int code = statusNode.path("code").asInt(500);
|
||||
if (code != 0) {
|
||||
log.warn("ASR task {} progress fetch failed for meeting {}, will resubmit task. response={}",
|
||||
taskId, meetingId, queryResp);
|
||||
return false;
|
||||
}
|
||||
|
||||
String status = statusNode.path("data").path("status").asText();
|
||||
if ("failed".equalsIgnoreCase(status)) {
|
||||
log.warn("ASR task {} already failed for meeting {}, will resubmit task.", taskId, meetingId);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
} catch (Exception ex) {
|
||||
log.warn("ASR task {} progress fetch threw exception for meeting {}, will resubmit task.",
|
||||
taskId, meetingId, ex);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private void clearAsrTaskId(AiTask taskRecord) {
|
||||
if (taskRecord.getResponseData() == null || taskRecord.getResponseData().isEmpty()) {
|
||||
return;
|
||||
}
|
||||
Map<String, Object> responseData = new HashMap<>(taskRecord.getResponseData());
|
||||
responseData.remove("task_id");
|
||||
taskRecord.setResponseData(responseData.isEmpty() ? null : responseData);
|
||||
this.updateById(taskRecord);
|
||||
}
|
||||
|
||||
private String submitAsrTask(Meeting meeting, AiTask taskRecord, AiModelVO asrModel, String submitUrl) throws Exception {
|
||||
updateProgress(meeting.getId(), 5, "重新提交任务...", 0);
|
||||
Map<String, Object> req = buildAsrRequest(meeting, taskRecord, asrModel);
|
||||
taskRecord.setRequestData(req);
|
||||
this.updateById(taskRecord);
|
||||
|
||||
String respBody = postJson(submitUrl, req, asrModel.getApiKey());
|
||||
JsonNode submitNode = objectMapper.readTree(respBody);
|
||||
if (submitNode.path("code").asInt() != 0) {
|
||||
updateAiTaskFail(taskRecord, "ASR识别失败: " + respBody);
|
||||
throw new RuntimeException("ASR识别失败: " + submitNode.path("msg").asText());
|
||||
}
|
||||
String taskId = submitNode.path("data").path("task_id").asText();
|
||||
taskRecord.setResponseData(Map.of("task_id", taskId));
|
||||
this.updateById(taskRecord);
|
||||
return taskId;
|
||||
}
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
protected String saveTranscripts(Meeting meeting, JsonNode resultNode) {
|
||||
// 关键:入库前清理旧记录,防止恢复任务导致数据重复
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import React, { useState, useEffect } from 'react';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import PageContainer from "@/components/shared/PageContainer";
|
||||
import AppPagination from '@/components/shared/AppPagination';
|
||||
import { Row, Col, Card, Statistic, List, Tag, Typography, Button, Space, Empty, Steps, Progress, Divider } from 'antd';
|
||||
import {
|
||||
HistoryOutlined,
|
||||
|
|
@ -16,8 +17,8 @@ import {
|
|||
} from '@ant-design/icons';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import dayjs from 'dayjs';
|
||||
import { getDashboardStats, getRecentTasks, DashboardStats } from '@/api/business/dashboard';
|
||||
import { MeetingVO, getMeetingProgress, MeetingProgress } from '@/api/business/meeting';
|
||||
import { getDashboardStats, DashboardStats } from '@/api/business/dashboard';
|
||||
import { MeetingVO, getMeetingPage, getMeetingProgress, MeetingProgress } from '@/api/business/meeting';
|
||||
|
||||
const { Title, Text } = Typography;
|
||||
|
||||
|
|
@ -33,12 +34,12 @@ const MeetingProgressDisplay: React.FC<{ meeting: MeetingVO }> = ({ meeting }) =
|
|||
if (res.data?.data) {
|
||||
setProgress(res.data.data);
|
||||
}
|
||||
} catch (err) {
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
};
|
||||
|
||||
fetchProgress();
|
||||
void fetchProgress();
|
||||
const timer = setInterval(fetchProgress, 3000);
|
||||
return () => clearInterval(timer);
|
||||
}, [meeting.id, meeting.status]);
|
||||
|
|
@ -50,7 +51,7 @@ const MeetingProgressDisplay: React.FC<{ meeting: MeetingVO }> = ({ meeting }) =
|
|||
|
||||
return (
|
||||
<div style={{ marginTop: 12, padding: '12px 16px', background: 'var(--app-bg-surface-soft)', borderRadius: 8, border: '1px solid var(--app-border-color)' }}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 6 }}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', gap: 12, marginBottom: 6 }}>
|
||||
<Text type="secondary" style={{ fontSize: 12 }}>
|
||||
<LoadingOutlined style={{ marginRight: 6, color: '#1890ff' }} spin={!isError} />
|
||||
{progress?.message || '准备分析中...'}
|
||||
|
|
@ -73,33 +74,62 @@ export const Dashboard: React.FC = () => {
|
|||
const [stats, setStats] = useState<DashboardStats | null>(null);
|
||||
const [recentTasks, setRecentTasks] = useState<MeetingVO[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [pagination, setPagination] = useState({ current: 1, pageSize: 10, total: 0 });
|
||||
|
||||
const processingCount = Number(stats?.processingTasks || 0);
|
||||
const dashboardLoading = loading && processingCount > 0;
|
||||
const dashboardLoading = loading;
|
||||
|
||||
useEffect(() => {
|
||||
fetchDashboardData();
|
||||
const timer = setInterval(fetchDashboardData, 5000);
|
||||
return () => clearInterval(timer);
|
||||
}, []);
|
||||
void fetchDashboardData();
|
||||
}, [pagination.current, pagination.pageSize]);
|
||||
|
||||
const fetchDashboardData = async () => {
|
||||
useEffect(() => {
|
||||
const timer = setInterval(() => {
|
||||
void fetchDashboardData(true);
|
||||
}, 5000);
|
||||
return () => clearInterval(timer);
|
||||
}, [pagination.current, pagination.pageSize]);
|
||||
|
||||
const fetchDashboardData = async (silent = false) => {
|
||||
if (!silent) {
|
||||
setLoading(true);
|
||||
}
|
||||
try {
|
||||
const [statsRes, tasksRes] = await Promise.all([getDashboardStats(), getRecentTasks()]);
|
||||
const [statsRes, tasksRes] = await Promise.all([
|
||||
getDashboardStats(),
|
||||
getMeetingPage({
|
||||
current: pagination.current,
|
||||
size: pagination.pageSize,
|
||||
}),
|
||||
]);
|
||||
setStats(statsRes.data.data);
|
||||
setRecentTasks(tasksRes.data.data || []);
|
||||
setRecentTasks(tasksRes.data?.data?.records || []);
|
||||
setPagination((prev) => ({
|
||||
...prev,
|
||||
total: tasksRes.data?.data?.total || 0,
|
||||
}));
|
||||
} catch (err) {
|
||||
console.error('Dashboard data load failed', err);
|
||||
} finally {
|
||||
if (!silent) {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handlePageChange = (page: number, pageSize: number) => {
|
||||
setPagination((prev) => ({
|
||||
...prev,
|
||||
current: page,
|
||||
pageSize,
|
||||
}));
|
||||
};
|
||||
|
||||
const renderTaskProgress = (item: MeetingVO) => {
|
||||
const currentStep = item.status === 4 ? 0 : (item.status === 3 ? 2 : item.status);
|
||||
|
||||
return (
|
||||
<div style={{ width: '100%', maxWidth: 450 }}>
|
||||
<div style={{ width: '100%', maxWidth: '100%' }}>
|
||||
<Steps
|
||||
size="small"
|
||||
current={currentStep}
|
||||
|
|
@ -108,7 +138,7 @@ export const Dashboard: React.FC = () => {
|
|||
{
|
||||
title: '语音转录',
|
||||
icon: item.status === 1 ? <LoadingOutlined spin /> : <AudioOutlined />,
|
||||
description: item.status > 1 ? '识别完成' : (item.status === 1 ? 'AI转录中' : '排队中')
|
||||
description: item.status > 1 ? '识别完成' : (item.status === 1 ? 'AI 转录中' : '排队中')
|
||||
},
|
||||
{
|
||||
title: '智能总结',
|
||||
|
|
@ -139,12 +169,13 @@ export const Dashboard: React.FC = () => {
|
|||
|
||||
return (
|
||||
<PageContainer
|
||||
title="仪表板"
|
||||
title="仪表盘"
|
||||
subtitle="系统运行概览与最近任务动态"
|
||||
style={{ overflow: 'hidden' }}
|
||||
>
|
||||
<Row gutter={24} style={{ marginBottom: 24 }}>
|
||||
<Row gutter={[16, 16]} style={{ marginBottom: 16, flexShrink: 0 }}>
|
||||
{statCards.map((s, idx) => (
|
||||
<Col span={6} key={idx}>
|
||||
<Col xs={24} sm={12} xl={6} key={idx}>
|
||||
<Card variant="borderless" style={{ borderRadius: 16, boxShadow: 'var(--app-shadow)', background: 'var(--app-bg-card)', border: '1px solid var(--app-border-color)', backdropFilter: 'blur(16px)' }}>
|
||||
<Statistic
|
||||
title={<Text type="secondary" style={{ fontSize: 13 }}>{s.label}</Text>}
|
||||
|
|
@ -158,44 +189,48 @@ export const Dashboard: React.FC = () => {
|
|||
</Row>
|
||||
|
||||
<Card
|
||||
className="dashboard-task-card"
|
||||
title={
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', flexWrap: 'wrap', gap: 12 }}>
|
||||
<Space><ClockCircleOutlined /> 最近任务动态</Space>
|
||||
<Button type="link" onClick={() => navigate('/meetings')}>查看历史记录</Button>
|
||||
</div>
|
||||
}
|
||||
variant="borderless"
|
||||
style={{ borderRadius: 16, boxShadow: 'var(--app-shadow)', background: 'var(--app-bg-card)', border: '1px solid var(--app-border-color)', backdropFilter: 'blur(16px)' }}
|
||||
style={{ flex: 1, minHeight: 0, borderRadius: 16, boxShadow: 'var(--app-shadow)', background: 'var(--app-bg-card)', border: '1px solid var(--app-border-color)', backdropFilter: 'blur(16px)', overflow: 'hidden' }}
|
||||
styles={{ body: { display: 'flex', flexDirection: 'column', gap: 16, flex: 1, minHeight: 0, overflow: 'hidden',height:'90%' } }}
|
||||
>
|
||||
<div className="dashboard-task-list">
|
||||
<List
|
||||
loading={dashboardLoading}
|
||||
dataSource={recentTasks}
|
||||
renderItem={(item) => (
|
||||
<List.Item style={{ padding: '24px 0', borderBottom: '1px solid #f0f2f5' }}>
|
||||
<div style={{ width: '100%' }}>
|
||||
<Row gutter={32} align="middle">
|
||||
<Col span={8}>
|
||||
<Space direction="vertical" size={4}>
|
||||
<Title level={5} style={{ margin: 0, cursor: 'pointer' }} onClick={() => navigate(`/meetings/${item.id}`)}>
|
||||
<Row gutter={[24, 16]} align="middle">
|
||||
<Col xs={24} xl={8}>
|
||||
<Space direction="vertical" size={4} style={{ width: '100%' }}>
|
||||
<Title level={5} style={{ margin: 0, cursor: 'pointer', wordBreak: 'break-word' }} onClick={() => navigate(`/meetings/${item.id}`)}>
|
||||
{item.title}
|
||||
</Title>
|
||||
<Space size={12} split={<Divider type="vertical" style={{ margin: 0 }} />}>
|
||||
<Space size={12} wrap split={<Divider type="vertical" style={{ margin: 0 }} />}>
|
||||
<Text type="secondary"><CalendarOutlined /> {dayjs(item.meetingTime).format('MM-DD HH:mm')}</Text>
|
||||
<Text type="secondary"><TeamOutlined /> {item.participants || item.creatorName || '未指定'}</Text>
|
||||
</Space>
|
||||
<div style={{ marginTop: 8 }}>
|
||||
<div style={{ marginTop: 8, display: 'flex', flexWrap: 'wrap', gap: 8 }}>
|
||||
{item.tags?.split(',').filter(Boolean).map((t) => (
|
||||
<Tag key={t} style={{ border: '1px solid var(--app-border-color)', background: 'color-mix(in srgb, var(--app-primary-color) 12%, var(--app-bg-surface-strong))', color: 'var(--app-text-main)', borderRadius: 4, fontSize: 11 }}>{t}</Tag>
|
||||
<Tag key={t} style={{ marginInlineEnd: 0, border: '1px solid var(--app-border-color)', background: 'color-mix(in srgb, var(--app-primary-color) 12%, var(--app-bg-surface-strong))', color: 'var(--app-text-main)', borderRadius: 4, fontSize: 11 }}>{t}</Tag>
|
||||
))}
|
||||
</div>
|
||||
</Space>
|
||||
</Col>
|
||||
|
||||
<Col span={12}>
|
||||
<Col xs={24} xl={12}>
|
||||
{renderTaskProgress(item)}
|
||||
</Col>
|
||||
|
||||
<Col span={4} style={{ textAlign: 'right' }}>
|
||||
<Col xs={24} xl={4}>
|
||||
<div className="dashboard-task-action">
|
||||
<Button
|
||||
type={item.status === 3 ? 'primary' : 'default'}
|
||||
ghost={item.status === 3}
|
||||
|
|
@ -204,6 +239,7 @@ export const Dashboard: React.FC = () => {
|
|||
>
|
||||
{item.status === 3 ? '查看纪要' : '监控详情'}
|
||||
</Button>
|
||||
</div>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
|
|
@ -213,11 +249,44 @@ export const Dashboard: React.FC = () => {
|
|||
)}
|
||||
locale={{ emptyText: <Empty description="暂无近期分析任务" /> }}
|
||||
/>
|
||||
</div>
|
||||
<AppPagination
|
||||
current={pagination.current}
|
||||
pageSize={pagination.pageSize}
|
||||
total={pagination.total}
|
||||
onChange={handlePageChange}
|
||||
/>
|
||||
</Card>
|
||||
|
||||
<style>{`
|
||||
.dashboard-task-card .ant-card-head,
|
||||
.dashboard-task-card .ant-card-body,
|
||||
.dashboard-task-card .ant-list,
|
||||
.dashboard-task-card .ant-list-items,
|
||||
.dashboard-task-card .ant-list-item {
|
||||
min-width: 0;
|
||||
}
|
||||
.dashboard-task-list {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
padding-right: 4px;
|
||||
overscroll-behavior: contain;
|
||||
}
|
||||
.dashboard-task-action {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
}
|
||||
.ant-steps-item-title { font-size: 13px !important; font-weight: 600 !important; }
|
||||
.ant-steps-item-description { font-size: 11px !important; }
|
||||
@media (max-width: 1199px) {
|
||||
.dashboard-task-action {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
}
|
||||
`}</style>
|
||||
</PageContainer>
|
||||
);
|
||||
|
|
|
|||
Loading…
Reference in New Issue