feat: M0-M2 骨架落地(服务端/前端/宿主/数据库/积分引擎)
- server: Go 单二进制骨架,embed 内嵌前端,配置/迁移/HTTP 路由 - store: system.db + event.db 两套迁移,业务表 + 种子数据 - domain: 积分引擎(并列三口径/计分通道/流水/排名/破纪录)+ 68 个测试 - web: Vite + Vue 三入口骨架 + shared 层(类型/API/WebSocket 客户端) - console: Electron 宿主骨架(选屏/防熄屏/进程守护/托盘)
This commit is contained in:
1 parent
a61d793d79
commit
7d5d94f815
76 files changed
+10131
No files matched your search
@@ -0,0 +1,79 @@
|
||||
'use strict'
|
||||
|
||||
const fs = require('fs')
|
||||
const path = require('path')
|
||||
|
||||
// 日志总线:内存环形缓冲供管理窗口实时查看,同时落一份文件供现场排障。
|
||||
// 现场没有外网也没有终端,文件日志是事后定位问题的唯一线索。
|
||||
class LogBus {
|
||||
constructor(options = {}) {
|
||||
const { maxLines = 2000, file = null, maxFileBytes = 4 * 1024 * 1024 } = options
|
||||
this.maxLines = maxLines
|
||||
this.maxFileBytes = maxFileBytes
|
||||
this.lines = []
|
||||
this.listeners = new Set()
|
||||
// 子进程的输出是流式分片的,可能把一行拆到两次 data 事件里
|
||||
this.pending = new Map()
|
||||
this.stream = null
|
||||
if (file) this.openFile(file)
|
||||
}
|
||||
|
||||
openFile(file) {
|
||||
try {
|
||||
fs.mkdirSync(path.dirname(file), { recursive: true })
|
||||
if (fs.existsSync(file) && fs.statSync(file).size > this.maxFileBytes) {
|
||||
fs.renameSync(file, `${file}.1`)
|
||||
}
|
||||
this.stream = fs.createWriteStream(file, { flags: 'a' })
|
||||
} catch (err) {
|
||||
this.stream = null
|
||||
this.push('console', `日志文件不可写,仅保留内存日志:${err.message}`)
|
||||
}
|
||||
}
|
||||
|
||||
append(source, chunk) {
|
||||
const text = `${this.pending.get(source) || ''}${String(chunk)}`
|
||||
const parts = text.split(/\r?\n/)
|
||||
// 最后一段是没有换行的残行,留到下一次拼接
|
||||
this.pending.set(source, parts.pop() || '')
|
||||
for (const part of parts) this.push(source, part)
|
||||
}
|
||||
|
||||
push(source, message) {
|
||||
const line = `[${new Date().toISOString()}] [${source}] ${message}`
|
||||
this.lines.push(line)
|
||||
if (this.lines.length > this.maxLines) {
|
||||
this.lines.splice(0, this.lines.length - this.maxLines)
|
||||
}
|
||||
if (this.stream) this.stream.write(`${line}\n`)
|
||||
for (const listener of this.listeners) {
|
||||
try {
|
||||
listener(line)
|
||||
} catch {
|
||||
// 单个订阅者出错不能拖垮日志链路
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tail() {
|
||||
return this.lines.slice()
|
||||
}
|
||||
|
||||
clear() {
|
||||
this.lines = []
|
||||
}
|
||||
|
||||
onLine(listener) {
|
||||
this.listeners.add(listener)
|
||||
return () => this.listeners.delete(listener)
|
||||
}
|
||||
|
||||
close() {
|
||||
if (this.stream) {
|
||||
this.stream.end()
|
||||
this.stream = null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { LogBus }
|
||||
Reference in new issue
Block a user