forked from quant-speed-AI/Scoring-System
创赢未来评分系统 - 初始化提交(移除大文件)
This commit is contained in:
516
backend/community/views.py
Normal file
516
backend/community/views.py
Normal file
@@ -0,0 +1,516 @@
|
||||
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, OpenApiParameter, OpenApiTypes
|
||||
|
||||
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, AdminActivitySerializer, AdminTopicSerializer
|
||||
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', 'order']
|
||||
ordering = ['-is_pinned', 'order', '-created_at']
|
||||
|
||||
def get_queryset(self):
|
||||
qs = super().get_queryset()
|
||||
# 列表接口仅显示已发布的帖子
|
||||
if self.action == 'list':
|
||||
qs = qs.filter(status='published')
|
||||
return qs
|
||||
|
||||
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:
|
||||
# 如果关联了系统用户(user字段不为空),则是管理员/内部人员,直接发布
|
||||
# 否则进入审核流程
|
||||
status = 'published' if user.user else 'pending'
|
||||
serializer.save(author=user, status=status)
|
||||
|
||||
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)
|
||||
|
||||
@action(detail=True, methods=['post'], permission_classes=[permissions.AllowAny])
|
||||
def like(self, request, pk=None):
|
||||
obj = self.get_object()
|
||||
user = get_current_wechat_user(request)
|
||||
if not user:
|
||||
return Response({'error': '请先登录'}, status=401)
|
||||
|
||||
if obj.likes.filter(id=user.id).exists():
|
||||
obj.likes.remove(user)
|
||||
liked = False
|
||||
else:
|
||||
obj.likes.add(user)
|
||||
liked = True
|
||||
|
||||
return Response({'liked': liked, 'count': obj.likes.count()})
|
||||
|
||||
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)
|
||||
|
||||
@action(detail=True, methods=['post'], permission_classes=[permissions.AllowAny])
|
||||
def like(self, request, pk=None):
|
||||
obj = self.get_object()
|
||||
user = get_current_wechat_user(request)
|
||||
if not user:
|
||||
return Response({'error': '请先登录'}, status=401)
|
||||
|
||||
if obj.likes.filter(id=user.id).exists():
|
||||
obj.likes.remove(user)
|
||||
liked = False
|
||||
else:
|
||||
obj.likes.add(user)
|
||||
liked = True
|
||||
|
||||
return Response({'liked': liked, 'count': obj.likes.count()})
|
||||
|
||||
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-Type,requests 会自动设置 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')
|
||||
|
||||
class AdminPublishViewSet(viewsets.ViewSet):
|
||||
"""
|
||||
管理员/API发布接口
|
||||
"""
|
||||
permission_classes = []
|
||||
authentication_classes = []
|
||||
|
||||
def check_api_key(self, request):
|
||||
key = request.headers.get('X-API-KEY') or request.query_params.get('apikey')
|
||||
if key != '123quant-speed':
|
||||
return False
|
||||
return True
|
||||
|
||||
def get_admin_user_by_phone(self, phone):
|
||||
if not phone:
|
||||
return None
|
||||
# Find WeChatUser by phone
|
||||
user = WeChatUser.objects.filter(phone_number=phone).first()
|
||||
if not user:
|
||||
return None
|
||||
|
||||
# Check if linked to a system user and has admin privileges (is_staff)
|
||||
if user.user and user.user.is_staff:
|
||||
return user
|
||||
|
||||
return None
|
||||
|
||||
@extend_schema(
|
||||
summary="API发布活动",
|
||||
request=AdminActivitySerializer,
|
||||
parameters=[
|
||||
OpenApiParameter(
|
||||
name='apikey',
|
||||
description='API访问密钥',
|
||||
required=True,
|
||||
type=OpenApiTypes.STR,
|
||||
location=OpenApiParameter.QUERY
|
||||
),
|
||||
OpenApiParameter(
|
||||
name='phone_number',
|
||||
description='管理员手机号 (用于关联发布者)',
|
||||
required=True,
|
||||
type=OpenApiTypes.STR,
|
||||
location=OpenApiParameter.QUERY
|
||||
)
|
||||
]
|
||||
)
|
||||
@action(detail=False, methods=['post'])
|
||||
def publish_activity(self, request):
|
||||
if not self.check_api_key(request):
|
||||
return Response({'error': 'Invalid API Key'}, status=403)
|
||||
|
||||
phone = request.data.get('phone_number') or request.query_params.get('phone_number')
|
||||
user = self.get_admin_user_by_phone(phone)
|
||||
if not user:
|
||||
return Response({'error': 'Admin user not found with this phone number'}, status=404)
|
||||
|
||||
data = request.data.copy()
|
||||
serializer = AdminActivitySerializer(data=data)
|
||||
if serializer.is_valid():
|
||||
activity = serializer.save(author=user)
|
||||
return Response(serializer.data, status=201)
|
||||
return Response(serializer.errors, status=400)
|
||||
|
||||
@extend_schema(
|
||||
summary="API发布帖子",
|
||||
request=AdminTopicSerializer,
|
||||
parameters=[
|
||||
OpenApiParameter(
|
||||
name='apikey',
|
||||
description='API访问密钥',
|
||||
required=True,
|
||||
type=OpenApiTypes.STR,
|
||||
location=OpenApiParameter.QUERY
|
||||
),
|
||||
OpenApiParameter(
|
||||
name='phone_number',
|
||||
description='管理员手机号 (用于关联发布者)',
|
||||
required=True,
|
||||
type=OpenApiTypes.STR,
|
||||
location=OpenApiParameter.QUERY
|
||||
)
|
||||
]
|
||||
)
|
||||
@action(detail=False, methods=['post'])
|
||||
def publish_topic(self, request):
|
||||
if not self.check_api_key(request):
|
||||
return Response({'error': 'Invalid API Key'}, status=403)
|
||||
|
||||
phone = request.data.get('phone_number') or request.query_params.get('phone_number')
|
||||
user = self.get_admin_user_by_phone(phone)
|
||||
if not user:
|
||||
return Response({'error': 'Admin user not found with this phone number'}, status=404)
|
||||
|
||||
data = request.data.copy()
|
||||
serializer = AdminTopicSerializer(data=data)
|
||||
if serializer.is_valid():
|
||||
# Only set status to published if not provided, otherwise respect the input
|
||||
status = data.get('status', 'published')
|
||||
topic = serializer.save(author=user, status=status)
|
||||
return Response(serializer.data, status=201)
|
||||
return Response(serializer.errors, status=400)
|
||||
Reference in New Issue
Block a user