| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285 |
- 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(/<script>([\s\S]*?)<\/script>/g)].map(match => match[1]);
- assert.equal(html.includes('jquery-1.6.4'), false);
- assert.equal(html.includes('param.money'), false);
- assert.equal(html.includes('http://miaguo-1253256735'), false);
- assert.match(html, /const createPayEndpoint = '\[支付链接\]'/);
- assert.match(html, /const orderStatusEndpoint = '\[查询链接\]'/);
- assert.match(html, /waitForPaidOrder\(payParameters\.TradeNo/);
- assert.match(html, /getBrandWCPayRequest/);
- for (const script of scripts) new vm.Script(script);
- });
|