余额为 0 时改为发探针,不再无条件全部跳过
balance 只有前端上报这一条上升通道,没升级小程序的老用户在微信侧仍在 真实累积额度却永远不上报,记账会系统性低估。低估到 0 就一条不发,比 改造前更糟——改造前至少还会试。 改为:余额为 0 时仍发出优先级最高的那一条当探针。成功则证实微信侧有 额度,本轮继续发;43101 则立即中止(与原行为一致);非额度错误记 failed 且不再重复探。每用户每轮最多探一次。 探针成功仍走 quota.consume:内部 MAX(0, balance-1) 扣不出负数,净效果是 balance 保持 0、sentTotal +1。不趁机把 balance 调高——探针只证明「至少 还有 1 条」,凭空补猜出来的数字会让首页风险预告变成乐观的假话。 注:三个原有用例断言的是「余额 0 就一个请求都不发」这一被本次刻意推翻的 旧契约,已按新行为改写;改写后仍用变异测试确认能抓到探针被关掉。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
+39
-4
@@ -52,8 +52,8 @@ function isQuotaError(err) {
|
||||
/**
|
||||
* 处理单个用户的当日提醒
|
||||
*
|
||||
* 按「当天 > 提前」、同级「重要程度降序」排序后依次发送,余额耗尽即停,
|
||||
* 不在明知没额度时继续打无谓请求。
|
||||
* 按「当天 > 提前」、同级「重要程度降序」排序后依次发送。
|
||||
* 记账余额耗尽时不会直接全部放弃,而是发一条「探针」向微信求证(见下方 probe 注释)。
|
||||
*/
|
||||
async function runForUser(openid, items, today = new Date()) {
|
||||
const due = []
|
||||
@@ -79,6 +79,20 @@ async function runForUser(openid, items, today = new Date()) {
|
||||
let halted = false
|
||||
let ok = 0, fail = 0, skipped = 0
|
||||
|
||||
// ---- 探针机制 ----
|
||||
// balance 只有「前端授权后上报 +1」这一条上升通道。没升级小程序的老用户在微信侧
|
||||
// 仍在真实累积额度(每存一条纪念日就授权一次)却永远不会上报,于是我们的记账会
|
||||
// 系统性低估。如果低估到 0 就一条都不发,反而比改造前更糟——改造前至少还会试一下。
|
||||
//
|
||||
// 所以余额为 0 时仍然把「优先级最高的那一条」发出去,拿它当探针问微信要个答案:
|
||||
// 探针成功 → 证实微信侧确实还有额度、是我们低估了,本轮继续往下发,
|
||||
// 直到遇到 43101 或发完
|
||||
// 探针返回 43101 → 证实确实没额度,立即中止,剩余全部记 skipped(与原行为一致)
|
||||
// 探针遇非额度错误 → 什么都没证明(网络/配置问题),记 failed、不动余额、不中止
|
||||
// 每个用户每轮最多探一次:在「有额度」这个结论被证实之前,不值得再赌第二个请求。
|
||||
let probeUsed = false // 本轮已经用掉探针机会
|
||||
let quotaProven = false // 探针已证实微信侧有额度,后续不必再受记账余额约束
|
||||
|
||||
// errorMsg 默认是「根本没发请求」的兜底文案;真正打了请求并被 43101 拒绝的那一条
|
||||
// 会传入微信返回的原始错误信息,两者都记为 skipped,但 error 字段要能区分开
|
||||
const logSkip = (item, errorMsg = 'quota_exhausted') => insertLog.run({
|
||||
@@ -92,11 +106,22 @@ async function runForUser(openid, items, today = new Date()) {
|
||||
})
|
||||
|
||||
for (const item of due) {
|
||||
if (halted || balance <= 0) {
|
||||
if (halted) {
|
||||
logSkip(item); skipped++
|
||||
continue
|
||||
}
|
||||
|
||||
// 记账余额为 0 且尚未证实微信侧有额度时,只允许发一条探针
|
||||
let isProbe = false
|
||||
if (balance <= 0 && !quotaProven) {
|
||||
if (probeUsed) {
|
||||
logSkip(item); skipped++
|
||||
continue
|
||||
}
|
||||
isProbe = true
|
||||
probeUsed = true
|
||||
}
|
||||
|
||||
const anniv = item.anniv
|
||||
const typeName = getTypeName(anniv.type, anniv.customTypeName)
|
||||
try {
|
||||
@@ -112,8 +137,18 @@ async function runForUser(openid, items, today = new Date()) {
|
||||
thing5: { value: anniv.remark || '别忘了准备一份礼物哦!' }
|
||||
}
|
||||
})
|
||||
// 探针成功时余额怎么记:照常 consume。
|
||||
// consume 内部是 MAX(0, balance - 1),余额本来就是 0,扣不出负数,
|
||||
// 净效果是「balance 保持 0、sentTotal +1」。
|
||||
// 之所以不趁机把 balance 调高:微信不提供余额查询,探针只证明「至少还有 1 条」,
|
||||
// 凭空补一个猜出来的数字会让首页的风险预告变成乐观的假话。宁可让 balance 保持
|
||||
// 保守的 0,靠每轮的探针去发现真实额度——账面继续低估是安全的,因为低估不再等于停发。
|
||||
quota.consume(openid, 1)
|
||||
balance--
|
||||
balance = Math.max(0, balance - 1)
|
||||
if (isProbe) {
|
||||
quotaProven = true
|
||||
console.log(`[reminder] ${openid} 探针发送成功,微信侧仍有额度,记账偏低,本轮继续发送`)
|
||||
}
|
||||
insertLog.run({
|
||||
anniversaryId: anniv.id, personName: anniv.personName, typeName,
|
||||
daysUntil: item.daysUntil, sendDate: Date.now(), status: 'success', error: null
|
||||
|
||||
Reference in New Issue
Block a user