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 字典以保持原边界行为。
This commit is contained in:
TermiNexus committed 2026-09-14 06:47:01 +08:00
1 parent c26286af1a
commit d600315829
8 files changed
+296 -94

No files matched your search

+150
View File
@@ -0,0 +1,150 @@
"""模块:Community 社区数据访问层
官网社区(discuss.gvsds.com)的板块/分类/帖子/回复/点赞数据访问收敛在此,
视图层不直接操作 ORM。所有方法遵循帕斯卡命名法。
"""
import uuid
from django.db.models import Count, F
from .models import (
CommunityBoard,
CommunityCategory,
CommunityLike,
CommunityPost,
CommunityReply,
)
class CommunityManager:
"""社区管理器"""
# ========== 板块 / 分类 ==========
@staticmethod
def ListActiveBoards():
"""启用板块列表"""
return list(CommunityBoard.objects.filter(BoardStatus=1))
@staticmethod
def CountPostsByBoards(BoardUUIDs):
"""各板块的已发布帖子数 {板块bytes: 帖子数}(BoardUUIDs 为空返回 {})"""
if not BoardUUIDs:
return {}
return {
BoardUUID: PostCount
for BoardUUID, PostCount in CommunityPost.objects.filter(
PostStatus=1, BoardUUID__in=BoardUUIDs,
).values_list('BoardUUID').annotate(c=Count('PostUUID'))
}
@staticmethod
def ListActiveCategories():
"""启用分类列表"""
return list(CommunityCategory.objects.filter(CategoryStatus=1))
@staticmethod
def GetBoard(BoardUUID):
"""按 UUID 取板块(不存在返回 None)"""
return CommunityBoard.objects.filter(BoardUUID=BoardUUID).first()
@staticmethod
def GetActiveBoard(BoardUUID):
"""按 UUID 取启用板块(不存在或已停用返回 None)"""
return CommunityBoard.objects.filter(BoardUUID=BoardUUID, BoardStatus=1).first()
@staticmethod
def GetCategory(CategoryUUID):
"""按 UUID 取分类(UUID 为空返回 None)"""
if not CategoryUUID:
return None
return CommunityCategory.objects.filter(CategoryUUID=CategoryUUID).first()
# ========== 帖子 ==========
@staticmethod
def QueryPosts(Filters=None):
"""已发布帖子查询集
Filters 为附加过滤条件(键为 ORM lookup、值为已转换好的 bytes/str),
由调用方按「参数存在才过滤」的语义构造;为空表示仅限已发布。
"""
return CommunityPost.objects.filter(PostStatus=1, **(Filters or {}))
@staticmethod
def GetPost(PostUUID, OnlyPublished=False):
"""按 UUID 取帖子(OnlyPublished=True 时排除已删除;不存在返回 None)"""
qs = CommunityPost.objects.filter(PostUUID=PostUUID)
if OnlyPublished:
qs = qs.filter(PostStatus=1)
return qs.first()
@staticmethod
def CreatePost(BoardUUID, Title, Content, AuthorUUID, AuthorName, CategoryUUID=None):
"""发帖"""
return CommunityPost.objects.create(
PostUUID=uuid.uuid4().bytes,
BoardUUID=BoardUUID,
CategoryUUID=CategoryUUID,
Title=Title,
Content=Content,
AuthorUUID=AuthorUUID,
AuthorName=AuthorName,
)
@staticmethod
def IncreaseViewCount(PostUUID):
"""浏览量 +1(DB 侧原子自增)"""
CommunityPost.objects.filter(PostUUID=PostUUID).update(ViewCount=F('ViewCount') + 1)
@staticmethod
def IncreaseReplyCount(PostUUID):
"""回复数 +1(DB 侧原子自增)"""
CommunityPost.objects.filter(PostUUID=PostUUID).update(ReplyCount=F('ReplyCount') + 1)
# ========== 回复 ==========
@staticmethod
def ListReplies(PostUUID):
"""帖子下未删除回复列表"""
return CommunityReply.objects.filter(PostUUID=PostUUID, ReplyStatus=1)
@staticmethod
def CreateReply(PostUUID, AuthorUUID, AuthorName, Content, ReplyToUUID=None):
"""新增回复"""
return CommunityReply.objects.create(
ReplyUUID=uuid.uuid4().bytes,
PostUUID=PostUUID,
AuthorUUID=AuthorUUID,
AuthorName=AuthorName,
Content=Content,
ReplyToUUID=ReplyToUUID,
)
# ========== 点赞 ==========
@staticmethod
def HasLiked(PostUUID, AuthorUUID):
"""判断用户是否已点赞该帖子"""
return CommunityLike.objects.filter(PostUUID=PostUUID, AuthorUUID=AuthorUUID).exists()
@staticmethod
def ToggleLike(PostUUID, AuthorUUID):
"""切换点赞状态并同步帖子点赞数
Returns:
True 表示本次为点赞,False 表示本次为取消点赞
"""
existing = CommunityLike.objects.filter(PostUUID=PostUUID, AuthorUUID=AuthorUUID).first()
if existing is not None:
existing.delete()
CommunityPost.objects.filter(PostUUID=PostUUID).update(LikeCount=F('LikeCount') - 1)
return False
CommunityLike.objects.create(
LikeUUID=uuid.uuid4().bytes,
PostUUID=PostUUID,
AuthorUUID=AuthorUUID,
)
CommunityPost.objects.filter(PostUUID=PostUUID).update(LikeCount=F('LikeCount') + 1)
return True
+26 -50
View File
@@ -9,22 +9,14 @@
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,
)
from .manager import CommunityManager
def _ToBytes(value):
@@ -88,14 +80,11 @@ 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))
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({
@@ -126,13 +115,14 @@ class CommunityPostsView(APIView):
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)
filters = {}
if board_str:
qs = qs.filter(BoardUUID=_ToBytes(board_str))
filters['BoardUUID'] = _ToBytes(board_str)
if category_str:
qs = qs.filter(CategoryUUID=_ToBytes(category_str))
filters['CategoryUUID'] = _ToBytes(category_str)
if keyword:
qs = qs.filter(Title__icontains=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]:
@@ -158,18 +148,17 @@ class CommunityPostDetailView(APIView):
permission_classes = [AllowAny]
def get(self, request, post_uuid):
post = CommunityPost.objects.filter(PostUUID=_ToBytes(post_uuid), PostStatus=1).first()
post = CommunityManager.GetPost(_ToBytes(post_uuid), OnlyPublished=True)
if post is None:
return Response({'code': 1, 'msg': '帖子不存在或已删除'}, status=404)
CommunityPost.objects.filter(PostUUID=post.PostUUID).update(ViewCount=F('ViewCount') + 1)
CommunityManager.IncreaseViewCount(post.PostUUID)
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
liked = 1 if CommunityManager.HasLiked(post.PostUUID, user_bytes) else 0
replies = []
for r in CommunityReply.objects.filter(PostUUID=post.PostUUID, ReplyStatus=1):
for r in CommunityManager.ListReplies(post.PostUUID):
replies.append({
'ReplyUUID': UUIDObj(r.ReplyUUID).str,
'AuthorName': r.AuthorName,
@@ -178,9 +167,8 @@ class CommunityPostDetailView(APIView):
'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
board = CommunityManager.GetBoard(post.BoardUUID)
category = CommunityManager.GetCategory(post.CategoryUUID)
data = {
'PostUUID': UUIDObj(post.PostUUID).str,
'BoardUUID': UUIDObj(post.BoardUUID).str,
@@ -220,21 +208,20 @@ class CommunityPostCreateView(APIView):
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()
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 = CommunityPost.objects.create(
PostUUID=uuid.uuid4().bytes,
post = CommunityManager.CreatePost(
BoardUUID=_ToBytes(board_str),
CategoryUUID=category_bytes,
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}})
@@ -254,7 +241,7 @@ class CommunityReplyCreateView(APIView):
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()
post = CommunityManager.GetPost(_ToBytes(post_str), OnlyPublished=True)
if post is None:
return Response({'code': 1, 'msg': '帖子不存在或已删除'}, status=404)
if post.IsLocked:
@@ -263,15 +250,14 @@ class CommunityReplyCreateView(APIView):
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,
CommunityManager.CreateReply(
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)
CommunityManager.IncreaseReplyCount(post.PostUUID)
return Response({'code': 0, 'msg': '回复成功'})
@@ -287,20 +273,10 @@ class CommunityLikeView(APIView):
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()
post = CommunityManager.GetPost(_ToBytes(post_str), OnlyPublished=True)
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)
liked = CommunityManager.ToggleLike(post.PostUUID, user_bytes)
if not liked:
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}})
+20
View File
@@ -137,6 +137,26 @@ class DocManager:
serie.delete()
return True, None
# ========== 查询集(供视图 get_queryset 复用) ==========
@staticmethod
def QueryDocuments(Keyword='', DocSerieUUID=None):
"""文档查询集——按标题模糊 / 所属系列过滤(空值表示不过滤)"""
qs = CommunityDocument.objects.all()
if Keyword:
qs = qs.filter(DocTitle__icontains=Keyword)
if DocSerieUUID:
qs = qs.filter(DocSerieUUID=UUIDObj(DocSerieUUID).bytes)
return qs
@staticmethod
def QuerySeries(Keyword=''):
"""系列查询集——按名称模糊过滤"""
qs = CommunitySeries.objects.all()
if Keyword:
qs = qs.filter(DocSerieName__icontains=Keyword)
return qs
# ========== 系列附加查询(供视图收敛裸 ORM) ==========
@staticmethod
+5 -15
View File
@@ -23,16 +23,10 @@ class CommunityDocumentViewSet(viewsets.ModelViewSet):
permission_classes = [IsAuthenticated]
def get_queryset(self):
qs = CommunityDocument.objects.all()
keyword = self.request.query_params.get('keyword', '')
serie_uuid = self.request.query_params.get('DocSerieUUID', '')
if keyword:
qs = qs.filter(DocTitle__icontains=keyword)
if serie_uuid:
serie_bytes = UUIDObj(serie_uuid).bytes
if serie_bytes:
qs = qs.filter(DocSerieUUID=serie_bytes)
return qs
return DocManager.QueryDocuments(
Keyword=self.request.query_params.get('keyword', ''),
DocSerieUUID=self.request.query_params.get('DocSerieUUID', ''),
)
def list(self, request, *args, **kwargs):
qs = self.get_queryset()
@@ -128,11 +122,7 @@ class CommunitySeriesViewSet(viewsets.ModelViewSet):
permission_classes = [IsAuthenticated]
def get_queryset(self):
qs = CommunitySeries.objects.all()
keyword = self.request.query_params.get('keyword', '')
if keyword:
qs = qs.filter(DocSerieName__icontains=keyword)
return qs
return DocManager.QuerySeries(Keyword=self.request.query_params.get('keyword', ''))
def list(self, request, *args, **kwargs):
qs = self.get_queryset()
+33
View File
@@ -0,0 +1,33 @@
"""模块:Invite AI 面试官数据访问层
AI 面试官对话记录的数据访问收敛在此,视图层不直接操作 ORM。
所有方法遵循帕斯卡命名法。
"""
from .models import InviteConversation
class InviteManager:
"""AI 面试官对话管理器"""
@staticmethod
def QuerySet():
"""基础查询集(供 DRF queryset 复用)"""
return InviteConversation.objects.all()
@staticmethod
def GetByInviteID(InviteID):
"""按 InviteID 取对话(不存在返回 None)"""
try:
return InviteConversation.objects.get(InviteID=InviteID)
except InviteConversation.DoesNotExist:
return None
@staticmethod
def Create(InviteID, Messages, CreateTime):
"""新建对话记录"""
return InviteConversation.objects.create(
InviteID=InviteID,
Messages=Messages,
CreateTime=CreateTime,
)
+9 -13
View File
@@ -12,7 +12,7 @@ from rest_framework.response import Response
from rest_framework.permissions import IsAuthenticated, AllowAny
from openai import OpenAI
from .models import InviteConversation
from .manager import InviteManager
from .serializers import (
InviteConversationSerializer,
InitConversationSerializer,
@@ -73,7 +73,7 @@ def _parse_final_status(ai_reply):
class InviteConversationViewSet(viewsets.ModelViewSet):
queryset = InviteConversation.objects.all()
queryset = InviteManager.QuerySet()
serializer_class = InviteConversationSerializer
permission_classes = [AllowAny]
@@ -83,21 +83,19 @@ class InviteConversationViewSet(viewsets.ModelViewSet):
ser.IsValid(raise_exception=True)
invite_id = ser.ValidatedData['InviteID']
try:
conv = InviteConversation.objects.get(InviteID=invite_id)
conv = InviteManager.GetByInviteID(invite_id)
if conv is not None:
return Response({
'code': 0,
'msg': '对话已存在',
'data': InviteConversationSerializer(conv).data,
})
except InviteConversation.DoesNotExist:
pass
greeting = _call_ai([{"role": "user", "content": "你好,我准备好了,请开始面试。"}])
messages = [
{"role": "assistant", "content": greeting},
]
conv = InviteConversation.objects.create(
conv = InviteManager.Create(
InviteID=invite_id,
Messages=messages,
CreateTime=time.time(),
@@ -115,9 +113,8 @@ class InviteConversationViewSet(viewsets.ModelViewSet):
invite_id = ser.ValidatedData['InviteID']
user_message = ser.ValidatedData['Message']
try:
conv = InviteConversation.objects.get(InviteID=invite_id)
except InviteConversation.DoesNotExist:
conv = InviteManager.GetByInviteID(invite_id)
if conv is None:
return Response({'code': 1, 'msg': '对话不存在'}, status=404)
if conv.FinalStatus is not None:
@@ -156,9 +153,8 @@ class InviteConversationViewSet(viewsets.ModelViewSet):
if not invite_id:
return Response({'code': 1, 'msg': '缺少invite_id'}, status=400)
try:
conv = InviteConversation.objects.get(InviteID=invite_id)
except InviteConversation.DoesNotExist:
conv = InviteManager.GetByInviteID(invite_id)
if conv is None:
return Response({'code': 1, 'msg': '对话不存在'}, status=404)
messages = conv.Messages or []
+45
View File
@@ -0,0 +1,45 @@
"""模块:SVE 安全漏洞百科数据访问层
SveID 为 5 字节二进制主键,存量存在「16 字节补零」「UTF-8 文本字节」等历史形态,
按候选顺序回退匹配的逻辑收敛在此,视图层不直接操作 ORM。所有方法遵循帕斯卡命名法。
"""
from .models import SVEBase
class SveManager:
"""漏洞百科管理器"""
@staticmethod
def QuerySet():
"""基础查询集(供 DRF queryset 与列表筛选复用)"""
return SVEBase.objects.all()
@staticmethod
def GetByCandidates(Candidates):
"""按候选主键依次匹配(规范 5 字节 → 旧补零 16 字节 → 旧 UTF-8 字节)
全部候选均不存在时返回 None。
"""
for Candidate in Candidates:
try:
return SVEBase.objects.get(SveID=Candidate)
except SVEBase.DoesNotExist:
continue
return None
@staticmethod
def Upsert(SveID, Defaults, CreateDefaults=None):
"""按 SveID 覆盖写入
CreateDefaults 仅在新建时写入(如 SveCreateTime),不覆盖存量值。
Returns:
(实例, 是否新建)
"""
instance, created = SVEBase.objects.update_or_create(SveID=SveID, defaults=Defaults)
if created and CreateDefaults:
for Field, Value in CreateDefaults.items():
setattr(instance, Field, Value)
instance.save(update_fields=list(CreateDefaults))
return instance, created
+8 -16
View File
@@ -15,7 +15,7 @@ from rest_framework.response import Response
from rest_framework.permissions import AllowAny
from django.db.models import Q
from .models import SVEBase
from .manager import SveManager
from .serializers import SVESerializer, SVEListSerializer, SVEWriteSerializer
@@ -79,7 +79,7 @@ def _detail_data(sve):
class SVEViewSet(viewsets.ModelViewSet):
queryset = SVEBase.objects.all()
queryset = SveManager.QuerySet()
serializer_class = SVESerializer
permission_classes = [AllowAny]
@@ -92,7 +92,7 @@ class SVEViewSet(viewsets.ModelViewSet):
ser.IsValid(raise_exception=True)
data = ser.ValidatedData
qs = SVEBase.objects.all()
qs = SveManager.QuerySet()
Keyword = data.get('Keyword', '')
if Keyword:
qs = qs.filter(
@@ -165,13 +165,7 @@ class SVEViewSet(viewsets.ModelViewSet):
SveID.encode('utf-8')[:16].ljust(16, b'\x00'),
SveID.encode('utf-8'),
)
sve = None
for candidate in candidates:
try:
sve = SVEBase.objects.get(SveID=candidate)
break
except SVEBase.DoesNotExist:
continue
sve = SveManager.GetByCandidates(candidates)
if sve is None:
return Response({'code': 1, 'msg': 'SVE不存在'}, status=404)
@@ -199,13 +193,11 @@ class SVEViewSet(viewsets.ModelViewSet):
'SveLastModified': datetime.datetime.utcnow(),
}
sve, created = SVEBase.objects.update_or_create(
SveID=sve_id_bytes,
defaults=defaults,
sve, created = SveManager.Upsert(
sve_id_bytes,
defaults,
CreateDefaults={'SveCreateTime': datetime.datetime.utcnow()},
)
if created:
sve.SveCreateTime = datetime.datetime.utcnow()
sve.save(update_fields=['SveCreateTime'])
return Response({
'code': 0,