Files
TermiNexus 99fd632bf1 refactor(custom-apps): doc/invite/sve/community 视图裸 ORM 收敛进各 app Manager
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 字典以保持原边界行为。
2026-09-14 06:47:01 +08:00

283 lines
11 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""模块: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}})