155 lines
5.1 KiB
JavaScript
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,
|
|
}),
|
|
}
|