import fs from 'fs' import { Tail } from 'tail' import { update, update_explorer } from './update.js' class 计数 { 日 = new Map() 周 = new Map() 月 = new Map() 年 = new Map() 总 = new Map() } class 类型 { 浏览数 = new 计数() 评论数 = new 计数() 点赞数 = new 计数() 收藏数 = new 计数() 搜索词 = new 计数() } const 文章 = new 类型() const 作品 = new 类型() const 游戏 = new 类型() const 截图 = new 类型() const 收藏 = new 类型() const 综合 = new 类型() // 获取浏览数 export function get_views(name, ids = []) { const all = { 文章, 作品, 游戏, 截图, 收藏, 综合 } const counts = all[name].浏览数.总 return ids.map(id => ({ id, count: counts.get(id) || 0 })) } // 获取当日热门搜索词 export function get_search(n = 10) { const ex = new Map() const all = { 文章, 作品, 游戏, 截图, 收藏, 综合 } for (const item of all) { item.搜索词.forEach((value, key) => { ex.set(key, (ex.get(key) || 0) + value) }) } return [...ex.entries()].sort((a, b) => b[1] - a[1]).slice(0, n) } const 日期格式 = new Intl.DateTimeFormat('zh-CN', { year: 'numeric', month: '2-digit', day: '2-digit' }) // 匹配文章和截图的正则表达式 const imagesRegex = /\/web\/v1\/images\/detail\/v2\?imagesId=(\d+)¤tUserId=(\d+)/ const articleRegex = /\/web\/v1\/article\/get\?id=(\d+)&userId=(\d+)/ const collectionRegex = /\/web\/v1\/member\/explorer\/article\/get\?id=(\d+)/ var 当前日期 = 日期格式.format(new Date()).replace(/\//g, '-') var 正在存档 = false const 存档 = async (message = '正在存档...') => { if (正在存档) return 正在存档 = true console.log(message) // 将今日浏览数并入总数 const all = { 文章, 作品, 游戏, 截图, 收藏, 综合 } for (const item of all) { item.浏览数.日.forEach((value, key) => { item.浏览数.总.set(key, (item.浏览数.总.get(key) || 0) + value) }) } // 数据写入日志 await fs.promises.writeFile(`./data/${当前日期}.json`, JSON.stringify({ 文章: Object.fromEntries(文章.浏览数.日), 截图: Object.fromEntries(截图.浏览数.日), 收藏: Object.fromEntries(收藏.浏览数.日), 文章搜索: Object.fromEntries(文章.搜索词.日), 截图搜索: Object.fromEntries(截图.搜索词.日), 收藏搜索: Object.fromEntries(收藏.搜索词.日), }, null, 2)) 正在存档 = false } export function main() { console.log(当前日期, '启动时加载全部数据...') fs.readdirSync('./data').filter(file => file.endsWith('.json')).map(name => { console.log('filename:', name) const file = fs.readFileSync(`./data/${name}`, 'utf8') if (file) { const data = JSON.parse(file) Object.entries(data.文章).forEach(([key, value]) => 文章.浏览数.总.set(key, value)) Object.entries(data.截图).forEach(([key, value]) => 截图.浏览数.总.set(key, value)) Object.entries(data.收藏).forEach(([key, value]) => 收藏.浏览数.总.set(key, value)) } }) console.log(当前日期, '启动时加载当日数据...') if (fs.existsSync(`./data/${当前日期}.json`)) { const file = fs.readFileSync(`./data/${当前日期}.json`, 'utf8') if (file) { const data = JSON.parse(file) Object.entries(data.文章).forEach(([key, value]) => 文章.浏览数.日.set(key, value)) Object.entries(data.截图).forEach(([key, value]) => 截图.浏览数.日.set(key, value)) Object.entries(data.收藏).forEach(([key, value]) => 收藏.浏览数.日.set(key, value)) Object.entries(data.文章搜索).forEach(([key, value]) => 文章.搜索词.日.set(key, value)) Object.entries(data.截图搜索).forEach(([key, value]) => 截图.搜索词.日.set(key, value)) Object.entries(data.收藏搜索).forEach(([key, value]) => 收藏.搜索词.日.set(key, value)) } } // 定时执行任务,确保上一次执行完毕后才开始下一轮 async function startScheduledTask() { while (true) { try { 存档('每10分钟自动存档...') await update('web_images', 'day_rank', Object.entries(Object.fromEntries(截图.浏览数.日))) await update('web_article', 'day_rank', Object.entries(Object.fromEntries(文章.浏览数.日))) await update_explorer('web_member_explorer', 'day_rank', Object.entries(Object.fromEntries(收藏.浏览数.日))) } catch (err) { console.error(err) } await new Promise(resolve => setTimeout(resolve, 10 * 60 * 1000)) } } // 启动定时任务 startScheduledTask() console.log(当前日期, '开始收集日志...') const tail = new Tail('/opt/log/caddy/access.log') tail.on('line', async (line) => { const item = JSON.parse(line) if (item.level !== 'debug') return if (item.msg !== 'upstream roundtrip') return // 检查日志的日期是否跨天 const logDate = 日期格式.format(new Date(item.ts * 1000)).replace(/\//g, '-') if (当前日期 !== logDate) { await 存档('跨日期存档...') 文章.浏览数.日.clear() 截图.浏览数.日.clear() 收藏.浏览数.日.clear() 文章.搜索词.日.clear() 截图.搜索词.日.clear() 收藏.搜索词.日.clear() 当前日期 = logDate } // 处理文章日志 if (item.request.uri.startsWith('/web/v1/article/get')) { const [uri, id] = item.request.uri.match(articleRegex) ?? [] if (uri && id) { 文章.浏览数.日.set(id, 文章.浏览数.日.has(id) ? 文章.浏览数.日.get(id) + 1 : 1) } } // 处理截图日志 if (item.request.uri.startsWith('/web/v1/images/detail/v2')) { const [uri, id] = item.request.uri.match(imagesRegex) ?? [] if (uri && id) { 截图.浏览数.日.set(id, 截图.浏览数.日.has(id) ? 截图.浏览数.日.get(id) + 1 : 1) } return } // 处理收藏日志 if (item.request.uri.startsWith('/web/v1/member/explorer/article/get')) { const [uri, id] = item.request.uri.match(collectionRegex) ?? [] if (uri && id) { 收藏.浏览数.日.set(id, 收藏.浏览数.日.has(id) ? 收藏.浏览数.日.get(id) + 1 : 1) } return } // 处理搜索日志 if (item.request.uri.startsWith('/api?query=')) { console.log(item.request.uri) return } }) // 日志尾部事件监听 tail.on('end', () => console.log('没有更多内容,停止读取')) tail.on('error', (error) => console.error('错误:', error)) // 捕获退出信号,确保数据存档 process.on('SIGINT', async () => { tail.unwatch() // 停止监听日志 await 存档('退出前存档数据...') process.exit() }) }