198 lines
7.6 KiB
JavaScript
198 lines
7.6 KiB
JavaScript
const path = require('path')
|
|
const fs = require('fs')
|
|
const crypto = require('crypto')
|
|
const Database = require('better-sqlite3')
|
|
const { initCrypto, dec, ensureDecrypted } = require('./weekly_crypto')
|
|
|
|
const cfgPath = path.join(process.cwd(), 'config', 'weekly_embed.json')
|
|
if (!fs.existsSync(path.dirname(cfgPath))) fs.mkdirSync(path.dirname(cfgPath), { recursive: true })
|
|
if (!fs.existsSync(cfgPath)) {
|
|
fs.writeFileSync(cfgPath, JSON.stringify({ machine_salt: crypto.randomBytes(16).toString('hex'), access_key: 'K049876543' }, null, 2))
|
|
}
|
|
const getConfig = () => {
|
|
try { return JSON.parse(fs.readFileSync(cfgPath, 'utf-8')) } catch { return {} }
|
|
}
|
|
const cfg = getConfig()
|
|
|
|
const passphrase = process.env.SYSTEM_AUTH_PASS_93220 || process.env.WEEKLY_PASSWORD || ''
|
|
const salt = cfg.machine_salt || 'salt'
|
|
initCrypto(passphrase, salt)
|
|
|
|
const dataDir = path.join(process.cwd(), 'data')
|
|
const dbPath = path.join(dataDir, 'weekly.db')
|
|
const sealPath = path.join(dataDir, 'weekly.enc')
|
|
|
|
let db = null
|
|
const getDB = () => {
|
|
if (db) return db
|
|
if (!fs.existsSync(dataDir)) fs.mkdirSync(dataDir, { recursive: true })
|
|
ensureDecrypted(dbPath, sealPath)
|
|
db = new Database(dbPath, { readonly: true })
|
|
try { db.pragma('journal_mode = WAL') } catch {}
|
|
try { db.pragma('synchronous = NORMAL') } catch {}
|
|
try { db.pragma('busy_timeout = 4000') } catch {}
|
|
return db
|
|
}
|
|
|
|
const weekIdForDate = (dateStr) => {
|
|
try {
|
|
const d = new Date(dateStr)
|
|
const day = d.getUTCDay()
|
|
const diffToMon = (day + 6) % 7
|
|
const mon = new Date(Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate() - diffToMon))
|
|
const firstDay = new Date(Date.UTC(mon.getUTCFullYear(), 0, 1))
|
|
const firstDayWeekday = (firstDay.getUTCDay() + 6) % 7
|
|
const days = Math.floor((mon - firstDay) / 86400000) + firstDayWeekday + 1
|
|
const week = Math.ceil(days / 7)
|
|
return `${mon.getUTCFullYear()}-W${String(week).padStart(2, '0')}`
|
|
} catch {
|
|
return `${new Date().getUTCFullYear()}-W01`
|
|
}
|
|
}
|
|
|
|
const weeksForYear = (year) => {
|
|
const y = parseInt(year, 10)
|
|
const tz = 'Asia/Shanghai'
|
|
const dateInTZ = (d) => new Date(d)
|
|
const firstThursday = new Date(Date.UTC(y, 0, 4))
|
|
const day = firstThursday.getUTCDay()
|
|
const diffToMon = (day + 6) % 7
|
|
let mon = new Date(Date.UTC(y, 0, 4 - diffToMon))
|
|
const out = []
|
|
let weekNum = 1
|
|
while (weekNum <= 53) {
|
|
const thu = new Date(Date.UTC(mon.getUTCFullYear(), mon.getUTCMonth(), mon.getUTCDate() + 3))
|
|
const isoYear = thu.getUTCFullYear()
|
|
if (isoYear !== y) break
|
|
const sun = new Date(Date.UTC(mon.getUTCFullYear(), mon.getUTCMonth(), mon.getUTCDate() + 6))
|
|
const weekLabel = `${y}-W${String(weekNum).padStart(2, '0')}`
|
|
const startStr = new Intl.DateTimeFormat('zh-CN', { timeZone: tz, year: 'numeric', month: '2-digit', day: '2-digit' }).format(dateInTZ(mon)).replace(/\//g, '-')
|
|
const endStr = new Intl.DateTimeFormat('zh-CN', { timeZone: tz, year: 'numeric', month: '2-digit', day: '2-digit' }).format(dateInTZ(sun)).replace(/\//g, '-')
|
|
out.push({ week_id: weekLabel, start: startStr, end: endStr })
|
|
mon = new Date(Date.UTC(mon.getUTCFullYear(), mon.getUTCMonth(), mon.getUTCDate() + 7))
|
|
weekNum++
|
|
}
|
|
|
|
try {
|
|
const db = getDB()
|
|
const reports = db.prepare(`SELECT id, week_id FROM reports WHERE week_id LIKE ?`).all(`${y}-%`)
|
|
const reportMap = new Map()
|
|
reports.forEach(r => reportMap.set(r.week_id, r.id))
|
|
if (reports.length > 0) {
|
|
const placeholders = reports.map(() => '?').join(',')
|
|
const reportIds = reports.map(r => r.id)
|
|
const eventRows = db.prepare(`SELECT DISTINCT report_id FROM events WHERE report_id IN (${placeholders})`).all(...reportIds)
|
|
const eventSet = new Set(eventRows.map(r => r.report_id))
|
|
out.forEach(w => {
|
|
const rid = reportMap.get(w.week_id)
|
|
w.has_report = !!rid
|
|
w.has_events = rid ? eventSet.has(rid) : false
|
|
})
|
|
} else {
|
|
out.forEach(w => { w.has_report = false; w.has_events = false })
|
|
}
|
|
} catch {}
|
|
|
|
return out
|
|
}
|
|
|
|
const getReportByWeek = (week_id) => {
|
|
const row = getDB().prepare(`SELECT * FROM reports WHERE week_id=? ORDER BY id DESC LIMIT 1`).get(week_id)
|
|
if (!row) return null
|
|
return {
|
|
id: row.id,
|
|
week_id: row.week_id,
|
|
title: dec(row.title_enc, row.title_iv, row.title_tag),
|
|
summary: dec(row.summary_enc, row.summary_iv, row.summary_tag),
|
|
content: dec(row.content_enc, row.content_iv, row.content_tag),
|
|
created_at: row.created_at,
|
|
updated_at: row.updated_at
|
|
}
|
|
}
|
|
|
|
const listEvents = (report_id) => {
|
|
const rows = getDB().prepare(`SELECT * FROM events WHERE report_id=? ORDER BY id ASC`).all(parseInt(report_id, 10))
|
|
return rows.map(r => ({
|
|
id: r.id,
|
|
report_id: r.report_id,
|
|
title: dec(r.title_enc, r.title_iv, r.title_tag),
|
|
content: dec(r.content_enc, r.content_iv, r.content_tag),
|
|
status_code: r.status_code,
|
|
status: dec(r.status_enc, r.status_iv, r.status_tag),
|
|
deadline: r.deadline,
|
|
parent_event_id: r.parent_event_id,
|
|
completed_at: r.completed_at,
|
|
push_required: !!r.push_required,
|
|
created_at: r.created_at,
|
|
updated_at: r.updated_at
|
|
}))
|
|
}
|
|
|
|
const bindRoutes = (app) => {
|
|
const accessKey = String(cfg.access_key || 'K049876543')
|
|
const mustKey = (req, res) => {
|
|
const key = String(req.get('X-Weekly-Embed-Key') || req.query.key || '')
|
|
if (!key || key !== accessKey) {
|
|
res.status(403).json({ ok: false, error: 'forbidden' })
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
app.use('/api/weekly_embed', (req, res, next) => {
|
|
res.set('Cache-Control', 'no-store, no-cache, must-revalidate, proxy-revalidate')
|
|
res.set('Pragma', 'no-cache')
|
|
res.set('Expires', '0')
|
|
res.set('Surrogate-Control', 'no-store')
|
|
if (!mustKey(req, res)) return
|
|
next()
|
|
})
|
|
|
|
app.get('/api/weekly_embed/years', (req, res) => {
|
|
try {
|
|
const trace = []
|
|
trace.push('打开只读周报数据库')
|
|
const db = getDB()
|
|
trace.push('查询可用年份')
|
|
const rows = db.prepare(`SELECT DISTINCT SUBSTR(week_id,1,4) AS y FROM reports WHERE week_id IS NOT NULL AND LENGTH(week_id)>=4 ORDER BY y DESC`).all()
|
|
trace.push(`返回年份 ${rows.length} 条`)
|
|
const years = rows.map(r => parseInt(String(r.y), 10)).filter(y => y > 0)
|
|
res.json({ ok: true, years, trace })
|
|
} catch (e) {
|
|
res.status(500).json({ ok: false, error: String(e && e.message ? e.message : e) })
|
|
}
|
|
})
|
|
|
|
app.get('/api/weekly_embed/weeks', (req, res) => {
|
|
const trace = []
|
|
const year = parseInt(String(req.query.year || new Date().getFullYear()), 10)
|
|
trace.push(`计算 ${year} 年 ISO 周列表`)
|
|
const weeks = weeksForYear(year)
|
|
trace.push(`合并周报/事件存在标记 ${weeks.length} 条`)
|
|
const currentWeekId = weekIdForDate(new Date().toISOString())
|
|
trace.push(`定位当前周 ${currentWeekId}`)
|
|
res.json({ ok: true, weeks, currentWeekId, trace })
|
|
})
|
|
|
|
app.get('/api/weekly_embed/report', (req, res) => {
|
|
const trace = []
|
|
const weekId = String(req.query.week_id || '')
|
|
trace.push(`按 week_id 读取周报 ${weekId}`)
|
|
const report = weekId ? getReportByWeek(weekId) : null
|
|
trace.push(report ? `读取到周报 id=${report.id}` : '未找到周报')
|
|
res.json({ ok: true, report, trace })
|
|
})
|
|
|
|
app.get('/api/weekly_embed/events', (req, res) => {
|
|
const trace = []
|
|
const reportId = String(req.query.report_id || '')
|
|
if (!reportId) return res.json({ ok: true, events: [], rows: [], trace: ['缺少 report_id,返回空列表'] })
|
|
trace.push(`按 report_id=${reportId} 读取事件列表`)
|
|
const events = listEvents(reportId)
|
|
trace.push(`事件数量 ${events.length}`)
|
|
res.json({ ok: true, events, rows: events, trace })
|
|
})
|
|
}
|
|
|
|
module.exports = { bindRoutes }
|