feat: 异步沉淀主题摘要和成长档案

This commit is contained in:
2026-08-03 11:35:58 +08:00
parent a3946013f5
commit d2e08ab36c
24 changed files with 825 additions and 58 deletions

View File

@@ -47,6 +47,7 @@ const selectedUserDetail = ref<AdminUserDetail | null>(null);
const userDetailOpen = ref(false);
const userDetailLoading = ref(false);
const userReportGenerating = ref("");
const userSettlementRetrying = ref<number | null>(null);
const userKeyword = ref("");
const entitlementPlans = ref<EntitlementPlan[]>([]);
const models = ref<ModelItem[]>([]);
@@ -375,11 +376,26 @@ async function refreshSelectedUserDetail() {
}
}
async function retryTopicSettlement(topicId: number) {
if (!selectedUserDetail.value || userSettlementRetrying.value !== null) return;
userSettlementRetrying.value = topicId;
try {
await api.retryTopicSettlement(selectedUserDetail.value.user.id, topicId);
selectedUserDetail.value = await api.userDetail(selectedUserDetail.value.user.id);
ElMessage.success("主题沉淀任务已重新进入后台队列");
} catch (error) {
ElMessage.error(error instanceof Error ? error.message : "主题沉淀重试失败");
} finally {
userSettlementRetrying.value = null;
}
}
function reportStatusLabel(status: string) {
const labels: Record<string, string> = {
pending: "等待生成",
running: "生成中",
success: "已完成",
fallback: "已降级沉淀",
empty: "暂无沉淀",
failed: "生成失败",
};
@@ -1714,13 +1730,28 @@ function formatRecordDateTime(value: string, boundary: "start" | "end") {
<span>更新{{ topic.updatedAt }}</span>
</div>
<template v-if="topic.summary">
<pre>{{ topic.summary.summary }}</pre>
<div class="topic-summary-fields">
<div class="topic-summary-meta">
<span>沉淀状态{{ reportStatusLabel(topic.summary.status) }}</span>
<span>执行次数{{ topic.summary.attemptCount }}/{{ topic.summary.maxAttempts }}</span>
<span v-if="topic.summary.lastStartedAt">开始执行{{ topic.summary.lastStartedAt }}</span>
<span v-if="topic.summary.finishedAt">完成{{ topic.summary.finishedAt }}</span>
</div>
<p v-if="topic.summary.errorMessage" class="report-error">最近错误{{ topic.summary.errorMessage }}</p>
<pre v-if="topic.summary.status === 'success' || topic.summary.status === 'fallback'">{{ topic.summary.summary }}</pre>
<p v-else-if="topic.summary.status === 'pending' || topic.summary.status === 'running'" class="report-pending">主题已结束摘要和成长档案正在后台生成</p>
<div v-if="topic.summary.status === 'success' || topic.summary.status === 'fallback'" class="topic-summary-fields">
<p v-if="topic.summary.emotions"><strong>情绪</strong>{{ topic.summary.emotions }}</p>
<p v-if="topic.summary.bodyFeelings"><strong>身体感受</strong>{{ topic.summary.bodyFeelings }}</p>
<p v-if="topic.summary.recommendedHomework"><strong>推荐功课</strong>{{ topic.summary.recommendedHomework }}</p>
<p v-if="topic.summary.nextObservation"><strong>下一步观察</strong>{{ topic.summary.nextObservation }}</p>
</div>
<el-button
v-if="topic.summary.status === 'failed'"
size="small"
type="primary"
:loading="userSettlementRetrying === topic.id"
@click="retryTopicSettlement(topic.id)"
>重新沉淀</el-button>
</template>
<el-empty v-else description="该主题尚未沉淀摘要" :image-size="60" />
</section>

View File

@@ -35,6 +35,7 @@ import type {
QuestionInsightRefreshResult,
QuestionInsightSummary,
TopicSessionRecord,
TopicSummaryRecord,
} from "../types/api";
const API_BASE = import.meta.env.VITE_API_BASE_URL ?? "/api";
@@ -135,6 +136,8 @@ export const api = {
userReports: (id: number, limit = 20) => request<PeriodicReportRecord[]>(`/admin/user/${id}/reports${queryString({ limit })}`),
generateUserReport: (id: number, payload: { reportType: string; periodStart?: string | null; periodEnd?: string | null }) =>
request<PeriodicReportRecord>(`/admin/user/${id}/reports/generate`, { method: "POST", body: JSON.stringify(payload) }),
retryTopicSettlement: (userId: number, topicId: number) =>
request<TopicSummaryRecord>(`/admin/user/${userId}/topic/${topicId}/settlement/retry`, { method: "POST", body: "{}" }),
createUser: (payload: Record<string, unknown>) =>
request<AdminUser>("/admin/user", { method: "POST", body: JSON.stringify(payload) }),
importUsers: (students: Record<string, unknown>[]) =>

View File

@@ -500,6 +500,11 @@ export interface TopicSummaryRecord {
modelName?: string | null;
status: string;
errorMessage?: string | null;
attemptCount: number;
maxAttempts: number;
nextRunAt?: string | null;
lastStartedAt?: string | null;
finishedAt?: string | null;
generatedAt: string;
}