新增订阅消息额度记账表与模块
This commit is contained in:
@@ -62,6 +62,14 @@ db.exec(`
|
|||||||
);
|
);
|
||||||
CREATE INDEX IF NOT EXISTS idx_log_anniv ON remind_logs(anniversaryId);
|
CREATE INDEX IF NOT EXISTS idx_log_anniv ON remind_logs(anniversaryId);
|
||||||
CREATE INDEX IF NOT EXISTS idx_log_date ON remind_logs(sendDate);
|
CREATE INDEX IF NOT EXISTS idx_log_date ON remind_logs(sendDate);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS subscribe_quota (
|
||||||
|
openid TEXT PRIMARY KEY,
|
||||||
|
balance INTEGER NOT NULL DEFAULT 0,
|
||||||
|
grantedTotal INTEGER NOT NULL DEFAULT 0,
|
||||||
|
sentTotal INTEGER NOT NULL DEFAULT 0,
|
||||||
|
updateTime INTEGER
|
||||||
|
);
|
||||||
`)
|
`)
|
||||||
|
|
||||||
// 旧库迁移:CREATE TABLE IF NOT EXISTS 不会给已存在的表加列,需要手动 ALTER
|
// 旧库迁移:CREATE TABLE IF NOT EXISTS 不会给已存在的表加列,需要手动 ALTER
|
||||||
|
|||||||
@@ -0,0 +1,60 @@
|
|||||||
|
/**
|
||||||
|
* 订阅消息额度记账
|
||||||
|
*
|
||||||
|
* ⚠️ balance 是估算值,不是权威数据。真值只存在于微信服务器,且没有接口可查。
|
||||||
|
* 我们靠「前端授权成功上报 +1、发送成功 -1」维护,并在发送返回 43101 时归零校正。
|
||||||
|
* 任何地方都不要把 balance 当作可信的强校验依据。
|
||||||
|
*
|
||||||
|
* 额度是「每用户 × 每模板」维度的,所以以 openid 为主键;本项目只有一个模板,故不再分列。
|
||||||
|
*/
|
||||||
|
|
||||||
|
const db = require('./db')
|
||||||
|
|
||||||
|
const MAX_GRANT_PER_CALL = 50 // 单次上报上限,防御异常入参
|
||||||
|
|
||||||
|
function getBalance(openid) {
|
||||||
|
const row = db.prepare('SELECT balance FROM subscribe_quota WHERE openid = ?').get(openid)
|
||||||
|
return row ? row.balance : 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// 把任意入参收敛成 1..MAX_GRANT_PER_CALL 的整数
|
||||||
|
function _normalize(count) {
|
||||||
|
const n = parseInt(count, 10)
|
||||||
|
if (!Number.isFinite(n) || n < 1) return 1
|
||||||
|
return Math.min(n, MAX_GRANT_PER_CALL)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 授权上报:纯累加、不去重——微信侧确实每次授权都 +1,如实记录即可
|
||||||
|
function grant(openid, count = 1) {
|
||||||
|
const n = _normalize(count)
|
||||||
|
db.prepare(`
|
||||||
|
INSERT INTO subscribe_quota (openid, balance, grantedTotal, sentTotal, updateTime)
|
||||||
|
VALUES (@openid, @n, @n, 0, @now)
|
||||||
|
ON CONFLICT(openid) DO UPDATE SET
|
||||||
|
balance = balance + @n,
|
||||||
|
grantedTotal = grantedTotal + @n,
|
||||||
|
updateTime = @now
|
||||||
|
`).run({ openid, n, now: Date.now() })
|
||||||
|
return getBalance(openid)
|
||||||
|
}
|
||||||
|
|
||||||
|
function consume(openid, count = 1) {
|
||||||
|
const n = _normalize(count)
|
||||||
|
db.prepare(`
|
||||||
|
UPDATE subscribe_quota
|
||||||
|
SET balance = MAX(0, balance - @n), sentTotal = sentTotal + @n, updateTime = @now
|
||||||
|
WHERE openid = @openid
|
||||||
|
`).run({ openid, n, now: Date.now() })
|
||||||
|
return getBalance(openid)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 余额归零:发送返回 43101 时调用,说明微信侧实际已无额度(或用户关了通知总开关)
|
||||||
|
function reset(openid) {
|
||||||
|
db.prepare(`
|
||||||
|
INSERT INTO subscribe_quota (openid, balance, grantedTotal, sentTotal, updateTime)
|
||||||
|
VALUES (@openid, 0, 0, 0, @now)
|
||||||
|
ON CONFLICT(openid) DO UPDATE SET balance = 0, updateTime = @now
|
||||||
|
`).run({ openid, now: Date.now() })
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { getBalance, grant, consume, reset }
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
const test = require('node:test')
|
||||||
|
const assert = require('node:assert')
|
||||||
|
const { useTempDb } = require('./helper')
|
||||||
|
|
||||||
|
useTempDb() // 必须在 require quota/db 之前
|
||||||
|
const quota = require('../src/quota')
|
||||||
|
|
||||||
|
test('未记录过的用户余额为 0', () => {
|
||||||
|
assert.strictEqual(quota.getBalance('u_new'), 0)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('grant 纯累加,不去重', () => {
|
||||||
|
quota.grant('u1', 1)
|
||||||
|
quota.grant('u1', 1)
|
||||||
|
quota.grant('u1', 1)
|
||||||
|
assert.strictEqual(quota.getBalance('u1'), 3)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('consume 扣减余额', () => {
|
||||||
|
quota.grant('u2', 5)
|
||||||
|
quota.consume('u2', 2)
|
||||||
|
assert.strictEqual(quota.getBalance('u2'), 3)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('consume 不会把余额扣成负数', () => {
|
||||||
|
quota.grant('u3', 1)
|
||||||
|
quota.consume('u3', 10)
|
||||||
|
assert.strictEqual(quota.getBalance('u3'), 0)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('reset 把余额归零', () => {
|
||||||
|
quota.grant('u4', 8)
|
||||||
|
quota.reset('u4')
|
||||||
|
assert.strictEqual(quota.getBalance('u4'), 0)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('grant 对非法 count 做兜底', () => {
|
||||||
|
quota.grant('u5', 'abc')
|
||||||
|
assert.strictEqual(quota.getBalance('u5'), 1)
|
||||||
|
quota.grant('u5', -5)
|
||||||
|
assert.strictEqual(quota.getBalance('u5'), 2)
|
||||||
|
quota.grant('u5', 9999)
|
||||||
|
assert.strictEqual(quota.getBalance('u5'), 52) // 单次上限 50
|
||||||
|
})
|
||||||
|
|
||||||
|
test('各用户额度互不影响', () => {
|
||||||
|
quota.grant('a', 3)
|
||||||
|
quota.grant('b', 1)
|
||||||
|
assert.strictEqual(quota.getBalance('a'), 3)
|
||||||
|
assert.strictEqual(quota.getBalance('b'), 1)
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user