From b1949ac73e0059f35de9306bb024e46b5f1d7e56 Mon Sep 17 00:00:00 2001 From: yuming Date: Sat, 15 Aug 2026 17:55:40 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9E=20/api/subscribe=20=E6=8E=A5?= =?UTF-8?q?=E5=8F=A3=EF=BC=9A=E6=8E=88=E6=9D=83=E4=B8=8A=E6=8A=A5=E4=B8=8E?= =?UTF-8?q?=E9=A3=8E=E9=99=A9=E6=9F=A5=E8=AF=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - grantQuota/getQuotaStatus 从 index.js 导出供测试直接调用 - app.listen 加 require.main 守卫,避免测试 require 本文件时 真的起服务占用端口、cron 定时器让进程无法退出 --- server/src/index.js | 52 +++++++++++++++++++++++++--- server/test/subscribeApi.test.js | 58 ++++++++++++++++++++++++++++++++ 2 files changed, 105 insertions(+), 5 deletions(-) create mode 100644 server/test/subscribeApi.test.js diff --git a/server/src/index.js b/server/src/index.js index fda56d1..b802fbf 100644 --- a/server/src/index.js +++ b/server/src/index.js @@ -4,6 +4,8 @@ const express = require('express') const db = require('./db') const wx = require('./wx') const reminder = require('./reminder') +const quota = require('./quota') +const atRisk = require('./atRisk') const app = express() app.use(express.json({ limit: '1mb' })) @@ -221,6 +223,39 @@ function getPersons(openid) { 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) => { try { @@ -231,10 +266,17 @@ app.post('/api/reminder/run', async (req, res) => { } }) +module.exports = { grantQuota, getQuotaStatus } + // ---- 启动 ---- -const PORT = process.env.PORT || 3000 -app.listen(PORT, () => { - console.log(`生日提醒后端已启动,监听 ${PORT}`) - reminder.start() -}) +// 只在直接执行本文件(node src/index.js / npm start)时才真正监听端口、注册定时任务。 +// 测试用 require('../src/index') 只是拿函数引用,不需要(也不能)真的起服务—— +// 否则会占住端口且 cron 定时器让进程一直不退出,测试永远卡住。 +if (require.main === module) { + const PORT = process.env.PORT || 3000 + app.listen(PORT, () => { + console.log(`生日提醒后端已启动,监听 ${PORT}`) + reminder.start() + }) +} diff --git a/server/test/subscribeApi.test.js b/server/test/subscribeApi.test.js new file mode 100644 index 0000000..1133a5c --- /dev/null +++ b/server/test/subscribeApi.test.js @@ -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('乙'), '时间靠后的乙必然有风险') +})