【项目实训个人工作记录(五)】—— 阅读商城的功能实现
日期: 2026年4月13日——2026年4月14日
项目: 阅见:基于大模型的交互式小说阅读平台
一、概述
本阶段实现了项目的「阅读商城」核心能力,页面对应 frontend/src/views/BookCoinShopView.vue,主要包含书币、商城、流水三大部分内容。我的书币:展示当前余额、今日阅读时长、连续打卡天数,并提供「打卡领书币」入口;称号商城:展示可购买称号列表,支持用书币购买并标记已拥有;最近流水:展示书币变动记录(增加/减少与原因)。
这套功能依赖前一阶段已存在的阅读时长统计(vr_reading_daily_stat),后端通过读取「今日阅读秒数」决定是否允许打卡与计算赠币。
二、功能目标
- 书币与打卡
获取用户书币余额(含今日阅读秒数、是否已基础打卡、是否满足打卡条件、当前连续天数),支持当日打卡领取书币,并返回本次赠币明细;若已打卡则给出“已领取”状态。 - 称号购买
列出启用中的称号商品,并标记当前用户是否已拥有。支持购买操作:余额不足应拒绝;重复购买应拒绝。 - 书币流水
分页/限制数量拉取最近变动记录,前端用于展示「最近流水」表格。 - 前端体验
页面加载态(skeleton)、操作按钮 loading、空状态提示;支持刷新称号列表;购买/打卡后刷新钱包、称号、流水,保证数据一致。
三、技术实现
(一)数据库设计
阅读商城本阶段主要新增/依赖以下表
vr_user_coin_wallet:用户书币余额表(balance、updated_at)
vr_user_coin_state:用户连续打卡状态(current_streak、last_streak_date、streak_milestone_mask)
vr_coin_ledger:书币流水表(delta、reason、全局唯一 ref_key 幂等、calendar_date、created_at)
vr_daily_check_in:每日打卡记录((user_id, calendar_date) 唯一约束,避免重复基础打卡)
vr_shop_title:称号商品表(code、display_name、description、price_coins、enabled、sort_order)
vr_user_title:用户已拥有称号表((user_id, title_id) 唯一约束)
同时依赖已存在的 vr_reading_daily_stat:作为「今日阅读秒数」数据源。
| 表名 | 作用 | 关键字段/约束 | 在阅读商城中的使用点 |
|---|---|---|---|
vr_reading_daily_stat |
依赖的已存在阅读日统计(按用户+自然日汇总阅读秒数) | user_id, calendar_date(自然日维度);total_seconds |
作为「是否满足打卡条件」与「今日已读时长」的来源(前端展示 todayReadingSeconds、后端校验 MIN_READ_SECONDS=600) |
vr_user_coin_wallet |
用户书币钱包(余额) | user_id 为 PK;balance;updated_at |
CoinWalletService.getBalance() 获取余额;applyChange() 计算新余额并更新 |
vr_user_coin_state |
用户连续打卡状态(streak) | user_id 为 PK;current_streak;last_streak_date;streak_milestone_mask |
CoinCheckInService 在每日打卡时更新连续天数与里程碑是否已领取(3/7/30 天) |
vr_coin_ledger |
书币流水(增减记录) | ref_key 全局唯一(幂等);user_id 外键;delta、reason、calendar_date、created_at |
CoinWalletService.applyChange() 写入流水;前端「最近流水」展示增减与原因 |
vr_daily_check_in |
每日基础打卡记录(避免同日重复基础奖励) | (user_id, calendar_date) 唯一约束;base_claimed_at;streak_after_claim |
CoinCheckInService.checkIn() 判断今日是否已基础领取;首次打卡时插入记录 |
vr_shop_title |
称号商品上架表(商店商品) | id PK;code 唯一;enabled 控制上架;price_coins;sort_order |
ShopTitleService.listTitles() 拉取可购买称号;purchase() 校验商品可用性与价格 |
vr_user_title |
用户已拥有称号(用户购买结果) | (user_id, title_id) 唯一约束;purchased_at |
ShopTitleService.listTitles() 判断 owned;purchase() 写入购买成功记录、避免重复购买 |
(二)后端实现
1. 书币钱包/打卡/流水接口(CoinController)
Controller 前缀:/api/coins
- GET /wallet?zoneId=…:返回 CoinWalletResponse(余额、今日阅读秒数、打卡状态与连续天数等)
- POST /check-in:入参 CoinCheckInRequest(只包含 zoneId),返回 CoinCheckInResponse(是否已领取、赠币明细、余额等)
- GET /ledger?limit=…:返回 CoinLedgerListResponse(最近流水列表)
2. 打卡计算与状态机(CoinCheckInService)
打卡核心逻辑位于 CoinCheckInService.checkIn,关键规则都在代码里是常量:
-
有效阅读门槛:MIN_READ_SECONDS = 600(10 分钟)
-
基础奖励:BASE_REWARD = 10
-
阅读加赠:每额外 BONUS_UNIT_SECONDS=1800(30 分钟)给 5 书币,且最多封顶 BONUS_MAX_COINS=20(通过 extraUnits 决定)
-
连续里程碑奖励:达到连续天数阈值发放额外币
3 天:+5 7 天:+15 30 天:+50 -
防重复:
1.同一天重复基础打卡通过 vr_daily_check_in 判断 existing != null 返回 alreadyClaimed=true,不会再次发放;
2.书币余额变更通过 CoinWalletService.applyChange 使用全局唯一 ref_key 做幂等,避免重复流水扣币。
此外,buildWallet 会把今日阅读秒数从 vr_reading_daily_stat 读取,计算出:
- canBaseCheckIn:是否满足“今日有效阅读未满 10 分钟”的解锁条件
- todayBaseCheckedIn:今日是否已打卡领取基础奖励
- currentStreak:连续天数来自 vr_user_coin_state
3. 书币流水与余额扣增(CoinWalletService)
CoinWalletService.applyChange 做的是事务化的“写流水 + 更新余额”:
- 确保用户钱包行存在(ensureWallet)
- 计算新余额,余额不足(newBalance < 0)直接抛错
- 插入 CoinLedger(ref_key);若 ref_key 重复会抛出异常(避免重复扣币)
- 更新 user_coin_wallet.balance
4. 称号商城(ShopTitleController + ShopTitleService)
Controller 前缀:/api/shop
- GET /titles:返回 ShopTitleListResponse(每个称号带 owned、priceCoins、displayName 等)
- POST /titles/{titleId}/purchase:购买称号,返回 TitlePurchaseResponse(购买后余额等)
(三)前端实现
前端页面是 frontend/src/views/BookCoinShopView.vue,整体数据拉取与交互流程为:
1. 页面加载 onMounted -> loadAll()
- GET /coins/wallet?zoneId=…
- GET /shop/titles
- GET /coins/ledger?limit=30;使用 Promise.all 并行请求,loading 控制 skeleton。
2. 打卡 doCheckIn()
- POST /coins/check-in,携带同一套 zoneId
- 若 alreadyClaimed=true:提示“今日已领取基础打卡奖励”;否则把本次 grants 拼成提示信息
- 打卡成功后刷新:loadWallet + loadLedger(同时保持称号列表不必强制刷新)
3. 购买称号 purchase(t)
- POST /shop/titles/{t.id}/purchase
- 成功后刷新钱包/称号/流水(Promise.all([loadWallet(), loadShop(), loadLedger()]))
4. 交互细节
- purchaseId 控制按钮 loading
- 称号列表中 owned 为 true 的按钮禁用,并显示 已拥有 tag
- “最近流水”通过 delta 正负号样式区分增加/减少
(四)业务流程总结
- 用户在阅读器持续阅读并产生阅读时长统计(来自阅读日历模块,写入 vr_reading_daily_stat)。
- 用户进入阅读商城页面:根据阅读时长查看是否可以打卡,同时在商城页面显示已购商品和流水记录。
- 用户点击「打卡领书币」:
用户当日阅读时长大于10分钟后,即可打卡,打卡时会计算总奖励=计算基础奖励 + 阅读加赠 + 连续里程碑奖励。 - 用户用书币购买称号:
后端检验后创建流水和用户拥有记录。
四、问题反馈
- 重复打卡可能导致重复扣币
现象:重复进入页面操作点击打卡按钮。
原因:打卡必须受“同一天只能领取一次”的约束控制。
处理:后端通过 vr_daily_check_in (user_id, calendar_date) 唯一约束 + 业务逻辑 existing != null -> alreadyClaimed=true 返回,前端根据 alreadyClaimed 做提示,不再重复发放。 - 购买称号时余额不足或重复购买
现象:前端禁用失效。
原因:余额不足/已拥有属于后端业务规则。
处理:ShopTitleService.purchase 检查余额与 vr_user_title 是否存在;分别抛出 书币余额不足 或 CONFLICT 已拥有。前端通过全局拦截器统一提示错误。
五、界面展示

六、优化思路
- 分页/缓存优化:/coins/ledger 可支持翻页而不是固定 limit=30;称号列表 loadShop 可结合缓存减少刷新频率。
- 前端更强的状态提示:打卡按钮可展示「距离 10 分钟还差多少」来自服务器的 todayReadingSeconds 计算,体验更直观(目前是基于 canBaseCheckIn 直接禁用)。
七、总结
本阶段我完成了阅读商城的端到端落地:数据库新增书币钱包、连续打卡状态、书币流水、每日打卡记录及称号商品/用户称号表,并依赖阅读日历统计表提供今日阅读秒数;后端通过 CoinController / CoinCheckInService / CoinWalletService 实现“读取今日阅读秒数→打卡赠币→写流水并更新余额”,通过 ShopTitleController / ShopTitleService 实现“称号购买扣币并做幂等防重”;前端在 BookCoinShopView.vue 中并行加载钱包、称号与流水,并在打卡/购买后刷新关键数据,保证交互一致性与使用体验。
AI提示词记录:
/speckit.specify
请根据当前项目(阅见:基于大模型的交互式小说阅读平台)的架构与代码风格,生成一个「阅读商城(书币与称号商城)」功能需求说明。该功能面向用户在阅读达到条件后,通过「打卡领书币」获得书币,并使用书币在称号商城购买称号。
请输出:需求说明(Spec)+ 接口字段说明(请求/响应结构字段含义)+ 关键业务规则 + 边界条件。
/speckit.plan 请根据当前项目技术栈(Spring Boot 3.x + Java 17 + MyBatis-Plus +
JWT 拦截器 + Vue3 + Vite + Axios + Element
Plus),为「阅读商城(书币与称号商城)」给出技术实现方案与分层计划。
/speckit.tasks 请把「阅读商城(书币与称号商城)」拆成可执行任务,并按依赖顺序生成 tasks.md。任务必须覆盖:
- 数据库:基于现有
backend/src/main/resources/update_book_coin_shop.sql的表结构要求,确保包含
- vr_user_coin_wallet、vr_user_coin_state、vr_coin_ledger、vr_daily_check_in、vr_shop_title、vr_user_title
并保持关键唯一约束(vr_daily_check_in 的 (user_id, calendar_date) 唯一;vr_coin_ledger 的 ref_key 唯一;vr_user_title 的 (user_id, title_id)唯一;vr_shop_title 的 code 唯一)。如已有表/脚本,任务需说明“复用/校验”而非重复造轮子。
- 后端接口与服务:
- Controller:
CoinController:GET /api/coins/wallet、POST /api/coins/check-in、GET /api/coins/ledgerShopTitleController:GET /api/shop/titles、POST /api/shop/titles/{titleId}/purchase- Service:
CoinCheckInService:打卡奖励计算、连续天数 streak、milestone mask 更新、调用 wallet 写流水与余额CoinWalletService:余额与流水 applyChange(ref_key 幂等、余额不足校验)ShopTitleService:称号列表 owned 标记、purchase 扣币与写入用户称号- DTO/Request/Response:CoinCheckInRequest、CoinCheckInResponse、CoinWalletResponse、CoinLedgerListResponse、CoinLedgerItemDto、ShopTitleListResponse、ShopTitleItemDto、TitlePurchaseResponse
- Entity/Mapper:对应表实体与 Mapper(如无自定义 SQL 用 MyBatis-Plus 默认即可)
- 阅读日历依赖:确认读取今日阅读秒数使用 vr_reading_daily_stat,并与 zoneId 口径一致。
- 前端:
frontend/src/views/BookCoinShopView.vue:实现钱包/称号/流水的三块并行加载;打卡与购买成功后的刷新;zoneId
传参;按钮 loading 与提示文案。- 若与 Reader/ReadingCalendar 需要联动,上报时机已在之前版本完成,本任务只需确认入口不冲突。
/speckit.implement 现在请按 tasks.md 完成「阅读商城(书币与称号商城)」功能的实现与联调。要求:
- 最小修改原则:优先复用现有架构与风格(Spring Boot Controller/Service/DTO/Entity、Vue3 + Element Plus + axios/http.js、JWT 鉴权)。
- 在任何需要改动关键逻辑(奖励计算、幂等、余额扣减、阅读秒数口径、定时上报时机)的操作前先与我确认;避免一次性直接提交大改动。
- 不随意改动与数据库无关的全局配置;若确需新增 SQL 需限定为本功能所需表与索引,不破坏既有表结构与契约。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐

所有评论(0)