保存纪念日去掉 await 上报并加防重入,避免弱网下存出重复数据

onSubmit 原本 await requestSubscribe(),而它内部的 grant 上报走 api.request、
超时 10 秒。对已勾「总是保持以上选择」的用户连订阅弹窗都不会出现,点「保存」后
界面最长 10 秒毫无反馈;期间又没有任何提交中标志,重复点击会走两遍
storage.addAnniversary(),generateId() 每次生成新 id,直接存出两条重复纪念日。

改为:订阅授权 fire-and-forget(上报失败本就有 pending_sync_queue 兜底),
onSubmit 去掉 async、加实例级 submitting 标志防重入,本地写失败时放开标志并提示。

手势铁律未破:onSubmit 由 bindtap 直接绑定,方法体去掉了 async,从入口到
requestSubscribe 之间只有 if 判断、解构和三个同步校验,无任何 await 或 then。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
yuming
2026-08-16 10:25:05 +08:00
parent e47a399ad5
commit 7bf84bff61
+41 -11
View File
@@ -6,6 +6,10 @@ const lunar = require('../../utils/lunar')
const subscribe = require('../../utils/subscribe')
Page({
// 保存中标志。放在实例上而不是 data 里:它只用于防重入,不参与渲染,
// 走 setData 反而会多一次没必要的视图层通信。
submitting: false,
data: {
anniversaryId: null,
personId: null,
@@ -276,8 +280,21 @@ Page({
/**
* 提交
*
* ⚠️ 本方法绝对不能加 async,方法体内在调用 requestSubscribe 之前也不能出现任何 await
* 或其他异步跳跃(Promise.then、wx 异步 API 的回调里再调等):
* wx.requestSubscribeMessage 必须由用户的真实点击手势同步触发,一旦中间断开,
* 手势上下文丢失,真机上必然报 "fail can only be invoked by user TAP gesture"。
* 从 bindtap="onSubmit" 进来到 requestSubscribe 之间,只有 setData 读取和几个同步校验。
*/
async onSubmit() {
onSubmit() {
// 防重入。
// Why:保存链路本身是同步的,但保存成功后要等 1.5 秒的 toast 才 navigateBack
// 这段时间用户完全可能再点一次「保存」;而 storage.addAnniversary 每次都会
// generateId() 生成一个新 id,重复点击的结果就是存出两条一模一样的纪念日。
// 注:这一句是同步的 if 判断,不影响下面订阅调用的手势上下文。
if (this.submitting) return
const { formData, anniversaryId, inputName } = this.data
let { personId } = this.data
@@ -300,21 +317,26 @@ Page({
return
}
// 如果开启了提醒,请求订阅消息授权
// 注:requestSubscribe 委托给 utils/subscribe.js 的 requestAndReport
// 该函数只会 resolvetrue=同意/false=拒绝),不会 reject
// 无论用户是否同意,都不阻塞后续保存流程
// 如果开启了提醒,请求订阅消息授权
// 这里是 fire-and-forget,故意不 await
// 1) await 会切断手势上下文,订阅调用必然失败(见方法头注释)
// 2) requestAndReport 内部的 grant 上报走 api.request,超时长达 10 秒。对已勾选
// 「总是保持以上选择」的用户连订阅弹窗都不会出现,await 会让用户点完「保存」后
// 最长干等 10 秒、界面毫无反馈;
// 3) 上报失败本来就有 pending_sync_queue 兜底,下次启动会自动 flush,等它没有意义。
// requestAndReport 只 resolve、不 reject,所以这里不挂 catch 也不会产生未处理拒绝。
if (formData.remindEnabled) {
const accepted = await this.requestSubscribe()
if (!accepted) {
console.log('用户未同意订阅消息,本次不上报额度')
}
this.requestSubscribe()
}
// 订阅调用已经发出,后面就没有手势上下文的顾虑了,可以安全地置位防重入标志
this.submitting = true
// 新增模式且没绑定 personId → 按姓名查找或自动创建
if (!anniversaryId && !personId) {
const person = storage.ensurePerson(name)
if (!person) {
this.submitting = false
wx.showToast({ title: '保存失败', icon: 'none' })
return
}
@@ -348,9 +370,13 @@ Page({
personName,
...formData
}, 'update')
wx.showToast({ title: '保存成功', icon: 'success' })
setTimeout(() => wx.navigateBack(), 1500)
} else {
// 写本地失败:必须放开防重入标志,否则用户连重试的机会都没有
this.submitting = false
wx.showToast({ title: '保存失败', icon: 'none' })
}
} else {
// 新增模式
@@ -363,9 +389,13 @@ Page({
if (newAnniversary) {
// 同步到云端
this.syncToCloud(newAnniversary.id, newAnniversary, 'add')
wx.showToast({ title: '添加成功', icon: 'success' })
setTimeout(() => wx.navigateBack(), 1500)
} else {
// 同上:写本地失败要放开标志,让用户能重试
this.submitting = false
wx.showToast({ title: '保存失败', icon: 'none' })
}
}
},