Files
yuming d8573359a2 修复首页风险预告与实际发送的排序不一致问题
atRisk.js 之前只按 fireInDays 排序,同一天内多条提醒的先后完全未定义,
和 reminder.js 实际发送时「当天>提前、重要程度降序」的规则对不上,导致
额度卡在中间时首页提示的风险人名和定时任务实际跳过的人不一致。

- 把 importanceRank 从 reminder.js 抽到共享的 src/importance.js,两处复用
- atRisk.js 的 computeAtRisk 排序改为三段式:fireInDays 升序 →
  同天内 kind(当天优先于提前)→ 同 kind 内重要程度降序
- 补充 atRisk.test.js 用例覆盖「同一天多条事件、额度只够一部分」场景
2026-08-16 09:21:09 +08:00

126 lines
5.1 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
const test = require('node:test')
const assert = require('node:assert')
const { expandEvents, computeAtRisk, LOOKAHEAD_DAYS } = require('../src/atRisk')
const TODAY = new Date(2026, 7, 15) // 2026-08-15
// 距今 10 天的公历纪念日,提前 3 天提醒
const near = {
id: 'a1', personName: '张三', remindEnabled: 1, importance: 'high',
isLunar: false, solarMonth: 8, solarDay: 25, remindDays: 3
}
// 距今 40 天
const mid = {
id: 'a2', personName: '李四', remindEnabled: 1, importance: 'low',
isLunar: false, solarMonth: 9, solarDay: 24, remindDays: 7
}
test('LOOKAHEAD_DAYS 为 60', () => {
assert.strictEqual(LOOKAHEAD_DAYS, 60)
})
test('一条纪念日展开出「当天」和「提前」两个事件', () => {
const events = expandEvents(near, TODAY)
assert.strictEqual(events.length, 2)
const onDay = events.find(e => e.kind === 'onDay')
const ahead = events.find(e => e.kind === 'ahead')
assert.strictEqual(onDay.fireInDays, 10)
assert.strictEqual(ahead.fireInDays, 7)
})
test('remindDays 为 0 时只有当天事件', () => {
const events = expandEvents({ ...near, remindDays: 0 }, TODAY)
assert.strictEqual(events.length, 1)
assert.strictEqual(events[0].kind, 'onDay')
})
test('超出 60 天窗口的事件被过滤掉', () => {
const far = { ...near, solarMonth: 12, solarDay: 25, remindDays: 3 }
assert.strictEqual(expandEvents(far, TODAY).length, 0)
})
test('提前日已过、当天未到时,只剩当天事件', () => {
// 距今 2 天,但提前 7 天的时点早已过去
const soon = { ...near, solarMonth: 8, solarDay: 17, remindDays: 7 }
const events = expandEvents(soon, TODAY)
assert.strictEqual(events.length, 1)
assert.strictEqual(events[0].kind, 'onDay')
})
test('余额足够时没有风险', () => {
const r = computeAtRisk([near, mid], 10, TODAY)
assert.strictEqual(r.atRiskCount, 0)
assert.deepStrictEqual(r.atRiskNames, [])
})
test('余额为 0 时全部有风险', () => {
const r = computeAtRisk([near, mid], 0, TODAY)
assert.strictEqual(r.atRiskCount, 4)
assert.deepStrictEqual(r.atRiskNames, ['张三', '李四'])
})
test('按时间先后消耗额度,后面的才有风险', () => {
// 张三的两个事件在第 7、10 天,李四的在第 33、40 天
const r = computeAtRisk([near, mid], 2, TODAY)
assert.strictEqual(r.atRiskCount, 2)
assert.deepStrictEqual(r.atRiskNames, ['李四'])
})
test('人名去重,同一人占两条只出现一次', () => {
const r = computeAtRisk([near], 0, TODAY)
assert.strictEqual(r.atRiskCount, 2)
assert.deepStrictEqual(r.atRiskNames, ['张三'])
})
test('未开启提醒的纪念日不参与计算', () => {
const r = computeAtRisk([{ ...near, remindEnabled: 0 }], 0, TODAY)
assert.strictEqual(r.atRiskCount, 0)
})
// ------ 同一天内排序需与 reminder.js 的 runForUser 完全对齐 ------
// 四条纪念日都恰好在第 5 天产生一个事件:
// 甲(当天/high) 乙(当天/low) 丙(提前/high) 丁(提前/medium)
// 丙、丁的「当天」事件被安排在第 65 天(超出 60 天窗口),不会进入计算,
// 这样每人恰好只贡献一个事件,排序结果可以精确断言。
const onDayHigh = {
id: 'e1', personName: '甲', remindEnabled: 1, importance: 'high',
isLunar: false, solarMonth: 8, solarDay: 20, remindDays: 0
}
const onDayLow = {
id: 'e2', personName: '乙', remindEnabled: 1, importance: 'low',
isLunar: false, solarMonth: 8, solarDay: 20, remindDays: 0
}
const aheadHigh = {
id: 'e3', personName: '丙', remindEnabled: 1, importance: 'high',
isLunar: false, solarMonth: 10, solarDay: 19, remindDays: 60
}
const aheadMedium = {
id: 'e4', personName: '丁', remindEnabled: 1, importance: 'medium',
isLunar: false, solarMonth: 10, solarDay: 19, remindDays: 60
}
// 刻意打乱输入顺序(不按「当天>提前、重要程度降序」排列),
// 这样如果排序逻辑退化成只按 fireInDays 排(Array.sort 是稳定排序,
// 同值会保留输入顺序),测试才能真正暴露出排序规则失效,而不是被输入顺序碰巧掩盖。
const sameDayGroup = [aheadMedium, aheadHigh, onDayLow, onDayHigh]
test('同一天内,当天事件排在提前事件之前(与 reminder.js 一致)', () => {
// 额度只够 2 条:应先消耗「当天」的甲、乙,风险留给「提前」的丙、丁
const r = computeAtRisk(sameDayGroup, 2, TODAY)
assert.strictEqual(r.atRiskCount, 2)
assert.deepStrictEqual(r.atRiskNames, ['丙', '丁'])
})
test('同一天内,当天事件按重要程度降序(high 先于 low)', () => {
// 额度只够 1 条:当天里 high 的甲应排在 low 的乙前面,先被消耗
const r = computeAtRisk(sameDayGroup, 1, TODAY)
assert.strictEqual(r.atRiskCount, 3)
assert.deepStrictEqual(r.atRiskNames, ['乙', '丙', '丁'])
})
test('同一天内,提前事件按重要程度降序(high 先于 medium', () => {
// 额度够 3 条:甲、乙(当天)先消耗,提前事件里 high 的丙应先于 medium 的丁被消耗
const r = computeAtRisk(sameDayGroup, 3, TODAY)
assert.strictEqual(r.atRiskCount, 1)
assert.deepStrictEqual(r.atRiskNames, ['丁'])
})