diff --git a/backend/community/models.py b/backend/community/models.py index 71fa95f..d810aa3 100644 --- a/backend/community/models.py +++ b/backend/community/models.py @@ -55,9 +55,9 @@ class Activity(models.Model): @property def current_signups(self): """ - 当前有效报名人数 + 当前有效报名人数(仅统计已确认/已支付的报名) """ - return self.signups.exclude(status='cancelled').count() + return self.signups.filter(status='confirmed').count() def __str__(self): return self.title diff --git a/backend/community/serializers.py b/backend/community/serializers.py index ef3abfd..0fb9498 100644 --- a/backend/community/serializers.py +++ b/backend/community/serializers.py @@ -9,6 +9,7 @@ class ActivitySerializer(serializers.ModelSerializer): current_signups = serializers.IntegerField(read_only=True) has_signed_up = serializers.SerializerMethodField() is_signed_up = serializers.SerializerMethodField() + my_signup_status = serializers.SerializerMethodField() class Meta: model = Activity @@ -17,14 +18,25 @@ class ActivitySerializer(serializers.ModelSerializer): def get_has_signed_up(self, obj): return self.get_is_signed_up(obj) + def get_my_signup_status(self, obj): + request = self.context.get('request') + if not request: + return None + user = get_current_wechat_user(request) + if user: + # Return the status of the non-cancelled signup + signup = obj.signups.filter(user=user).exclude(status='cancelled').first() + return signup.status if signup else None + return None + def get_is_signed_up(self, obj): request = self.context.get('request') if not request: return False user = get_current_wechat_user(request) if user: - # Check if there is a valid signup (not cancelled) - return obj.signups.filter(user=user).exclude(status='cancelled').exists() + # Check if there is a valid signup (only confirmed counts) + return obj.signups.filter(user=user, status='confirmed').exists() return False def get_signup_form_config(self, obj): diff --git a/backend/community/views.py b/backend/community/views.py index 17d4f7b..15b9be0 100644 --- a/backend/community/views.py +++ b/backend/community/views.py @@ -93,6 +93,11 @@ class ActivityViewSet(viewsets.ReadOnlyModelViewSet): contact_phone = signup_info.get('phone') or user.phone_number or '' if contact_name: order.customer_name = contact_name if contact_phone: order.phone_number = contact_phone + + # Ensure activity is linked + if not order.activity: + order.activity = activity + order.save() if not order: @@ -105,6 +110,9 @@ class ActivityViewSet(viewsets.ReadOnlyModelViewSet): if pending_order: order = pending_order + # Ensure shipping address is up-to-date + order.shipping_address = activity.location or '线下活动' + order.save() else: contact_name = signup_info.get('name') or signup_info.get('participant_name') or user.nickname or 'Activity User' contact_phone = signup_info.get('phone') or user.phone_number or '' @@ -136,7 +144,8 @@ class ActivityViewSet(viewsets.ReadOnlyModelViewSet): 'total': int(activity.price * 100), 'currency': 'CNY' }, - notify_url=wxpay._notify_url + notify_url=wxpay._notify_url, + attach=f'{{"type":"activity","activity_id":{activity.id}}}' ) import json diff --git a/backend/config/settings.py b/backend/config/settings.py index 1bb69af..24ceae8 100644 --- a/backend/config/settings.py +++ b/backend/config/settings.py @@ -100,7 +100,7 @@ DATABASES = { } # 从环境变量获取数据库配置 (Docker 环境会自动注入这些变量) -# DB_HOST = os.environ.get('DB_HOST', '121.43.104.161') +#DB_HOST = os.environ.get('DB_HOST', '121.43.104.161') DB_HOST = os.environ.get('DB_HOST', '6.6.6.66') if DB_HOST: DATABASES['default'] = { diff --git a/backend/shop/admin.py b/backend/shop/admin.py index 7817c08..04e8ccf 100644 --- a/backend/shop/admin.py +++ b/backend/shop/admin.py @@ -373,12 +373,14 @@ class OrderAdmin(ModelAdmin): return f"[硬件] {obj.config.name}" if obj.course: return f"[课程] {obj.course.title}" + if obj.activity: + return f"[活动] {obj.activity.title}" return "未知商品" get_item_name.short_description = "购买商品" fieldsets = ( ('订单信息', { - 'fields': ('config', 'course', 'quantity', 'total_price', 'status', 'created_at') + 'fields': ('config', 'course', 'activity', 'quantity', 'total_price', 'status', 'created_at') }), ('客户信息', { 'fields': ('customer_name', 'phone_number', 'shipping_address', 'wechat_user') diff --git a/backend/shop/serializers.py b/backend/shop/serializers.py index 7afbebb..173a07b 100644 --- a/backend/shop/serializers.py +++ b/backend/shop/serializers.py @@ -207,6 +207,7 @@ 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) @@ -215,7 +216,7 @@ class OrderSerializer(serializers.ModelSerializer): class Meta: model = Order - fields = ['id', 'config', 'config_name', 'config_image', 'course', 'course_title', 'quantity', 'total_price', 'status', 'created_at', 'updated_at', 'wechat_trade_no', + fields = ['id', 'config', 'config_name', 'config_image', 'course', 'course_title', 'activity', 'activity_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 = { @@ -230,11 +231,14 @@ class OrderSerializer(serializers.ModelSerializer): config = data.get('config') course = data.get('course') + activity = data.get('activity') - if not config and not course: - raise serializers.ValidationError("必须选择一种商品(硬件配置或课程)") + if not config and not course and not activity: + raise serializers.ValidationError("必须选择一种商品(硬件配置、课程或活动)") - if config and course: + # Count how many types are selected + selected_types = sum([bool(config), bool(course), bool(activity)]) + if selected_types > 1: raise serializers.ValidationError("一次只能购买一种类型的商品") if config and not data.get('shipping_address'): @@ -255,6 +259,12 @@ class OrderSerializer(serializers.ModelSerializer): return obj.course.cover_image_url if obj.course.cover_image: return obj.course.cover_image.url + elif obj.activity: + # Use activity.display_banner_url logic + if obj.activity.banner: + return obj.activity.banner.url + if obj.activity.banner_url: + return obj.activity.banner_url return None def create(self, validated_data): @@ -263,6 +273,7 @@ class OrderSerializer(serializers.ModelSerializer): """ config = validated_data.get('config') course = validated_data.get('course') + activity = validated_data.get('activity') quantity = validated_data.get('quantity', 1) ref_code = validated_data.pop('ref_code', None) @@ -270,6 +281,8 @@ class OrderSerializer(serializers.ModelSerializer): validated_data['total_price'] = config.price * quantity elif course: validated_data['total_price'] = course.price * quantity + elif activity: + validated_data['total_price'] = activity.price * quantity # 尝试关联销售员或分销员 if ref_code: diff --git a/frontend/src/pages/activity/Detail.jsx b/frontend/src/pages/activity/Detail.jsx index 119cb95..8ff0691 100644 --- a/frontend/src/pages/activity/Detail.jsx +++ b/frontend/src/pages/activity/Detail.jsx @@ -387,9 +387,11 @@ const ActivityDetail = () => { variants={buttonTap} whileTap="tap" onClick={handleSignUp} - disabled={signUpMutation.isPending || activity.is_signed_up} + disabled={signUpMutation.isPending || activity.is_signed_up || (activity.my_signup_status === 'pending' && !activity.is_paid)} > - {signUpMutation.isPending ? '提交中...' : activity.is_signed_up ? '已报名' : '立即报名'} + {signUpMutation.isPending ? '提交中...' : + activity.is_signed_up ? '已报名' : + (activity.my_signup_status === 'pending' ? (activity.is_paid ? '去支付' : '审核中') : '立即报名')} diff --git a/miniprogram/src/pages/order/detail.tsx b/miniprogram/src/pages/order/detail.tsx index 109fbd3..fd21d3c 100644 --- a/miniprogram/src/pages/order/detail.tsx +++ b/miniprogram/src/pages/order/detail.tsx @@ -74,7 +74,7 @@ export default function OrderDetail() { 商品信息 商品名称 - {order.config_name || order.course_title} + {order.config_name || order.course_title || (order.activity_title ? `报名活动:${order.activity_title}` : '未知商品')} 数量 diff --git a/miniprogram/src/pages/order/list.tsx b/miniprogram/src/pages/order/list.tsx index 165f4e4..c42d40b 100644 --- a/miniprogram/src/pages/order/list.tsx +++ b/miniprogram/src/pages/order/list.tsx @@ -40,7 +40,9 @@ export default function OrderList() { - {order.config_name} + + {order.config_name || order.course_title || (order.activity_title ? `报名活动:${order.activity_title}` : '未知商品')} + x {order.quantity} diff --git a/miniprogram/src/subpackages/forum/activity/detail.tsx b/miniprogram/src/subpackages/forum/activity/detail.tsx index 0804ea2..18cbf62 100644 --- a/miniprogram/src/subpackages/forum/activity/detail.tsx +++ b/miniprogram/src/subpackages/forum/activity/detail.tsx @@ -145,7 +145,16 @@ const ActivityDetail = () => { const isFull = activity.max_participants > 0 && (activity.current_signups || 0) >= activity.max_participants const isEnded = new Date(activity.end_time) < new Date() - const canSignup = activity.is_active && !isFull && !isEnded && !activity.has_signed_up + + const hasConfirmed = activity.has_signed_up + const isPending = activity.my_signup_status === 'pending' + const isPaid = activity.is_paid + + const canSignup = activity.is_active && !isEnded && + ( + (!hasConfirmed && !isPending && !isFull) || + (isPending && isPaid) + ) return ( @@ -223,9 +232,11 @@ const ActivityDetail = () => { onClick={handleSignup} > {submitting ? '提交中...' : ( - activity.has_signed_up ? '您已报名' : ( - isEnded ? '活动已结束' : ( - isFull ? '名额已满' : '立即报名' + hasConfirmed ? '您已报名' : ( + isPending ? (isPaid ? '去支付' : '审核中') : ( + isEnded ? '活动已结束' : ( + isFull ? '名额已满' : '立即报名' + ) ) ) )}