This commit is contained in:
Daniel
2026-03-04 00:07:14 +08:00
parent ac24c528f3
commit 95e2fe1c41
7 changed files with 271 additions and 45 deletions

View File

@@ -236,6 +236,19 @@ function runMigrations(db) {
INSERT OR IGNORE INTO share_count (id, total) VALUES (1, 0);
`)
} catch (_) {}
try {
exec(`
CREATE TABLE IF NOT EXISTS display_stats (
id INTEGER PRIMARY KEY CHECK (id = 1),
viewers INTEGER NULL,
cumulative INTEGER NULL,
share_count INTEGER NULL,
like_count INTEGER NULL,
feedback_count INTEGER NULL
);
INSERT OR IGNORE INTO display_stats (id) VALUES (1);
`)
} catch (_) {}
}
async function initDb() {

View File

@@ -105,12 +105,12 @@ function getClientIp(req) {
return req.ip || req.socket?.remoteAddress || 'unknown'
}
// 优先用前端传来的 viewer-id 去重(每设备一个),否则用 IP这样多设备同 WiFi 也能正确统计「在看」
function getVisitKey(req) {
const vid = req.headers['x-viewer-id']
const ip = getClientIp(req)
const isLocal = ip === '127.0.0.1' || ip === '::1' || ip === '::ffff:127.0.0.1'
if (typeof vid === 'string' && vid.trim().length > 0 && (process.env.NODE_ENV === 'development' || isLocal)) {
return 'vid:' + vid.trim()
if (typeof vid === 'string' && vid.trim().length > 0) {
return 'vid:' + vid.trim().slice(0, 64)
}
return ip
}
@@ -129,7 +129,7 @@ router.post('/visit', (req, res) => {
res.json(getStats())
} catch (err) {
console.error(err)
res.status(500).json({ viewers: 0, cumulative: 0, feedbackCount: 0, shareCount: 0 })
res.status(500).json({ viewers: 0, cumulative: 0, feedbackCount: 0, shareCount: 0, likeCount: 0 })
}
})
@@ -168,7 +168,7 @@ router.get('/stats', (req, res) => {
res.json(getStats())
} catch (err) {
console.error(err)
res.status(500).json({ viewers: 0, cumulative: 0, feedbackCount: 0, shareCount: 0 })
res.status(500).json({ viewers: 0, cumulative: 0, feedbackCount: 0, shareCount: 0, likeCount: 0 })
}
})
@@ -205,11 +205,28 @@ router.get('/edit/raw', (req, res) => {
const updates = db.prepare('SELECT id, timestamp, category, summary, severity FROM situation_update ORDER BY timestamp DESC LIMIT 80').all()
const summaryUs = db.prepare('SELECT * FROM force_summary WHERE side = ?').get('us')
const summaryIr = db.prepare('SELECT * FROM force_summary WHERE side = ?').get('iran')
let displayStats = null
try {
displayStats = db.prepare('SELECT viewers, cumulative, share_count, like_count, feedback_count FROM display_stats WHERE id = 1').get()
} catch (_) {}
const realCumulative = db.prepare('SELECT total FROM visitor_count WHERE id = 1').get()?.total ?? 0
const realShare = db.prepare('SELECT total FROM share_count WHERE id = 1').get()?.total ?? 0
const liveViewers = db.prepare(
"SELECT COUNT(*) as n FROM visits WHERE last_seen > datetime('now', '-2 minutes')"
).get()?.n ?? 0
const realFeedback = db.prepare('SELECT COUNT(*) as n FROM feedback').get()?.n ?? 0
res.json({
combatLosses: { us: lossesUs || null, iran: lossesIr || null },
keyLocations: { us: locUs || [], iran: locIr || [] },
situationUpdates: updates || [],
forceSummary: { us: summaryUs || null, iran: summaryIr || null },
displayStats: {
viewers: displayStats?.viewers ?? liveViewers,
cumulative: displayStats?.cumulative ?? realCumulative,
shareCount: displayStats?.share_count ?? realShare,
likeCount: displayStats?.like_count ?? 0,
feedbackCount: displayStats?.feedback_count ?? realFeedback,
},
})
} catch (err) {
console.error(err)
@@ -343,4 +360,39 @@ router.put('/edit/force-summary', (req, res) => {
}
})
/** PUT 更新展示统计(看过、在看、分享、点赞、留言)。传 null 表示清除覆盖、改用实时统计 */
router.put('/edit/display-stats', (req, res) => {
try {
db.prepare('INSERT OR IGNORE INTO display_stats (id) VALUES (1)').run()
const updates = []
const values = []
const setField = (key, bodyKey) => {
const v = req.body?.[bodyKey ?? key]
if (v === undefined) return
if (v === null) {
updates.push(`${key} = ?`)
values.push(null)
return
}
const n = Math.max(0, parseInt(v, 10))
if (!Number.isFinite(n)) throw new Error(`${bodyKey ?? key} must be number`)
updates.push(`${key} = ?`)
values.push(n)
}
setField('viewers')
setField('cumulative')
setField('share_count', 'shareCount')
setField('like_count', 'likeCount')
setField('feedback_count', 'feedbackCount')
if (updates.length === 0) return res.status(400).json({ error: 'no fields to update' })
values.push(1)
db.prepare(`UPDATE display_stats SET ${updates.join(', ')} WHERE id = ?`).run(...values)
broadcastAfterEdit(req)
res.json({ ok: true })
} catch (err) {
console.error(err)
res.status(400).json({ error: err.message })
}
})
module.exports = router

View File

@@ -1,13 +1,35 @@
const db = require('./db')
function toNum(v) {
if (v == null || v === '') return 0
const n = Number(v)
return Number.isFinite(n) ? Math.max(0, Math.floor(n)) : 0
}
function getStats() {
const viewers = db.prepare(
const viewersRow = db.prepare(
"SELECT COUNT(*) as n FROM visits WHERE last_seen > datetime('now', '-2 minutes')"
).get().n
const cumulative = db.prepare('SELECT total FROM visitor_count WHERE id = 1').get()?.total ?? 0
const feedbackCount = db.prepare('SELECT COUNT(*) as n FROM feedback').get().n ?? 0
const shareCount = db.prepare('SELECT total FROM share_count WHERE id = 1').get()?.total ?? 0
return { viewers, cumulative, feedbackCount, shareCount }
).get()
const cumulativeRow = db.prepare('SELECT total FROM visitor_count WHERE id = 1').get()
const feedbackRow = db.prepare('SELECT COUNT(*) as n FROM feedback').get()
const shareRow = db.prepare('SELECT total FROM share_count WHERE id = 1').get()
let viewers = toNum(viewersRow?.n)
let cumulative = toNum(cumulativeRow?.total)
let feedbackCount = toNum(feedbackRow?.n)
let shareCount = toNum(shareRow?.total)
let likeCount = 0
let display = null
try {
display = db.prepare('SELECT viewers, cumulative, share_count, like_count, feedback_count FROM display_stats WHERE id = 1').get()
} catch (_) {}
if (display) {
if (display.viewers != null) viewers = toNum(display.viewers)
if (display.cumulative != null) cumulative = toNum(display.cumulative)
if (display.share_count != null) shareCount = toNum(display.share_count)
if (display.like_count != null) likeCount = toNum(display.like_count)
if (display.feedback_count != null) feedbackCount = toNum(display.feedback_count)
}
return { viewers, cumulative, feedbackCount, shareCount, likeCount }
}
module.exports = { getStats }