307 lines
12 KiB
Python
307 lines
12 KiB
Python
"""模块:Community 社区视图
|
||
|
||
官网社区 API(discuss.gvsds.com 前端调用):
|
||
GET /API/Community/Boards — 板块 + 分类 + 帖子数(公开)
|
||
GET /API/Community/Posts — 帖子列表(分页/板块/分类/关键字,公开)
|
||
GET /API/Community/Post/<uuid> — 帖子详情 + 回复(公开,浏览量+1)
|
||
POST /API/Community/Post — 发帖(登录)
|
||
POST /API/Community/Reply — 回复(登录)
|
||
POST /API/Community/Like — 点赞/取消(登录)
|
||
"""
|
||
import datetime
|
||
import uuid
|
||
|
||
from django.db.models import Count, F
|
||
from rest_framework.permissions import IsAuthenticated, AllowAny
|
||
from rest_framework.response import Response
|
||
from rest_framework.views import APIView
|
||
|
||
from gvsdsdk.model_utils import UUIDObj
|
||
|
||
from .models import (
|
||
CommunityBoard,
|
||
CommunityCategory,
|
||
CommunityPost,
|
||
CommunityReply,
|
||
CommunityLike,
|
||
)
|
||
|
||
|
||
def _ToBytes(value):
|
||
"""统一转 bytes(兼容 UUIDObj/bytes/str/uuid.UUID)"""
|
||
if value is None:
|
||
return None
|
||
if isinstance(value, bytes):
|
||
return value
|
||
if hasattr(value, 'bytes'):
|
||
return value.bytes
|
||
if isinstance(value, str):
|
||
try:
|
||
return UUIDObj(value).bytes
|
||
except Exception:
|
||
return None
|
||
return None
|
||
|
||
|
||
def _UserBytes(user):
|
||
"""当前认证用户 UserUUID bytes;未认证返回 None"""
|
||
if user is None:
|
||
return None
|
||
authenticated = getattr(user, 'is_authenticated', None)
|
||
if authenticated is None:
|
||
authenticated = bool(getattr(user, 'IsAuthenticated', False))
|
||
if not authenticated:
|
||
return None
|
||
uu = getattr(user, 'UserUUID', None)
|
||
if uu is None:
|
||
return None
|
||
return _ToBytes(uu)
|
||
|
||
|
||
def _UserName(user):
|
||
"""当前认证用户显示名"""
|
||
if user is None:
|
||
return ''
|
||
return getattr(user, 'UserName', '') or getattr(user, 'username', '') or ''
|
||
|
||
|
||
def _TimeStr(dt):
|
||
if not dt:
|
||
return ''
|
||
if isinstance(dt, datetime.datetime):
|
||
if dt.tzinfo is None:
|
||
dt = dt.replace(tzinfo=datetime.timezone.utc)
|
||
return dt.astimezone(datetime.timezone(datetime.timedelta(hours=8))).strftime('%Y-%m-%d %H:%M')
|
||
return str(dt)
|
||
|
||
|
||
def _AvatarURL(user_bytes):
|
||
"""作者头像 URL(ugm 公开头像接口,按用户 UUID)"""
|
||
if not user_bytes:
|
||
return ''
|
||
return 'https://www.gvsds.com/api/ugm/user-avatar/by-uuid/?UserUUID=' + UUIDObj(user_bytes).str
|
||
|
||
|
||
class CommunityBoardsView(APIView):
|
||
"""板块列表(含分类与各板块帖子数)"""
|
||
|
||
permission_classes = [AllowAny]
|
||
|
||
def get(self, request):
|
||
boards = list(CommunityBoard.objects.filter(BoardStatus=1))
|
||
post_counts = {
|
||
_ToBytes(b): c
|
||
for b, c in CommunityPost.objects.filter(
|
||
PostStatus=1, BoardUUID__in=[_ToBytes(x.BoardUUID) for x in boards],
|
||
).values_list('BoardUUID').annotate(c=Count('PostUUID'))
|
||
} if boards else {}
|
||
categories = list(CommunityCategory.objects.filter(CategoryStatus=1))
|
||
cat_map = {}
|
||
for cat in categories:
|
||
cat_map.setdefault(_ToBytes(cat.BoardUUID), []).append({
|
||
'CategoryUUID': UUIDObj(cat.CategoryUUID).str,
|
||
'CategoryName': cat.CategoryName,
|
||
})
|
||
rows = [{
|
||
'BoardUUID': UUIDObj(b.BoardUUID).str,
|
||
'BoardName': b.BoardName,
|
||
'BoardDesc': b.BoardDesc or '',
|
||
'PostCount': post_counts.get(_ToBytes(b.BoardUUID), 0),
|
||
'Categories': cat_map.get(_ToBytes(b.BoardUUID), []),
|
||
} for b in boards]
|
||
return Response({'code': 0, 'data': {'list': rows, 'total': len(rows)}})
|
||
|
||
|
||
class CommunityPostsView(APIView):
|
||
"""帖子列表(分页 + 板块/分类/关键字过滤,置顶优先)"""
|
||
|
||
permission_classes = [AllowAny]
|
||
|
||
def get(self, request):
|
||
board_str = (request.query_params.get('BoardUUID') or '').strip()
|
||
category_str = (request.query_params.get('CategoryUUID') or '').strip()
|
||
keyword = (request.query_params.get('Keyword') or '').strip()
|
||
try:
|
||
page = max(1, int(request.query_params.get('Page') or 1))
|
||
page_size = min(50, max(1, int(request.query_params.get('PageSize') or 20)))
|
||
except (TypeError, ValueError):
|
||
page, page_size = 1, 20
|
||
qs = CommunityPost.objects.filter(PostStatus=1)
|
||
if board_str:
|
||
qs = qs.filter(BoardUUID=_ToBytes(board_str))
|
||
if category_str:
|
||
qs = qs.filter(CategoryUUID=_ToBytes(category_str))
|
||
if keyword:
|
||
qs = qs.filter(Title__icontains=keyword)
|
||
total = qs.count()
|
||
rows = []
|
||
for p in qs.order_by('-IsPinned', '-CreateTime')[(page - 1) * page_size: page * page_size]:
|
||
rows.append({
|
||
'PostUUID': UUIDObj(p.PostUUID).str,
|
||
'BoardUUID': UUIDObj(p.BoardUUID).str,
|
||
'Title': p.Title,
|
||
'AuthorName': p.AuthorName,
|
||
'AuthorAvatar': _AvatarURL(p.AuthorUUID),
|
||
'ViewCount': p.ViewCount,
|
||
'LikeCount': p.LikeCount,
|
||
'ReplyCount': p.ReplyCount,
|
||
'IsPinned': p.IsPinned,
|
||
'IsLocked': p.IsLocked,
|
||
'CreateTime': _TimeStr(p.CreateTime),
|
||
})
|
||
return Response({'code': 0, 'data': {'list': rows, 'total': total, 'page': page, 'page_size': page_size}})
|
||
|
||
|
||
class CommunityPostDetailView(APIView):
|
||
"""帖子详情(浏览量 +1)+ 回复列表"""
|
||
|
||
permission_classes = [AllowAny]
|
||
|
||
def get(self, request, post_uuid):
|
||
post = CommunityPost.objects.filter(PostUUID=_ToBytes(post_uuid), PostStatus=1).first()
|
||
if post is None:
|
||
return Response({'code': 1, 'msg': '帖子不存在或已删除'}, status=404)
|
||
CommunityPost.objects.filter(PostUUID=post.PostUUID).update(ViewCount=F('ViewCount') + 1)
|
||
post.ViewCount += 1
|
||
liked = 0
|
||
user_bytes = _UserBytes(request.user)
|
||
if user_bytes:
|
||
liked = 1 if CommunityLike.objects.filter(
|
||
PostUUID=post.PostUUID, AuthorUUID=user_bytes).exists() else 0
|
||
replies = []
|
||
for r in CommunityReply.objects.filter(PostUUID=post.PostUUID, ReplyStatus=1):
|
||
replies.append({
|
||
'ReplyUUID': UUIDObj(r.ReplyUUID).str,
|
||
'AuthorName': r.AuthorName,
|
||
'AuthorAvatar': _AvatarURL(r.AuthorUUID),
|
||
'Content': r.Content,
|
||
'ReplyToUUID': UUIDObj(r.ReplyToUUID).str if r.ReplyToUUID else '',
|
||
'CreateTime': _TimeStr(r.CreateTime),
|
||
})
|
||
board = CommunityBoard.objects.filter(BoardUUID=post.BoardUUID).first()
|
||
category = CommunityCategory.objects.filter(CategoryUUID=post.CategoryUUID).first() \
|
||
if post.CategoryUUID else None
|
||
data = {
|
||
'PostUUID': UUIDObj(post.PostUUID).str,
|
||
'BoardUUID': UUIDObj(post.BoardUUID).str,
|
||
'BoardName': board.BoardName if board else '',
|
||
'CategoryName': category.CategoryName if category else '',
|
||
'Title': post.Title,
|
||
'Content': post.Content,
|
||
'AuthorName': post.AuthorName,
|
||
'AuthorAvatar': _AvatarURL(post.AuthorUUID),
|
||
'ViewCount': post.ViewCount,
|
||
'LikeCount': post.LikeCount,
|
||
'ReplyCount': post.ReplyCount,
|
||
'IsPinned': post.IsPinned,
|
||
'IsLocked': post.IsLocked,
|
||
'Liked': liked,
|
||
'CreateTime': _TimeStr(post.CreateTime),
|
||
'Replies': replies,
|
||
}
|
||
return Response({'code': 0, 'data': data})
|
||
|
||
|
||
class CommunityPostCreateView(APIView):
|
||
"""发帖(登录用户)"""
|
||
|
||
permission_classes = [IsAuthenticated]
|
||
|
||
def post(self, request):
|
||
user_bytes = _UserBytes(request.user)
|
||
if not user_bytes:
|
||
return Response({'code': 1, 'msg': '未登录或登录已失效'}, status=403)
|
||
board_str = (request.data.get('BoardUUID') or '').strip()
|
||
title = (request.data.get('Title') or '').strip()
|
||
content = (request.data.get('Content') or '').strip()
|
||
if not board_str:
|
||
return Response({'code': 1, 'msg': '请选择板块'}, status=400)
|
||
if not title:
|
||
return Response({'code': 1, 'msg': '请填写标题'}, status=400)
|
||
if not content:
|
||
return Response({'code': 1, 'msg': '请填写内容'}, status=400)
|
||
board = CommunityBoard.objects.filter(BoardUUID=_ToBytes(board_str), BoardStatus=1).first()
|
||
if board is None:
|
||
return Response({'code': 1, 'msg': '板块不存在或已停用'}, status=400)
|
||
category_bytes = None
|
||
category_str = (request.data.get('CategoryUUID') or '').strip()
|
||
if category_str:
|
||
category_bytes = _ToBytes(category_str)
|
||
post = CommunityPost.objects.create(
|
||
PostUUID=uuid.uuid4().bytes,
|
||
BoardUUID=_ToBytes(board_str),
|
||
CategoryUUID=category_bytes,
|
||
Title=title,
|
||
Content=content,
|
||
AuthorUUID=user_bytes,
|
||
AuthorName=_UserName(request.user),
|
||
)
|
||
return Response({'code': 0, 'msg': '发布成功', 'data': {'PostUUID': UUIDObj(post.PostUUID).str}})
|
||
|
||
|
||
class CommunityReplyCreateView(APIView):
|
||
"""回复(登录用户)"""
|
||
|
||
permission_classes = [IsAuthenticated]
|
||
|
||
def post(self, request):
|
||
user_bytes = _UserBytes(request.user)
|
||
if not user_bytes:
|
||
return Response({'code': 1, 'msg': '未登录或登录已失效'}, status=403)
|
||
post_str = (request.data.get('PostUUID') or '').strip()
|
||
content = (request.data.get('Content') or '').strip()
|
||
if not post_str:
|
||
return Response({'code': 1, 'msg': '缺少帖子 UUID'}, status=400)
|
||
if not content:
|
||
return Response({'code': 1, 'msg': '请填写回复内容'}, status=400)
|
||
post = CommunityPost.objects.filter(PostUUID=_ToBytes(post_str), PostStatus=1).first()
|
||
if post is None:
|
||
return Response({'code': 1, 'msg': '帖子不存在或已删除'}, status=404)
|
||
if post.IsLocked:
|
||
return Response({'code': 1, 'msg': '帖子已锁定,不可回复'}, status=403)
|
||
reply_to_bytes = None
|
||
reply_to_str = (request.data.get('ReplyToUUID') or '').strip()
|
||
if reply_to_str:
|
||
reply_to_bytes = _ToBytes(reply_to_str)
|
||
CommunityReply.objects.create(
|
||
ReplyUUID=uuid.uuid4().bytes,
|
||
PostUUID=post.PostUUID,
|
||
AuthorUUID=user_bytes,
|
||
AuthorName=_UserName(request.user),
|
||
Content=content,
|
||
ReplyToUUID=reply_to_bytes,
|
||
)
|
||
CommunityPost.objects.filter(PostUUID=post.PostUUID).update(ReplyCount=F('ReplyCount') + 1)
|
||
return Response({'code': 0, 'msg': '回复成功'})
|
||
|
||
|
||
class CommunityLikeView(APIView):
|
||
"""点赞/取消点赞(登录用户,切换式)"""
|
||
|
||
permission_classes = [IsAuthenticated]
|
||
|
||
def post(self, request):
|
||
user_bytes = _UserBytes(request.user)
|
||
if not user_bytes:
|
||
return Response({'code': 1, 'msg': '未登录或登录已失效'}, status=403)
|
||
post_str = (request.data.get('PostUUID') or '').strip()
|
||
if not post_str:
|
||
return Response({'code': 1, 'msg': '缺少帖子 UUID'}, status=400)
|
||
post = CommunityPost.objects.filter(PostUUID=_ToBytes(post_str), PostStatus=1).first()
|
||
if post is None:
|
||
return Response({'code': 1, 'msg': '帖子不存在或已删除'}, status=404)
|
||
liked = CommunityLike.objects.filter(PostUUID=post.PostUUID, AuthorUUID=user_bytes).first()
|
||
if liked:
|
||
liked.delete()
|
||
CommunityPost.objects.filter(PostUUID=post.PostUUID).update(
|
||
LikeCount=F('LikeCount') - 1)
|
||
return Response({'code': 0, 'msg': '已取消点赞', 'data': {'Liked': 0}})
|
||
CommunityLike.objects.create(
|
||
LikeUUID=uuid.uuid4().bytes,
|
||
PostUUID=post.PostUUID,
|
||
AuthorUUID=user_bytes,
|
||
)
|
||
CommunityPost.objects.filter(PostUUID=post.PostUUID).update(
|
||
LikeCount=F('LikeCount') + 1)
|
||
return Response({'code': 0, 'msg': '点赞成功', 'data': {'Liked': 1}})
|