开始多平台 多店铺整改

This commit is contained in:
yml
2026-04-08 22:21:11 +08:00
parent d5b10e3f7f
commit a2b09c89e8
40 changed files with 1750 additions and 685 deletions
+27 -9
View File
@@ -1,3 +1,7 @@
import { logError, logWarn } from './logger.js'
const RESPONSE_TIME_ZONE = 'Asia/Shanghai'
export function buildSuccessPayload(data, msg = 'ok') {
return {
code: 0,
@@ -21,8 +25,12 @@ export function buildErrorPayload(error, fallbackMessage) {
export function sendRouteError(res, error, fallbackMessage, scope) {
const classification = classifyRouteError(error)
const logger = classification.statusCode >= 500 ? console.error : console.warn
logger(`${scope} ${fallbackMessage}:`, error)
const logger = classification.statusCode >= 500 ? logError : logWarn
logger(scope, fallbackMessage, {
statusCode: classification.statusCode,
errorCode: classification.errorCode,
error,
})
res.status(classification.statusCode).json(buildErrorPayload(error, fallbackMessage))
}
@@ -150,12 +158,22 @@ function formatResponseDateTime(value) {
return String(value || '').replace('T', ' ').replace('Z', '')
}
const year = date.getFullYear()
const month = String(date.getMonth() + 1).padStart(2, '0')
const day = String(date.getDate()).padStart(2, '0')
const hours = String(date.getHours()).padStart(2, '0')
const minutes = String(date.getMinutes()).padStart(2, '0')
const seconds = String(date.getSeconds()).padStart(2, '0')
const parts = new Intl.DateTimeFormat('zh-CN', {
timeZone: RESPONSE_TIME_ZONE,
hour12: false,
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
}).formatToParts(date)
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`
const tokens = Object.fromEntries(
parts
.filter((item) => item.type !== 'literal')
.map((item) => [item.type, item.value]),
)
return `${tokens.year}-${tokens.month}-${tokens.day} ${tokens.hour}:${tokens.minute}:${tokens.second}`
}
+134
View File
@@ -0,0 +1,134 @@
import { existsSync } from 'node:fs'
import fs from 'node:fs/promises'
import path from 'node:path'
import process from 'node:process'
import { PROJECT_ROOT, runtimeConfig } from '../config/runtime.js'
const DATA_ROOT = resolveDataRoot()
const LOG_DIR = path.join(DATA_ROOT, 'logs')
const APP_LOG_FILE = path.join(LOG_DIR, 'app.log')
const WEBHOOK_LOG_FILE = path.join(LOG_DIR, 'webhook.log')
let writeQueue = Promise.resolve()
export function createRequestId(prefix = 'req') {
return `${prefix}-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`
}
export function logDebug(scope, message, detail) {
return writeLog('debug', scope, message, detail)
}
export function logInfo(scope, message, detail) {
return writeLog('info', scope, message, detail)
}
export function logWarn(scope, message, detail) {
return writeLog('warn', scope, message, detail)
}
export function logError(scope, message, detail) {
return writeLog('error', scope, message, detail)
}
export function logWebhook(scope, message, detail, { level = 'info' } = {}) {
return writeLog(level, scope, message, detail, { channel: 'webhook' })
}
function writeLog(level, scope, message, detail, { channel = 'app' } = {}) {
const entry = {
time: new Date().toISOString(),
level,
scope: String(scope || 'app').trim() || 'app',
message: String(message || '').trim() || '-',
pid: process.pid,
detail: normalizeLogValue(detail),
}
writeConsole(entry)
enqueueFileWrite(entry, channel === 'webhook' ? WEBHOOK_LOG_FILE : APP_LOG_FILE)
return entry
}
function enqueueFileWrite(entry, filePath) {
const line = JSON.stringify(entry)
writeQueue = writeQueue
.then(async () => {
await fs.mkdir(LOG_DIR, { recursive: true })
await fs.appendFile(filePath, `${line}\n`, 'utf8')
})
.catch((error) => {
const reason = error instanceof Error ? error.message : String(error || '未知错误')
console.error('[logger] failed to write log file:', reason)
})
}
function writeConsole(entry) {
const prefix = `[${entry.time}] [${entry.level}] [${entry.scope}] ${entry.message}`
const logger = resolveConsoleMethod(entry.level)
if (typeof entry.detail === 'undefined') {
logger(prefix)
return
}
logger(prefix, entry.detail)
}
function resolveConsoleMethod(level) {
if (level === 'error') {
return console.error
}
if (level === 'warn') {
return console.warn
}
return console.log
}
function resolveDataRoot() {
const configuredDatabasePath = String(runtimeConfig.database?.filePath || '').trim()
if (configuredDatabasePath) {
const configuredDataRoot = path.dirname(configuredDatabasePath)
if (existsSync(configuredDataRoot)) {
return configuredDataRoot
}
}
return path.resolve(PROJECT_ROOT, 'data')
}
function normalizeLogValue(value) {
if (typeof value === 'undefined') {
return undefined
}
try {
return JSON.parse(
JSON.stringify(value, (_key, current) => {
if (current instanceof Error) {
return {
name: current.name,
message: current.message,
stack: current.stack,
statusCode: current.statusCode,
errorCode: current.errorCode,
}
}
if (typeof current === 'bigint') {
return String(current)
}
return current
}),
)
} catch {
return String(value)
}
}