Files
wxserver/server/src/reminder.js
T
yuming 756d57d818
部署到群晖 / deploy (push) Successful in 44s
简化纪念日类型:去掉"农历生日"类型,公历/农历改由 isLunar 字段决定
- typeList 从 5 项简化到 4 项:生日 / 结婚纪念日 / 订婚纪念日 / 其他纪念日
- TYPE_NAMES / TYPE_ICONS 中 lunar_birthday 保留兼容映射(也映射到「生日」+ 🎂),
  让线上历史数据自然回显,无需数据库迁移(方案 A)
- getTypeIndex('lunar_birthday') = 0,老数据编辑时正确回显「生日」
- index.js 列表筛选和 wxml 图标判断本来已含 lunar_birthday 兼容,无需改

老数据自然淘汰:用户重新保存时新数据写 type='birthday',老数据 type 保留
直到下次编辑保存才升级。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-02 07:30:37 +08:00

157 lines
5.0 KiB
JavaScript

const cron = require('node-cron')
const db = require('./db')
const wx = require('./wx')
const lunar = require('./lunar')
const TEMPLATE_ID = process.env.WX_TEMPLATE_ID
const MINIPROGRAM_STATE = process.env.WX_MINIPROGRAM_STATE || 'formal'
const TYPE_NAMES = {
birthday: '生日',
// 老数据 type=lunar_birthday 兼容回显(公历/农历由 isLunar 决定)
lunar_birthday: '生日',
wedding: '结婚纪念日',
engagement: '订婚纪念日',
other: '其他纪念日'
}
function getTypeName(type, customName) {
if (type === 'other' && customName) return customName
return TYPE_NAMES[type] || '纪念日'
}
function formatDate(date) {
const y = date.getFullYear()
const m = String(date.getMonth() + 1).padStart(2, '0')
const d = String(date.getDate()).padStart(2, '0')
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()
start.setHours(0, 0, 0, 0)
const row = db.prepare(
'SELECT COUNT(*) AS n FROM remind_logs WHERE anniversaryId = ? AND sendDate >= ? AND status = ?'
).get(anniversaryId, start.getTime(), 'success')
return row.n > 0
}
const insertLog = db.prepare(`
INSERT INTO remind_logs (anniversaryId, personName, typeName, daysUntil, sendDate, status, error)
VALUES (@anniversaryId, @personName, @typeName, @daysUntil, @sendDate, @status, @error)
`)
async function runOnce() {
console.log('[reminder] 开始扫描纪念日...')
const list = db.prepare('SELECT * FROM anniversaries WHERE remindEnabled = 1').all()
console.log(`[reminder] 启用提醒的纪念日 ${list.length}`)
let ok = 0
let fail = 0
for (const anniv of list) {
try {
const target = getThisYearDate(anniv)
const daysUntil = daysBetween(target)
const shouldRemind = (daysUntil === 0) || (daysUntil === (anniv.remindDays || 0))
if (!shouldRemind) continue
if (alreadySentToday(anniv.id)) {
console.log(`[reminder] 今天已发过,跳过: ${anniv.personName}`)
continue
}
const typeName = getTypeName(anniv.type, anniv.customTypeName)
await wx.sendSubscribeMessage({
touser: anniv.openid,
page: 'pages/index/index',
templateId: TEMPLATE_ID,
miniprogramState: MINIPROGRAM_STATE,
data: {
name1: { value: anniv.personName },
thing2: { value: daysUntil === 0 ? '今天' : `还有${daysUntil}` },
thing6: { value: formatDate(target) },
thing5: { value: anniv.remark || '别忘了准备一份礼物哦!' }
}
})
insertLog.run({
anniversaryId: anniv.id,
personName: anniv.personName,
typeName,
daysUntil,
sendDate: Date.now(),
status: 'success',
error: null
})
ok++
console.log(`[reminder] 发送成功: ${anniv.personName} (${typeName}, ${daysUntil}天)`)
} catch (err) {
fail++
console.error(`[reminder] 发送失败: ${anniv.personName}`, err.message)
insertLog.run({
anniversaryId: anniv.id,
personName: anniv.personName,
typeName: null,
daysUntil: null,
sendDate: Date.now(),
status: 'failed',
error: err.message
})
}
}
console.log(`[reminder] 完成: 成功 ${ok}, 失败 ${fail}`)
return { total: list.length, ok, fail }
}
function start() {
const expr = process.env.REMINDER_CRON || '0 9 * * *'
if (!cron.validate(expr)) {
console.error(`[reminder] 无效的 cron 表达式: ${expr},定时任务未启动`)
return
}
cron.schedule(expr, runOnce, { timezone: 'Asia/Shanghai' })
console.log(`[reminder] 定时任务已注册: ${expr} (Asia/Shanghai)`)
}
module.exports = { start, runOnce }