【项目实训个人工作记录(四)】—— 阅读日历的功能实现
日期: 2026年4月13日——2026年4月14日
项目: 阅见:基于大模型的交互式小说阅读平台
一、概述
本阶段我主要开发项目的阅读日历功能,使用户能直观看到自己每天读了多久,而不是只有阅读器里的进度条。整体思路是——阅读器负责计时上报,后端按自然日汇总落库,日历页负责查区间、做展示。
在实现过程中,前端依赖 Intl 时区与接口参数 zoneId 与后端 ZoneId 对齐,避免「跨日边界」与服务器时区不一致导致的统计偏差;后端对单日总量与上报频率做上限与限流,保证数据可解释、服务不被刷接口拖垮。
二、功能目标
- 阅读时长的统计:可以统计用户在阅读界面的停止时间,并对该时间进行更新,从而实现用户每日阅读时长的统计;
- 日历页查询与展示:应该包括多个视图
日视图:展示当日累计时长;
月视图:月历格子展示每日时长,可点选打开对应页面看某日详情;
年视图:12 个月块展示各月合计,可通过点击进入到月视图。 - 规则与安全: 查询区间有最大跨度与相关校验;单次上报阅读时长有上下界;服务端滑动窗口限流防止恶意高频上报。
三、技术实现
(一)阅读日历功能的数据库设计
设计了表 vr_reading_daily_stat用于保存阅读时间,除主键id外,包含下列字段属性
| 字段 | 含义 |
|---|---|
| user_id | 用户 ID |
| calendar_date | 自然日(DATE) |
| total_seconds | 该日累计阅读秒数 |
| updated_at | 最近更新时间 |
(二)阅读日历功能的后端实现
1. 实体类设计
实体 ReadingDailyStat 与表名 vr_reading_daily_stat 映射一致,用于后端实现
@Data
@TableName("vr_reading_daily_stat")
public class ReadingDailyStat {
@TableId(type = IdType.AUTO)
private Long id;
private Long userId;
private LocalDate calendarDate;
private Integer totalSeconds;
private LocalDateTime updatedAt;
}
2. 接口设计
接口在ReadingCalendarController中实现,接口地址前缀为*“/api/reading/calendar”*,在该java文件下,我实现了两个对应接口,分别是累积当日秒数接口和查询对应区间阅读时间数:
| 方法 | 接口路径 | 作用 |
|---|---|---|
| POST | /accumulate | 累加当日秒数;请求体 CalendarAccumulateRequest:seconds(1~120)、zoneId(必填) |
| GET | /days | 查询区间每日秒数;参数 from、to(ISO 日期 yyyy-MM-dd)、zoneId |
以下是对应的代码实现:
@RestController
@RequestMapping("/api/reading/calendar")
@RequiredArgsConstructor
public class ReadingCalendarController {
private final ReadingCalendarService readingCalendarService;
@PostMapping("/accumulate")
public ApiResult<CalendarAccumulateResult> accumulate(
HttpServletRequest request,
@Valid @RequestBody CalendarAccumulateRequest body) {
long uid = (Long) request.getAttribute(JwtAuthInterceptor.ATTR_USER_ID);
return ApiResult.ok(readingCalendarService.accumulate(uid, body));
}
@GetMapping("/days")
public ApiResult<CalendarDaysResponse> days(
HttpServletRequest request,
@RequestParam String from,
@RequestParam String to,
@RequestParam String zoneId) {
long uid = (Long) request.getAttribute(JwtAuthInterceptor.ATTR_USER_ID);
return ApiResult.ok(readingCalendarService.listDays(uid, from, to, zoneId));
}
}
3. 服务层核心逻辑
在服务层中,我主要实现了accumulate方法和listDays方法:
- accumulate:用请求中的 zoneId 解析为 ZoneId,取该时区 LocalDate.now 作为「今天」;按 user_id + calendar_date 查行;不存在则插入并封顶单日 MAX_DAILY_SECONDS;存在则累加后再封顶;返回 CalendarAccumulateResult(日期、累计秒数、是否触发封顶 capped)。上报前走 acceptRateOrThrow:每用户 60 秒滑动窗口内累计上报秒数不超过 150,否则抛 「上报过于频繁」。
- listDays:解析 from/to,校验 from ≤ to、区间天数 ≤ 800、to 不晚于该时区下的今日;查询库表后构建连续日期序列(无记录日为 0 秒),返回 CalendarDaysResponse
此外考虑到阅读时长统计上下限问题,这里使用了dto校验机制,CalendarAccumulateRequest 对 seconds 使用 @Min(1) @Max(120),与阅读器单次上报上限一致。
(三)阅读日历功能的前端实现
1. 阅读器页面
阅读器界面是已有的实现,在Reader.vue中实现,这里考虑到需要统计用户停留在阅读器中的时间,对前端界面的功能进行的部分添加,实现对阅读时长的统计
- 使用 Intl.DateTimeFormat().resolvedOptions().timeZone(缺省 Asia/Shanghai)作为 zoneId。
- startReadingCalendarCapture:在路由为阅读器且 document.visibilityState === ‘visible’ 时,每 30 秒 调用 flushReadingCalendarSeconds:根据自上次锚点经过的秒数,至少 1 秒、至多 120 秒 一包,调用 POST /reading/calendar/accumulate;页签隐藏或换路由时 stopReadingCalendarCapture 并再冲刷一次。上报失败 catch 静默,避免弹错影响阅读
2. 日历页面
日历页面为新页面,由ReadingCalendar.vue实现
2.1 多视图前端展示
在日历页面中,我实现了年/月/日的多界面视图,初日阅读时长视图外,年月的阅读时间均是通过计算得到,进而通过模块进行展示。
- 日视图:from = to = 今日 拉取;今日阅读用 todaySeconds 计算属性。
- 月视图:按锚点年月算当月 from/to,拉取后渲染 7 列月历;点击某日打开 Drawer 展示 formatDuration。
- 年视图:from = yyyy-01-01,to = yyyy-12-31;yearMonthTotals 由 dayMap 聚合成 12 个月总量;点击某月切换到月视图并 loadMonthView。
2.2 查询日期的自动检查
对查询日期进行限制,仅允许查看当日之前的阅读时长统计,查看当日后的阅读时长会进行提示
2.3 错误提示
捕获接口错误信息,若含跨度/800 天/结束日期等关键字则提示用户调整范围。
(四)阅读日历功能业务流程总结
- 用户打开阅读器且页签在前台 → 启动定时器 → 每隔约 30s(及离开时)将一段 1~120 秒 的阅读时长 POST /reading/calendar/accumulate,携带 zoneId。
- 服务端按用户 + 该时区下的自然日 写入或更新vr_reading_daily_stat.total_seconds,并受单日封顶与滑动窗口限流约束。
- 用户打开阅读日历页 → 按当前视图计算 from/to → GET /reading/calendar/days → 将返回结果填入月历/年汇总/日卡片展示。
四、问题反馈
- 阅读后日历显示当日阅读时长为0
现象: 日历显示为 0,但明明读过书。
可能原因: 阅读时页签在后台(visibilityState 非 visible,不上报);或网络失败且阅读器选择静默失败。
处理: 确认提示文案「保持页签前台」;必要时在开发环境打开 Network 查看 accumulate 是否 200。 - 界面出现提示:当前范围不可查询
现象: 日历页告警「范围不可查」。
原因: to 晚于今日或区间超过 800 天。
处理: 与 ReadingCalendarService.listDays 校验规则一致,缩小查询范围或勿选未来日期。
五、界面展示
下面展示日历界面的日、月、年视图以及月视图下单日阅读时间的查看



六、优化思路
- 日历页: 视图模式可像书架一样 localStorage 持久化;年/月切换时可加缓存减少重复请求同一天区间。
- 多端一致: 若未来有 App,需统一自然日以服务端为准还是以客户端 zoneId 为准的产品定义。
- UI美化设计:可以考虑参考微信阅读的阅读时长统计UI或是github的代码日历进行美化。
七、总结
本阶段将阅读时长统计从阅读行为中解耦为独立模块:写入路径短(单表 upsert)、查询契约清晰(连续天 + 秒数),并在服务层用封顶与限流体现对异常使用的防御;前端通过时区显式传递减少统计歧义,整体便于后续与书币打卡、阅读 streak 等业务扩展对接。
该功能从数据库到页面前端的具体层级实现如下:
| 层级 | 职责 |
|---|---|
| 表 vr_reading_daily_stat | 用户 × 自然日唯一一行,累计秒数 |
| ReadingCalendarService | 时区解析、按日累加与封顶、区间查询与补零、限流 |
| ReadingCalendarController | JWT 取用户;accumulate / days 两个 REST 入口 |
| Reader.vue | 可见性 + 路由控制计时器;分片上报 accumulate |
| ReadingCalendar.vue | 按视图拉取 days;月历/年汇总/UI 与错误提示 |
AI提示词记录:
/speckit.specify 请你根据目前项目(阅见:基于大模型的交互式小说阅读平台)的架构与代码风格,生成一个「阅读日历」功能需求说明(Spec)。目标:统计用户阅读时长,并在前端展示每日/每月/每年的阅读时长(支持自然日口径)。要求:
- 数据维度:以用户 + 自然日汇总阅读秒数(一天内累计),展示时区由前端传入 .并在后端解析为 ZoneId。
- 统计来源:阅读器页面在前台可见时定时上报阅读时长到后端(避免后台累计),同时在离开/切换路由时做一次 flush。
- 后端接口契约:提供 accumulate(上报 seconds、zoneId)与 days(查询 from、to、zoneId)的 REST 设计;接口返回结构需便于前端渲染日历格子/汇总。
- 边界条件:seconds 有最小/最大范围;按日累计封顶(例如 86400);查询区间跨度上限(例如 800 天);结束日期不能晚于“该时区下的今天”;from<=to 校验;无数据日期补 0。
- 防滥用/幂等:对 accumulate 进行限流(例如滑动窗口累计上报上限);每日基础打卡等后续扩展要能基于这张统计表稳定读取(不要影响后续书币打卡)。
- 前端交互:ReadingCalendar 页面使用日/月/年三种视图;月视图渲染月历格子并可点击查看当日详情;年视图展示 12 个月汇总并可切换到月视图。
- 不破坏既有分层:Java 业务逻辑在 Spring Boot,前端仅调用 REST;不引入 AI-service 依赖本功能路径。
/speckit.plan 技术栈以当前项目为主:前端 Vue3 + Vite + Vue Router + Axios + Element Plus;后端 Spring Boot 3.2.x + Java17 + MyBatis-Plus + JWT 拦截器;数据库 MySQL。
/speckit.tasks 请将「阅读日历」功能拆成可执行任务,并按依赖顺序生成 tasks.md。任务需要覆盖:
- 数据库:新增/更新阅读日历统计表的 SQL 脚本(例如 update_reading_daily_stat.sql),包含唯一约束与索引。
- 后端:新增 ReadingCalendarController、ReadingCalendarService、DTO(accumulate request/result、days response/day entry)、Entity(ReadingDailyStat)与 Mapper;补齐路由路径与参数校验。
- 前端:实现 ReadingCalendar.vue(日/月/年视图、抽屉展示、错误提示、加载态)。
- 集成点:修改 Reader.vue,使其在路由为阅读器且页签可见时定时上报阅读秒数;离开时 flush;zoneId 与后端保持一致。
- 可选测试:若项目已有 ReadingCalendarServiceTest/ControllerTest 风格,请补齐关键用例(边界校验、跨度限制、无数据补 0、限流行为)。
每个任务给出:输入/输出、涉及文件路径、以及验收标准。
/speckit.implement
/speckit.implement 现在请按 tasks.md 完成「阅读日历」功能的实现与联调。要求:
- 最小修改原则;尽量复用现有风格与接口封装(Axios/http.js、后端 ApiResult 结构等)。
- 任何需要新增/修改关键逻辑的操作前先与我确认(尤其是修改 Reader.vue 的上报时机、后端的校验/封顶/限流策略)。
- 不随意改数据库配置或已有表结构的外部契约;如需 SQL 变更,仅新增阅读日历统计表/索引(与需求一致)。
- 实现后给出本地运行与验证步骤(端口/接口路径/前端入口/如何观察累积与日历刷新)。
- 补充必要的容错与错误提示,确保前端不会因 accumulate/查询失败而阻塞阅读或渲染。
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐
所有评论(0)