余额为 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:
+157
-34
@@ -47,35 +47,132 @@ test('额度够时全部发出', async () => {
|
||||
test('额度不足时当天优先、同级按重要程度', async () => {
|
||||
resetLogs()
|
||||
quota.grant('uB', 1)
|
||||
const attempts = [] // 所有真正打出去的请求
|
||||
const sent = [] // 其中发送成功的
|
||||
const restore = wx.sendSubscribeMessage
|
||||
// 第一条放行、之后一律 43101。
|
||||
// Why 要让微信亲口说 43101:记账余额只有 1,但第二条会被当作「探针」发出去(见探针机制),
|
||||
// 只有微信侧确认没额度,「额度不足时的取舍」才真正成立。
|
||||
wx.sendSubscribeMessage = async (p) => {
|
||||
attempts.push(p.data.name1.value)
|
||||
if (attempts.length > 1) {
|
||||
const e = new Error('发送订阅消息失败')
|
||||
e.errcode = 43101
|
||||
throw e
|
||||
}
|
||||
sent.push(p.data.name1.value)
|
||||
return { errcode: 0 }
|
||||
}
|
||||
|
||||
try {
|
||||
const items = [
|
||||
makeAnniv('B1', 'uB', '提前的', 'high', 3, 3), // 提前事件(daysUntil=3=remindDays)
|
||||
makeAnniv('B2', 'uB', '当天低', 'low', 0, 3), // 当天事件
|
||||
makeAnniv('B3', 'uB', '当天高', 'high', 0, 3) // 当天事件,重要程度更高
|
||||
]
|
||||
const r = await reminder.runForUser('uB', items, TODAY)
|
||||
|
||||
assert.strictEqual(r.ok, 1)
|
||||
assert.strictEqual(r.skipped, 2)
|
||||
assert.deepStrictEqual(sent, ['当天高'], '应当只发出当天且最重要的那条')
|
||||
assert.deepStrictEqual(attempts, ['当天高', '当天低'], '探针也要按优先级挑下一条,且被拒后不再打第三个请求')
|
||||
} finally {
|
||||
wx.sendSubscribeMessage = restore
|
||||
}
|
||||
})
|
||||
|
||||
test('记账余额为 0 但微信侧仍有额度:探针成功后本轮继续发送', async () => {
|
||||
resetLogs()
|
||||
// uP1 从未 grant 过,记账余额为 0——模拟「没升级小程序、授权从不上报」的老用户
|
||||
assert.strictEqual(quota.getBalance('uP1'), 0)
|
||||
const sent = []
|
||||
const restore = wx.sendSubscribeMessage
|
||||
wx.sendSubscribeMessage = async (p) => { sent.push(p.data.name1.value); return { errcode: 0 } }
|
||||
|
||||
const items = [
|
||||
makeAnniv('B1', 'uB', '提前的', 'high', 3, 3), // 提前事件(daysUntil=3=remindDays)
|
||||
makeAnniv('B2', 'uB', '当天低', 'low', 0, 3), // 当天事件
|
||||
makeAnniv('B3', 'uB', '当天高', 'high', 0, 3) // 当天事件,重要程度更高
|
||||
]
|
||||
const r = await reminder.runForUser('uB', items, TODAY)
|
||||
wx.sendSubscribeMessage = restore
|
||||
try {
|
||||
const items = [
|
||||
makeAnniv('P1', 'uP1', '甲', 'high', 0, 3),
|
||||
makeAnniv('P2', 'uP1', '乙', 'medium', 0, 3),
|
||||
makeAnniv('P3', 'uP1', '丙', 'low', 0, 3)
|
||||
]
|
||||
const r = await reminder.runForUser('uP1', items, TODAY)
|
||||
|
||||
assert.strictEqual(r.ok, 1)
|
||||
assert.strictEqual(r.skipped, 2)
|
||||
assert.deepStrictEqual(sent, ['当天高'], '应当只发出当天且最重要的那条')
|
||||
assert.strictEqual(r.ok, 3, '探针证实微信侧有额度后,剩下两条也应照常发出')
|
||||
assert.strictEqual(r.skipped, 0)
|
||||
assert.deepStrictEqual(sent, ['甲', '乙', '丙'])
|
||||
assert.strictEqual(quota.getBalance('uP1'), 0, '探针成功不凭空补余额,账面保持保守的 0')
|
||||
const row = db.prepare('SELECT sentTotal FROM subscribe_quota WHERE openid = ?').get('uP1')
|
||||
assert.strictEqual(row.sentTotal, 3, 'sentTotal 要如实记录每一次成功发送,便于排查')
|
||||
} finally {
|
||||
wx.sendSubscribeMessage = restore
|
||||
}
|
||||
})
|
||||
|
||||
test('余额为 0 时不发任何请求', async () => {
|
||||
test('记账余额为 0 且微信侧确实没额度:探针被 43101 拒绝后立即中止', async () => {
|
||||
resetLogs()
|
||||
const WX_ERROR_MESSAGE = '发送订阅消息失败: {"errcode":43101}'
|
||||
let called = 0
|
||||
const restore = wx.sendSubscribeMessage
|
||||
wx.sendSubscribeMessage = async () => {
|
||||
called++
|
||||
const e = new Error(WX_ERROR_MESSAGE)
|
||||
e.errcode = 43101
|
||||
throw e
|
||||
}
|
||||
|
||||
try {
|
||||
const items = [
|
||||
makeAnniv('Q1', 'uP2', '甲', 'high', 0, 3),
|
||||
makeAnniv('Q2', 'uP2', '乙', 'high', 0, 3),
|
||||
makeAnniv('Q3', 'uP2', '丙', 'high', 0, 3)
|
||||
]
|
||||
const r = await reminder.runForUser('uP2', items, TODAY)
|
||||
|
||||
assert.strictEqual(called, 1, '只探一次,被拒后不该继续打请求')
|
||||
assert.strictEqual(r.ok, 0)
|
||||
assert.strictEqual(r.skipped, 3, '含探针那条在内全部计为 skipped')
|
||||
assert.strictEqual(quota.getBalance('uP2'), 0)
|
||||
|
||||
const q1 = db.prepare("SELECT * FROM remind_logs WHERE anniversaryId = 'Q1'").get()
|
||||
const q2 = db.prepare("SELECT * FROM remind_logs WHERE anniversaryId = 'Q2'").get()
|
||||
assert.strictEqual(q1.error, WX_ERROR_MESSAGE, '探针那条真打了请求,应保留微信原始错误信息')
|
||||
assert.strictEqual(q2.error, 'quota_exhausted', '后续条目根本没发请求,记兜底文案')
|
||||
} finally {
|
||||
wx.sendSubscribeMessage = restore
|
||||
}
|
||||
})
|
||||
|
||||
test('探针遇非额度错误:记 failed、不动余额、本轮不再重复探', async () => {
|
||||
resetLogs()
|
||||
let called = 0
|
||||
const restore = wx.sendSubscribeMessage
|
||||
wx.sendSubscribeMessage = async () => { called++; return { errcode: 0 } }
|
||||
wx.sendSubscribeMessage = async () => {
|
||||
called++
|
||||
const e = new Error('网络炸了')
|
||||
e.errcode = 40003
|
||||
throw e
|
||||
}
|
||||
|
||||
const items = [makeAnniv('C1', 'uC', '丙', 'high', 0, 3)]
|
||||
const r = await reminder.runForUser('uC', items, TODAY)
|
||||
wx.sendSubscribeMessage = restore
|
||||
try {
|
||||
const items = [
|
||||
makeAnniv('R1', 'uP3', '甲', 'high', 0, 3),
|
||||
makeAnniv('R2', 'uP3', '乙', 'high', 0, 3),
|
||||
makeAnniv('R3', 'uP3', '丙', 'high', 0, 3)
|
||||
]
|
||||
const r = await reminder.runForUser('uP3', items, TODAY)
|
||||
|
||||
assert.strictEqual(called, 0, '没额度就不该打请求')
|
||||
assert.strictEqual(r.skipped, 1)
|
||||
// 网络/配置错误什么都没证明,既不能当作「有额度」继续发,也不该反复赌请求
|
||||
assert.strictEqual(called, 1, '探针机会用掉就没了,不该对同一个用户反复重试')
|
||||
assert.strictEqual(r.fail, 1, '探针那条记 failed,不是 skipped')
|
||||
assert.strictEqual(r.skipped, 2)
|
||||
assert.strictEqual(quota.getBalance('uP3'), 0, '非额度错误不动余额')
|
||||
|
||||
const r1 = db.prepare("SELECT * FROM remind_logs WHERE anniversaryId = 'R1'").get()
|
||||
assert.strictEqual(r1.status, 'failed')
|
||||
assert.strictEqual(r1.error, '网络炸了')
|
||||
} finally {
|
||||
wx.sendSubscribeMessage = restore
|
||||
}
|
||||
})
|
||||
|
||||
test('遇到 43101 立即归零并中止本用户剩余发送', async () => {
|
||||
@@ -145,11 +242,26 @@ test('非额度类错误只记 failed,不中止后续发送,也不动余额'
|
||||
|
||||
test('skipped 会写入 remind_logs 便于排查', async () => {
|
||||
resetLogs()
|
||||
const items = [makeAnniv('F1', 'uF', '甲', 'high', 0, 3)]
|
||||
await reminder.runForUser('uF', items, TODAY)
|
||||
const row = db.prepare("SELECT * FROM remind_logs WHERE status = 'skipped'").get()
|
||||
assert.ok(row, '应写入 skipped 日志')
|
||||
assert.strictEqual(row.error, 'quota_exhausted')
|
||||
// 余额为 0 时第一条会被当探针发出去,所以要放两条,第二条才是「压根没发请求」的 skipped
|
||||
const restore = wx.sendSubscribeMessage
|
||||
wx.sendSubscribeMessage = async () => {
|
||||
const e = new Error('发送订阅消息失败')
|
||||
e.errcode = 43101
|
||||
throw e
|
||||
}
|
||||
try {
|
||||
const items = [
|
||||
makeAnniv('F1', 'uF', '甲', 'high', 0, 3),
|
||||
makeAnniv('F2', 'uF', '乙', 'high', 0, 3)
|
||||
]
|
||||
await reminder.runForUser('uF', items, TODAY)
|
||||
const row = db.prepare("SELECT * FROM remind_logs WHERE anniversaryId = 'F2'").get()
|
||||
assert.ok(row, '应写入 skipped 日志')
|
||||
assert.strictEqual(row.status, 'skipped')
|
||||
assert.strictEqual(row.error, 'quota_exhausted')
|
||||
} finally {
|
||||
wx.sendSubscribeMessage = restore
|
||||
}
|
||||
})
|
||||
|
||||
// runOnce 是从数据库读数据的(SELECT * FROM anniversaries WHERE remindEnabled = 1),
|
||||
@@ -170,23 +282,34 @@ test('runOnce 按 openid 分组结算,各用户额度互不影响', async () =
|
||||
db.prepare('DELETE FROM anniversaries').run()
|
||||
|
||||
quota.grant('uG', 5) // uG 有额度
|
||||
// uH 不 grant,余额保持 0
|
||||
// uH 不 grant,余额保持 0,且微信侧也确实没额度(下面 mock 里对 uH 回 43101)
|
||||
|
||||
insertAnniv('G1', 'uG', '甲', 0, 3) // 今天到期
|
||||
insertAnniv('H1', 'uH', '乙', 0, 3) // 今天到期
|
||||
|
||||
let called = 0
|
||||
const attempts = []
|
||||
const restore = wx.sendSubscribeMessage
|
||||
wx.sendSubscribeMessage = async () => { called++; return { errcode: 0 } }
|
||||
wx.sendSubscribeMessage = async (p) => {
|
||||
attempts.push(p.touser)
|
||||
if (p.touser === 'uH') {
|
||||
const e = new Error('发送订阅消息失败')
|
||||
e.errcode = 43101
|
||||
throw e
|
||||
}
|
||||
return { errcode: 0 }
|
||||
}
|
||||
|
||||
const r = await reminder.runOnce()
|
||||
wx.sendSubscribeMessage = restore
|
||||
try {
|
||||
const r = await reminder.runOnce()
|
||||
|
||||
assert.strictEqual(r.total, 2)
|
||||
assert.strictEqual(r.ok, 1, '有额度的 uG 应正常发出')
|
||||
assert.strictEqual(r.skipped, 1, '没额度的 uH 应被跳过')
|
||||
assert.strictEqual(r.fail, 0)
|
||||
assert.strictEqual(called, 1, '没额度的用户不该真的发起请求,两用户不能互相借额度')
|
||||
assert.strictEqual(quota.getBalance('uG'), 4, 'uG 消费后余额正确减少')
|
||||
assert.strictEqual(quota.getBalance('uH'), 0, 'uH 的余额不受 uG 影响')
|
||||
assert.strictEqual(r.total, 2)
|
||||
assert.strictEqual(r.ok, 1, '有额度的 uG 应正常发出')
|
||||
assert.strictEqual(r.skipped, 1, '微信侧确认没额度的 uH 应被跳过')
|
||||
assert.strictEqual(r.fail, 0)
|
||||
assert.deepStrictEqual(attempts, ['uG', 'uH'], 'uH 只有一次探针请求,不能借用 uG 的额度')
|
||||
assert.strictEqual(quota.getBalance('uG'), 4, 'uG 消费后余额正确减少')
|
||||
assert.strictEqual(quota.getBalance('uH'), 0, 'uH 的余额不受 uG 影响')
|
||||
} finally {
|
||||
wx.sendSubscribeMessage = restore
|
||||
}
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user