报名表单

This commit is contained in:
jeremygan2021
2026-02-23 15:07:55 +08:00
parent 6a391c5eab
commit db7a3bd000
11 changed files with 289 additions and 22 deletions

View File

@@ -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('<pre style="white-space: pre-wrap; word-break: break-all;">{}</pre>', 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('<a href="/admin/shop/order/{}/change/">Order #{}</a>', 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')

View File

@@ -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='关联订单'),
),
]

View File

@@ -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}"

View File

@@ -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):

View File

@@ -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)

View File

@@ -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'),
}

View File

@@ -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='所选活动'),
),
]

View File

@@ -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="订单状态")

View File

@@ -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