pay is ok
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -209,6 +209,8 @@ logs/
|
|||||||
.env.development.local
|
.env.development.local
|
||||||
.env.test.local
|
.env.test.local
|
||||||
.env.production.local
|
.env.production.local
|
||||||
|
*.pem
|
||||||
|
*.p12
|
||||||
|
|
||||||
# Docker
|
# Docker
|
||||||
.dockerignore
|
.dockerignore
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -259,57 +259,116 @@ def payment_finish(request):
|
|||||||
}
|
}
|
||||||
|
|
||||||
body = request.body.decode('utf-8')
|
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:
|
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()
|
wxpay, error_msg = get_wechat_pay_client()
|
||||||
if not wxpay:
|
if not wxpay:
|
||||||
print(f"错误: 无法初始化客户端: {error_msg}")
|
|
||||||
return HttpResponse(error_msg, status=500)
|
return HttpResponse(error_msg, status=500)
|
||||||
|
|
||||||
# 3. 打印当前证书状态,帮助排查“平台证书”问题
|
# 3. 解析并校验基础信息
|
||||||
cert_count = len(wxpay._core._certificates)
|
try:
|
||||||
print(f"当前已加载平台证书数量: {cert_count}")
|
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 标准方法处理 (内部包含:验签 + 解密)
|
# 4. 尝试解密
|
||||||
# 只有当 验签通过 且 解密成功 时,result 才有值
|
apiv3_key = str(wechat_config.apiv3_key).strip()
|
||||||
result = wxpay.callback(headers, body)
|
print(f"正在使用 Key[{apiv3_key[:3]}...{apiv3_key[-3:]}] (长度: {len(apiv3_key)}) 尝试解密...")
|
||||||
|
|
||||||
if result:
|
# 优先使用 SDK 的 callback 方法
|
||||||
print(f"验证身份与解密成功: {result}")
|
try:
|
||||||
# 处理订单逻辑...
|
print("尝试使用 SDK callback 方法解密并验证签名...")
|
||||||
data = result
|
# 调试:打印 headers 关键信息
|
||||||
if 'out_trade_no' not in data and 'resource' in data:
|
print(f"Headers: Timestamp={headers.get('Wechatpay-Timestamp')}, Serial={headers.get('Wechatpay-Serial')}")
|
||||||
data = data.get('resource', {})
|
|
||||||
|
|
||||||
out_trade_no = data.get('out_trade_no')
|
result_str = wxpay.callback(headers, body)
|
||||||
transaction_id = data.get('transaction_id')
|
if result_str:
|
||||||
trade_state = data.get('trade_state')
|
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':
|
resource = data.get('resource', {})
|
||||||
try:
|
ciphertext = resource.get('ciphertext')
|
||||||
order = None
|
nonce = resource.get('nonce')
|
||||||
if out_trade_no.startswith('PAY'):
|
associated_data = resource.get('associated_data')
|
||||||
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)
|
print(f"提取的解密参数: nonce={nonce}, associated_data={associated_data}, ciphertext_len={len(ciphertext) if ciphertext else 0}")
|
||||||
else:
|
|
||||||
print("错误: 微信支付身份验证(验签)失败或解密失败。")
|
try:
|
||||||
print("请检查: 1. API V3 密钥是否正确; 2. 平台证书是否下载成功。")
|
if not all([ciphertext, nonce, apiv3_key]):
|
||||||
return HttpResponse("Signature verification failed", status=401)
|
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:
|
except Exception as e:
|
||||||
import traceback
|
import traceback
|
||||||
|
|||||||
10
frontend/package-lock.json
generated
10
frontend/package-lock.json
generated
@@ -14,6 +14,7 @@
|
|||||||
"axios": "^1.13.4",
|
"axios": "^1.13.4",
|
||||||
"framer-motion": "^12.29.2",
|
"framer-motion": "^12.29.2",
|
||||||
"jszip": "^3.10.1",
|
"jszip": "^3.10.1",
|
||||||
|
"qrcode.react": "^4.2.0",
|
||||||
"react": "^19.2.0",
|
"react": "^19.2.0",
|
||||||
"react-dom": "^19.2.0",
|
"react-dom": "^19.2.0",
|
||||||
"react-router-dom": "^7.13.0",
|
"react-router-dom": "^7.13.0",
|
||||||
@@ -4244,6 +4245,15 @@
|
|||||||
"node": ">=6"
|
"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": {
|
"node_modules/react": {
|
||||||
"version": "19.2.4",
|
"version": "19.2.4",
|
||||||
"resolved": "https://registry.npmmirror.com/react/-/react-19.2.4.tgz",
|
"resolved": "https://registry.npmmirror.com/react/-/react-19.2.4.tgz",
|
||||||
|
|||||||
@@ -16,6 +16,7 @@
|
|||||||
"axios": "^1.13.4",
|
"axios": "^1.13.4",
|
||||||
"framer-motion": "^12.29.2",
|
"framer-motion": "^12.29.2",
|
||||||
"jszip": "^3.10.1",
|
"jszip": "^3.10.1",
|
||||||
|
"qrcode.react": "^4.2.0",
|
||||||
"react": "^19.2.0",
|
"react": "^19.2.0",
|
||||||
"react-dom": "^19.2.0",
|
"react-dom": "^19.2.0",
|
||||||
"react-router-dom": "^7.13.0",
|
"react-router-dom": "^7.13.0",
|
||||||
|
|||||||
@@ -10,7 +10,9 @@ const api = axios.create({
|
|||||||
|
|
||||||
export const getConfigs = () => api.get('/configs/');
|
export const getConfigs = () => api.get('/configs/');
|
||||||
export const createOrder = (data) => api.post('/orders/', data);
|
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 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 initiatePayment = (orderId) => api.post(`/orders/${orderId}/initiate_payment/`);
|
||||||
export const confirmPayment = (orderId) => api.post(`/orders/${orderId}/confirm_payment/`);
|
export const confirmPayment = (orderId) => api.post(`/orders/${orderId}/confirm_payment/`);
|
||||||
|
|
||||||
|
|||||||
@@ -1,26 +1,65 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import { useParams, useNavigate } from 'react-router-dom';
|
import { useParams, useNavigate, useLocation } from 'react-router-dom';
|
||||||
import { Button, message, Result, Spin, QRCode } from 'antd';
|
import { Button, message, Result, Spin } from 'antd';
|
||||||
import { WechatOutlined, AlipayCircleOutlined, CheckCircleOutlined } from '@ant-design/icons';
|
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';
|
import './Payment.css';
|
||||||
|
|
||||||
const Payment = () => {
|
const Payment = () => {
|
||||||
const { orderId } = useParams();
|
const { orderId: initialOrderId } = useParams();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [order, setOrder] = useState(null);
|
const location = useLocation();
|
||||||
const [loading, setLoading] = useState(true);
|
const [currentOrderId, setCurrentOrderId] = useState(location.state?.order_id || initialOrderId);
|
||||||
const [paying, setPaying] = useState(false);
|
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 [paySuccess, setPaySuccess] = useState(false);
|
||||||
const [paymentMethod, setPaymentMethod] = useState('wechat');
|
const [paymentMethod, setPaymentMethod] = useState('wechat');
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchOrder();
|
if (codeUrl && !paying) {
|
||||||
}, [orderId]);
|
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 () => {
|
const fetchOrder = async () => {
|
||||||
try {
|
try {
|
||||||
const response = await getOrder(orderId);
|
const response = await getOrder(currentOrderId);
|
||||||
setOrder(response.data);
|
setOrder(response.data);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to fetch order:', error);
|
console.error('Failed to fetch order:', error);
|
||||||
@@ -38,69 +77,41 @@ const Payment = () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (codeUrl) {
|
||||||
|
setPaying(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!order) {
|
||||||
|
message.error('正在加载订单信息,请稍后...');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
setPaying(true);
|
setPaying(true);
|
||||||
try {
|
try {
|
||||||
// 1. 获取微信支付参数
|
const orderData = {
|
||||||
const response = await initiatePayment(orderId);
|
goodid: order.config || order.goodid,
|
||||||
const payData = response.data;
|
quantity: order.quantity,
|
||||||
|
customer_name: order.customer_name,
|
||||||
if (typeof WeixinJSBridge === 'undefined') {
|
phone_number: order.phone_number,
|
||||||
message.warning('请在微信内置浏览器中打开以完成支付');
|
shipping_address: order.shipping_address,
|
||||||
setPaying(false);
|
ref_code: order.ref_code
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if (typeof window.WeixinJSBridge == "undefined") {
|
const response = await nativePay(orderData);
|
||||||
if (document.addEventListener) {
|
setCodeUrl(response.data.code_url);
|
||||||
document.addEventListener('WeixinJSBridgeReady', onBridgeReady, false);
|
if (response.data.order_id) {
|
||||||
} else if (document.attachEvent) {
|
setCurrentOrderId(response.data.order_id);
|
||||||
document.attachEvent('WeixinJSBridgeReady', onBridgeReady);
|
|
||||||
document.attachEvent('onWeixinJSBridgeReady', onBridgeReady);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
onBridgeReady();
|
|
||||||
}
|
}
|
||||||
|
message.success('支付二维码已生成');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
if (error.response && error.response.data && error.response.data.error) {
|
message.error('生成支付二维码失败,请重试');
|
||||||
message.error(error.response.data.error);
|
|
||||||
} else {
|
|
||||||
message.error('支付发起失败,请稍后重试');
|
|
||||||
}
|
|
||||||
setPaying(false);
|
setPaying(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if (loading) return <div style={{ padding: 50, textAlign: 'center' }}><Spin size="large" /></div>;
|
if (loading) return <div style={{ padding: 50, textAlign: 'center' }}><Spin size="large" tip="正在加载订单信息..." /></div>;
|
||||||
|
|
||||||
if (paySuccess) {
|
if (paySuccess) {
|
||||||
return (
|
return (
|
||||||
@@ -109,7 +120,7 @@ const Payment = () => {
|
|||||||
status="success"
|
status="success"
|
||||||
icon={<CheckCircleOutlined style={{ color: '#00b96b' }} />}
|
icon={<CheckCircleOutlined style={{ color: '#00b96b' }} />}
|
||||||
title={<span style={{ color: '#fff' }}>支付成功</span>}
|
title={<span style={{ color: '#fff' }}>支付成功</span>}
|
||||||
subTitle={<span style={{ color: '#888' }}>订单 {orderId} 已完成支付,我们将尽快为您发货。</span>}
|
subTitle={<span style={{ color: '#888' }}>订单 {currentOrderId} 已完成支付,我们将尽快为您发货。</span>}
|
||||||
extra={[
|
extra={[
|
||||||
<Button type="primary" key="home" onClick={() => navigate('/')}>
|
<Button type="primary" key="home" onClick={() => navigate('/')}>
|
||||||
返回首页
|
返回首页
|
||||||
@@ -135,7 +146,7 @@ const Payment = () => {
|
|||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<div className="payment-info">
|
<div className="payment-info">
|
||||||
<p>订单 ID: {orderId}</p>
|
<p>订单 ID: {currentOrderId}</p>
|
||||||
<p>无法加载详情,但您可以尝试支付。</p>
|
<p>无法加载详情,但您可以尝试支付。</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -159,9 +170,20 @@ const Payment = () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{paying && (
|
{paying && (
|
||||||
<div style={{ margin: '20px 0', padding: 20, background: '#fff', borderRadius: 8, display: 'inline-block' }}>
|
<div style={{ margin: '20px 0', padding: 20, background: '#fff', borderRadius: 8, display: 'inline-block', minWidth: 240, minHeight: 280 }}>
|
||||||
<QRCode value={`mock-payment-${orderId}`} bordered={false} />
|
{codeUrl ? (
|
||||||
<p style={{ color: '#000', marginTop: 10 }}>请扫码支付 (模拟)</p>
|
<>
|
||||||
|
<div style={{ background: '#fff', padding: '10px', borderRadius: '4px', display: 'inline-block' }}>
|
||||||
|
<QRCodeSVG value={codeUrl} size={200} />
|
||||||
|
</div>
|
||||||
|
<p style={{ color: '#000', marginTop: 15, fontWeight: 'bold', fontSize: 18 }}>请使用微信扫码支付</p>
|
||||||
|
<p style={{ color: '#666', fontSize: 14 }}>支付完成后将自动跳转</p>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<div style={{ padding: '40px 0' }}>
|
||||||
|
<Spin tip="正在生成支付二维码..." />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import { useParams, useNavigate, useSearchParams } from 'react-router-dom';
|
import { useParams, useNavigate, useSearchParams } from 'react-router-dom';
|
||||||
import { Button, Row, Col, Tag, Statistic, Modal, Form, Input, InputNumber, message, Spin, Descriptions } from 'antd';
|
import { Button, Row, Col, Tag, Statistic, Modal, Form, Input, InputNumber, message, Spin, Descriptions, Radio } from 'antd';
|
||||||
import { ShoppingCartOutlined, SafetyCertificateOutlined, ThunderboltOutlined, EyeOutlined, StarOutlined } from '@ant-design/icons';
|
import { ShoppingCartOutlined, SafetyCertificateOutlined, ThunderboltOutlined, EyeOutlined, StarOutlined } from '@ant-design/icons';
|
||||||
import { getConfigs, createOrder } from '../api';
|
import { getConfigs, createOrder, nativePay } from '../api';
|
||||||
import ModelViewer from '../components/ModelViewer';
|
import ModelViewer from '../components/ModelViewer';
|
||||||
import './ProductDetail.css';
|
import './ProductDetail.css';
|
||||||
|
|
||||||
@@ -16,7 +16,7 @@ const ProductDetail = () => {
|
|||||||
const [submitting, setSubmitting] = useState(false);
|
const [submitting, setSubmitting] = useState(false);
|
||||||
const [form] = Form.useForm();
|
const [form] = Form.useForm();
|
||||||
|
|
||||||
const refCode = searchParams.get('ref');
|
const refCode = searchParams.get('ref') || 'flw666';
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchProduct();
|
fetchProduct();
|
||||||
@@ -43,17 +43,29 @@ const ProductDetail = () => {
|
|||||||
const handleBuy = async (values) => {
|
const handleBuy = async (values) => {
|
||||||
setSubmitting(true);
|
setSubmitting(true);
|
||||||
try {
|
try {
|
||||||
|
const isPickup = values.delivery_method === 'pickup';
|
||||||
const orderData = {
|
const orderData = {
|
||||||
config: product.id,
|
goodid: product.id,
|
||||||
quantity: values.quantity,
|
quantity: values.quantity,
|
||||||
customer_name: values.customer_name,
|
customer_name: values.customer_name,
|
||||||
phone_number: values.phone_number,
|
phone_number: values.phone_number,
|
||||||
shipping_address: values.shipping_address,
|
shipping_address: isPickup ? '线下自提' : values.shipping_address,
|
||||||
ref_code: refCode
|
ref_code: refCode
|
||||||
};
|
};
|
||||||
const response = await createOrder(orderData);
|
const response = await nativePay(orderData);
|
||||||
message.success('订单创建成功');
|
message.success('订单已创建,请完成支付');
|
||||||
navigate(`/payment/${response.data.id}`);
|
navigate(`/payment/${response.data.order_id}`, {
|
||||||
|
state: {
|
||||||
|
codeUrl: response.data.code_url,
|
||||||
|
order_id: response.data.order_id,
|
||||||
|
orderInfo: {
|
||||||
|
...orderData,
|
||||||
|
id: response.data.order_id,
|
||||||
|
config_name: product.name,
|
||||||
|
total_price: product.price * values.quantity
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
message.error('创建订单失败,请检查填写信息');
|
message.error('创建订单失败,请检查填写信息');
|
||||||
@@ -217,8 +229,14 @@ const ProductDetail = () => {
|
|||||||
form={form}
|
form={form}
|
||||||
layout="vertical"
|
layout="vertical"
|
||||||
onFinish={handleBuy}
|
onFinish={handleBuy}
|
||||||
initialValues={{ quantity: 1 }}
|
initialValues={{ quantity: 1, delivery_method: 'shipping' }}
|
||||||
>
|
>
|
||||||
|
<Form.Item label="配送方式" name="delivery_method">
|
||||||
|
<Radio.Group buttonStyle="solid">
|
||||||
|
<Radio.Button value="shipping">快递配送</Radio.Button>
|
||||||
|
<Radio.Button value="pickup">线下自提</Radio.Button>
|
||||||
|
</Radio.Group>
|
||||||
|
</Form.Item>
|
||||||
<Form.Item label="购买数量" name="quantity" rules={[{ required: true }]}>
|
<Form.Item label="购买数量" name="quantity" rules={[{ required: true }]}>
|
||||||
<InputNumber min={1} max={100} style={{ width: '100%' }} />
|
<InputNumber min={1} max={100} style={{ width: '100%' }} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
@@ -228,8 +246,22 @@ const ProductDetail = () => {
|
|||||||
<Form.Item label="联系电话" name="phone_number" rules={[{ required: true, message: '请输入电话' }]}>
|
<Form.Item label="联系电话" name="phone_number" rules={[{ required: true, message: '请输入电话' }]}>
|
||||||
<Input placeholder="13800000000" />
|
<Input placeholder="13800000000" />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item label="收货地址" name="shipping_address" rules={[{ required: true, message: '请输入地址' }]}>
|
<Form.Item
|
||||||
<Input.TextArea rows={3} placeholder="北京市..." />
|
noStyle
|
||||||
|
shouldUpdate={(prevValues, currentValues) => prevValues.delivery_method !== currentValues.delivery_method}
|
||||||
|
>
|
||||||
|
{({ getFieldValue }) =>
|
||||||
|
getFieldValue('delivery_method') === 'shipping' ? (
|
||||||
|
<Form.Item label="收货地址" name="shipping_address" rules={[{ required: true, message: '请输入地址' }]}>
|
||||||
|
<Input.TextArea rows={3} placeholder="北京市..." />
|
||||||
|
</Form.Item>
|
||||||
|
) : (
|
||||||
|
<div style={{ marginBottom: 24, padding: '12px', background: '#f5f5f5', borderRadius: '4px', border: '1px solid #d9d9d9' }}>
|
||||||
|
<p style={{ margin: 0, color: '#666' }}>自提地址:昆明市云纺国际商厦B座1406</p>
|
||||||
|
<p style={{ margin: 0, fontSize: '12px', color: '#999' }}>请在工作日 9:00 - 18:00 期间前往提货</p>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
<div style={{ display: 'flex', justifyContent: 'flex-end', gap: 10, marginTop: 20 }}>
|
<div style={{ display: 'flex', justifyContent: 'flex-end', gap: 10, marginTop: 20 }}>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import { useParams, useNavigate } from 'react-router-dom';
|
import { useParams, useNavigate, useSearchParams } from 'react-router-dom';
|
||||||
import { Typography, Button, Spin, Empty, Descriptions, Tag, Row, Col, Modal, Form, Input, message, Statistic } from 'antd';
|
import { Typography, Button, Spin, Empty, Descriptions, Tag, Row, Col, Modal, Form, Input, message, Statistic } from 'antd';
|
||||||
import { ArrowLeftOutlined, ClockCircleOutlined, GiftOutlined, ShoppingCartOutlined } from '@ant-design/icons';
|
import { ArrowLeftOutlined, ClockCircleOutlined, GiftOutlined, ShoppingCartOutlined } from '@ant-design/icons';
|
||||||
import { getServiceDetail, createServiceOrder } from '../api';
|
import { getServiceDetail, createServiceOrder } from '../api';
|
||||||
@@ -10,12 +10,15 @@ const { Title, Paragraph } = Typography;
|
|||||||
const ServiceDetail = () => {
|
const ServiceDetail = () => {
|
||||||
const { id } = useParams();
|
const { id } = useParams();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
const [searchParams] = useSearchParams();
|
||||||
const [service, setService] = useState(null);
|
const [service, setService] = useState(null);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||||
const [submitting, setSubmitting] = useState(false);
|
const [submitting, setSubmitting] = useState(false);
|
||||||
const [form] = Form.useForm();
|
const [form] = Form.useForm();
|
||||||
|
|
||||||
|
const refCode = searchParams.get('ref') || 'flw666';
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchDetail = async () => {
|
const fetchDetail = async () => {
|
||||||
try {
|
try {
|
||||||
@@ -39,7 +42,8 @@ const ServiceDetail = () => {
|
|||||||
company_name: values.company_name,
|
company_name: values.company_name,
|
||||||
phone_number: values.phone_number,
|
phone_number: values.phone_number,
|
||||||
email: values.email,
|
email: values.email,
|
||||||
requirements: values.requirements
|
requirements: values.requirements,
|
||||||
|
ref_code: refCode
|
||||||
};
|
};
|
||||||
await createServiceOrder(orderData);
|
await createServiceOrder(orderData);
|
||||||
message.success('需求已提交,我们的销售顾问将尽快与您联系!');
|
message.success('需求已提交,我们的销售顾问将尽快与您联系!');
|
||||||
|
|||||||
Reference in New Issue
Block a user