fix: update
This commit is contained in:
BIN
server/data.db
BIN
server/data.db
Binary file not shown.
28
server/db.js
28
server/db.js
@@ -7,6 +7,8 @@ const fs = require('fs')
|
||||
|
||||
const dbPath = process.env.DB_PATH || path.join(__dirname, 'data.db')
|
||||
let _db = null
|
||||
/** sql.js 构造函数,initDb 时注入,供 reloadFromFile 使用 */
|
||||
let _sqlJs = null
|
||||
|
||||
function getDb() {
|
||||
if (!_db) throw new Error('DB not initialized. Call initDb() first.')
|
||||
@@ -239,6 +241,7 @@ function runMigrations(db) {
|
||||
async function initDb() {
|
||||
const initSqlJs = require('sql.js')
|
||||
const SQL = await initSqlJs()
|
||||
_sqlJs = SQL
|
||||
let data = new Uint8Array(0)
|
||||
if (fs.existsSync(dbPath)) {
|
||||
data = new Uint8Array(fs.readFileSync(dbPath))
|
||||
@@ -261,6 +264,30 @@ async function initDb() {
|
||||
return _db
|
||||
}
|
||||
|
||||
/**
|
||||
* 从磁盘重新加载 DB(爬虫写入同一文件后调用,使 Node 内存中的库与文件一致)
|
||||
*/
|
||||
function reloadFromFile() {
|
||||
if (!_sqlJs || !_db) throw new Error('DB not initialized. Call initDb() first.')
|
||||
let data = new Uint8Array(0)
|
||||
if (fs.existsSync(dbPath)) {
|
||||
data = new Uint8Array(fs.readFileSync(dbPath))
|
||||
}
|
||||
const nativeDb = new _sqlJs.Database(data)
|
||||
function persist() {
|
||||
try {
|
||||
const buf = nativeDb.export()
|
||||
fs.writeFileSync(dbPath, Buffer.from(buf))
|
||||
} catch (e) {
|
||||
console.error('[db] persist error:', e.message)
|
||||
}
|
||||
}
|
||||
nativeDb.run('PRAGMA journal_mode = WAL')
|
||||
const wrapped = wrapDatabase(nativeDb, persist)
|
||||
runMigrations(wrapped)
|
||||
_db = wrapped
|
||||
}
|
||||
|
||||
const proxy = {
|
||||
prepare(sql) {
|
||||
return getDb().prepare(sql)
|
||||
@@ -276,3 +303,4 @@ const proxy = {
|
||||
module.exports = proxy
|
||||
module.exports.initDb = initDb
|
||||
module.exports.getDb = getDb
|
||||
module.exports.reloadFromFile = reloadFromFile
|
||||
|
||||
@@ -14,6 +14,9 @@ const openApiSpec = require('./openapi')
|
||||
const app = express()
|
||||
const PORT = process.env.API_PORT || 3001
|
||||
|
||||
// 爬虫通知用的共享密钥:API_CRAWLER_TOKEN(仅在服务端与爬虫进程间传递)
|
||||
const CRAWLER_TOKEN = process.env.API_CRAWLER_TOKEN || ''
|
||||
|
||||
app.set('trust proxy', 1)
|
||||
app.use(cors())
|
||||
app.use(express.json())
|
||||
@@ -23,7 +26,14 @@ app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(openApiSpec))
|
||||
|
||||
app.use('/api', routes)
|
||||
app.get('/api/health', (_, res) => res.json({ ok: true }))
|
||||
app.post('/api/crawler/notify', (_, res) => {
|
||||
app.post('/api/crawler/notify', (req, res) => {
|
||||
// 若配置了 API_CRAWLER_TOKEN,则要求爬虫携带 X-Crawler-Token 头
|
||||
if (CRAWLER_TOKEN) {
|
||||
const token = req.headers['x-crawler-token']
|
||||
if (typeof token !== 'string' || token !== CRAWLER_TOKEN) {
|
||||
return res.status(401).json({ error: 'unauthorized' })
|
||||
}
|
||||
}
|
||||
notifyCrawlerUpdate()
|
||||
res.json({ ok: true })
|
||||
})
|
||||
@@ -59,13 +69,18 @@ function broadcastSituation() {
|
||||
app.set('broadcastSituation', broadcastSituation)
|
||||
setInterval(broadcastSituation, 3000)
|
||||
|
||||
// 供爬虫调用:更新 situation.updated_at 并立即广播
|
||||
// 供爬虫调用:先从磁盘重载 DB(纳入爬虫写入),再更新 updated_at 并立即广播
|
||||
function notifyCrawlerUpdate() {
|
||||
try {
|
||||
const db = require('./db')
|
||||
db.reloadFromFile()
|
||||
db.prepare("INSERT OR REPLACE INTO situation (id, data, updated_at) VALUES (1, '{}', ?)").run(new Date().toISOString())
|
||||
broadcastSituation()
|
||||
} catch (_) {}
|
||||
const n = db.prepare('SELECT COUNT(*) as c FROM situation_update').get().c
|
||||
console.log('[crawler/notify] DB 已重载并广播,situation_update 条数:', n)
|
||||
} catch (e) {
|
||||
console.error('[crawler/notify]', e?.message || e)
|
||||
}
|
||||
}
|
||||
|
||||
db.initDb().then(() => {
|
||||
|
||||
@@ -5,8 +5,22 @@ const db = require('./db')
|
||||
|
||||
const router = express.Router()
|
||||
|
||||
// 数据库 Dashboard:返回各表原始数据
|
||||
router.get('/db/dashboard', (req, res) => {
|
||||
// 简单鉴权:通过环境变量配置的 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',
|
||||
@@ -58,8 +72,14 @@ router.get('/db/dashboard', (req, res) => {
|
||||
}
|
||||
})
|
||||
|
||||
// 资讯内容(独立表,供后续消费)
|
||||
// 资讯内容(独立表,供后续消费,可选 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)
|
||||
|
||||
Reference in New Issue
Block a user