const express = require('express') const { getSituation } = require('./situationData') const { getStats } = require('./stats') const db = require('./db') const router = express.Router() // 简单鉴权:通过环境变量配置的 API_ADMIN_KEY 保护敏感接口(不返回真实密钥) const ADMIN_API_KEY = process.env.API_ADMIN_KEY || '' function requireAdmin(req, res, next) { if (!ADMIN_API_KEY) { return res.status(500).json({ error: 'admin key not configured' }) } const token = req.headers['x-api-key'] if (typeof token !== 'string' || token !== ADMIN_API_KEY) { return res.status(401).json({ error: 'unauthorized' }) } return next() } // 数据库 Dashboard:返回各表原始数据(需 admin 鉴权) router.get('/db/dashboard', requireAdmin, (req, res) => { try { const tables = [ 'feedback', 'situation', 'force_summary', 'power_index', 'force_asset', 'key_location', 'combat_losses', 'wall_street_trend', 'retaliation_current', 'retaliation_history', 'situation_update', 'news_content', 'gdelt_events', 'conflict_stats', ] const data = {} const timeSort = { feedback: 'created_at DESC', situation: 'updated_at DESC', situation_update: 'timestamp DESC', news_content: 'published_at DESC', gdelt_events: 'event_time DESC', wall_street_trend: 'time DESC', retaliation_history: 'time DESC', conflict_stats: 'updated_at DESC', } for (const name of tables) { try { const order = timeSort[name] let rows try { rows = order ? db.prepare(`SELECT * FROM ${name} ORDER BY ${order}`).all() : db.prepare(`SELECT * FROM ${name}`).all() } catch (qerr) { rows = db.prepare(`SELECT * FROM ${name}`).all() } data[name] = rows } catch (e) { data[name] = { error: e.message } } } res.json(data) } catch (err) { console.error(err) res.status(500).json({ error: err.message }) } }) // 资讯内容(独立表,供后续消费,可选 admin key;若配置了 ADMIN_API_KEY 则也要求鉴权) router.get('/news', (req, res) => { if (ADMIN_API_KEY) { const token = req.headers['x-api-key'] if (typeof token !== 'string' || token !== ADMIN_API_KEY) { return res.status(401).json({ error: 'unauthorized' }) } } try { const limit = Math.min(parseInt(req.query.limit, 10) || 50, 200) const rows = db.prepare('SELECT id, title, summary, url, source, published_at, category, severity, created_at FROM news_content ORDER BY published_at DESC LIMIT ?').all(limit) res.json({ items: rows }) } catch (err) { res.status(500).json({ error: err.message }) } }) router.get('/situation', (req, res) => { try { res.json(getSituation()) } catch (err) { console.error(err) res.status(500).json({ error: err.message }) } }) // 来访统计:记录 IP(或开发环境下每标签 viewer-id),返回在看/看过 function getClientIp(req) { const forwarded = req.headers['x-forwarded-for'] if (forwarded) return forwarded.split(',')[0].trim() return req.ip || req.socket?.remoteAddress || 'unknown' } 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() } return ip } router.post('/visit', (req, res) => { try { const visitKey = getVisitKey(req) db.prepare( "INSERT OR REPLACE INTO visits (ip, last_seen) VALUES (?, datetime('now'))" ).run(visitKey) db.prepare( 'INSERT INTO visitor_count (id, total) VALUES (1, 1) ON CONFLICT(id) DO UPDATE SET total = total + 1' ).run() const broadcast = req.app?.get?.('broadcastSituation') if (typeof broadcast === 'function') broadcast() res.json(getStats()) } catch (err) { console.error(err) res.status(500).json({ viewers: 0, cumulative: 0, feedbackCount: 0, shareCount: 0 }) } }) router.post('/feedback', (req, res) => { try { const content = (req.body?.content ?? '').toString().trim() if (!content || content.length > 2000) { return res.status(400).json({ ok: false, error: '留言内容 1–2000 字' }) } const ip = getClientIp(req) db.prepare( 'INSERT INTO feedback (content, ip) VALUES (?, ?)' ).run(content.slice(0, 2000), ip) res.json({ ok: true }) } catch (err) { console.error(err) res.status(500).json({ ok: false, error: err.message }) } }) router.post('/share', (req, res) => { try { db.prepare( 'INSERT INTO share_count (id, total) VALUES (1, 1) ON CONFLICT(id) DO UPDATE SET total = total + 1' ).run() const shareCount = db.prepare('SELECT total FROM share_count WHERE id = 1').get()?.total ?? 0 res.json({ ok: true, shareCount }) } catch (err) { console.error(err) res.status(500).json({ ok: false, shareCount: 0 }) } }) router.get('/stats', (req, res) => { try { res.json(getStats()) } catch (err) { console.error(err) res.status(500).json({ viewers: 0, cumulative: 0, feedbackCount: 0, shareCount: 0 }) } }) router.get('/events', (req, res) => { try { const s = getSituation() res.json({ updated_at: s.lastUpdated, count: (s.conflictEvents || []).length, events: s.conflictEvents || [], conflict_stats: s.conflictStats || {}, }) } catch (err) { console.error(err) res.status(500).json({ error: err.message }) } }) // ---------- 手动修正看板数据(编辑页用) ---------- function broadcastAfterEdit(req) { try { const broadcast = req.app?.get?.('broadcastSituation') if (typeof broadcast === 'function') broadcast() } catch (_) {} } /** GET 原始可编辑数据:战损、据点、事件脉络、军力概要 */ router.get('/edit/raw', (req, res) => { try { const lossesUs = db.prepare('SELECT * FROM combat_losses WHERE side = ?').get('us') const lossesIr = db.prepare('SELECT * FROM combat_losses WHERE side = ?').get('iran') const locUs = db.prepare('SELECT id, side, name, lat, lng, type, region, status, damage_level FROM key_location WHERE side = ?').all('us') const locIr = db.prepare('SELECT id, side, name, lat, lng, type, region, status, damage_level FROM key_location WHERE side = ?').all('iran') 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') res.json({ combatLosses: { us: lossesUs || null, iran: lossesIr || null }, keyLocations: { us: locUs || [], iran: locIr || [] }, situationUpdates: updates || [], forceSummary: { us: summaryUs || null, iran: summaryIr || null }, }) } catch (err) { console.error(err) res.status(500).json({ error: err.message }) } }) /** PUT 更新战损(美/伊) */ router.put('/edit/combat-losses', (req, res) => { try { const side = req.body?.side if (side !== 'us' && side !== 'iran') { return res.status(400).json({ error: 'side must be us or iran' }) } const row = db.prepare('SELECT * FROM combat_losses WHERE side = ?').get(side) if (!row) return res.status(404).json({ error: 'combat_losses row not found' }) const cols = ['bases_destroyed', 'bases_damaged', 'personnel_killed', 'personnel_wounded', 'civilian_killed', 'civilian_wounded', 'aircraft', 'warships', 'armor', 'vehicles', 'drones', 'missiles', 'helicopters', 'submarines', 'tanks', 'carriers', 'civilian_ships', 'airport_port'] const updates = [] const values = [] for (const c of cols) { if (req.body[c] !== undefined) { updates.push(`${c} = ?`) values.push(Number(req.body[c]) || 0) } } if (updates.length === 0) return res.status(400).json({ error: 'no fields to update' }) values.push(side) db.prepare(`UPDATE combat_losses SET ${updates.join(', ')} WHERE side = ?`).run(...values) db.prepare("INSERT OR REPLACE INTO situation (id, data, updated_at) VALUES (1, '{}', ?)").run(new Date().toISOString()) broadcastAfterEdit(req) res.json({ ok: true }) } catch (err) { console.error(err) res.status(500).json({ error: err.message }) } }) /** PATCH 更新单个据点 */ router.patch('/edit/key-location/:id', (req, res) => { try { const id = parseInt(req.params.id, 10) if (!Number.isFinite(id)) return res.status(400).json({ error: 'invalid id' }) const row = db.prepare('SELECT id FROM key_location WHERE id = ?').get(id) if (!row) return res.status(404).json({ error: 'key_location not found' }) const allowed = ['name', 'lat', 'lng', 'type', 'region', 'status', 'damage_level'] const updates = [] const values = [] for (const k of allowed) { if (req.body[k] !== undefined) { if (k === 'status' && !['operational', 'damaged', 'attacked'].includes(req.body[k])) continue updates.push(`${k} = ?`) values.push(k === 'lat' || k === 'lng' ? Number(req.body[k]) : req.body[k]) } } if (updates.length === 0) return res.status(400).json({ error: 'no fields to update' }) values.push(id) db.prepare(`UPDATE key_location SET ${updates.join(', ')} WHERE id = ?`).run(...values) db.prepare("INSERT OR REPLACE INTO situation (id, data, updated_at) VALUES (1, '{}', ?)").run(new Date().toISOString()) broadcastAfterEdit(req) res.json({ ok: true }) } catch (err) { console.error(err) res.status(500).json({ error: err.message }) } }) /** POST 新增事件脉络 */ router.post('/edit/situation-update', (req, res) => { try { const id = (req.body?.id || '').toString().trim() || `man_${Date.now()}_${Math.random().toString(36).slice(2, 9)}` const timestamp = (req.body?.timestamp || new Date().toISOString()).toString().trim() const category = (req.body?.category || 'other').toString().toLowerCase() const summary = (req.body?.summary || '').toString().trim().slice(0, 500) const severity = (req.body?.severity || 'medium').toString().toLowerCase() if (!summary) return res.status(400).json({ error: 'summary required' }) const validCat = ['deployment', 'alert', 'intel', 'diplomatic', 'other'].includes(category) ? category : 'other' const validSev = ['low', 'medium', 'high', 'critical'].includes(severity) ? severity : 'medium' db.prepare('INSERT OR REPLACE INTO situation_update (id, timestamp, category, summary, severity) VALUES (?, ?, ?, ?, ?)').run(id, timestamp, validCat, summary, validSev) db.prepare("INSERT OR REPLACE INTO situation (id, data, updated_at) VALUES (1, '{}', ?)").run(new Date().toISOString()) broadcastAfterEdit(req) res.json({ ok: true, id }) } catch (err) { console.error(err) res.status(500).json({ error: err.message }) } }) /** DELETE 删除一条事件脉络 */ router.delete('/edit/situation-update/:id', (req, res) => { try { const id = (req.params.id || '').toString().trim() if (!id) return res.status(400).json({ error: 'id required' }) const r = db.prepare('DELETE FROM situation_update WHERE id = ?').run(id) if (r.changes === 0) return res.status(404).json({ error: 'not found' }) db.prepare("INSERT OR REPLACE INTO situation (id, data, updated_at) VALUES (1, '{}', ?)").run(new Date().toISOString()) broadcastAfterEdit(req) res.json({ ok: true }) } catch (err) { console.error(err) res.status(500).json({ error: err.message }) } }) /** PUT 更新军力概要(美/伊) */ router.put('/edit/force-summary', (req, res) => { try { const side = req.body?.side if (side !== 'us' && side !== 'iran') { return res.status(400).json({ error: 'side must be us or iran' }) } const cols = ['total_assets', 'personnel', 'naval_ships', 'aircraft', 'ground_units', 'uav', 'missile_consumed', 'missile_stock'] const updates = [] const values = [] for (const c of cols) { if (req.body[c] !== undefined) { updates.push(`${c} = ?`) values.push(Number(req.body[c]) || 0) } } if (updates.length === 0) return res.status(400).json({ error: 'no fields to update' }) values.push(side) db.prepare(`UPDATE force_summary SET ${updates.join(', ')} WHERE side = ?`).run(...values) db.prepare("INSERT OR REPLACE INTO situation (id, data, updated_at) VALUES (1, '{}', ?)").run(new Date().toISOString()) broadcastAfterEdit(req) res.json({ ok: true }) } catch (err) { console.error(err) res.status(500).json({ error: err.message }) } }) module.exports = router