解决报名支付
All checks were successful
Deploy to Server / deploy (push) Successful in 37s

This commit is contained in:
jeremygan2021
2026-02-23 16:02:09 +08:00
parent 0bf5f94483
commit 6bbbb49d90
4 changed files with 74 additions and 58 deletions

View File

@@ -1,6 +1,6 @@
from rest_framework import serializers
from .models import Activity, ActivitySignup, Topic, Reply, TopicMedia, Announcement
from shop.serializers import WeChatUserSerializer, ESP32ConfigSerializer, ServiceSerializer, VCCourseSerializer, OrderSerializer
from shop.serializers import WeChatUserSerializer, ESP32ConfigSerializer, ServiceSerializer, VCCourseSerializer
from .utils import get_current_wechat_user
class ActivitySerializer(serializers.ModelSerializer):
@@ -47,11 +47,10 @@ class ActivitySerializer(serializers.ModelSerializer):
class ActivitySignupSerializer(serializers.ModelSerializer):
activity_info = serializers.SerializerMethodField()
order = OrderSerializer(read_only=True)
class Meta:
model = ActivitySignup
fields = ['id', 'activity', 'activity_info', 'user', 'signup_time', 'status', 'signup_info', 'order']
fields = ['id', 'activity', 'activity_info', 'user', 'signup_time', 'status', 'signup_info']
read_only_fields = ['signup_time', 'status', 'user']
def get_activity_info(self, obj):

View File

@@ -207,7 +207,6 @@ class OrderSerializer(serializers.ModelSerializer):
"""
config_name = serializers.CharField(source='config.name', read_only=True)
course_title = serializers.CharField(source='course.title', read_only=True)
activity_title = serializers.CharField(source='activity.title', read_only=True)
config_image = serializers.SerializerMethodField()
salesperson_name = serializers.CharField(source='salesperson.name', read_only=True)
salesperson_code = serializers.CharField(source='salesperson.code', read_only=True)
@@ -216,7 +215,7 @@ class OrderSerializer(serializers.ModelSerializer):
class Meta:
model = Order
fields = ['id', 'config', 'config_name', 'config_image', 'course', 'course_title', 'activity', 'activity_title', 'quantity', 'total_price', 'status', 'created_at', 'updated_at', 'wechat_trade_no',
fields = ['id', 'config', 'config_name', 'config_image', 'course', 'course_title', 'quantity', 'total_price', 'status', 'created_at', 'updated_at', 'wechat_trade_no',
'customer_name', 'phone_number', 'shipping_address', 'ref_code', 'salesperson_name', 'salesperson_code', 'courier_name', 'tracking_number']
read_only_fields = ['total_price', 'status', 'wechat_trade_no', 'created_at', 'updated_at']
extra_kwargs = {

View File

@@ -1,7 +1,8 @@
import React, { useState, useEffect } from 'react';
import { Form, Input, Button, Card, List, Tag, Typography, message, Space, Statistic, Divider, Modal, Descriptions, Tabs } from 'antd';
import { MobileOutlined, LockOutlined, SearchOutlined, CarOutlined, InboxOutlined, SafetyCertificateOutlined, CheckCircleOutlined, ClockCircleOutlined, CloseCircleOutlined, UserOutlined, EnvironmentOutlined, PhoneOutlined, CalendarOutlined } from '@ant-design/icons';
import api, { getMySignups } from '../api';
import { queryMyOrders, getMySignups } from '../api';
import { motion } from 'framer-motion';
import LoginModal from '../components/LoginModal';
import { useAuth } from '../context/AuthContext';
import { useNavigate } from 'react-router-dom';
@@ -40,6 +41,8 @@ const MyOrders = () => {
const handleQueryData = async () => {
setLoading(true);
try {
const { default: api } = await import('../api');
// Parallel fetch
const [ordersRes, activitiesRes] = await Promise.allSettled([
api.get('/orders/'),
@@ -341,7 +344,7 @@ const MyOrders = () => {
onLoginSuccess={(userData) => {
login(userData);
if (userData.phone_number) {
handleQueryData();
handleQueryOrders(userData.phone_number);
}
}}
/>

View File

@@ -28,7 +28,7 @@ const ActivityDetail = () => {
const [signupFormVisible, setSignupFormVisible] = useState(false);
const [paymentModalVisible, setPaymentModalVisible] = useState(false);
const [paymentInfo, setPaymentInfo] = useState(null);
const [isPaidSuccess, setIsPaidSuccess] = useState(false);
const [paymentStatus, setPaymentStatus] = useState('unpaid'); // 'unpaid' | 'paying' | 'paid'
const [form] = Form.useForm();
// Header animation: transparent to white with shadow
@@ -93,13 +93,17 @@ const ActivityDetail = () => {
const signUpMutation = useMutation({
mutationFn: (values) => signUpActivity(id, { signup_info: values || {} }),
onSuccess: (data) => {
onSuccess: (response) => {
// API returns axios response object, so we need to access .data
const data = response.data || response; // Fallback in case it was already unwrapped
console.log('Signup response:', data);
// 检查是否需要支付
if (data.payment_required) {
setPaymentInfo(data);
setIsPaidSuccess(false);
// 先关闭报名表单,确保层级正确
setSignupFormVisible(false);
setPaymentStatus('paying');
// 不关闭报名表单,直接打开支付弹窗
// 延迟一点点时间打开支付弹窗,避免状态更新冲突
setTimeout(() => {
setPaymentModalVisible(true);
@@ -110,6 +114,7 @@ const ActivityDetail = () => {
message.success('报名成功!');
setSignupFormVisible(false);
setPaymentStatus('paid'); // In case it was free but we track it
confetti({
particleCount: 150,
spread: 70,
@@ -127,13 +132,15 @@ const ActivityDetail = () => {
// Polling for payment status
useEffect(() => {
let timer;
if (paymentModalVisible && paymentInfo?.order_id && !isPaidSuccess) {
if (paymentModalVisible && paymentInfo?.order_id) {
timer = setInterval(async () => {
try {
const response = await queryOrderStatus(paymentInfo.order_id);
if (response.data.status === 'paid') {
setIsPaidSuccess(true);
message.success('支付成功,报名已确认!');
message.success('支付成功,请点击“完成报名”!');
setPaymentModalVisible(false);
setPaymentInfo(null);
setPaymentStatus('paid');
// Trigger success effects
confetti({
@@ -153,7 +160,7 @@ const ActivityDetail = () => {
}, 3000);
}
return () => clearInterval(timer);
}, [paymentModalVisible, paymentInfo, id, queryClient, isPaidSuccess]);
}, [paymentModalVisible, paymentInfo, id, queryClient]);
const handleShare = async () => {
const url = window.location.href;
@@ -180,11 +187,15 @@ const ActivityDetail = () => {
return;
}
// Check if we need to collect info
if (activity.signup_form_config && activity.signup_form_config.length > 0) {
setPaymentStatus('unpaid');
setPaymentInfo(null);
// Check if we need to collect info OR if it's a paid activity
// We want to use the modal for payment flow as well
if ((activity.signup_form_config && activity.signup_form_config.length > 0) || (activity.is_paid && activity.price > 0)) {
setSignupFormVisible(true);
} else {
// Direct signup if no info needed
// Direct signup if no info needed and free
signUpMutation.mutate({});
}
};
@@ -395,9 +406,36 @@ const ActivityDetail = () => {
title="填写报名信息"
open={signupFormVisible}
onCancel={() => setSignupFormVisible(false)}
onOk={form.submit}
okText={activity?.is_paid && activity?.price > 0 ? `支付 ¥${activity.price}` : '提交报名'}
confirmLoading={signUpMutation.isPending}
footer={[
<Button key="cancel" onClick={() => setSignupFormVisible(false)}>
取消
</Button>,
(activity?.is_paid && activity?.price > 0) ? (
<React.Fragment key="paid-actions">
<Button
key="pay"
type="primary"
onClick={form.submit}
loading={signUpMutation.isPending && paymentStatus === 'unpaid'}
disabled={paymentStatus === 'paid'}
>
{paymentStatus === 'paid' ? '已支付' : `支付 ¥${activity.price}`}
</Button>
<Button
key="complete"
type="primary"
onClick={() => setSignupFormVisible(false)}
disabled={paymentStatus !== 'paid'}
>
完成报名
</Button>
</React.Fragment>
) : (
<Button key="submit" type="primary" onClick={form.submit} loading={signUpMutation.isPending}>
提交报名
</Button>
)
]}
destroyOnHidden
>
<Form form={form} onFinish={handleFormSubmit} layout="vertical">
@@ -476,27 +514,10 @@ const ActivityDetail = () => {
</Form>
</Modal>
<Modal
title={isPaidSuccess ? "支付成功" : "微信支付"}
title="微信支付"
open={paymentModalVisible}
onCancel={() => {
setPaymentModalVisible(false);
if (isPaidSuccess) {
setPaymentInfo(null);
}
}}
footer={[
<Button
key="ok"
type="primary"
disabled={!isPaidSuccess}
onClick={() => {
setPaymentModalVisible(false);
setPaymentInfo(null);
}}
>
{isPaidSuccess ? '完成' : '等待支付...'}
</Button>
]}
onCancel={() => setPaymentModalVisible(false)}
footer={null}
destroyOnHidden
width={360}
zIndex={1001} // 确保层级高于其他弹窗
@@ -504,26 +525,20 @@ const ActivityDetail = () => {
<div style={{ textAlign: 'center', padding: '20px 0' }}>
{paymentInfo?.code_url ? (
<>
{!isPaidSuccess ? (
<>
<div style={{ background: '#fff', padding: 10, display: 'inline-block' }}>
<QRCodeSVG value={paymentInfo.code_url} size={200} />
</div>
<p style={{ marginTop: 20, fontSize: 18, fontWeight: 'bold' }}>¥{paymentInfo.price}</p>
<p style={{ color: '#666' }}>请使用微信扫一扫支付</p>
</>
) : (
<div style={{ padding: '20px 0' }}>
<div style={{ fontSize: 48, color: '#52c41a', marginBottom: 16 }}>
<i className="anticon anticon-check-circle"><svg viewBox="64 64 896 896" focusable="false" data-icon="check-circle" width="1em" height="1em" fill="currentColor" aria-hidden="true"><path d="M512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64zm193.5 301.7l-210.6 292a31.8 31.8 0 01-51.7 0L318.5 484.9c-3.8-5.3 0-12.7 6.5-12.7h46.9c10.2 0 19.9 4.9 25.9 13.3l71.2 98.8 157.2-218c6-8.3 15.6-13.3 25.9-13.3H699c6.5 0 10.3 7.4 6.5 12.7z"></path></svg></i>
</div>
<p style={{ fontSize: 16, fontWeight: 'bold' }}>支付成功</p>
</div>
)}
<div style={{ background: '#fff', padding: 10, display: 'inline-block' }}>
<QRCodeSVG value={paymentInfo.code_url} size={200} />
</div>
<p style={{ marginTop: 20, fontSize: 18, fontWeight: 'bold' }}>¥{paymentInfo.price}</p>
<p style={{ color: '#666' }}>请使用微信扫一扫支付</p>
</>
) : (
<Spin tip="正在生成二维码..." />
)}
<div style={{ marginTop: 20 }}>
<Button type="primary" onClick={() => window.location.reload()}>
我已支付
</Button>
</div>
</div>
</Modal>