Files
2026-09-29 01:14:24 +08:00

155 lines
5.1 KiB
JavaScript

// JSONL 后端:一行一条 JSON 追加写(改状态就追加新行),启动时读一遍建索引
// 量小、单进程够用;也是没配数据库时的默认后端,自测和回滚都靠它
const fs = require('fs')
const path = require('path')
const { newActivationCode: randomCode } = require('./code.js')
const DATA_DIR = process.env.DATA_DIR || path.join(__dirname, 'data')
const FILE = path.join(DATA_DIR, 'orders.jsonl')
const USERS_FILE = path.join(DATA_DIR, 'users.jsonl')
const SETTINGS_FILE = path.join(DATA_DIR, 'settings.json')
const orders = new Map() // outTradeNo -> 最新一条记录
const usedCodes = new Set()
const users = new Map() // openid -> 记录
const persistedSeen = new Map() // openid -> 最近一次落盘的活跃时间
function newActivationCode() {
for (;;) {
const code = randomCode()
if (!usedCodes.has(code)) return code
}
}
function load() {
orders.clear()
usedCodes.clear()
users.clear()
persistedSeen.clear()
if (fs.existsSync(FILE)) {
fs.readFileSync(FILE, 'utf8')
.split('\n')
.forEach((line) => {
if (!line.trim()) return
const record = JSON.parse(line)
orders.set(record.outTradeNo, record) // 同一单号,后写的覆盖先写的
if (record.code) usedCodes.add(record.code)
})
}
if (fs.existsSync(USERS_FILE)) {
fs.readFileSync(USERS_FILE, 'utf8')
.split('\n')
.forEach((line) => {
if (!line.trim()) return
const record = JSON.parse(line)
users.set(record.openid, record)
persistedSeen.set(record.openid, record.lastSeenAt)
})
}
}
load()
module.exports = {
backend: 'jsonl',
file: FILE,
ready: async () => {},
get: async (outTradeNo) => orders.get(outTradeNo) || null,
// 全量落一条(新建、改状态、迁移导入都走它)
saveOrder: async (record) => {
fs.mkdirSync(DATA_DIR, { recursive: true })
fs.appendFileSync(FILE, JSON.stringify(record) + '\n')
orders.set(record.outTradeNo, record)
if (record.code) usedCodes.add(record.code)
return record
},
// 发货:生成激活码并绑定;已经有码就直接返回(幂等)
assignCode: async (outTradeNo, extra) => {
const record = orders.get(outTradeNo)
if (!record) throw new Error('没有这笔业务单号:' + outTradeNo)
if (record.code) return record
return module.exports.saveOrder(
Object.assign(
{},
record,
{ status: 'delivered', code: newActivationCode(), deliveredAt: Date.now(), notified: false },
extra
)
)
},
markNotified: async (outTradeNo) => {
const record = orders.get(outTradeNo)
if (!record) return
await module.exports.saveOrder(Object.assign({}, record, { notified: true }))
},
markRefunded: async (outTradeNo, refundOrderId) => {
const record = orders.get(outTradeNo)
if (!record) return
await module.exports.saveOrder(Object.assign({}, record, { refunded: true, refundOrderId, refundedAt: Date.now() }))
},
listByOpenid: async (openid) =>
Array.from(orders.values())
.filter((o) => o.openid === openid && o.code && !o.refunded)
.sort((a, b) => b.deliveredAt - a.deliveredAt),
listPendingByOpenid: async (openid) =>
Array.from(orders.values())
.filter((o) => o.openid === openid && !o.refunded && (!o.code || !o.notified))
.sort((a, b) => b.createdAt - a.createdAt),
listPendingAll: async (since) =>
Array.from(orders.values())
.filter((o) => !o.refunded && (!o.code || !o.notified) && o.createdAt >= since)
.sort((a, b) => a.createdAt - b.createdAt),
listGoods: async () => [],
getUser: async (openid) => {
const record = users.get(openid)
return record ? { openid, createdAt: record.createdAt, lastSeenAt: record.lastSeenAt, isAdmin: Boolean(record.isAdmin) } : null
},
// 文件后端:想改文案就在 data/settings.json 里写 {"buy.title": "xx"}
getSettings: async () => {
if (!fs.existsSync(SETTINGS_FILE)) return {}
try {
return JSON.parse(fs.readFileSync(SETTINGS_FILE, 'utf8'))
} catch (err) {
console.error('settings.json 解析失败:', err.message)
return {}
}
},
touchUser: async (openid, firstSeenAt) => {
const now = Date.now()
const existed = users.get(openid)
const record = {
openid,
createdAt: existed ? existed.createdAt : firstSeenAt || now,
lastSeenAt: now,
isAdmin: existed ? existed.isAdmin : false,
}
users.set(openid, record)
// 活跃时间最多每小时落一次盘,免得文件被刷爆
if (!persistedSeen.has(openid) || now - persistedSeen.get(openid) > 3600000) {
fs.mkdirSync(DATA_DIR, { recursive: true })
fs.appendFileSync(USERS_FILE, JSON.stringify(record) + '\n')
persistedSeen.set(openid, now)
}
return record
},
// 核对用:订单数、激活码数、未完成的单
stats: async () => ({
orders: orders.size,
codes: Array.from(orders.values()).filter((o) => o.code).length,
pending: Array.from(orders.values()).filter((o) => !o.refunded && (!o.code || !o.notified)).length,
users: users.size,
}),
}