diff --git a/.gitignore b/.gitignore index 4c1ac06..0e8c4e2 100644 --- a/.gitignore +++ b/.gitignore @@ -209,6 +209,8 @@ logs/ .env.development.local .env.test.local .env.production.local +*.pem +*.p12 # Docker .dockerignore diff --git a/backend/db.sqlite3 b/backend/db.sqlite3 index 79ac30d..1d3123d 100644 Binary files a/backend/db.sqlite3 and b/backend/db.sqlite3 differ diff --git a/backend/shop/__pycache__/urls.cpython-312.pyc b/backend/shop/__pycache__/urls.cpython-312.pyc index e7299a5..716190f 100644 Binary files a/backend/shop/__pycache__/urls.cpython-312.pyc and b/backend/shop/__pycache__/urls.cpython-312.pyc differ diff --git a/backend/shop/__pycache__/views.cpython-312.pyc b/backend/shop/__pycache__/views.cpython-312.pyc index cca386e..bb7436f 100644 Binary files a/backend/shop/__pycache__/views.cpython-312.pyc and b/backend/shop/__pycache__/views.cpython-312.pyc differ diff --git a/backend/shop/__pycache__/views.cpython-313.pyc b/backend/shop/__pycache__/views.cpython-313.pyc index a98b40a..7e83a10 100644 Binary files a/backend/shop/__pycache__/views.cpython-313.pyc and b/backend/shop/__pycache__/views.cpython-313.pyc differ diff --git a/backend/shop/views.py b/backend/shop/views.py index bc21bc4..407701a 100644 --- a/backend/shop/views.py +++ b/backend/shop/views.py @@ -259,57 +259,116 @@ def payment_finish(request): } body = request.body.decode('utf-8') - print(f"收到回调 Body (长度: {len(body)})") + print(f"收到回调 Body (长度: {len(body)}):") + print(f"--- BODY START ---") + print(body) + print(f"--- BODY END ---") + + # 打印所有微信支付相关的头信息 + print("收到回调 Headers:") + for key, value in request.headers.items(): + if key.lower().startswith('wechatpay'): + print(f" {key}: {value}") try: - # 2. 初始化微信支付客户端 + # 2. 获取支付配置并初始化 + wechat_config = WeChatPayConfig.objects.filter(is_active=True).first() + if not wechat_config: + print("错误: 数据库中没有启用的微信支付配置") + return HttpResponse("Config not found", status=500) + + print(f"当前使用的配置 ID: {wechat_config.id}, 商户号: {wechat_config.mch_id}") + wxpay, error_msg = get_wechat_pay_client() if not wxpay: - print(f"错误: 无法初始化客户端: {error_msg}") return HttpResponse(error_msg, status=500) - - # 3. 打印当前证书状态,帮助排查“平台证书”问题 - cert_count = len(wxpay._core._certificates) - print(f"当前已加载平台证书数量: {cert_count}") + + # 3. 解析并校验基础信息 + try: + data = json.loads(body) + print(f"解析后的回调数据概览: id={data.get('id')}, event_type={data.get('event_type')}, resource_type={data.get('resource_type')}") + except Exception as json_e: + print(f"JSON 解析失败: {str(json_e)}") + return HttpResponse("Invalid JSON", status=400) - # 4. 使用 SDK 标准方法处理 (内部包含:验签 + 解密) - # 只有当 验签通过 且 解密成功 时,result 才有值 - result = wxpay.callback(headers, body) + # 4. 尝试解密 + apiv3_key = str(wechat_config.apiv3_key).strip() + print(f"正在使用 Key[{apiv3_key[:3]}...{apiv3_key[-3:]}] (长度: {len(apiv3_key)}) 尝试解密...") - if result: - print(f"验证身份与解密成功: {result}") - # 处理订单逻辑... - data = result - if 'out_trade_no' not in data and 'resource' in data: - data = data.get('resource', {}) + # 优先使用 SDK 的 callback 方法 + try: + print("尝试使用 SDK callback 方法解密并验证签名...") + # 调试:打印 headers 关键信息 + print(f"Headers: Timestamp={headers.get('Wechatpay-Timestamp')}, Serial={headers.get('Wechatpay-Serial')}") - out_trade_no = data.get('out_trade_no') - transaction_id = data.get('transaction_id') - trade_state = data.get('trade_state') + result_str = wxpay.callback(headers, body) + if result_str: + result = json.loads(result_str) + print(f"SDK 解密成功: {result.get('out_trade_no')}") + else: + print("SDK callback 返回空,可能是签名验证失败。") + raise Exception("SDK callback returned None") + except Exception as sdk_e: + print(f"SDK callback 失败: {str(sdk_e)},尝试手动解密...") - if trade_state == 'SUCCESS': - try: - order = None - if out_trade_no.startswith('PAY'): - t_index = out_trade_no.find('T') - order_id = int(out_trade_no[3:t_index]) - order = Order.objects.get(id=order_id) - else: - order = Order.objects.get(out_trade_no=out_trade_no) - - if order and order.status != 'paid': - order.status = 'paid' - order.wechat_trade_no = transaction_id - order.save() - print(f"订单 {order.id} 状态已更新") - except Exception as e: - print(f"订单更新失败: {str(e)}") + resource = data.get('resource', {}) + ciphertext = resource.get('ciphertext') + nonce = resource.get('nonce') + associated_data = resource.get('associated_data') - return HttpResponse(status=200) - else: - print("错误: 微信支付身份验证(验签)失败或解密失败。") - print("请检查: 1. API V3 密钥是否正确; 2. 平台证书是否下载成功。") - return HttpResponse("Signature verification failed", status=401) + print(f"提取的解密参数: nonce={nonce}, associated_data={associated_data}, ciphertext_len={len(ciphertext) if ciphertext else 0}") + + try: + if not all([ciphertext, nonce, apiv3_key]): + raise ValueError(f"缺少解密必要参数: ciphertext={bool(ciphertext)}, nonce={bool(nonce)}, key={bool(apiv3_key)}") + + if len(apiv3_key) != 32: + raise ValueError(f"APIV3 Key 长度错误: 预期 32 字节,实际 {len(apiv3_key)} 字节") + + aesgcm = AESGCM(apiv3_key.encode('utf-8')) + decrypted_data = aesgcm.decrypt( + nonce.encode('utf-8'), + base64.b64decode(ciphertext), + associated_data.encode('utf-8') if associated_data else b"" + ) + result = json.loads(decrypted_data.decode('utf-8')) + print(f"手动解密成功: {result.get('out_trade_no')}") + except Exception as e: + import traceback + error_type = type(e).__name__ + error_msg = str(e) + print(f"手动解密依然失败: {error_type}: {error_msg}") + if "InvalidTag" in error_msg or error_type == "InvalidTag": + print(f"提示: InvalidTag 通常意味着 Key 正确但与数据不匹配。") + print(f"当前使用的 Key: {apiv3_key}") + print(f"请确认该 Key 是否确实是商户号 {wechat_config.mch_id} 的 APIV3 密钥。") + traceback.print_exc() + return HttpResponse("Decryption failed", status=400) + + # 5. 订单处理 (保持原有逻辑) + out_trade_no = result.get('out_trade_no') + transaction_id = result.get('transaction_id') + trade_state = result.get('trade_state') + + if trade_state == 'SUCCESS': + try: + order = None + if out_trade_no.startswith('PAY'): + t_index = out_trade_no.find('T') + order_id = int(out_trade_no[3:t_index]) + order = Order.objects.get(id=order_id) + else: + order = Order.objects.get(out_trade_no=out_trade_no) + + if order and order.status != 'paid': + order.status = 'paid' + order.wechat_trade_no = transaction_id + order.save() + print(f"订单 {order.id} 状态已更新") + except Exception as e: + print(f"订单更新失败: {str(e)}") + + return HttpResponse(status=200) except Exception as e: import traceback diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 3de1a82..d89e2fb 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -14,6 +14,7 @@ "axios": "^1.13.4", "framer-motion": "^12.29.2", "jszip": "^3.10.1", + "qrcode.react": "^4.2.0", "react": "^19.2.0", "react-dom": "^19.2.0", "react-router-dom": "^7.13.0", @@ -4244,6 +4245,15 @@ "node": ">=6" } }, + "node_modules/qrcode.react": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/qrcode.react/-/qrcode.react-4.2.0.tgz", + "integrity": "sha512-QpgqWi8rD9DsS9EP3z7BT+5lY5SFhsqGjpgW5DY/i3mK4M9DTBNz3ErMi8BWYEfI3L0d8GIbGmcdFAS1uIRGjA==", + "license": "ISC", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/react": { "version": "19.2.4", "resolved": "https://registry.npmmirror.com/react/-/react-19.2.4.tgz", diff --git a/frontend/package.json b/frontend/package.json index 0712903..ff054a8 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -16,6 +16,7 @@ "axios": "^1.13.4", "framer-motion": "^12.29.2", "jszip": "^3.10.1", + "qrcode.react": "^4.2.0", "react": "^19.2.0", "react-dom": "^19.2.0", "react-router-dom": "^7.13.0", diff --git a/frontend/src/api.js b/frontend/src/api.js index 29ea1d0..0ca8e3f 100644 --- a/frontend/src/api.js +++ b/frontend/src/api.js @@ -10,7 +10,9 @@ const api = axios.create({ export const getConfigs = () => api.get('/configs/'); export const createOrder = (data) => api.post('/orders/', data); +export const nativePay = (data) => api.post('/pay/', data); export const getOrder = (id) => api.get(`/orders/${id}/`); +export const queryOrderStatus = (id) => api.get(`/orders/${id}/query_status/`); export const initiatePayment = (orderId) => api.post(`/orders/${orderId}/initiate_payment/`); export const confirmPayment = (orderId) => api.post(`/orders/${orderId}/confirm_payment/`); diff --git a/frontend/src/pages/Payment.jsx b/frontend/src/pages/Payment.jsx index 500665f..ffcff0e 100644 --- a/frontend/src/pages/Payment.jsx +++ b/frontend/src/pages/Payment.jsx @@ -1,26 +1,65 @@ import React, { useEffect, useState } from 'react'; -import { useParams, useNavigate } from 'react-router-dom'; -import { Button, message, Result, Spin, QRCode } from 'antd'; +import { useParams, useNavigate, useLocation } from 'react-router-dom'; +import { Button, message, Result, Spin } from 'antd'; import { WechatOutlined, AlipayCircleOutlined, CheckCircleOutlined } from '@ant-design/icons'; -import { getOrder, initiatePayment, confirmPayment } from '../api'; +import { QRCodeSVG } from 'qrcode.react'; +import { getOrder, initiatePayment, confirmPayment, nativePay, queryOrderStatus } from '../api'; import './Payment.css'; const Payment = () => { - const { orderId } = useParams(); + const { orderId: initialOrderId } = useParams(); const navigate = useNavigate(); - const [order, setOrder] = useState(null); - const [loading, setLoading] = useState(true); - const [paying, setPaying] = useState(false); + const location = useLocation(); + const [currentOrderId, setCurrentOrderId] = useState(location.state?.order_id || initialOrderId); + const [order, setOrder] = useState(location.state?.orderInfo || null); + const [codeUrl, setCodeUrl] = useState(location.state?.codeUrl || null); + const [loading, setLoading] = useState(!location.state?.orderInfo && !location.state?.codeUrl); + const [paying, setPaying] = useState(!!location.state?.codeUrl); const [paySuccess, setPaySuccess] = useState(false); const [paymentMethod, setPaymentMethod] = useState('wechat'); useEffect(() => { - fetchOrder(); - }, [orderId]); + if (codeUrl && !paying) { + setPaying(true); + } + }, [codeUrl]); + + useEffect(() => { + console.log('Payment page state:', { currentOrderId, order, codeUrl, paying }); + if (!order && !codeUrl) { + fetchOrder(); + } + }, [currentOrderId]); + + useEffect(() => { + if (paying && !codeUrl && order) { + handlePay(); + } + }, [paying, codeUrl, order]); + + // 轮询订单状态 + useEffect(() => { + let timer; + if (paying && !paySuccess) { + timer = setInterval(async () => { + try { + const response = await queryOrderStatus(currentOrderId); + if (response.data.status === 'paid') { + setPaySuccess(true); + setPaying(false); + clearInterval(timer); + } + } catch (error) { + console.error('Check payment status failed:', error); + } + }, 3000); + } + return () => clearInterval(timer); + }, [paying, paySuccess, currentOrderId]); const fetchOrder = async () => { try { - const response = await getOrder(orderId); + const response = await getOrder(currentOrderId); setOrder(response.data); } catch (error) { console.error('Failed to fetch order:', error); @@ -38,69 +77,41 @@ const Payment = () => { return; } + if (codeUrl) { + setPaying(true); + return; + } + + if (!order) { + message.error('正在加载订单信息,请稍后...'); + return; + } + setPaying(true); try { - // 1. 获取微信支付参数 - const response = await initiatePayment(orderId); - const payData = response.data; - - if (typeof WeixinJSBridge === 'undefined') { - message.warning('请在微信内置浏览器中打开以完成支付'); - setPaying(false); - return; - } - - // 2. 调用微信支付 - const onBridgeReady = () => { - window.WeixinJSBridge.invoke( - 'getBrandWCPayRequest', { - "appId": payData.appId, // 公众号名称,由商户传入 - "timeStamp": payData.timeStamp, // 时间戳,自1970年以来的秒数 - "nonceStr": payData.nonceStr, // 随机串 - "package": payData.package, - "signType": payData.signType, // 微信签名方式: - "paySign": payData.paySign // 微信签名 - }, - function(res) { - setPaying(false); - if (res.err_msg == "get_brand_wcpay_request:ok") { - message.success('支付成功!'); - setPaySuccess(true); - // 这里可以再次调用后端查询接口确认状态,但通常 JSAPI 回调 ok 即可认为成功 - // 为了保险,可以去轮询一下后端状态,或者直接展示成功页 - } else if (res.err_msg == "get_brand_wcpay_request:cancel") { - message.info('支付已取消'); - } else { - message.error('支付失败,请重试'); - console.error('WeChat Pay Error:', res); - } - } - ); + const orderData = { + goodid: order.config || order.goodid, + quantity: order.quantity, + customer_name: order.customer_name, + phone_number: order.phone_number, + shipping_address: order.shipping_address, + ref_code: order.ref_code }; - - if (typeof window.WeixinJSBridge == "undefined") { - if (document.addEventListener) { - document.addEventListener('WeixinJSBridgeReady', onBridgeReady, false); - } else if (document.attachEvent) { - document.attachEvent('WeixinJSBridgeReady', onBridgeReady); - document.attachEvent('onWeixinJSBridgeReady', onBridgeReady); - } - } else { - onBridgeReady(); + + const response = await nativePay(orderData); + setCodeUrl(response.data.code_url); + if (response.data.order_id) { + setCurrentOrderId(response.data.order_id); } - + message.success('支付二维码已生成'); } catch (error) { console.error(error); - if (error.response && error.response.data && error.response.data.error) { - message.error(error.response.data.error); - } else { - message.error('支付发起失败,请稍后重试'); - } + message.error('生成支付二维码失败,请重试'); setPaying(false); } }; - if (loading) return
; + if (loading) return
; if (paySuccess) { return ( @@ -109,7 +120,7 @@ const Payment = () => { status="success" icon={} title={支付成功} - subTitle={订单 {orderId} 已完成支付,我们将尽快为您发货。} + subTitle={订单 {currentOrderId} 已完成支付,我们将尽快为您发货。} extra={[