小程序接入支付功能

This commit is contained in:
Yeomai committed 2026-09-29 01:14:24 +08:00
1 parent edf1e51846
commit 07a90e860d
38 files changed
+3644 -90

No files matched your search

+457
View File
@@ -0,0 +1,457 @@
// 用法:node server/test.js
// 1) 纯函数断言(HMAC 官方向量、价目表、signData 字段、单号格式、激活码格式)
// 2) 起一个假的微信接口 + 真的 server/index.js,走一遍下单、发货、我的
const assert = require('assert')
const crypto = require('crypto')
const fs = require('fs')
const http = require('http')
const os = require('os')
const path = require('path')
const { spawn } = require('child_process')
const { GOODS, hmacSha256, newOutTradeNo, buildSignData, paySig } = require('./sign.js')
const { newActivationCode } = require('./code.js')
const APP_KEY = 'test-appkey'
const SANDBOX_APP_KEY = 'test-sandbox-appkey'
const OFFER_ID = '1450663371'
const SESSION_KEY = 'sk-test'
const OPENID = 'oTestUser'
const PUSH_TOKEN = 'test-push-token'
// ---------- 1. 纯函数 ----------
assert.strictEqual(
hmacSha256(Buffer.alloc(20, 0x0b), 'Hi There'), // RFC 4231 官方向量
'b0344c61d8db38535ca8afceaf0bf12b881dc200c9833da726e9376c2e32cff7'
)
assert.deepStrictEqual(
{ 90001: GOODS[90001].price, 10001: GOODS[10001].price, 10002: GOODS[10002].price, 10000: GOODS[10000].price },
{ 90001: 10, 10001: 3900, 10002: 6900, 10000: 9900 }
)
const sample = buildSignData({ offerId: OFFER_ID, productId: 10000, price: 9900, outTradeNo: 'T1700ABCDEF', attach: 'test-10000' })
assert.deepStrictEqual(JSON.parse(sample), {
offerId: OFFER_ID,
buyQuantity: 1,
env: 0,
currencyType: 'CNY',
productId: '10000',
goodsPrice: 9900,
outTradeNo: 'T1700ABCDEF',
attach: 'test-10000',
})
assert.notStrictEqual(
paySig(APP_KEY, 'requestVirtualPayment', sample),
paySig(APP_KEY, 'requestVirtualPayment', sample + ' '),
'签名必须认原文'
)
for (let i = 0; i < 500; i++) {
const no = newOutTradeNo(1700000000000 + i)
assert.ok(no.length >= 8 && no.length <= 32, '单号长度越界: ' + no)
assert.ok(/^[0-9A-Za-z_\-|*@]+$/.test(no), '单号含非法字符: ' + no)
assert.ok(!no.startsWith('_'), '单号不能以下划线开头: ' + no)
}
// 激活码:4 组 4 位、不含容易看错的 I/L/O/U、不重复
const codes = new Set()
for (let i = 0; i < 1000; i++) {
const made = newActivationCode()
assert.ok(/^[0-9A-HJKMNP-TV-Z]{4}(-[0-9A-HJKMNP-TV-Z]{4}){3}$/.test(made), '激活码格式不对: ' + made)
codes.add(made)
}
assert.strictEqual(codes.size, 1000, '激活码不应重复')
// ---------- 2. 起假微信接口 ----------
const seen = { notifyCalls: 0, unpaid: null, failNotify: false }
const fakeWx = http.createServer((req, res) => {
const [urlPath, query] = req.url.split('?')
let raw = ''
req.on('data', (chunk) => (raw += chunk))
req.on('end', () => {
res.setHeader('Content-Type', 'application/json')
const reply = (obj) => res.end(JSON.stringify(obj))
if (urlPath === '/sns/jscode2session') {
const jsCode = new URLSearchParams(query).get('js_code')
seen.code = jsCode
return reply({ openid: jsCode === 'code-other' ? 'oOtherUser' : OPENID, session_key: SESSION_KEY })
}
if (urlPath === '/cgi-bin/token') return reply({ access_token: 'tok-test', expires_in: 7200 })
if (urlPath === '/xpay/query_order') {
seen.queryBody = raw
seen.querySig = new URLSearchParams(query).get('pay_sig')
seen.queryToken = new URLSearchParams(query).get('access_token')
const { order_id: orderId, env } = JSON.parse(raw)
const status = orderId === seen.unpaid ? 1 : 2 // 1 = 已创建未支付
return reply({
errcode: 0,
errmsg: 'ok',
order: { order_id: orderId, status, paid_fee: 10, left_fee: 10, wx_order_id: `wx-${env}-${orderId}` },
})
}
if (urlPath === '/xpay/notify_provide_goods') {
seen.notifyCalls++
seen.notifyBody = raw
seen.notifyToken = new URLSearchParams(query).get('access_token')
seen.notifySig = new URLSearchParams(query).get('pay_sig')
if (seen.failNotify) return reply({ errcode: 44002, errmsg: 'empty post data' })
return reply({ errcode: 0, errmsg: 'ok' })
}
if (urlPath === '/xpay/refund_order') {
seen.refundBody = raw
seen.refundSig = new URLSearchParams(query).get('pay_sig')
return reply({ errcode: 0, errmsg: 'ok', refund_order_id: JSON.parse(raw).refund_order_id })
}
res.statusCode = 404
reply({ errcode: -1, errmsg: 'no such path: ' + urlPath })
})
})
function listen(server) {
return new Promise((resolve) => server.listen(0, '127.0.0.1', () => resolve(server.address().port)))
}
function fetchJson(port, urlPath, payload) {
return new Promise((resolve, reject) => {
const body = payload === undefined ? null : JSON.stringify(payload)
const req = http.request(
{ host: '127.0.0.1', port, path: urlPath, method: body ? 'POST' : 'GET', headers: body ? { 'Content-Type': 'application/json' } : {} },
(res) => {
let raw = ''
res.on('data', (chunk) => (raw += chunk))
res.on('end', () => resolve(JSON.parse(raw)))
}
)
req.on('error', reject)
req.end(body || undefined)
})
}
// 需要看原始响应的场合(推送接口回的是 XML / 纯文本)
function rawRequest(port, urlPath, options) {
const opts = options || {}
return new Promise((resolve, reject) => {
const req = http.request(
{ host: '127.0.0.1', port, path: urlPath, method: opts.method || (opts.body ? 'POST' : 'GET') },
(res) => {
let raw = ''
res.on('data', (chunk) => (raw += chunk))
res.on('end', () => resolve({ status: res.statusCode, body: raw }))
}
)
req.on('error', reject)
req.end(opts.body || undefined)
})
}
// 消息推送的签名:sha1(sort([token, timestamp, nonce]).join(''))
function pushQuery(token) {
const timestamp = '1700000000'
const nonce = 'abc123'
const signature = crypto.createHash('sha1').update([token || PUSH_TOKEN, timestamp, nonce].sort().join('')).digest('hex')
return `?signature=${signature}&timestamp=${timestamp}&nonce=${nonce}`
}
function startServer(env) {
return new Promise((resolve) => {
const child = spawn(process.execPath, [path.join(__dirname, 'index.js')], {
env: Object.assign({}, process.env, env),
stdio: ['ignore', 'pipe', 'inherit'],
})
let out = ''
child.stdout.on('data', (chunk) => {
out += chunk
const port = (out.match(/http:\/\/localhost:(\d+)/) || [])[1]
if (port) resolve({ port: Number(port), child })
})
})
}
// 轮询等某个单号出现激活码(定时补发货是异步的)
async function waitForCode(port, outTradeNo, timeoutMs) {
const deadline = Date.now() + timeoutMs
while (Date.now() < deadline) {
const mine = await fetchJson(port, '/pay/my', { code: 'code-1' })
const hit = mine.list.find((item) => item.outTradeNo === outTradeNo)
if (hit) return hit
await new Promise((resolve) => setTimeout(resolve, 100))
}
return null
}
async function main() {
const wxPort = await listen(fakeWx)
const dataDir = fs.mkdtempSync(path.join(os.tmpdir(), 'xpay-data-'))
// 先放一条管理员记录:沙箱、补单道具、退款这些管理员功能要靠它
fs.writeFileSync(
path.join(dataDir, 'users.jsonl'),
JSON.stringify({ openid: OPENID, createdAt: 1700000000000, lastSeenAt: 1700000000000, isAdmin: true }) + '\n'
)
// 文案配置:只覆盖标题,其它项应该退回代码里的默认值
fs.writeFileSync(path.join(dataDir, 'settings.json'), JSON.stringify({ 'buy.title': '限时特惠', 'buy.button': '立即开通' }))
const serverEnv = {
PORT: '0',
PAY_POLL_MS: '0', // 主流程把定时任务关掉,免得跟断言的时序打架
PAY_STORE: 'jsonl', // 自测一律用文件后端,绝不碰你的数据库
WX_API_ORIGIN: `http://127.0.0.1:${wxPort}`,
DATA_DIR: dataDir,
PAY_APP_ID: 'wxtest',
PAY_APP_SECRET: 'secret-test',
PAY_OFFER_ID: OFFER_ID,
PAY_APP_KEY: APP_KEY,
PAY_SANDBOX_APP_KEY: SANDBOX_APP_KEY,
PAY_PUSH_TOKEN: PUSH_TOKEN,
}
const { port, child } = await startServer(serverEnv)
try {
assert.deepStrictEqual(await fetchJson(port, '/health'), {
ok: true,
missing: [],
sandbox: true,
goods: ['90001', '10001', '10002', '10000'], // 价格从低到高
store: 'jsonl',
routes: ['/pay/order', '/pay/deliver', '/pay/my', '/pay/pending', '/pay/refund', '/pay/me'],
})
// 下单:签名要能被独立重算出来
const paid = await fetchJson(port, '/pay/order', { productId: '10000', code: 'code-1' })
assert.strictEqual(paid.ok, true, JSON.stringify(paid))
assert.strictEqual(seen.code, 'code-1')
const { signData, paySig: gotPaySig, signature, mode } = paid.payData
assert.strictEqual(mode, 'short_series_goods')
assert.strictEqual(gotPaySig, paySig(APP_KEY, 'requestVirtualPayment', signData))
assert.strictEqual(signature, hmacSha256(SESSION_KEY, signData))
const parsed = JSON.parse(signData)
assert.strictEqual(parsed.offerId, OFFER_ID)
assert.strictEqual(parsed.productId, '10000')
assert.strictEqual(parsed.goodsPrice, 9900)
assert.strictEqual(parsed.env, 0)
assert.strictEqual(gotPaySig, paySig(APP_KEY, 'requestVirtualPayment', signData))
assert.strictEqual(paid.outTradeNo, parsed.outTradeNo)
// 页面重进后能找回还没发货的单(补发货用)
const pending = await fetchJson(port, '/pay/pending', { code: 'code-1' })
assert.strictEqual(pending.outTradeNo, paid.outTradeNo)
// 半路放弃、根本没付款的单,不该被当成「待发货」提示
seen.unpaid = paid.outTradeNo
assert.deepStrictEqual(await fetchJson(port, '/pay/pending', { code: 'code-1' }), { ok: true, outTradeNo: '' })
seen.unpaid = null
assert.deepStrictEqual(await fetchJson(port, '/pay/pending', { code: 'code-other' }), { ok: true, outTradeNo: '' })
// 沙箱单:env=1、用沙箱 AppKey 签名,和现网签名不能一样
const sandbox = await fetchJson(port, '/pay/order', { productId: '90001', code: 'code-1', env: 1 })
assert.strictEqual(sandbox.ok, true, JSON.stringify(sandbox))
const sandboxSignData = sandbox.payData.signData
assert.strictEqual(JSON.parse(sandboxSignData).env, 1)
assert.strictEqual(JSON.parse(sandboxSignData).goodsPrice, 10)
assert.strictEqual(sandbox.payData.paySig, paySig(SANDBOX_APP_KEY, 'requestVirtualPayment', sandboxSignData))
assert.notStrictEqual(sandbox.payData.paySig, paySig(APP_KEY, 'requestVirtualPayment', sandboxSignData))
// 前端万一把 env 传成字符串 '1',也必须按沙箱签,绝不能悄悄走现网真扣钱
const stringEnv = await fetchJson(port, '/pay/order', { productId: '90001', code: 'code-1', env: '1' })
assert.strictEqual(JSON.parse(stringEnv.payData.signData).env, 1)
assert.strictEqual(stringEnv.payData.paySig, paySig(SANDBOX_APP_KEY, 'requestVirtualPayment', stringEnv.payData.signData))
// 未发布的道具 ID 不该能下单
const bad = await fetchJson(port, '/pay/order', { productId: '12345', code: 'code-1' })
assert.strictEqual(bad.ok, false)
assert.ok(/未知道具/.test(bad.error), bad.error)
// 我是谁 + 我能看到哪些道具:管理员才多看到「补单专用」(0.1 元测试价)
const me = await fetchJson(port, '/pay/me', { code: 'code-1' })
assert.strictEqual(me.isAdmin, true)
assert.deepStrictEqual(
me.goods.map((item) => [item.productId, item.price]),
[
['90001', 10],
['10001', 3900],
['10002', 6900],
['10000', 9900],
]
)
assert.ok(me.goods.find((item) => item.productId === '90001').note, '备注文案要能带回小程序')
// 页面文案:库里有就用库里的,没有就退回默认值
assert.strictEqual(me.copy['buy.title'], '限时特惠')
assert.strictEqual(me.copy['buy.button'], '立即开通') // 库里的覆盖生效
assert.strictEqual(me.copy['tab.buy'], '开通')
const normalMe = await fetchJson(port, '/pay/me', { code: 'code-other' })
assert.strictEqual(normalMe.isAdmin, false)
assert.ok(!normalMe.goods.some((item) => item.productId === '90001'), '普通用户不该看到补单专用')
// 普通用户直接调接口也绕不过去
const sneakyGoods = await fetchJson(port, '/pay/order', { productId: '90001', code: 'code-other' })
assert.strictEqual(sneakyGoods.ok, false)
assert.ok(/管理员/.test(sneakyGoods.error), sneakyGoods.error)
const sneakySandbox = await fetchJson(port, '/pay/order', { productId: '10001', code: 'code-other', env: 1 })
assert.strictEqual(sneakySandbox.ok, false)
assert.ok(/沙箱/.test(sneakySandbox.error), sneakySandbox.error)
// 查单该用 pay_sig + access_token
seen.unpaid = paid.outTradeNo
const unpaid = await fetchJson(port, '/pay/deliver', { outTradeNo: paid.outTradeNo, code: 'code-1' })
assert.strictEqual(unpaid.ok, true)
assert.strictEqual(unpaid.paid, false, '没付款就不能发货')
assert.strictEqual(unpaid.code, undefined)
assert.strictEqual(seen.notifyCalls, 0, '没付款不该通知平台发货')
assert.deepStrictEqual(JSON.parse(seen.queryBody), { openid: OPENID, env: 0, order_id: paid.outTradeNo })
assert.strictEqual(seen.querySig, paySig(APP_KEY, '/xpay/query_order', seen.queryBody))
assert.strictEqual(seen.queryToken, 'tok-test')
// 付款后发货:发一张码 + 通知平台
seen.unpaid = null
const delivered = await fetchJson(port, '/pay/deliver', { outTradeNo: paid.outTradeNo, code: 'code-1' })
assert.strictEqual(delivered.paid, true)
assert.ok(/^[0-9A-HJKMNP-TV-Z]{4}(-[0-9A-HJKMNP-TV-Z]{4}){3}$/.test(delivered.code), delivered.code)
assert.strictEqual(delivered.productName, '永久激活码')
assert.strictEqual(delivered.notified, true)
assert.deepStrictEqual(JSON.parse(seen.notifyBody), { order_id: paid.outTradeNo, wx_order_id: 'wx-0-' + paid.outTradeNo, env: 0 })
assert.strictEqual(seen.notifyToken, 'tok-test')
assert.strictEqual(seen.notifyCalls, 1)
// 平台要求 pay_sig,缺了会回 268490002
assert.strictEqual(seen.notifySig, paySig(APP_KEY, '/xpay/notify_provide_goods', seen.notifyBody))
// 幂等:同一单再点一次,还是同一张码,也不再重复通知平台
const again = await fetchJson(port, '/pay/deliver', { outTradeNo: paid.outTradeNo, code: 'code-1' })
assert.strictEqual(again.code, delivered.code)
assert.strictEqual(seen.notifyCalls, 1)
// 别人的单不能领
const stolen = await fetchJson(port, '/pay/deliver', { outTradeNo: paid.outTradeNo, code: 'code-other' })
assert.strictEqual(stolen.ok, false)
assert.ok(/不属于当前用户/.test(stolen.error), stolen.error)
// 我的:本人能看到,别人看不到
const sandboxDelivered = await fetchJson(port, '/pay/deliver', { outTradeNo: sandbox.outTradeNo, code: 'code-1' })
assert.strictEqual(sandboxDelivered.paid, true, JSON.stringify(sandboxDelivered))
assert.deepStrictEqual(JSON.parse(seen.queryBody), { openid: OPENID, env: 1, order_id: sandbox.outTradeNo })
assert.strictEqual(seen.querySig, paySig(SANDBOX_APP_KEY, '/xpay/query_order', seen.queryBody))
// 沙箱单不通知平台:通知次数还停在前面的现网单上
assert.strictEqual(seen.notifyCalls, 1, '沙箱单不该调 notify_provide_goods')
assert.strictEqual(sandboxDelivered.notified, true)
assert.strictEqual(sandboxDelivered.productName, '补单专用')
const mine = await fetchJson(port, '/pay/my', { code: 'code-1' })
assert.deepStrictEqual(
mine.list.map((item) => [item.code, item.productName]).sort(),
[
[delivered.code, '永久激活码'],
[sandboxDelivered.code, '补单专用'],
].sort()
)
const others = await fetchJson(port, '/pay/my', { code: 'code-other' })
assert.deepStrictEqual(others.list, [])
// 发过货的单不再出现在待发货里
const pendingAfter = await fetchJson(port, '/pay/pending', { code: 'code-1' })
assert.notStrictEqual(pendingAfter.outTradeNo, paid.outTradeNo)
assert.notStrictEqual(pendingAfter.outTradeNo, sandbox.outTradeNo)
// 平台拒绝通知时:码照样给用户,但要把平台原话带回去,方便直接在手机上看到原因
// 先把试探 env 的那笔也发掉,保证后面只剩一笔未完成单,断言不被时序搅浑
await fetchJson(port, '/pay/deliver', { outTradeNo: stringEnv.outTradeNo, code: 'code-1' })
const failOrder = await fetchJson(port, '/pay/order', { productId: '10001', code: 'code-1' })
seen.failNotify = true
const failed = await fetchJson(port, '/pay/deliver', { outTradeNo: failOrder.outTradeNo, code: 'code-1' })
assert.strictEqual(failed.paid, true)
assert.ok(failed.code, '平台通知失败也要把激活码给用户')
assert.strictEqual(failed.notified, false)
assert.ok(/44002/.test(failed.notifyError), failed.notifyError)
seen.failNotify = false
// 码发了但平台没确认的单,要能找回来继续重试通知
const pendingRetry = await fetchJson(port, '/pay/pending', { code: 'code-1' })
assert.strictEqual(pendingRetry.outTradeNo, failOrder.outTradeNo)
const retried = await fetchJson(port, '/pay/deliver', { outTradeNo: failOrder.outTradeNo, code: 'code-1' })
assert.strictEqual(retried.code, failed.code, '重试要给回同一张码')
assert.strictEqual(retried.notified, true)
// 通知成功后就不再提示了
assert.deepStrictEqual(await fetchJson(port, '/pay/pending', { code: 'code-1' }), { ok: true, outTradeNo: '' })
// ---------- 发货推送 ----------
// 配置 URL 时的 GET 校验:签名对就原样回 echostr,签名错要 403
assert.strictEqual((await rawRequest(port, '/pay/notify' + pushQuery() + '&echostr=hello-123')).body, 'hello-123')
assert.strictEqual((await rawRequest(port, '/pay/notify' + pushQuery('wrong-token') + '&echostr=hello')).status, 403)
const pushOrder = await fetchJson(port, '/pay/order', { productId: '10002', code: 'code-1' })
const pushXml =
'<xml>' +
'<ToUserName><![CDATA[gh_test]]></ToUserName>' +
'<MsgType><![CDATA[event]]></MsgType>' +
'<Event><![CDATA[xpay_goods_deliver_notify]]></Event>' +
`<OpenId><![CDATA[${OPENID}]]></OpenId>` +
`<OutTradeNo><![CDATA[${pushOrder.outTradeNo}]]></OutTradeNo>` +
'<Env>0</Env>' +
'<WeChatPayInfo><MchOrderNo><![CDATA[wx-push-1]]></MchOrderNo></WeChatPayInfo>' +
'<GoodsInfo><ProductId><![CDATA[10002]]></ProductId><Quantity>1</Quantity></GoodsInfo>' +
'</xml>'
const notifyCallsBeforePush = seen.notifyCalls
const pushRes = await rawRequest(port, '/pay/notify' + pushQuery(), { body: pushXml, method: 'POST' })
assert.ok(/<ErrCode>0<\/ErrCode>/.test(pushRes.body), pushRes.body)
const afterPush = await fetchJson(port, '/pay/my', { code: 'code-1' })
const pushCode = afterPush.list.find((item) => item.outTradeNo === pushOrder.outTradeNo)
assert.ok(pushCode && pushCode.productName === '高级激活码', '推送应该把货发出来')
// 推送已经回了 0,就不该再去调 notify_provide_goods
assert.strictEqual(seen.notifyCalls, notifyCallsBeforePush, '推送场景不用再手动通知平台')
// 同一个单再推一次:幂等,码不变
assert.ok(/<ErrCode>0<\/ErrCode>/.test((await rawRequest(port, '/pay/notify' + pushQuery(), { body: pushXml, method: 'POST' })).body))
const afterSecondPush = await fetchJson(port, '/pay/my', { code: 'code-1' })
assert.strictEqual(afterSecondPush.list.find((item) => item.outTradeNo === pushOrder.outTradeNo).code, pushCode.code)
// ---------- 退款 ----------
const refunded = await fetchJson(port, '/pay/refund', { outTradeNo: pushOrder.outTradeNo, code: 'code-1' })
assert.strictEqual(refunded.ok, true, JSON.stringify(refunded))
const refundBody = JSON.parse(seen.refundBody)
assert.strictEqual(refundBody.order_id, pushOrder.outTradeNo)
assert.strictEqual(refundBody.left_fee, 10)
assert.strictEqual(refundBody.refund_fee, 10)
assert.strictEqual(refundBody.env, 0)
assert.ok(/^[\w-]{8,32}$/.test(refundBody.refund_order_id), refundBody.refund_order_id)
assert.strictEqual(seen.refundSig, paySig(APP_KEY, '/xpay/refund_order', seen.refundBody))
// 退完就不在「我的」里了,也不能重复退
const afterRefund = await fetchJson(port, '/pay/my', { code: 'code-1' })
assert.ok(!afterRefund.list.some((item) => item.outTradeNo === pushOrder.outTradeNo), '退款后激活码要作废')
assert.strictEqual((await fetchJson(port, '/pay/refund', { outTradeNo: pushOrder.outTradeNo, code: 'code-1' })).ok, false)
// 普通用户能正常买单,但退款是管理员功能
const normalOrder = await fetchJson(port, '/pay/order', { productId: '10001', code: 'code-other' })
assert.strictEqual(normalOrder.ok, true, JSON.stringify(normalOrder))
await fetchJson(port, '/pay/deliver', { outTradeNo: normalOrder.outTradeNo, code: 'code-other' })
const normalRefund = await fetchJson(port, '/pay/refund', { outTradeNo: normalOrder.outTradeNo, code: 'code-other' })
assert.strictEqual(normalRefund.ok, false)
assert.ok(/管理员/.test(normalRefund.error), normalRefund.error)
// ---------- 定时查单补发货 ----------
const cronDataDir = fs.mkdtempSync(path.join(os.tmpdir(), 'xpay-cron-'))
const cron = await startServer(Object.assign({}, serverEnv, { DATA_DIR: cronDataDir, PAY_POLL_MS: '200' }))
try {
const cronOrder = await fetchJson(cron.port, '/pay/order', { productId: '10001', code: 'code-1' })
const autoCode = await waitForCode(cron.port, cronOrder.outTradeNo, 5000)
assert.ok(autoCode, '付了钱但没进页面,定时任务也应该自动发货')
} finally {
cron.child.kill()
fs.rmSync(cronDataDir, { recursive: true, force: true })
}
// 落盘了:重启也能恢复
assert.ok(fs.existsSync(path.join(dataDir, 'orders.jsonl')), '订单文件没写出来')
// 用户表:首次使用会建行;这条是我们预置的管理员,标志要保持 true 不被覆盖
const userRows = fs
.readFileSync(path.join(dataDir, 'users.jsonl'), 'utf8')
.trim()
.split('\n')
.map((line) => JSON.parse(line))
assert.strictEqual(userRows[0].openid, OPENID)
assert.strictEqual(userRows[0].isAdmin, true)
assert.ok(userRows[0].createdAt > 0, '首次使用时间没记上')
console.log('服务端测试通过:下单签名、查单、发货幂等、归属校验、我的列表、发货推送、退款、定时补发货')
} finally {
child.kill()
fakeWx.close()
fs.rmSync(dataDir, { recursive: true, force: true })
}
}
main().catch((err) => {
console.error('测试失败:', err.message)
process.exit(1)
})