第六期 · 每日心语与分享记录:把一次「分享动作」变成可统计的数据
关键词:确定性选词 · 注册表驱动 · 内容类型正交 · 分享动作捕获 · 3s 去重 · clientId 幂等 · 统计口径对齐 · 离屏 canvas 封面
目标读者:前端 / 后端 / 产品
一、为什么不能套用既有模型
对应提交:
45a5615每日情话分享记录、77ff972每日心语首页入口、3791be7分享类型跟随 Tab + fab-home 悬浮入口、b9115f6分享统计与记录明细页。目标只有一句:把「一次分享动作」变成一条可统计、可同步的数据,且不新增一套数据模型。
前五期覆盖的是用户主动写入的数据(情绪打卡、生理期)。内容分享有两个本质差异:
- 内容不是用户写的,由内容池按日产出 → 需要确定性选词,保证「你送出的」与「对方看到的」是同一句;
- 分享不是数据写入,微信只给
onShareAppMessage/onShareTimeline两个钩子,不回传是否送达 → 只能记录「发起了分享」,不能宣称成功。
因此三条目标:内容可预测可扩展;数据与 mood/period 同构(本地优先 + 云端同步);游客态与登录态表现一致。
二、分层全景
内容层 heart-registry + pools/* + content-types
↓ pickDaily(type, date)
展现层 love(日历/卡片/统计) love-share(落地页) index(首页预览) fab-home
动作层 onShareAppMessage / onShareTimeline → share-log.js → 3s 去重 → Api 门面
数据层 LocalAdapter.share()(游客,域 em_share) | RemoteAdapter.share()(登录,/sync/push)
服务端 ShareController + ShareService + ShareRecord + SyncService.applyShare
消费层 share-records(分页明细) love 统计卡(total/today/friend/timeline/byType)
核心原则:新增一种数据,只在注册表加类型、适配器加 share 分支、服务端加文档与同步分支,幂等/合并/导出/清空全部复用。
三、内容层:确定性选词
utils/heart-registry.js 是情感类型(love / friendship / family)的唯一事实来源,含 theme / salt / emojis / pool。新增类型只需追加一条 + 新增池文件,页面零改动——Tab 由 registry.list() 渲染,分布由注册表顺序推导。
function pickDaily(key, date) {
const type = get(key);
const pool = type.pool || [];
if (!pool.length) return { type, pool, index: 0, day: 1, note: null };
const idx = (dayOfYear(date) + (type.salt || 0)) % pool.length;
return { type, pool, index: idx, day: idx + 1, note: pool[idx] };
}
idx = (年内第几天 + salt) % 池长:salt(0/7/13)错开三种类型的排布;池长 ≥30 时连续 30 天不重样。纯函数无副作用,因此首页预览与进入后所见必然一致(index.js:210-218)。
content-types.js 定义 text / emoji / article / image / video / link,与情感类型正交:后者决定强调色相,前者决定背景基调。v1 只启用前两个,其余 enabled:false 不进选择器,但旧链接携带 ct=image 仍可渲染——向后兼容通道。
四、动作层:捕获与去重
onShareAppMessage() {
const { typeKey, picked, note, title } = this._sharePayload();
// 记录分享(本地先行 / 云端同步由 Api 路由决定);失败不影响分享流程
shareLog.recordShare({
type: typeKey, note, noteId: note.id, date: this.data.date,
mediaType: note.mediaType, channel: 'friend'
}).catch(() => {});
- 3 秒去重:键取
(type, noteId, channel)(share-log.js:11-27),连点两次或取消后重触发只入库一次;换渠道算两次。markShared()在去重判断之前执行,保证「已分享」标记立即点亮。 - 类型跟随 Tab:早期在浮层让用户二次选择,导致「看到友情、分享出去是爱情」的口径分歧。现由
_sharePayload()从this.data.typeKey推导(love.js:232-242),好友转发、朋友圈、胶囊菜单三条路径共用同一份 payload。
五、数据层:本地域与幂等
本地 em_share 域三条要点(local-adapter.js:317-366):
- 立即落盘:
setDomain(..., true)跳过 500ms 防抖,与凭证、待同步记录同级; - FIFO 上限 500:分享是高频动作,防止本地膨胀;
- 软删墓碑:
remove只置deleted=true,保留记录以便合并时向云端传播删除。
校验前后端一致:type 必填、文案与表情不可同时为空、noteText ≤ 300 字。
登录后写入不直连 REST,而是推 /sync/push(module:'share',remote-adapter.js:213-227),失败进 pendingOps 队列联网重放——断网也能记下。clientId 端侧生成(毫秒时间戳 + 4 位随机),既是记录 id 又是服务端幂等键,游客期上云后保持不变,杜绝重复。
六、服务端
索引(ShareRecord.java:18-23):uk_user_client(userId+clientId 唯一,幂等的物理保证)、idx_user_updated(增量拉取命脉)、idx_user_date。contentDate 用 YYYY-MM-DD 字符串避免时区串天;noteText/noteEmojis 存快照,内容池改版后历史仍可追溯。
接口:写操作一律走 /sync/push,REST 只读。
| 方法 | 路径 | 说明 |
|---|---|---|
| GET | /api/v1/shares | 分页列表,支持 type / channel 筛选(pageSize 上限收口 100) |
| GET | /api/v1/shares/stats | 总数 / 今日 / 好友 / 朋友圈 / 按类型分布 |
| GET | /api/v1/shares/{clientId} | 详情 |
统计口径必须前后端一致:「今日次数」按 createdAt 落在本地语义日期当天算(ShareService.java:67-75);本地adapter 用同口径聚合,故登录前后数字连续。前端对 todayCount == null(旧版服务端缺字段)显示「—」而非 0(love.js:294)。
同步仲裁(SyncService.java:200-235):updatedAt 早于服务端抛 ConflictException;remove 落墓碑以支持多端删除传播;批量中单条缺 type 只拒收该条,不中断整批。
导出/导入/清空:导出含 share 域;导入为可选域(旧文件无 share 不报错,按 clientId 去重);清空支持 scope=share,同样是软删。
七、消费层
- 统计卡:四数 + 一行类型分布,数据源统一走
shareLog门面,页面不感知游客/登录差异。 - 分页并发安全:用自增
reqId丢弃过期响应(share-records.js:50-56),避免「触底加载」与「返回重拉首页」并发导致列表错乱;浏览往日心语后再分享时,补「某日的心语」来源标签消除时间错位困惑。 - 事件驱动刷新:写入后
emit('share:changed'),love 与 share-records 订阅刷新,并在onUnload解绑。
落地页与封面:配色由 share-theme.js 双层融合——内容类型定基调层(渐变明度/冷暖/文字色),情感类型定强调层(徽标/CTA/描边色相),冲突时基调优先;未知组合回落 default,永不白屏,文字对比度 ≥ 4.5:1。封面用离屏 canvas 绘制 5:4,createOffscreenCanvas 缺失或绘制异常返回空串、回落微信默认截图——封面失败绝不阻断分享。
八、风险与应对
| 风险 | 应对 |
|---|---|
| 微信不回传结果 | 语义定为「记录动作」,不宣称成功 |
| 钩子多次触发 | 3s 去重窗口 |
| 本地膨胀 | FIFO 上限 500 |
| 上云重复 | clientId 端侧生成 + 唯一复合索引 |
| 跨时区串日 | 日期字符串 + 统一 DateUtil.ZONE |
| 内容池改版 | 存文案/表情快照 |
| 旧服务端缺字段 | null → 显示「—」 |
| 封面绘制失败 | 回落微信默认截图 |
| 脏数据拖垮整批 | 单条拒收,其余继续 |
九、验收清单
确定性(同日同类型恒定、三类型错开)|游客态落盘不丢|3 秒内重复只记一次、换渠道记两次|类型与 Tab 一致|登录合并不重复|断网进队列、联网补齐|删除多端同步|分页并发不错乱|导出含 share、导入旧文件不报错。
十、后续方向
内容池服务端下发 + 版本号(快照保证历史不受影响);统计改聚合管道 + 短缓存;channel 扩展 save-image / copy-link 并与 image/link 内容类型打通;落地页匿名回流埋点(不采集身份);canvas 封面模板化。
小结
三条工程原则:语义诚实(拿不到结果只记动作)、单一事实来源(分享类型由 Tab 推导而非二次选择)、口径一致(前后端同一套校验与统计规则,登录前后体验无缝)。
更多推荐


所有评论(0)