小程序接入支付功能
This commit is contained in:
1 parent
edf1e51846
commit
07a90e860d
38 files changed
+3644
-90
No files matched your search
@@ -0,0 +1,154 @@
|
||||
// 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,
|
||||
}),
|
||||
}
|
||||
Reference in new issue
Block a user