feat: schedule periodic reports asynchronously
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -2102,6 +2102,15 @@ textarea {
|
||||
color: #b42318;
|
||||
}
|
||||
|
||||
.report-pending {
|
||||
margin: 0;
|
||||
padding: 12px 14px;
|
||||
border-radius: 10px;
|
||||
background: #f2f8f6;
|
||||
color: #557068;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.chat-summary {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
|
||||
@@ -98,7 +98,7 @@ export interface AdminUserMetrics {
|
||||
export interface EntitlementPlan {
|
||||
id: number;
|
||||
name: string;
|
||||
planType: "basic" | "deep" | "addon" | "teacher";
|
||||
planType: "basic" | "deep" | "addon";
|
||||
description?: string | null;
|
||||
validityDays?: number | null;
|
||||
monthlyTopicLimit?: number | null;
|
||||
@@ -519,6 +519,11 @@ export interface PeriodicReportRecord {
|
||||
status: "success" | "failed" | "empty" | string;
|
||||
errorMessage?: string | null;
|
||||
generatedBy: string;
|
||||
attemptCount: number;
|
||||
maxAttempts: number;
|
||||
nextRunAt?: string | null;
|
||||
lastStartedAt?: string | null;
|
||||
finishedAt?: string | null;
|
||||
generatedAt: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
|
||||
Reference in New Issue
Block a user