Files
market_page/backend/community/views.py
jeremygan2021 cb66dd92c3
All checks were successful
Deploy to Server / deploy (push) Successful in 25s
sms 活动短信
2026-02-23 17:39:01 +08:00

372 lines
16 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 get_queryset(self):
qs = super().get_queryset()
# list 接口过滤 is_visible=True
if self.action == 'list':
qs = qs.filter(is_visible=True)
return qs
def retrieve(self, request, *args, **kwargs):
instance = self.get_object()
# Sync status for current user
user = get_current_wechat_user(request)
if user:
# Use filter to avoid exception if multiple exist (though unique_together constraint exists)
signup = instance.signups.filter(user=user).exclude(status='cancelled').first()
if signup:
has_changed = signup.check_payment_status()
if has_changed:
print(f"DEBUG: Synced signup status for user {user.id} activity {instance.id}")
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()
# 1. Check confirmed signup
if ActivitySignup.objects.filter(activity=activity, user=user, status='confirmed').exists():
return Response({'error': '您已报名该活动'}, status=400)
# 2. Get pending signup (for retry)
pending_signup = ActivitySignup.objects.filter(activity=activity, user=user, status='pending').first()
# 3. Check limit (exclude cancelled, exclude current pending)
query = activity.signups.exclude(status='cancelled')
if pending_signup:
query = query.exclude(id=pending_signup.id)
if query.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 or Get Order
order = None
if pending_signup and pending_signup.order:
# Reuse existing order if it's pending
if pending_signup.order.status == 'pending':
order = pending_signup.order
# Update contact info if needed
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 ''
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:
# Check independent pending order
pending_order = Order.objects.filter(
wechat_user=user,
activity=activity,
status='pending'
).first()
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 ''
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,
attach=f'{{"type":"activity","activity_id":{activity.id}}}'
)
import json
result = json.loads(message)
if code in range(200, 300):
code_url = result.get('code_url')
if pending_signup:
pending_signup.signup_info = signup_info
pending_signup.order = order
pending_signup.status = 'unpaid' # Explicitly set to unpaid
pending_signup.save()
else:
ActivitySignup.objects.create(
activity=activity,
user=user,
signup_info=signup_info,
status='unpaid',
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
# Check auto_confirm
status_val = 'confirmed' if activity.auto_confirm else 'pending'
signup = ActivitySignup.objects.create(
activity=activity,
user=user,
signup_info=signup_info,
status=status_val
)
# Send SMS for free activity signup (if confirmed)
if status_val == 'confirmed':
try:
from shop.sms_utils import notify_user_activity_signup_success
# Mock an order object for the SMS template
# The template expects: customer_name, wechat_user, phone_number
class MockOrder:
def __init__(self, user, signup_info):
# Ensure we get the name and phone from signup_info first
# signup_info keys might vary, let's try common ones
self.customer_name = signup_info.get('name') or signup_info.get('username') or user.nickname or "用户"
self.wechat_user = user
self.phone_number = signup_info.get('phone') or signup_info.get('mobile') or user.phone_number or ""
mock_order = MockOrder(user, signup_info)
# Check if we have a valid phone number before sending
if mock_order.phone_number:
notify_user_activity_signup_success(mock_order, signup)
else:
print(f"Skipping SMS for signup {signup.id}: No phone number found")
except Exception as e:
print(f"发送免费活动报名短信失败: {str(e)}")
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')
# Sync payment status
for signup in signups:
signup.check_payment_status()
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')