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 = ''; for (const [k, v] of Object.entries(obj)) { xml += `<${k}>${v}`; } 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;