import test from 'node:test'; import assert from 'node:assert/strict'; import fs from 'node:fs'; import vm from 'node:vm'; import config from '../src/config/index.js'; import { fulfillWechatServiceOrder, MiaoguoWechatServicePayLogin500, MiaoguoWechatServicePayNotify500, MiaoguoWechatServicePayOrderStatus500 } from '../src/api/wechatServicePay/wechatServicePayController.js'; import { buildV2Sign, buildV2Xml, parseV2Xml } from '../src/api/wechatServicePay/wechatPayV2.js'; function signed(fields) { const result = { ...fields }; result.sign = buildV2Sign(result, config.wx.payapisecret); return result; } function createConnection({ order, serviceUser, miaoguoUsers = [] }) { const state = { began: 0, committed: 0, rolledBack: 0, released: 0, queries: [] }; const connection = { async beginTransaction() { state.began += 1; }, async commit() { state.committed += 1; }, async rollback() { state.rolledBack += 1; }, release() { state.released += 1; }, async query(sql, params) { state.queries.push({ sql, params }); if (sql.includes('FROM ProductPayInfo')) return [[order]]; if (sql.includes('FROM WechatServiceWXUsers')) return [[serviceUser]]; if (sql.includes('FROM MiaoguoWXUsers')) return [miaoguoUsers]; return [{ affectedRows: 1 }]; } }; return { connection, state }; } function pendingOrder(overrides = {}) { return { ID: 501, TradeNo: 'WSP178784640012345678', ProductID: 167, PayType: 8, BuyType: 131, OpenID: 'service-openid', Money: 100, Status: 0, ...overrides }; } const serviceUser = { UserID: 88, OpenID: 'service-openid', UnionID: 'union-test' }; const paidResult = { openid: 'service-openid', totalFee: 100, transactionId: '420000000020260828000000001', timeEnd: '20260828120000', raw: { source: 'test' } }; test('服务号支付完成在一个事务中写完整订单并发放 16 天试用', async () => { const { connection, state } = createConnection({ order: pendingOrder(), serviceUser, miaoguoUsers: [{ UserID: 1, UnionID: 'union-test' }] }); const postActions = []; const result = await fulfillWechatServiceOrder(pendingOrder().TradeNo, paidResult, { getConnection: async () => connection, schedulePostCommit: callback => callback(), runPostPayActions: async payload => { postActions.push(payload); } }); await new Promise(resolve => setImmediate(resolve)); assert.equal(result.alreadyFulfilled, false); assert.equal(result.serviceUserID, 88); assert.deepEqual(result.miaoguoUserIDs, [1]); assert.equal(result.trialEnd, '2026-09-13 12:00:00'); assert.equal(state.committed, 1); assert.equal(state.rolledBack, 0); assert.equal(state.released, 1); assert.ok(state.queries.some(item => item.sql.includes('UPDATE WechatServiceWXUsers SET IsProbation=1'))); assert.ok(state.queries.some(item => item.sql.includes('UPDATE MiaoguoWXUsers SET ProductServiceTime='))); const payUpdate = state.queries.find(item => item.sql.includes('UPDATE ProductPayInfo')); assert.ok(payUpdate); assert.match(payUpdate.sql, /Status=1/); assert.equal(payUpdate.params[2], '2026-09-13 12:00:00'); assert.equal(payUpdate.params[3], 88); assert.equal(payUpdate.params[4], 501); assert.equal(postActions.length, 1); }); test('重复支付通知不会重复延长试用权益', async () => { const { connection, state } = createConnection({ order: pendingOrder({ Status: 1 }), serviceUser }); const result = await fulfillWechatServiceOrder(pendingOrder().TradeNo, paidResult, { getConnection: async () => connection, runPostPayActions: async () => assert.fail('重复通知不应触发支付后动作') }); assert.equal(result.alreadyFulfilled, true); assert.equal(state.committed, 1); assert.equal(state.queries.some(item => item.sql.includes('UPDATE ')), false); }); test('微信实付金额不等于 100 分时回滚且不发权益', async () => { const { connection, state } = createConnection({ order: pendingOrder(), serviceUser }); await assert.rejects( fulfillWechatServiceOrder(pendingOrder().TradeNo, { ...paidResult, totalFee: 1 }, { getConnection: async () => connection }), /金额与本地订单不一致/ ); assert.equal(state.committed, 0); assert.equal(state.rolledBack, 1); assert.equal(state.queries.some(item => item.sql.includes('UPDATE ')), false); }); test('下单忽略客户端 money,统一下单与数据库都固定为 100 分', async () => { const databaseCalls = []; const httpClient = { async get() { return { data: { openid: 'service-openid' } }; }, async post(_url, xml) { const request = parseV2Xml(xml); assert.equal(request.total_fee, '100'); assert.equal(request.trade_type, 'JSAPI'); assert.equal(request.openid, 'service-openid'); return { data: buildV2Xml(signed({ return_code: 'SUCCESS', result_code: 'SUCCESS', appid: String(config.wx.wechatservice_appid), mch_id: String(config.wx.mch_id), nonce_str: 'wechat-response-nonce', trade_type: 'JSAPI', prepay_id: 'wx-prepay-test' })) }; } }; const ctx = { req: { socket: { remoteAddress: '127.0.0.1' } }, request: { headers: { 'x-forwarded-for': '203.0.113.8' }, body: { code: 'oauth-code', money: '0.01' } } }; await MiaoguoWechatServicePayLogin500(ctx, { httpClient, query: async (sql, params) => { databaseCalls.push({ sql, params }); if (sql.includes('FROM WechatServiceWXUsers')) return [{ UserID: 88, OpenID: 'service-openid', Subscribe: 1, IsProbation: 0 }]; return { insertId: 501 }; } }); assert.equal(ctx.body.errcode, 10000); assert.equal(ctx.body.result.package, 'prepay_id=wx-prepay-test'); assert.match(ctx.body.result.timeStamp, /^\d{10}$/); const insert = databaseCalls.find(item => item.sql.includes('INSERT INTO ProductPayInfo')); assert.ok(insert); assert.equal(insert.params[1], 8); assert.equal(insert.params[2], 131); assert.equal(insert.params[7], 100); assert.equal(insert.params[9], 167); }); test('合法签名的微信通知进入统一完成函数并返回 SUCCESS', async () => { const calls = []; const fields = signed({ return_code: 'SUCCESS', result_code: 'SUCCESS', appid: String(config.wx.wechatservice_appid), mch_id: String(config.wx.mch_id), nonce_str: 'notify-nonce', out_trade_no: pendingOrder().TradeNo, openid: 'service-openid', total_fee: '100', fee_type: 'CNY', trade_type: 'JSAPI', transaction_id: paidResult.transactionId, time_end: paidResult.timeEnd }); const ctx = { request: { body: { xml: {}, rawBody: buildV2Xml(fields) } } }; await MiaoguoWechatServicePayNotify500(ctx, { fulfillOrder: async (...args) => { calls.push(args); return { alreadyFulfilled: false }; } }); assert.equal(calls.length, 1); assert.equal(calls[0][0], pendingOrder().TradeNo); assert.equal(calls[0][1].totalFee, 100); assert.equal(parseV2Xml(ctx.body).return_code, 'SUCCESS'); }); test('伪造或被篡改的微信通知不会发放权益', async () => { let fulfilled = false; const fields = signed({ return_code: 'SUCCESS', result_code: 'SUCCESS', appid: String(config.wx.wechatservice_appid), mch_id: String(config.wx.mch_id), nonce_str: 'notify-nonce', out_trade_no: pendingOrder().TradeNo, openid: 'service-openid', total_fee: '100', transaction_id: paidResult.transactionId }); fields.total_fee = '1'; const ctx = { request: { body: { xml: fields } } }; await MiaoguoWechatServicePayNotify500(ctx, { fulfillOrder: async () => { fulfilled = true; }, logger: { error() {} } }); assert.equal(fulfilled, false); assert.equal(parseV2Xml(ctx.body).return_code, 'FAIL'); }); test('主动查单确认 SUCCESS 后补走统一完成函数', async () => { const order = pendingOrder(); const fulfillCalls = []; const response = signed({ return_code: 'SUCCESS', result_code: 'SUCCESS', appid: String(config.wx.wechatservice_appid), mch_id: String(config.wx.mch_id), nonce_str: 'query-response-nonce', out_trade_no: order.TradeNo, openid: order.OpenID, total_fee: '100', trade_type: 'JSAPI', trade_state: 'SUCCESS', transaction_id: paidResult.transactionId, time_end: paidResult.timeEnd }); const ctx = { request: { body: { TradeNo: order.TradeNo } } }; await MiaoguoWechatServicePayOrderStatus500(ctx, { query: async () => [order], httpClient: { async post() { return { data: buildV2Xml(response) }; } }, fulfillOrder: async (...args) => { fulfillCalls.push(args); return { alreadyFulfilled: false, trialEnd: '2026-09-13 12:00:00' }; } }); assert.equal(ctx.body.errcode, 10000); assert.equal(ctx.body.result.Status, 1); assert.equal(fulfillCalls.length, 1); assert.equal(fulfillCalls[0][1].totalFee, 100); }); test('网页不再提交客户端金额,并在前端回调后主动查单', () => { const html = fs.readFileSync(new URL('../public/wcs/pay.html', import.meta.url), 'utf8'); const scripts = [...html.matchAll(/