From 7bf84bff617b37f351d3c8dd18f1bea14f7da5ac Mon Sep 17 00:00:00 2001 From: yuming Date: Sun, 16 Aug 2026 10:25:05 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=9D=E5=AD=98=E7=BA=AA=E5=BF=B5=E6=97=A5?= =?UTF-8?q?=E5=8E=BB=E6=8E=89=20await=20=E4=B8=8A=E6=8A=A5=E5=B9=B6?= =?UTF-8?q?=E5=8A=A0=E9=98=B2=E9=87=8D=E5=85=A5=EF=BC=8C=E9=81=BF=E5=85=8D?= =?UTF-8?q?=E5=BC=B1=E7=BD=91=E4=B8=8B=E5=AD=98=E5=87=BA=E9=87=8D=E5=A4=8D?= =?UTF-8?q?=E6=95=B0=E6=8D=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- pages/add-anniversary/add-anniversary.js | 52 +++++++++++++++++++----- 1 file changed, 41 insertions(+), 11 deletions(-) diff --git a/pages/add-anniversary/add-anniversary.js b/pages/add-anniversary/add-anniversary.js index 4a8a01d..81b5a0a 100644 --- a/pages/add-anniversary/add-anniversary.js +++ b/pages/add-anniversary/add-anniversary.js @@ -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, - // 该函数只会 resolve(true=同意/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' }) } } },