doc:DocManager 新增 QueryDocuments/QuerySeries 返回查询集,视图 get_queryset 改为委托;invite:新增 InviteManager(QuerySet/GetByInviteID/Create);sve:新增 SveManager(QuerySet/GetByCandidates/Upsert 支持 CreateDefaults);community:新增 CommunityManager(板块/分类/帖子/回复/点赞共 20 处,ToggleLike 封装点赞切换与计数同步,聚合查询保留原 values_list+annotate)。 视图层不再直接操作 ORM,类级 queryset 属性保留(DRF 必需);QueryPosts 收 Filters 字典以保持原边界行为。
283 lines
11 KiB
Python
283 lines
11 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
|
||
|
||
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 .manager import CommunityManager
|
||
|
||
|
||
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 = CommunityManager.ListActiveBoards()
|
||
post_counts = CommunityManager.CountPostsByBoards(
|
||
[_ToBytes(x.BoardUUID) for x in boards],
|
||
)
|
||
categories = CommunityManager.ListActiveCategories()
|
||
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
|
||
filters = {}
|
||
if board_str:
|
||
filters['BoardUUID'] = _ToBytes(board_str)
|
||
if category_str:
|
||
filters['CategoryUUID'] = _ToBytes(category_str)
|
||
if keyword:
|
||
filters['Title__icontains'] = keyword
|
||
qs = CommunityManager.QueryPosts(filters)
|
||
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 = CommunityManager.GetPost(_ToBytes(post_uuid), OnlyPublished=True)
|
||
if post is None:
|
||
return Response({'code': 1, 'msg': '帖子不存在或已删除'}, status=404)
|
||
CommunityManager.IncreaseViewCount(post.PostUUID)
|
||
post.ViewCount += 1
|
||
liked = 0
|
||
user_bytes = _UserBytes(request.user)
|
||
if user_bytes:
|
||
liked = 1 if CommunityManager.HasLiked(post.PostUUID, user_bytes) else 0
|
||
replies = []
|
||
for r in CommunityManager.ListReplies(post.PostUUID):
|
||
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 = CommunityManager.GetBoard(post.BoardUUID)
|
||
category = CommunityManager.GetCategory(post.CategoryUUID)
|
||
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 = CommunityManager.GetActiveBoard(_ToBytes(board_str))
|
||
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 = CommunityManager.CreatePost(
|
||
BoardUUID=_ToBytes(board_str),
|
||
Title=title,
|
||
Content=content,
|
||
AuthorUUID=user_bytes,
|
||
AuthorName=_UserName(request.user),
|
||
CategoryUUID=category_bytes,
|
||
)
|
||
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 = CommunityManager.GetPost(_ToBytes(post_str), OnlyPublished=True)
|
||
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)
|
||
CommunityManager.CreateReply(
|
||
PostUUID=post.PostUUID,
|
||
AuthorUUID=user_bytes,
|
||
AuthorName=_UserName(request.user),
|
||
Content=content,
|
||
ReplyToUUID=reply_to_bytes,
|
||
)
|
||
CommunityManager.IncreaseReplyCount(post.PostUUID)
|
||
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 = CommunityManager.GetPost(_ToBytes(post_str), OnlyPublished=True)
|
||
if post is None:
|
||
return Response({'code': 1, 'msg': '帖子不存在或已删除'}, status=404)
|
||
liked = CommunityManager.ToggleLike(post.PostUUID, user_bytes)
|
||
if not liked:
|
||
return Response({'code': 0, 'msg': '已取消点赞', 'data': {'Liked': 0}})
|
||
return Response({'code': 0, 'msg': '点赞成功', 'data': {'Liked': 1}})
|