抽出日期计算纯函数模块,并搭建后端测试基建

- 新增 server/src/occurrence.js:startOfDay/getNextOccurrence/daysBetween,today 可注入
- reminder.js 改为调用新模块,删除内联的 getThisYearDate/daysBetween
- 引入 node:test 作为测试框架,加 npm test 脚本
- 新增 test/helper.js(临时库辅助,供后续任务用)和 test/occurrence.test.js(7 个用例,覆盖农历跨年等边界)
This commit is contained in:
yuming
2026-08-15 17:30:42 +08:00
parent c92df874d1
commit 68acbb3809
5 changed files with 111 additions and 37 deletions
+48
View File
@@ -0,0 +1,48 @@
/**
* 纪念日日期计算(纯函数,today 可注入以便测试)
*
* 从 reminder.js 抽出,供定时发送与「风险预警」计算共用,避免两处各写一套算错。
*/
const lunar = require('./lunar')
// 把日期归到当天 00:00:00,消除时分秒对比较和天数差的干扰
function startOfDay(date) {
const d = new Date(date)
d.setHours(0, 0, 0, 0)
return d
}
/**
* 算出某条纪念日的下一次发生日(今天或以后最近的一次)
*
* 农历分支以「农历年」为基准递推:公历年和农历年不对齐,农历腊月通常落在下一个公历年,
* 直接拿公历年当农历年传会整整错一年。
* isLunar 为真但缺 lunarMonth/lunarDay 的老数据,安全降级按公历算。
*/
function getNextOccurrence(anniv, today = new Date()) {
const base = startOfDay(today)
if (anniv.isLunar && anniv.lunarMonth && anniv.lunarDay) {
const wantLeap = !!anniv.isLeapMonth
const todayLunar = lunar.solarToLunar(base)
let target = startOfDay(lunar.lunarToSolar(todayLunar.year, anniv.lunarMonth, anniv.lunarDay, wantLeap))
if (target < base) {
target = startOfDay(lunar.lunarToSolar(todayLunar.year + 1, anniv.lunarMonth, anniv.lunarDay, wantLeap))
}
return target
}
let target = startOfDay(new Date(base.getFullYear(), anniv.solarMonth - 1, anniv.solarDay))
if (target < base) {
target = startOfDay(new Date(base.getFullYear() + 1, anniv.solarMonth - 1, anniv.solarDay))
}
return target
}
// 距离目标日还有多少天(负数表示已过)
function daysBetween(target, today = new Date()) {
return Math.round((startOfDay(target) - startOfDay(today)) / 86400000)
}
module.exports = { startOfDay, getNextOccurrence, daysBetween }
+3 -36
View File
@@ -2,6 +2,7 @@ const cron = require('node-cron')
const db = require('./db')
const wx = require('./wx')
const lunar = require('./lunar')
const occurrence = require('./occurrence')
const TEMPLATE_ID = process.env.WX_TEMPLATE_ID
const MINIPROGRAM_STATE = process.env.WX_MINIPROGRAM_STATE || 'formal'
@@ -27,40 +28,6 @@ function formatDate(date) {
return `${y}${m}${d}`
}
// 算出"距离今年(或明年)这个纪念日还有多少天"
// 农历纪念日按 lunarMonth/lunarDay 计算每年对应的公历日期,否则按 solarMonth/solarDay
function getThisYearDate(anniv) {
const today = new Date()
today.setHours(0, 0, 0, 0)
if (anniv.isLunar && anniv.lunarMonth && anniv.lunarDay) {
// 闰月生日按 A 方案处理:若当年无对应闰月,lunarToSolar 内部已自动按普通月份算
const todayLunar = lunar.solarToLunar(today)
const wantLeap = !!anniv.isLeapMonth
let target = lunar.lunarToSolar(todayLunar.year, anniv.lunarMonth, anniv.lunarDay, wantLeap)
target.setHours(0, 0, 0, 0)
if (target < today) {
target = lunar.lunarToSolar(todayLunar.year + 1, anniv.lunarMonth, anniv.lunarDay, wantLeap)
target.setHours(0, 0, 0, 0)
}
return target
}
let target = new Date(today.getFullYear(), anniv.solarMonth - 1, anniv.solarDay)
target.setHours(0, 0, 0, 0)
if (target < today) {
target = new Date(today.getFullYear() + 1, anniv.solarMonth - 1, anniv.solarDay)
target.setHours(0, 0, 0, 0)
}
return target
}
function daysBetween(target) {
const today = new Date()
today.setHours(0, 0, 0, 0)
return Math.round((target - today) / 86400000)
}
// 检查今天是否已经给这条纪念日发过提醒
function alreadySentToday(anniversaryId) {
const start = new Date()
@@ -87,8 +54,8 @@ async function runOnce() {
for (const anniv of list) {
try {
const target = getThisYearDate(anniv)
const daysUntil = daysBetween(target)
const target = occurrence.getNextOccurrence(anniv)
const daysUntil = occurrence.daysBetween(target)
const shouldRemind = (daysUntil === 0) || (daysUntil === (anniv.remindDays || 0))
if (!shouldRemind) continue