接入自建后端 + Gitea CI/CD
部署到群晖 / deploy (push) Failing after 6m22s

- 新增 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:
yuming
2026-06-01 15:44:09 +08:00
parent 6747ade9c4
commit 3965e542fc
49 changed files with 5616 additions and 670 deletions
+139
View File
@@ -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 }