756d57d818
部署到群晖 / deploy (push) Successful in 44s
- 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>
157 lines
5.0 KiB
JavaScript
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 }
|