Files
market_page/backend/community/views.py
jeremygan2021 059b57d86d
All checks were successful
Deploy to Server / deploy (push) Successful in 36s
报名表单
2026-02-23 15:14:17 +08:00

290 lines
12 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
from rest_framework import viewsets, status, mixins, parsers, filters
from django_filters.rest_framework import DjangoFilterBackend
from rest_framework.decorators import action
from rest_framework.response import Response
from rest_framework import serializers, permissions
from django.core.signing import TimestampSigner, BadSignature, SignatureExpired
from django.utils import timezone
from django.db import models
from drf_spectacular.utils import extend_schema
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
from .permissions import IsAuthorOrReadOnly
class ActivityViewSet(viewsets.ReadOnlyModelViewSet):
"""
社区活动接口
"""
queryset = Activity.objects.filter(is_active=True).order_by('-created_at')
serializer_class = ActivitySerializer
def retrieve(self, request, *args, **kwargs):
instance = self.get_object()
serializer = self.get_serializer(instance)
# Debug print to verify data
print(f"DEBUG: Activity {instance.title} current_signups: {instance.current_signups}")
return Response(serializer.data)
@extend_schema(summary="报名活动")
@action(detail=True, methods=['post'])
def signup(self, request, pk=None):
user = get_current_wechat_user(request)
if not user:
return Response({'error': '请先登录'}, status=401)
activity = self.get_object()
# 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)
current_count = activity.signups.exclude(status='cancelled').count()
if current_count >= activity.max_participants:
return Response({'error': '活动名额已满'}, status=400)
# Get signup info
signup_info = request.data.get('signup_info', {})
# Validate signup info
effective_config = activity.signup_form_config
if not effective_config:
effective_config = []
if activity.ask_name:
effective_config.append({"name": "name", "label": "姓名", "type": "text", "required": True})
if activity.ask_phone:
effective_config.append({"name": "phone", "label": "手机号", "type": "number", "required": True})
if activity.ask_wechat:
effective_config.append({"name": "wechat", "label": "微信号", "type": "text", "required": True})
if activity.ask_company:
effective_config.append({"name": "company", "label": "公司/机构", "type": "text", "required": False})
if effective_config:
required_fields = [f['name'] for f in effective_config if f.get('required')]
for field in required_fields:
val = signup_info.get(field)
if val is None or (isinstance(val, str) and not val.strip()):
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:
# 优先从报名信息获取联系方式
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 ''
order = Order.objects.create(
wechat_user=user,
activity=activity,
total_price=activity.price,
status='pending',
quantity=1,
customer_name=contact_name,
phone_number=contact_phone,
shipping_address=activity.location 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,
status='confirmed'
)
serializer = ActivitySignupSerializer(signup)
return Response(serializer.data, status=201)
@extend_schema(summary="我的报名记录")
@action(detail=False, methods=['get'])
def my_signups(self, request):
user = get_current_wechat_user(request)
if not user:
return Response({'error': '请先登录'}, status=401)
signups = ActivitySignup.objects.filter(user=user).order_by('-signup_time')
serializer = ActivitySignupSerializer(signups, many=True)
return Response(serializer.data)
class TopicViewSet(viewsets.ModelViewSet):
"""
技术论坛帖子接口
"""
queryset = Topic.objects.all()
serializer_class = TopicSerializer
permission_classes = [IsAuthorOrReadOnly]
filter_backends = [filters.SearchFilter, filters.OrderingFilter, DjangoFilterBackend]
search_fields = ['title', 'content']
filterset_fields = ['category', 'is_pinned']
ordering_fields = ['created_at', 'view_count']
ordering = ['-is_pinned', '-created_at']
def perform_create(self, serializer):
user = get_current_wechat_user(self.request)
# Auth check is done in create or permission, but here we need user for save
if user:
serializer.save(author=user)
def create(self, request, *args, **kwargs):
user = get_current_wechat_user(request)
if not user:
return Response({'error': '请先登录'}, status=401)
return super().create(request, *args, **kwargs)
def retrieve(self, request, *args, **kwargs):
instance = self.get_object()
instance.view_count += 1
instance.save(update_fields=['view_count'])
serializer = self.get_serializer(instance)
return Response(serializer.data)
class ReplyViewSet(viewsets.ModelViewSet):
"""
帖子回复接口
"""
queryset = Reply.objects.all()
serializer_class = ReplySerializer
permission_classes = [IsAuthorOrReadOnly]
def perform_create(self, serializer):
user = get_current_wechat_user(self.request)
if user:
serializer.save(author=user)
def create(self, request, *args, **kwargs):
user = get_current_wechat_user(request)
if not user:
return Response({'error': '请先登录'}, status=401)
return super().create(request, *args, **kwargs)
import requests
class TopicMediaViewSet(viewsets.ViewSet):
"""
论坛多媒体资源上传接口 (代理到外部OSS服务)
"""
permission_classes = [] # 内部鉴权
parser_classes = [parsers.MultiPartParser, parsers.FormParser]
@extend_schema(summary="上传媒体文件 (返回URL用于Markdown)")
def create(self, request, *args, **kwargs):
user = get_current_wechat_user(request)
if not user:
return Response({'error': '请先登录'}, status=401)
file_obj = request.FILES.get('file')
if not file_obj:
return Response({'error': '未提供文件'}, status=400)
# 转发到外部 OSS 上传服务
upload_url = "https://data.tangledup-ai.com/upload?folder=uploads%2Fmarket%2Fforum_image"
files = {'file': (file_obj.name, file_obj, file_obj.content_type)}
try:
# 这里的 headers 不需要 Content-Typerequests 会自动设置 multipart/form-data
response = requests.post(upload_url, files=files, timeout=30)
if response.status_code == 200:
data = response.json()
if data.get('success'):
# Create TopicMedia record
media_type = 'image' if 'image' in file_obj.content_type else 'video'
media_obj = TopicMedia.objects.create(
file_url=data.get('file_url'),
media_type=media_type,
# topic will be associated later
)
# 返回符合前端预期的格式
return Response({
'id': media_obj.id, # Return real DB ID
'file': media_obj.file_url,
'media_type': media_obj.media_type,
'created_at': media_obj.created_at
})
else:
return Response({'error': '外部服务上传失败', 'detail': data}, status=400)
else:
return Response({'error': f'上传服务响应错误: {response.status_code}', 'detail': response.text}, status=502)
except Exception as e:
return Response({'error': str(e)}, status=500)
class AnnouncementViewSet(viewsets.ReadOnlyModelViewSet):
"""
社区公告接口
"""
queryset = Announcement.objects.all()
serializer_class = AnnouncementSerializer
permission_classes = [permissions.AllowAny]
def get_queryset(self):
now = timezone.now()
qs = Announcement.objects.filter(is_active=True)
# Filter by start_time (if set, must be <= now)
qs = qs.filter(models.Q(start_time__isnull=True) | models.Q(start_time__lte=now))
# Filter by end_time (if set, must be >= now)
qs = qs.filter(models.Q(end_time__isnull=True) | models.Q(end_time__gte=now))
return qs.order_by('-is_pinned', '-priority', '-created_at')