小程序开发日记④:消息推送与消息中心

消息模块既要面对微信订阅消息,也要维护小程序自己的消息中心。推送负责把用户叫回来,站内消息负责把事情说清楚,两者不能只完成其中一半。
1. 消息分页模型
store/modules/message.js 保存列表、当前消息、分页与加载状态:
const messages = ref([])const pageNum = ref(1)const pageSize = 20const total = ref(0)const loading = ref(false)const loadingMore = ref(false)const hasMore = computed(() => messages.value.length < total.value)下拉刷新请求最新第 1 页,触底后请求下一页。每页 20 条既避免首次加载过重,也让历史消息可以逐步展开。
2. 刷新与加载更多不能混用状态
async function load(reset = false) { if (loading.value || loadingMore.value) return if (!reset && !hasMore.value) return
const targetPage = reset ? 1 : pageNum.value + 1 reset ? loading.value = true : loadingMore.value = true // 请求并合并}两个 loading 分开后,页面可以在刷新时显示顶部状态,在追加历史消息时只显示底部“加载更多”,不会整页闪回骨架屏。
页面使用 scroll-view:
<scroll-view scroll-y refresher-enabled :refresher-triggered="refreshing" @refresherrefresh="refreshMessages" @scrolltolower="loadMore">3. 去重与排序
刷新与分页期间,服务端可能刚好插入新消息,导致相邻页数据发生位移。Store 使用 notificationId 合并去重,再按发送时间倒序:
const merged = new Map();(reset ? rows : messages.value.concat(rows)).forEach(item => { merged.set(String(item.notificationId), item)})messages.value = [...merged.values()].sort(sortBySendTimeDesc)这比简单 concat 更能抵抗分页边界变化。
4. 已读状态的本地兜底
接口暂未提供完整已读写回能力时,消息中心使用本地集合保存已读 ID:
const locallyReadIds = ref(new Set(uni.getStorageSync('readNotificationIds') || []))
function markMessageRead(id) { locallyReadIds.value.add(String(id)) uni.setStorageSync('readNotificationIds', [...locallyReadIds.value])}加载列表时再把本地状态覆盖到消息对象。它不是最终的多端同步方案,但能保证同一设备上返回列表后红点不会复活。
5. 冷启动进入消息详情
微信通知可能直接打开详情页,此时 Pinia 中没有缓存消息,而接口文档又没有单条详情接口。当前实现按较大页逐页查找目标 ID:
while ((page - 1) * pageSize < total) { const response = await getNotificationList({ pageNum: page, pageSize: 100 }) const found = response.rows.find(item => String(item.notificationId) === targetId) if (found) return found total = Number(response.total) page += 1}这是兼容方案。更理想的后端能力是提供 /notification/{id},这样消息历史很多时不会为了找一条记录扫描多页。
6. 关联订单显示业务订单号
消息卡片和详情中的“关联订单”优先展示 orderNo,不能把数据库自增 orderId 当订单号。若通知只返回 ID,则额外读取订单详情进行补全。用户联系客服时说的是业务订单号,内部主键对他没有意义。
7. 订阅消息的定位
小程序通过 wx.requestSubscribeMessage 请求用户授权,模板 ID 由统一工具封装。订阅请求应在用户主动操作后触发,例如下单成功、开启配送提醒,而不能在页面加载时强行弹窗。
站内消息作为事实记录,订阅消息作为触达渠道。即使用户拒绝订阅,消息中心仍应能查看状态变化。
8. 本篇小结
未读数的口径
当前列表只加载部分历史消息时,直接对 messages 数组计数只能得到“已加载消息中的未读数”,不一定等于账号全部未读。更完善的接口应单独返回 unreadTotal,底部导航红点使用总数,列表内部仍使用本地状态渲染单条消息。
推送打开后的路由恢复
订阅消息携带 notificationId 或业务订单号,小程序冷启动时先完成登录态恢复,再进入详情。若消息不存在,页面显示可返回、可重试的错误状态;若关联订单已删除或无权限,只展示消息正文,不让整个详情失效。
消息内容还需要按类型决定动作按钮:订单状态通知可以查看订单,异常处置通知可以查看反馈结果,纯系统公告则没有业务跳转。按钮由消息类型映射生成,不能靠正文关键词猜测。
消息中心最开始只有 20 条时,看起来像是接口少给了点;补上刷新、分页、去重和冷启动后,它才像一个真正能用的收件箱。消息不一定都让人开心,但至少不能让人找不到。
支持与分享
如果这篇文章对你有帮助,欢迎分享给更多人或打赏支持!





























