137 lines
4.4 KiB
JavaScript
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;
|