新增 /api/subscribe 接口:授权上报与风险查询

- grantQuota/getQuotaStatus 从 index.js 导出供测试直接调用
- app.listen 加 require.main 守卫,避免测试 require 本文件时
  真的起服务占用端口、cron 定时器让进程无法退出
This commit is contained in:
yuming
2026-08-15 17:55:40 +08:00
parent 3a61b7952b
commit b1949ac73e
2 changed files with 105 additions and 5 deletions
+42
View File
@@ -4,6 +4,8 @@ const express = require('express')
const db = require('./db') const db = require('./db')
const wx = require('./wx') const wx = require('./wx')
const reminder = require('./reminder') const reminder = require('./reminder')
const quota = require('./quota')
const atRisk = require('./atRisk')
const app = express() const app = express()
app.use(express.json({ limit: '1mb' })) app.use(express.json({ limit: '1mb' }))
@@ -221,6 +223,39 @@ function getPersons(openid) {
return { success: true, data: rows } return { success: true, data: rows }
} }
// ---- 订阅额度 ----
// 前端授权成功后上报。纯累加、不去重:微信侧确实每次授权都 +1,如实记录即可。
function grantQuota(openid, data) {
const count = (data && data.count) || 1
return { success: true, balance: quota.grant(openid, count) }
}
// 首页查询:返回余额和「哪几个人的提醒有风险」
function getQuotaStatus(openid) {
const balance = quota.getBalance(openid)
const rows = db.prepare('SELECT * FROM anniversaries WHERE openid = ? AND remindEnabled = 1').all(openid)
const { atRiskCount, atRiskNames } = atRisk.computeAtRisk(rows.map(normalize), balance)
return { success: true, balance, atRiskCount, atRiskNames }
}
app.post('/api/subscribe', (req, res) => {
try {
const openid = req.headers['x-openid'] || req.body.openid
if (!openid) return res.json({ success: false, error: '缺少 openid' })
const { action, data } = req.body
switch (action) {
case 'grant': return res.json(grantQuota(openid, data))
case 'get': return res.json(getQuotaStatus(openid))
default: return res.json({ success: false, error: '未知操作' })
}
} catch (err) {
console.error('订阅额度操作失败:', err.message)
res.json({ success: false, error: err.message })
}
})
// 手动触发提醒任务(便于调试,无需等到定点) // 手动触发提醒任务(便于调试,无需等到定点)
app.post('/api/reminder/run', async (req, res) => { app.post('/api/reminder/run', async (req, res) => {
try { try {
@@ -231,10 +266,17 @@ app.post('/api/reminder/run', async (req, res) => {
} }
}) })
module.exports = { grantQuota, getQuotaStatus }
// ---- 启动 ---- // ---- 启动 ----
// 只在直接执行本文件(node src/index.js / npm start)时才真正监听端口、注册定时任务。
// 测试用 require('../src/index') 只是拿函数引用,不需要(也不能)真的起服务——
// 否则会占住端口且 cron 定时器让进程一直不退出,测试永远卡住。
if (require.main === module) {
const PORT = process.env.PORT || 3000 const PORT = process.env.PORT || 3000
app.listen(PORT, () => { app.listen(PORT, () => {
console.log(`生日提醒后端已启动,监听 ${PORT}`) console.log(`生日提醒后端已启动,监听 ${PORT}`)
reminder.start() reminder.start()
}) })
}
+58
View File
@@ -0,0 +1,58 @@
const test = require('node:test')
const assert = require('node:assert')
const { useTempDb } = require('./helper')
useTempDb()
const db = require('../src/db')
const { grantQuota, getQuotaStatus } = require('../src/index')
const OPENID = 'o_test_1'
// 往指定用户名下插一条纪念日
function seed(openid, id, personName, offsetDays, remindDays) {
const d = new Date(Date.now() + offsetDays * 86400000)
db.prepare(`
INSERT OR REPLACE INTO anniversaries
(id, openid, personId, personName, type, isLunar, solarYear, solarMonth, solarDay,
importance, remindEnabled, remindDays, createTime, updateTime)
VALUES (?, ?, 'p1', ?, 'birthday', 0, ?, ?, ?, 'high', 1, ?, 0, 0)
`).run(id, openid, personName, d.getFullYear(), d.getMonth() + 1, d.getDate(), remindDays)
}
seed(OPENID, 'seed1', '张三', 10, 3)
test('grant 累加并返回新余额', () => {
assert.deepStrictEqual(grantQuota(OPENID, { count: 1 }), { success: true, balance: 1 })
assert.deepStrictEqual(grantQuota(OPENID, { count: 1 }), { success: true, balance: 2 })
})
test('grant 的 data 缺省时按 1 计', () => {
const before = getQuotaStatus(OPENID).balance
grantQuota(OPENID, undefined)
assert.strictEqual(getQuotaStatus(OPENID).balance, before + 1)
})
test('get 返回余额与风险信息', () => {
const res = getQuotaStatus(OPENID)
assert.strictEqual(res.success, true)
assert.strictEqual(typeof res.balance, 'number')
assert.ok(Array.isArray(res.atRiskNames))
})
test('无纪念日时没有风险', () => {
const res = getQuotaStatus('o_empty')
assert.strictEqual(res.atRiskCount, 0)
assert.deepStrictEqual(res.atRiskNames, [])
})
test('纪念日多于余额时报出风险人名', () => {
const OP = 'o_test_2'
// 造两条近期纪念日,共 4 个提醒事件,但只给 1 点额度
seed(OP, 'x1', '甲', 10, 3)
seed(OP, 'x2', '乙', 20, 3)
require('../src/quota').grant(OP, 1)
const res = getQuotaStatus(OP)
assert.ok(res.atRiskCount >= 1, '应当报出风险')
assert.ok(res.atRiskNames.includes('乙'), '时间靠后的乙必然有风险')
})