Files

137 lines
4.4 KiB
JavaScript

const express = require('express');
const router = express.Router();
const crypto = require('crypto');
const axios = require('axios');
const xml2js = require('xml2js');
const { v4: uuidv4 } = require('uuid');
const QRCode = require('qrcode');
const config = require('../config');
const storage = require('../utils/storage');
function buildSign(params, key) {
const str = Object.keys(params)
.filter(k => params[k] !== '' && k !== 'sign')
.sort()
.map(k => `${k}=${params[k]}`)
.join('&') + `&key=${key}`;
return crypto.createHash('md5').update(str, 'utf-8').digest('hex').toUpperCase();
}
function buildXml(obj) {
let xml = '<xml>';
for (const [k, v] of Object.entries(obj)) {
xml += `<${k}>${v}</${k}>`;
}
xml += '</xml>';
return xml;
}
// Step 1: Create order page - show QR code
router.get('/create/:productId', async (req, res) => {
const product = config.products.find(p => p.id === parseInt(req.params.productId));
if (!product) return res.status(404).send('商品不存在');
const orderId = uuidv4().replace(/-/g, '').substring(0, 28);
const order = {
orderId,
productId: product.id,
productName: product.name,
totalFee: product.price,
status: 'CREATING',
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
};
storage.saveOrder(order);
try {
const nonceStr = uuidv4().replace(/-/g, '').substring(0, 16);
const unifiedParams = {
appid: config.wechat.appid,
mch_id: config.wechat.mchid,
nonce_str: nonceStr,
body: product.name,
out_trade_no: orderId,
total_fee: product.price,
spbill_create_ip: req.ip || '127.0.0.1',
notify_url: config.wechat.notifyUrl,
trade_type: 'NATIVE',
};
unifiedParams.sign = buildSign(unifiedParams, config.wechat.key);
const xml = buildXml(unifiedParams);
const resp = await axios.post(
`${config.wechat.apiBase}/pay/unifiedorder`,
xml,
{ headers: { 'Content-Type': 'text/xml' } }
);
const parsed = await xml2js.parseStringPromise(resp.data, { explicitArray: false });
const result = parsed.xml;
if (result.return_code !== 'SUCCESS' || result.result_code !== 'SUCCESS') {
order.status = 'FAILED';
storage.saveOrder(order);
return res.render('pay', { error: `下单失败: ${result.err_code_des || result.return_msg}` });
}
order.codeUrl = result.code_url;
order.status = 'PENDING';
storage.saveOrder(order);
const qrDataUrl = await QRCode.toDataURL(result.code_url, { width: 300, margin: 2 });
res.render('pay', { order, qrDataUrl, error: null });
} catch (err) {
order.status = 'FAILED';
storage.saveOrder(order);
res.render('pay', { error: `系统错误: ${err.message}` });
}
});
// Step 2: Poll payment status
router.get('/status/:orderId', (req, res) => {
const order = storage.findOrder(req.params.orderId);
if (!order) return res.json({ status: 'NOT_FOUND' });
res.json({ status: order.status, orderId: order.orderId });
});
// Step 3: WeChat payment notification callback
router.post('/notify', async (req, res) => {
if (!req.body) {
return res.send(buildXml({ return_code: 'FAIL', return_msg: '空请求体' }));
}
try {
const parsed = await xml2js.parseStringPromise(req.body, { explicitArray: false });
const data = parsed.xml;
const expectedSign = buildSign(data, config.wechat.key);
if (data.sign !== expectedSign) {
return res.send(buildXml({ return_code: 'FAIL', return_msg: '签名失败' }));
}
if (data.return_code === 'SUCCESS' && data.result_code === 'SUCCESS') {
storage.updateOrderStatus(data.out_trade_no, 'SUCCESS', data.transaction_id);
return res.send(buildXml({ return_code: 'SUCCESS', return_msg: 'OK' }));
}
res.send(buildXml({ return_code: 'FAIL', return_msg: data.err_code_des || '支付失败' }));
} catch (err) {
res.send(buildXml({ return_code: 'FAIL', return_msg: '解析失败' }));
}
});
// Step 4: Order list
router.get('/orders', (req, res) => {
const orders = storage.readOrders().sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt));
res.render('orders', { orders });
});
// Success page
router.get('/success/:orderId', (req, res) => {
const order = storage.findOrder(req.params.orderId);
if (!order) return res.status(404).send('订单不存在');
res.render('success', { order });
});
module.exports = router;