- 新增 server/:Node + Express + SQLite + node-cron 实现登录、纪念日 CRUD 和定时订阅消息推送 - 新增 .gitea/workflows/deploy.yml:推送即触发群晖 Docker 部署,监听 15002 - utils/api.js:自动按 envVersion 切换本地/线上 BASE_URL - app.js 与 add-anniversary.js 移除 wx.cloud 调用,改走自建后端 - cloudfunctions/ 暂保留以便回滚 - 一并提交此前未入库的首页 / 设置页 / 日历 / 万年历等改造 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,139 @@
|
||||
const cron = require('node-cron')
|
||||
const db = require('./db')
|
||||
const wx = require('./wx')
|
||||
|
||||
const TEMPLATE_ID = process.env.WX_TEMPLATE_ID
|
||||
const MINIPROGRAM_STATE = process.env.WX_MINIPROGRAM_STATE || 'formal'
|
||||
|
||||
const TYPE_NAMES = {
|
||||
birthday: '公历生日',
|
||||
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}日`
|
||||
}
|
||||
|
||||
// 算出"距离今年(或明年)这个纪念日还有多少天"
|
||||
function getThisYearDate(anniv) {
|
||||
const today = new Date()
|
||||
today.setHours(0, 0, 0, 0)
|
||||
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 }
|
||||
Reference in New Issue
Block a user