feat: schedule periodic reports asynchronously

This commit is contained in:
2026-07-31 17:11:21 +08:00
parent a589a25bdc
commit 0008903e8d
21 changed files with 969 additions and 145 deletions

View File

@@ -98,7 +98,7 @@ const studentImportResult = ref<UserImportResult | null>(null);
const entitlementPlanForm = reactive({
name: "",
planType: "basic" as "basic" | "deep" | "addon" | "teacher",
planType: "basic" as "basic" | "deep" | "addon",
description: "",
validityDays: null as number | null,
monthlyTopicLimit: 30 as number | null,
@@ -343,7 +343,6 @@ function planTypeLabel(type: string) {
basic: "基础版",
deep: "深度陪伴版",
addon: "高频加购包",
teacher: "老师工作版",
}[type] || type;
}
@@ -423,7 +422,7 @@ async function generateUserReport(type: "weekly" | "monthly" | "stage") {
try {
await api.generateUserReport(selectedUserDetail.value.user.id, { reportType: type });
selectedUserDetail.value = await api.userDetail(selectedUserDetail.value.user.id);
ElMessage.success("报告已生成");
ElMessage.success("报告任务已进入后台队列,不会阻塞聊天服务");
} catch (error) {
ElMessage.error(error instanceof Error ? error.message : "报告生成失败");
} finally {
@@ -431,6 +430,27 @@ async function generateUserReport(type: "weekly" | "monthly" | "stage") {
}
}
async function refreshSelectedUserDetail() {
if (!selectedUserDetail.value) return;
userDetailLoading.value = true;
try {
selectedUserDetail.value = await api.userDetail(selectedUserDetail.value.user.id);
} finally {
userDetailLoading.value = false;
}
}
function reportStatusLabel(status: string) {
const labels: Record<string, string> = {
pending: "等待生成",
running: "生成中",
success: "已完成",
empty: "暂无沉淀",
failed: "生成失败",
};
return labels[status] || status;
}
async function loadRecordTab(tab = recordTab.value) {
loading.value = true;
try {
@@ -1231,7 +1251,6 @@ function formatRecordDateTime(value: string, boundary: "start" | "end") {
<el-option label="基础版" value="basic" />
<el-option label="深度陪伴版" value="deep" />
<el-option label="高频加购包" value="addon" />
<el-option label="老师工作版" value="teacher" />
</el-select>
</el-form-item>
<el-form-item label="有效天数">
@@ -1727,6 +1746,7 @@ function formatRecordDateTime(value: string, boundary: "start" | "end") {
<p>基于已沉淀的主题摘要生成不读取整段原始聊天同一周期重复生成会覆盖旧报告</p>
</div>
<div class="user-report-actions">
<el-button size="small" @click="refreshSelectedUserDetail">刷新状态</el-button>
<el-button size="small" :loading="userReportGenerating === 'weekly'" @click="generateUserReport('weekly')">生成周报</el-button>
<el-button size="small" :loading="userReportGenerating === 'monthly'" @click="generateUserReport('monthly')">生成月报</el-button>
<el-button size="small" :loading="userReportGenerating === 'stage'" @click="generateUserReport('stage')">生成阶段总结</el-button>
@@ -1736,19 +1756,24 @@ function formatRecordDateTime(value: string, boundary: "start" | "end") {
<el-collapse-item
v-for="report in selectedUserDetail.recentReports"
:key="report.id"
:title="`#${report.id} ${report.title} / ${report.status}`"
:title="`#${report.id} ${report.title} / ${reportStatusLabel(report.status)}`"
>
<section class="topic-summary-card">
<div class="topic-summary-meta">
<span>状态{{ reportStatusLabel(report.status) }}</span>
<span>类型{{ report.reportTypeLabel }}</span>
<span>周期{{ report.periodStart }} {{ report.periodEnd }}</span>
<span>来源主题{{ report.sourceTopicIds.length }}</span>
<span>来源摘要{{ report.sourceSummaryIds.length }}</span>
<span>模型{{ report.modelName || '-' }}</span>
<span>生成{{ report.generatedAt }}</span>
<span>触发方式{{ report.generatedBy.startsWith('schedule:') ? '系统定时' : '管理员手动' }}</span>
<span>执行次数{{ report.attemptCount }}/{{ report.maxAttempts }}</span>
<span v-if="report.nextRunAt">下次执行{{ report.nextRunAt }}</span>
<span>完成时间{{ report.finishedAt || '-' }}</span>
</div>
<p v-if="report.errorMessage" class="report-error">生成失败{{ report.errorMessage }}</p>
<pre>{{ report.content }}</pre>
<p v-if="report.errorMessage" class="report-error">最近错误{{ report.errorMessage }}</p>
<pre v-if="report.content">{{ report.content }}</pre>
<p v-else-if="report.status === 'pending' || report.status === 'running'" class="report-pending">后台正在生成报告可以先关闭抽屉稍后回来查看</p>
</section>
</el-collapse-item>
</el-collapse>