feat: #67 GET /activity/trend 新增 dailySeries 时间序列数据
All checks were successful
Deploy API Server / build-and-deploy (push) Successful in 43s

返回格式新增 dailySeries 字段 [{date, value, label}],支持 iOS 折线图/柱状图渲染。
数据来源:按日聚合 LearningActivity.durationSeconds。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
wangdl 2026-06-05 20:12:18 +08:00
parent 9c14bda0c2
commit 4b8653080e
2 changed files with 34 additions and 1 deletions

View File

@ -8,6 +8,12 @@ export const TrendItemSchema = z.object({
detail: z.string().max(500), detail: z.string().max(500),
}); });
export const DailySeriesItemSchema = z.object({
date: z.string(),
value: z.number(),
label: z.string(),
});
export const LearningTrendResultSchema = z.object({ export const LearningTrendResultSchema = z.object({
periodSummary: z.string().min(1).max(2000), periodSummary: z.string().min(1).max(2000),
overallScore: z.number().int().min(0).max(100), overallScore: z.number().int().min(0).max(100),
@ -17,9 +23,11 @@ export const LearningTrendResultSchema = z.object({
weaknesses: z.array(z.string().max(500)).max(10).default([]), weaknesses: z.array(z.string().max(500)).max(10).default([]),
recommendations: z.array(z.string().max(500)).max(10).default([]), recommendations: z.array(z.string().max(500)).max(10).default([]),
nextFocusAreas: z.array(z.string().max(200)).max(5).default([]), nextFocusAreas: z.array(z.string().max(200)).max(5).default([]),
dailySeries: z.array(DailySeriesItemSchema).default([]),
}); });
export type LearningTrendResult = z.infer<typeof LearningTrendResultSchema>; export type LearningTrendResult = z.infer<typeof LearningTrendResultSchema>;
export type DailySeriesItem = z.infer<typeof DailySeriesItemSchema>;
export const LEARNING_TREND_OUTPUT_SCHEMA_DESC = `{ export const LEARNING_TREND_OUTPUT_SCHEMA_DESC = `{
"periodSummary": "过去7天你的学习时长为320分钟完成了45次主动回忆和120张复习卡片。整体掌握水平有所提升。", "periodSummary": "过去7天你的学习时长为320分钟完成了45次主动回忆和120张复习卡片。整体掌握水平有所提升。",

View File

@ -66,6 +66,9 @@ export class LearningActivityService {
const prevTotalMinutes = Math.round(sum(previous, 'durationSeconds') / 60); const prevTotalMinutes = Math.round(sum(previous, 'durationSeconds') / 60);
// Build daily time-series for chart rendering
const dailySeries = this.buildDailySeries(recent, periodDays);
const trendInput = { const trendInput = {
userId, userId,
periodDays, periodDays,
@ -86,6 +89,28 @@ export class LearningActivityService {
} : undefined, } : undefined,
}; };
return this.trendWorkflow.execute(trendInput); const aiResult = await this.trendWorkflow.execute(trendInput);
return { ...aiResult, dailySeries };
}
private buildDailySeries(activities: any[], days: number) {
const series: { date: string; value: number; label: string }[] = [];
const now = new Date();
for (let i = days - 1; i >= 0; i--) {
const d = new Date(now);
d.setDate(d.getDate() - i);
const dateStr = d.toISOString().split('T')[0];
const dayActs = activities.filter((a) => {
const ad = a.activityDate instanceof Date ? a.activityDate : new Date(a.activityDate);
return ad.toISOString().split('T')[0] === dateStr;
});
const minutes = Math.round(dayActs.reduce((s: number, a: any) => s + a.durationSeconds, 0) / 60);
series.push({
date: dateStr,
value: minutes,
label: `${minutes}分钟`,
});
}
return series;
} }
} }