forum
This commit is contained in:
23
backend/community/permissions.py
Normal file
23
backend/community/permissions.py
Normal file
@@ -0,0 +1,23 @@
|
||||
from rest_framework import permissions
|
||||
from .utils import get_current_wechat_user
|
||||
|
||||
class IsAuthorOrReadOnly(permissions.BasePermission):
|
||||
"""
|
||||
Object-level permission to only allow authors of an object to edit it.
|
||||
Assumes the model instance has an `author` attribute.
|
||||
"""
|
||||
|
||||
def has_object_permission(self, request, view, obj):
|
||||
# Read permissions are allowed to any request,
|
||||
# so we'll always allow GET, HEAD or OPTIONS requests.
|
||||
if request.method in permissions.SAFE_METHODS:
|
||||
return True
|
||||
|
||||
# Write permissions are only allowed to the author of the object.
|
||||
# We need to manually get the user because we are using custom auth logic (get_current_wechat_user)
|
||||
# instead of request.user for some reason (or in addition to).
|
||||
# However, DRF's request.user might not be set if we don't use a standard authentication class.
|
||||
# Based on views.py, it uses `get_current_wechat_user(request)`.
|
||||
|
||||
current_user = get_current_wechat_user(request)
|
||||
return current_user and obj.author == current_user
|
||||
@@ -30,12 +30,24 @@ class TopicMediaSerializer(serializers.ModelSerializer):
|
||||
class ReplySerializer(serializers.ModelSerializer):
|
||||
author_info = WeChatUserSerializer(source='author', read_only=True)
|
||||
media = TopicMediaSerializer(many=True, read_only=True)
|
||||
media_ids = serializers.ListField(
|
||||
child=serializers.IntegerField(),
|
||||
write_only=True,
|
||||
required=False
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Reply
|
||||
fields = ['id', 'topic', 'content', 'author', 'author_info', 'reply_to', 'media', 'created_at']
|
||||
fields = ['id', 'topic', 'content', 'author', 'author_info', 'reply_to', 'media', 'created_at', 'media_ids']
|
||||
read_only_fields = ['author', 'created_at']
|
||||
|
||||
def create(self, validated_data):
|
||||
media_ids = validated_data.pop('media_ids', [])
|
||||
reply = super().create(validated_data)
|
||||
if media_ids:
|
||||
TopicMedia.objects.filter(id__in=media_ids).update(reply=reply)
|
||||
return reply
|
||||
|
||||
class TopicSerializer(serializers.ModelSerializer):
|
||||
author_info = WeChatUserSerializer(source='author', read_only=True)
|
||||
replies = ReplySerializer(many=True, read_only=True)
|
||||
|
||||
40
backend/community/utils.py
Normal file
40
backend/community/utils.py
Normal file
@@ -0,0 +1,40 @@
|
||||
from django.core.signing import TimestampSigner, BadSignature, SignatureExpired
|
||||
from shop.models import WeChatUser
|
||||
|
||||
def get_current_wechat_user(request):
|
||||
"""
|
||||
根据 Authorization 头获取当前微信用户
|
||||
增强逻辑:如果 Token 解析出的 OpenID 对应的用户不存在(可能已被合并删除),
|
||||
但该 OpenID 是 Web 虚拟 ID (web_phone),尝试通过手机号查找现有的主账号。
|
||||
"""
|
||||
auth_header = request.headers.get('Authorization')
|
||||
if not auth_header or not auth_header.startswith('Bearer '):
|
||||
return None
|
||||
token = auth_header.split(' ')[1]
|
||||
signer = TimestampSigner()
|
||||
try:
|
||||
# 签名包含 openid
|
||||
openid = signer.unsign(token, max_age=86400 * 30) # 30天有效
|
||||
user = WeChatUser.objects.filter(openid=openid).first()
|
||||
|
||||
if user:
|
||||
return user
|
||||
|
||||
# 如果没找到用户,检查是否是 Web 虚拟 OpenID
|
||||
# 场景:Web 用户已被合并到小程序账号,旧 Web Token 依然有效,指向合并后的账号
|
||||
if openid.startswith('web_'):
|
||||
try:
|
||||
# 格式: web_13800138000
|
||||
parts = openid.split('_', 1)
|
||||
if len(parts) == 2:
|
||||
phone = parts[1]
|
||||
# 尝试通过手机号查找(查找合并后的主账号)
|
||||
user = WeChatUser.objects.filter(phone_number=phone).first()
|
||||
if user:
|
||||
return user
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return None
|
||||
except (BadSignature, SignatureExpired):
|
||||
return None
|
||||
@@ -11,22 +11,8 @@ from drf_spectacular.utils import extend_schema
|
||||
from shop.models import WeChatUser
|
||||
from .models import Activity, ActivitySignup, Topic, Reply, TopicMedia, Announcement
|
||||
from .serializers import ActivitySerializer, ActivitySignupSerializer, TopicSerializer, ReplySerializer, TopicMediaSerializer, AnnouncementSerializer
|
||||
|
||||
def get_current_wechat_user(request):
|
||||
"""
|
||||
根据 Authorization 头获取当前微信用户 (复用 shop app 的逻辑)
|
||||
"""
|
||||
auth_header = request.headers.get('Authorization')
|
||||
if not auth_header or not auth_header.startswith('Bearer '):
|
||||
return None
|
||||
token = auth_header.split(' ')[1]
|
||||
signer = TimestampSigner()
|
||||
try:
|
||||
# 签名包含 openid
|
||||
openid = signer.unsign(token, max_age=86400 * 30) # 30天有效
|
||||
return WeChatUser.objects.filter(openid=openid).first()
|
||||
except (BadSignature, SignatureExpired):
|
||||
return None
|
||||
from .utils import get_current_wechat_user
|
||||
from .permissions import IsAuthorOrReadOnly
|
||||
|
||||
class ActivityViewSet(viewsets.ReadOnlyModelViewSet):
|
||||
"""
|
||||
@@ -71,6 +57,7 @@ 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']
|
||||
@@ -102,6 +89,7 @@ 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)
|
||||
|
||||
Reference in New Issue
Block a user