diff --git a/backend/community/admin.py b/backend/community/admin.py index 0c24fbf..9b858b3 100644 --- a/backend/community/admin.py +++ b/backend/community/admin.py @@ -39,8 +39,8 @@ class ActivityAdmin(ModelAdmin): ('基本信息', { 'fields': ('title', 'description', 'banner', 'banner_url', 'is_active') }), - ('时间与地点', { - 'fields': ('start_time', 'end_time', 'location'), + ('费用与时间', { + 'fields': ('is_paid', 'price', 'start_time', 'end_time', 'location'), 'classes': ('tab',) }), ('报名设置', { @@ -63,21 +63,34 @@ class ActivityAdmin(ModelAdmin): @admin.register(ActivitySignup) class ActivitySignupAdmin(ModelAdmin): - list_display = ('activity', 'user', 'signup_time', 'status_label') + list_display = ('activity', 'user', 'signup_time', 'status_label', 'order_link') list_filter = ('status', 'signup_time', 'activity') search_fields = ('user__nickname', 'activity__title') autocomplete_fields = ['activity', 'user'] fieldsets = ( ('报名详情', { - 'fields': ('activity', 'user', 'status') + 'fields': ('activity', 'user', 'status', 'order', 'signup_info_display') }), ('时间信息', { 'fields': ('signup_time',), 'classes': ('collapse',) }), ) - readonly_fields = ('signup_time',) + readonly_fields = ('signup_time', 'signup_info_display') + + @display(description="报名信息") + def signup_info_display(self, obj): + import json + if not obj.signup_info: + return "无" + + try: + # Format JSON nicely + formatted_json = json.dumps(obj.signup_info, indent=2, ensure_ascii=False) + return format_html('
{}', formatted_json)
+ except:
+ return str(obj.signup_info)
@display(
description="状态",
@@ -90,6 +103,12 @@ class ActivitySignupAdmin(ModelAdmin):
def status_label(self, obj):
return obj.status
+ @display(description="关联订单")
+ def order_link(self, obj):
+ if obj.order:
+ return format_html('Order #{}', obj.order.id, obj.order.id)
+ return "-"
+
@admin.register(Topic)
class TopicAdmin(ModelAdmin):
list_display = ('title', 'category', 'author', 'get_related_item', 'reply_count', 'view_count', 'is_pinned', 'created_at')
diff --git a/backend/community/migrations/0010_activity_is_paid_activity_price_activitysignup_order.py b/backend/community/migrations/0010_activity_is_paid_activity_price_activitysignup_order.py
new file mode 100644
index 0000000..ec6b871
--- /dev/null
+++ b/backend/community/migrations/0010_activity_is_paid_activity_price_activitysignup_order.py
@@ -0,0 +1,30 @@
+# Generated by Django 6.0.1 on 2026-02-23 07:04
+
+import django.db.models.deletion
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('community', '0009_activity_ask_company_activity_ask_name_and_more'),
+ ('shop', '0031_adminphonenumber'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='activity',
+ name='is_paid',
+ field=models.BooleanField(default=False, verbose_name='是否收费'),
+ ),
+ migrations.AddField(
+ model_name='activity',
+ name='price',
+ field=models.DecimalField(decimal_places=2, default=0, max_digits=10, verbose_name='报名费用'),
+ ),
+ migrations.AddField(
+ model_name='activitysignup',
+ name='order',
+ field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='activity_signups', to='shop.order', verbose_name='关联订单'),
+ ),
+ ]
diff --git a/backend/community/models.py b/backend/community/models.py
index af2b612..71fa95f 100644
--- a/backend/community/models.py
+++ b/backend/community/models.py
@@ -13,6 +13,11 @@ class Activity(models.Model):
end_time = models.DateTimeField(verbose_name="结束时间")
location = models.CharField(max_length=100, verbose_name="活动地点")
max_participants = models.IntegerField(default=50, verbose_name="最大报名人数")
+
+ # 费用设置
+ is_paid = models.BooleanField(default=False, verbose_name="是否收费")
+ price = models.DecimalField(max_digits=10, decimal_places=2, default=0, verbose_name="报名费用")
+
is_active = models.BooleanField(default=True, verbose_name="是否启用")
# 常用报名信息开关
@@ -81,6 +86,9 @@ class ActivitySignup(models.Model):
blank=True
)
status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='confirmed', verbose_name="状态")
+
+ # 关联订单(针对付费活动)
+ order = models.ForeignKey('shop.Order', on_delete=models.SET_NULL, null=True, blank=True, verbose_name="关联订单", related_name='activity_signups')
def __str__(self):
return f"{self.user.nickname} - {self.activity.title}"
diff --git a/backend/community/serializers.py b/backend/community/serializers.py
index 647f34c..ef3abfd 100644
--- a/backend/community/serializers.py
+++ b/backend/community/serializers.py
@@ -8,18 +8,23 @@ class ActivitySerializer(serializers.ModelSerializer):
signup_form_config = serializers.SerializerMethodField()
current_signups = serializers.IntegerField(read_only=True)
has_signed_up = serializers.SerializerMethodField()
+ is_signed_up = serializers.SerializerMethodField()
class Meta:
model = Activity
fields = '__all__'
def get_has_signed_up(self, obj):
+ return self.get_is_signed_up(obj)
+
+ 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:
- return obj.signups.filter(user=user).exists()
+ # Check if there is a valid signup (not cancelled)
+ return obj.signups.filter(user=user).exclude(status='cancelled').exists()
return False
def get_signup_form_config(self, obj):
diff --git a/backend/community/views.py b/backend/community/views.py
index 10186c7..93ad050 100644
--- a/backend/community/views.py
+++ b/backend/community/views.py
@@ -8,7 +8,8 @@ from django.utils import timezone
from django.db import models
from drf_spectacular.utils import extend_schema
-from shop.models import WeChatUser
+from shop.models import WeChatUser, Order
+from shop.views import get_wechat_pay_client
from .models import Activity, ActivitySignup, Topic, Reply, TopicMedia, Announcement
from .serializers import ActivitySerializer, ActivitySignupSerializer, TopicSerializer, ReplySerializer, TopicMediaSerializer, AnnouncementSerializer
from .utils import get_current_wechat_user
@@ -37,8 +38,9 @@ class ActivityViewSet(viewsets.ReadOnlyModelViewSet):
activity = self.get_object()
- # Check if already signed up
- if ActivitySignup.objects.filter(activity=activity, user=user).exists():
+ # Check if already signed up (and not cancelled)
+ existing_signup = ActivitySignup.objects.filter(activity=activity, user=user).exclude(status='cancelled').first()
+ if existing_signup:
return Response({'error': '您已报名该活动'}, status=400)
# Check limit (exclude cancelled)
@@ -49,12 +51,7 @@ class ActivityViewSet(viewsets.ReadOnlyModelViewSet):
# Get signup info
signup_info = request.data.get('signup_info', {})
- # Basic validation
- # Re-fetch the config from the object method or serializer logic if needed,
- # but here we can just use the serializer's method to get the effective config.
- # However, accessing serializer method from view is tricky without instantiating.
- # Let's replicate the logic or rely on the fact that we can construct it.
-
+ # Validate signup info
effective_config = activity.signup_form_config
if not effective_config:
effective_config = []
@@ -70,17 +67,87 @@ class ActivityViewSet(viewsets.ReadOnlyModelViewSet):
if effective_config:
required_fields = [f['name'] for f in effective_config if f.get('required')]
for field in required_fields:
- # Simple check: field exists and is not empty string (if it's a string)
val = signup_info.get(field)
if val is None or (isinstance(val, str) and not val.strip()):
- # Try to find label for better error message
label = next((f['label'] for f in effective_config if f['name'] == field), field)
return Response({'error': f'请填写: {label}'}, status=400)
+
+ # Handle Payment Logic
+ if activity.is_paid and activity.price > 0:
+ import time
+ from wechatpayv3 import WeChatPayType
+ # Create Order
+ # Check if there is a pending order
+ pending_order = Order.objects.filter(
+ wechat_user=user,
+ activity=activity,
+ status='pending'
+ ).first()
+
+ if pending_order:
+ order = pending_order
+ # Update info if needed? Maybe not.
+ else:
+ order = Order.objects.create(
+ wechat_user=user,
+ activity=activity,
+ total_price=activity.price,
+ status='pending',
+ quantity=1,
+ customer_name=signup_info.get('name') or user.nickname or 'Activity User',
+ phone_number=signup_info.get('phone') or user.phone_number or '',
+ )
+
+ # Generate Pay Code
+ out_trade_no = f"ACT{activity.id}U{user.id}T{int(time.time())}"
+ order.out_trade_no = out_trade_no
+ order.save()
+
+ wxpay, error_msg = get_wechat_pay_client(pay_type=WeChatPayType.NATIVE)
+ if not wxpay:
+ return Response({'error': f'支付配置错误: {error_msg}'}, status=500)
+
+ code, message = wxpay.pay(
+ description=f"报名: {activity.title}",
+ out_trade_no=out_trade_no,
+ amount={
+ 'total': int(activity.price * 100),
+ 'currency': 'CNY'
+ },
+ notify_url=wxpay._notify_url
+ )
+
+ import json
+ result = json.loads(message)
+ if code in range(200, 300):
+ code_url = result.get('code_url')
+
+ # Create a pending signup record so we can update it later
+ ActivitySignup.objects.create(
+ activity=activity,
+ user=user,
+ signup_info=signup_info,
+ status='pending',
+ order=order
+ )
+
+ return Response({
+ 'payment_required': True,
+ 'code_url': code_url,
+ 'order_id': order.id,
+ 'price': activity.price,
+ 'message': '请完成支付'
+ }, status=200)
+ else:
+ return Response({'error': '支付接口调用失败', 'detail': result}, status=500)
+
+ # Free Activity Signup
signup = ActivitySignup.objects.create(
activity=activity,
user=user,
- signup_info=signup_info
+ signup_info=signup_info,
+ status='confirmed'
)
serializer = ActivitySignupSerializer(signup)
return Response(serializer.data, status=201)
diff --git a/backend/config/settings.py b/backend/config/settings.py
index b44719b..1bb69af 100644
--- a/backend/config/settings.py
+++ b/backend/config/settings.py
@@ -109,7 +109,7 @@ if DB_HOST:
'USER': os.environ.get('DB_USER', 'market'),
'PASSWORD': os.environ.get('DB_PASSWORD', '123market'),
'HOST': DB_HOST,
- # 'PORT': os.environ.get('DB_PORT', '6433'),
+ #'PORT': os.environ.get('DB_PORT', '6433'),
'PORT': os.environ.get('DB_PORT', '5432'),
}
diff --git a/backend/shop/migrations/0032_order_activity.py b/backend/shop/migrations/0032_order_activity.py
new file mode 100644
index 0000000..48893e1
--- /dev/null
+++ b/backend/shop/migrations/0032_order_activity.py
@@ -0,0 +1,20 @@
+# Generated by Django 6.0.1 on 2026-02-23 07:04
+
+import django.db.models.deletion
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('community', '0010_activity_is_paid_activity_price_activitysignup_order'),
+ ('shop', '0031_adminphonenumber'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='order',
+ name='activity',
+ field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='orders', to='community.activity', verbose_name='所选活动'),
+ ),
+ ]
diff --git a/backend/shop/models.py b/backend/shop/models.py
index 42346c7..3447f8c 100644
--- a/backend/shop/models.py
+++ b/backend/shop/models.py
@@ -230,6 +230,7 @@ class Order(models.Model):
config = models.ForeignKey(ESP32Config, on_delete=models.CASCADE, verbose_name="所选配置", null=True, blank=True, related_name='orders')
course = models.ForeignKey('VCCourse', on_delete=models.SET_NULL, null=True, blank=True, verbose_name="所选课程", related_name='orders')
+ activity = models.ForeignKey('community.Activity', on_delete=models.SET_NULL, null=True, blank=True, verbose_name="所选活动", related_name='orders')
quantity = models.IntegerField(default=1, verbose_name="数量")
total_price = models.DecimalField(max_digits=10, decimal_places=2, verbose_name="总价")
status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='pending', verbose_name="订单状态")
diff --git a/backend/shop/views.py b/backend/shop/views.py
index aca8d2d..8e95ab9 100644
--- a/backend/shop/views.py
+++ b/backend/shop/views.py
@@ -508,6 +508,19 @@ def payment_finish(request):
order.save()
print(f"订单 {order.id} 状态已更新")
+ # Handle Activity Signup
+ if hasattr(order, 'activity') and order.activity:
+ try:
+ # Use string import to avoid circular dependency at module level
+ from community.models import ActivitySignup
+ signup = ActivitySignup.objects.filter(order=order).first()
+ if signup:
+ signup.status = 'confirmed'
+ signup.save()
+ print(f"活动报名状态已更新: {signup.id}")
+ except Exception as e:
+ print(f"更新活动报名状态失败: {str(e)}")
+
# 计算佣金 (旧版销售员系统)
try:
salesperson = order.salesperson
diff --git a/frontend/src/pages/MyOrders.jsx b/frontend/src/pages/MyOrders.jsx
index 24318e5..90722a4 100644
--- a/frontend/src/pages/MyOrders.jsx
+++ b/frontend/src/pages/MyOrders.jsx
@@ -15,6 +15,8 @@ const MyOrders = () => {
const [activities, setActivities] = useState([]);
const [modalVisible, setModalVisible] = useState(false);
const [currentOrder, setCurrentOrder] = useState(null);
+ const [signupInfoModalVisible, setSignupInfoModalVisible] = useState(false);
+ const [currentSignupInfo, setCurrentSignupInfo] = useState(null);
const [loginVisible, setLoginVisible] = useState(false);
const navigate = useNavigate();
@@ -31,6 +33,11 @@ const MyOrders = () => {
setModalVisible(true);
};
+ const showSignupInfo = (info) => {
+ setCurrentSignupInfo(info);
+ setSignupInfoModalVisible(true);
+ };
+
const handleQueryData = async () => {
setLoading(true);
try {
@@ -251,7 +258,14 @@ const MyOrders = () => {
¥{paymentInfo.price}
+请使用微信扫一扫支付
+ > + ) : ( +