This commit is contained in:
jeremygan2021
2026-02-12 15:51:18 +08:00
parent e69a24b555
commit 4ac8767659
14 changed files with 2851 additions and 141 deletions

View 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

View File

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

View 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

View File

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