From 3fd22c24eb6faba1c460cbdcacadb347a038a93b Mon Sep 17 00:00:00 2001 From: TermiNexus Date: Thu, 13 Aug 2026 10:18:01 +0800 Subject: [PATCH] =?UTF-8?q?feat(community):=20=E5=AE=98=E7=BD=91=E7=A4=BE?= =?UTF-8?q?=E5=8C=BA=E5=90=8E=E7=AB=AF=E2=80=94=E2=80=94CommunityBoard/Cat?= =?UTF-8?q?egory/Post/Reply/Like=20=E4=BA=94=E8=A1=A8=20+=20/API/Community?= =?UTF-8?q?/*=20=E6=8E=A5=E5=8F=A3=EF=BC=88=E6=9D=BF=E5=9D=97/=E5=B8=96?= =?UTF-8?q?=E5=AD=90/=E8=AF=A6=E6=83=85/=E5=8F=91=E5=B8=96/=E5=9B=9E?= =?UTF-8?q?=E5=A4=8D/=E7=82=B9=E8=B5=9E=EF=BC=89=EF=BC=8C=E6=B3=A8?= =?UTF-8?q?=E5=86=8C=20app=20=E4=B8=8E=E8=B7=AF=E7=94=B1=EF=BC=9BGVSite=20?= =?UTF-8?q?=E6=8C=87=E9=92=88=E6=9B=B4=E6=96=B0=EF=BC=88=E7=A4=BE=E5=8C=BA?= =?UTF-8?q?=E5=89=8D=E7=AB=AF=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- GVSite | 2 +- TempLate/index.html | 4 +- backup/www/index.html | 4 +- gvsds/apps/community/__init__.py | 1 + gvsds/apps/community/apps.py | 6 + .../apps/community/migrations/0001_initial.py | 111 +++++++ gvsds/apps/community/migrations/__init__.py | 0 gvsds/apps/community/models.py | 112 +++++++ gvsds/apps/community/urls.py | 13 + gvsds/apps/community/views.py | 296 ++++++++++++++++++ gvsds/settings.py | 1 + gvsds/urls.py | 3 + 12 files changed, 548 insertions(+), 5 deletions(-) create mode 100644 gvsds/apps/community/__init__.py create mode 100644 gvsds/apps/community/apps.py create mode 100644 gvsds/apps/community/migrations/0001_initial.py create mode 100644 gvsds/apps/community/migrations/__init__.py create mode 100644 gvsds/apps/community/models.py create mode 100644 gvsds/apps/community/urls.py create mode 100644 gvsds/apps/community/views.py diff --git a/GVSite b/GVSite index 4c1cdb3c..30752367 160000 --- a/GVSite +++ b/GVSite @@ -1 +1 @@ -Subproject commit 4c1cdb3c73f3c962aa22c2b51468c8e464fcdb64 +Subproject commit 30752367f62cfd7ef97fc0dce447879d215608d1 diff --git a/TempLate/index.html b/TempLate/index.html index 50f7142f..fd216eeb 100644 --- a/TempLate/index.html +++ b/TempLate/index.html @@ -5,8 +5,8 @@ 银河锋锐软件开发工作室 - - + +
diff --git a/backup/www/index.html b/backup/www/index.html index 2b4a0504..50f7142f 100644 --- a/backup/www/index.html +++ b/backup/www/index.html @@ -5,8 +5,8 @@ 银河锋锐软件开发工作室 - - + +
diff --git a/gvsds/apps/community/__init__.py b/gvsds/apps/community/__init__.py new file mode 100644 index 00000000..87383068 --- /dev/null +++ b/gvsds/apps/community/__init__.py @@ -0,0 +1 @@ +"""GVSDS 官网社区(Community)——板块/分类/帖子/回复/点赞。""" diff --git a/gvsds/apps/community/apps.py b/gvsds/apps/community/apps.py new file mode 100644 index 00000000..6ee70716 --- /dev/null +++ b/gvsds/apps/community/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class CommunityConfig(AppConfig): + name = 'gvsds.apps.community' + verbose_name = '官网社区' diff --git a/gvsds/apps/community/migrations/0001_initial.py b/gvsds/apps/community/migrations/0001_initial.py new file mode 100644 index 00000000..fa12a919 --- /dev/null +++ b/gvsds/apps/community/migrations/0001_initial.py @@ -0,0 +1,111 @@ +"""Add Community models (boards/categories/posts/replies/likes) + +官网社区(discuss.gvsds.com):板块、分类、帖子、回复、点赞五张表。 +""" +import datetime +from django.db import migrations, models +import gvsds.apps.core.models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0003_remove_ephemeraltoken'), + ] + + operations = [ + migrations.CreateModel( + name='CommunityBoard', + fields=[ + ('board_uuid', gvsds.apps.core.models.UUIDField(db_column='BoardUUID', primary_key=True, serialize=False, verbose_name='板块UUID')), + ('board_name', models.CharField(db_column='BoardName', max_length=64, verbose_name='板块名称')), + ('board_desc', models.CharField(db_column='BoardDesc', default='', max_length=255, verbose_name='板块描述')), + ('board_order', models.IntegerField(db_column='BoardOrder', default=0, verbose_name='排序')), + ('board_status', models.SmallIntegerField(db_column='BoardStatus', default=1, verbose_name='状态(1启用/0停用)')), + ('create_time', models.DateTimeField(db_column='CreateTime', auto_now_add=True, verbose_name='创建时间')), + ], + options={ + 'db_table': 'CommunityBoard', + 'ordering': ['BoardOrder', 'CreateTime'], + 'verbose_name': '社区板块', + 'verbose_name_plural': '社区板块', + }, + ), + migrations.CreateModel( + name='CommunityCategory', + fields=[ + ('category_uuid', gvsds.apps.core.models.UUIDField(db_column='CategoryUUID', primary_key=True, serialize=False, verbose_name='分类UUID')), + ('board_uuid', gvsds.apps.core.models.UUIDField(db_column='BoardUUID', verbose_name='板块UUID')), + ('category_name', models.CharField(db_column='CategoryName', max_length=64, verbose_name='分类名称')), + ('category_order', models.IntegerField(db_column='CategoryOrder', default=0, verbose_name='排序')), + ('category_status', models.SmallIntegerField(db_column='CategoryStatus', default=1, verbose_name='状态(1启用/0停用)')), + ('create_time', models.DateTimeField(db_column='CreateTime', auto_now_add=True, verbose_name='创建时间')), + ], + options={ + 'db_table': 'CommunityCategory', + 'ordering': ['CategoryOrder', 'CreateTime'], + 'verbose_name': '社区分类', + 'verbose_name_plural': '社区分类', + }, + ), + migrations.CreateModel( + name='CommunityPost', + fields=[ + ('post_uuid', gvsds.apps.core.models.UUIDField(db_column='PostUUID', primary_key=True, serialize=False, verbose_name='帖子UUID')), + ('board_uuid', gvsds.apps.core.models.UUIDField(db_column='BoardUUID', verbose_name='板块UUID')), + ('category_uuid', gvsds.apps.core.models.UUIDField(blank=True, db_column='CategoryUUID', null=True, verbose_name='分类UUID')), + ('title', models.CharField(db_column='Title', max_length=128, verbose_name='标题')), + ('content', models.TextField(db_column='Content', default='', verbose_name='内容')), + ('author_uuid', gvsds.apps.core.models.UUIDField(db_column='AuthorUUID', verbose_name='作者UUID')), + ('author_name', models.CharField(db_column='AuthorName', default='', max_length=64, verbose_name='作者名称')), + ('view_count', models.IntegerField(db_column='ViewCount', default=0, verbose_name='浏览量')), + ('like_count', models.IntegerField(db_column='LikeCount', default=0, verbose_name='点赞数')), + ('reply_count', models.IntegerField(db_column='ReplyCount', default=0, verbose_name='回复数')), + ('is_pinned', models.SmallIntegerField(db_column='IsPinned', default=0, verbose_name='置顶(1/0)')), + ('is_locked', models.SmallIntegerField(db_column='IsLocked', default=0, verbose_name='锁定(1/0)')), + ('post_status', models.SmallIntegerField(db_column='PostStatus', default=1, verbose_name='状态(1发布/0删除)')), + ('create_time', models.DateTimeField(db_column='CreateTime', auto_now_add=True, verbose_name='创建时间')), + ('update_time', models.DateTimeField(db_column='UpdateTime', auto_now=True, verbose_name='更新时间')), + ], + options={ + 'db_table': 'CommunityPost', + 'ordering': ['-IsPinned', '-CreateTime'], + 'verbose_name': '社区帖子', + 'verbose_name_plural': '社区帖子', + }, + ), + migrations.CreateModel( + name='CommunityReply', + fields=[ + ('reply_uuid', gvsds.apps.core.models.UUIDField(db_column='ReplyUUID', primary_key=True, serialize=False, verbose_name='回复UUID')), + ('post_uuid', gvsds.apps.core.models.UUIDField(db_column='PostUUID', verbose_name='帖子UUID')), + ('author_uuid', gvsds.apps.core.models.UUIDField(db_column='AuthorUUID', verbose_name='作者UUID')), + ('author_name', models.CharField(db_column='AuthorName', default='', max_length=64, verbose_name='作者名称')), + ('content', models.TextField(db_column='Content', default='', verbose_name='回复内容')), + ('reply_to_uuid', gvsds.apps.core.models.UUIDField(blank=True, db_column='ReplyToUUID', null=True, verbose_name='引用回复UUID')), + ('reply_status', models.SmallIntegerField(db_column='ReplyStatus', default=1, verbose_name='状态(1/0)')), + ('create_time', models.DateTimeField(db_column='CreateTime', auto_now_add=True, verbose_name='创建时间')), + ], + options={ + 'db_table': 'CommunityReply', + 'ordering': ['CreateTime'], + 'verbose_name': '社区回复', + 'verbose_name_plural': '社区回复', + }, + ), + migrations.CreateModel( + name='CommunityLike', + fields=[ + ('like_uuid', gvsds.apps.core.models.UUIDField(db_column='LikeUUID', primary_key=True, serialize=False, verbose_name='点赞UUID')), + ('post_uuid', gvsds.apps.core.models.UUIDField(db_column='PostUUID', verbose_name='帖子UUID')), + ('author_uuid', gvsds.apps.core.models.UUIDField(db_column='AuthorUUID', verbose_name='用户UUID')), + ('create_time', models.DateTimeField(db_column='CreateTime', auto_now_add=True, verbose_name='创建时间')), + ], + options={ + 'db_table': 'CommunityLike', + 'verbose_name': '帖子点赞', + 'verbose_name_plural': '帖子点赞', + 'unique_together': {('post_uuid', 'author_uuid')}, + }, + ), + ] diff --git a/gvsds/apps/community/migrations/__init__.py b/gvsds/apps/community/migrations/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/gvsds/apps/community/models.py b/gvsds/apps/community/models.py new file mode 100644 index 00000000..f1f0c5d5 --- /dev/null +++ b/gvsds/apps/community/models.py @@ -0,0 +1,112 @@ +"""模块:Community 社区数据模型 + +官网社区(discuss.gvsds.com): +- CommunityBoard 板块(一级分区,如「交流讨论 / 问题求助 / 项目展示」) +- CommunityCategory 板块内分类(二级,如「公告 / 建议」) +- CommunityPost 帖子(标题 + 内容,挂板块与可选分类) +- CommunityReply 回复(引用楼层可空) +- CommunityLike 点赞(帖子维度,同用户唯一) +""" +from django.db import models + +import gvsds.apps.core.models as core_models +from gvsds.model_base import QModel + + +class CommunityBoard(QModel): + """社区板块""" + + BoardUUID = core_models.UUIDField(primary_key=True, verbose_name='板块UUID') + BoardName = models.CharField(max_length=64, verbose_name='板块名称') + BoardDesc = models.CharField(max_length=255, default='', verbose_name='板块描述') + BoardOrder = models.IntegerField(default=0, verbose_name='排序') + BoardStatus = models.SmallIntegerField(default=1, verbose_name='状态(1启用/0停用)') + CreateTime = models.DateTimeField(auto_now_add=True, verbose_name='创建时间') + + class Meta: + db_table = 'CommunityBoard' + app_label = 'community' + ordering = ['BoardOrder', 'CreateTime'] + verbose_name = '社区板块' + verbose_name_plural = '社区板块' + + +class CommunityCategory(QModel): + """板块内分类""" + + CategoryUUID = core_models.UUIDField(primary_key=True, verbose_name='分类UUID') + BoardUUID = core_models.UUIDField(db_column='BoardUUID', verbose_name='板块UUID') + CategoryName = models.CharField(max_length=64, verbose_name='分类名称') + CategoryOrder = models.IntegerField(default=0, verbose_name='排序') + CategoryStatus = models.SmallIntegerField(default=1, verbose_name='状态(1启用/0停用)') + CreateTime = models.DateTimeField(auto_now_add=True, verbose_name='创建时间') + + class Meta: + db_table = 'CommunityCategory' + app_label = 'community' + ordering = ['CategoryOrder', 'CreateTime'] + verbose_name = '社区分类' + verbose_name_plural = '社区分类' + + +class CommunityPost(QModel): + """社区帖子""" + + PostUUID = core_models.UUIDField(primary_key=True, verbose_name='帖子UUID') + BoardUUID = core_models.UUIDField(db_column='BoardUUID', verbose_name='板块UUID') + CategoryUUID = core_models.UUIDField(null=True, blank=True, db_column='CategoryUUID', verbose_name='分类UUID') + Title = models.CharField(max_length=128, verbose_name='标题') + Content = models.TextField(default='', verbose_name='内容') + AuthorUUID = core_models.UUIDField(db_column='AuthorUUID', verbose_name='作者UUID') + AuthorName = models.CharField(max_length=64, default='', verbose_name='作者名称') + ViewCount = models.IntegerField(default=0, verbose_name='浏览量') + LikeCount = models.IntegerField(default=0, verbose_name='点赞数') + ReplyCount = models.IntegerField(default=0, verbose_name='回复数') + IsPinned = models.SmallIntegerField(default=0, verbose_name='置顶(1/0)') + IsLocked = models.SmallIntegerField(default=0, verbose_name='锁定(1/0)') + PostStatus = models.SmallIntegerField(default=1, verbose_name='状态(1发布/0删除)') + CreateTime = models.DateTimeField(auto_now_add=True, verbose_name='创建时间') + UpdateTime = models.DateTimeField(auto_now=True, verbose_name='更新时间') + + class Meta: + db_table = 'CommunityPost' + app_label = 'community' + ordering = ['-IsPinned', '-CreateTime'] + verbose_name = '社区帖子' + verbose_name_plural = '社区帖子' + + +class CommunityReply(QModel): + """社区回复""" + + ReplyUUID = core_models.UUIDField(primary_key=True, verbose_name='回复UUID') + PostUUID = core_models.UUIDField(db_column='PostUUID', verbose_name='帖子UUID') + AuthorUUID = core_models.UUIDField(db_column='AuthorUUID', verbose_name='作者UUID') + AuthorName = models.CharField(max_length=64, default='', verbose_name='作者名称') + Content = models.TextField(default='', verbose_name='回复内容') + ReplyToUUID = core_models.UUIDField(null=True, blank=True, db_column='ReplyToUUID', verbose_name='引用回复UUID') + ReplyStatus = models.SmallIntegerField(default=1, verbose_name='状态(1/0)') + CreateTime = models.DateTimeField(auto_now_add=True, verbose_name='创建时间') + + class Meta: + db_table = 'CommunityReply' + app_label = 'community' + ordering = ['CreateTime'] + verbose_name = '社区回复' + verbose_name_plural = '社区回复' + + +class CommunityLike(QModel): + """帖子点赞(同用户同帖子唯一)""" + + LikeUUID = core_models.UUIDField(primary_key=True, verbose_name='点赞UUID') + PostUUID = core_models.UUIDField(db_column='PostUUID', verbose_name='帖子UUID') + AuthorUUID = core_models.UUIDField(db_column='AuthorUUID', verbose_name='用户UUID') + CreateTime = models.DateTimeField(auto_now_add=True, verbose_name='创建时间') + + class Meta: + db_table = 'CommunityLike' + app_label = 'community' + unique_together = (('PostUUID', 'AuthorUUID'),) + verbose_name = '帖子点赞' + verbose_name_plural = '帖子点赞' diff --git a/gvsds/apps/community/urls.py b/gvsds/apps/community/urls.py new file mode 100644 index 00000000..02fcd0fe --- /dev/null +++ b/gvsds/apps/community/urls.py @@ -0,0 +1,13 @@ +"""Community 社区子路由(挂在主 URL conf 的 API/Community/*)""" +from django.urls import path + +from . import views + +urlpatterns = [ + path('Boards', views.CommunityBoardsView.as_view(), name='community_boards'), + path('Posts', views.CommunityPostsView.as_view(), name='community_posts'), + path('Post/', views.CommunityPostDetailView.as_view(), name='community_post_detail'), + path('Post', views.CommunityPostCreateView.as_view(), name='community_post_create'), + path('Reply', views.CommunityReplyCreateView.as_view(), name='community_reply'), + path('Like', views.CommunityLikeView.as_view(), name='community_like'), +] diff --git a/gvsds/apps/community/views.py b/gvsds/apps/community/views.py new file mode 100644 index 00000000..fa868e8c --- /dev/null +++ b/gvsds/apps/community/views.py @@ -0,0 +1,296 @@ +"""模块:Community 社区视图 + +官网社区 API(discuss.gvsds.com 前端调用): + GET /API/Community/Boards — 板块 + 分类 + 帖子数(公开) + GET /API/Community/Posts — 帖子列表(分页/板块/分类/关键字,公开) + GET /API/Community/Post/ — 帖子详情 + 回复(公开,浏览量+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) + + +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, + '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, + '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, + '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}}) diff --git a/gvsds/settings.py b/gvsds/settings.py index 6fbbd532..1aac38a0 100644 --- a/gvsds/settings.py +++ b/gvsds/settings.py @@ -62,6 +62,7 @@ INSTALLED_APPS = _app_getlist('apps', 'INSTALLED_APPS', fallback=[ 'gvsds.apps.mc', 'gvsds.apps.doc', 'gvsds.apps.api', + 'gvsds.apps.community', 'gvsdsdk.msync', 'gvsds.apps.payment', 'gvsds.apps.commission', diff --git a/gvsds/urls.py b/gvsds/urls.py index a3228544..be70271f 100644 --- a/gvsds/urls.py +++ b/gvsds/urls.py @@ -160,6 +160,9 @@ urlpatterns = [ path('API/App/Config', gerp_views.AppConfigView.as_view(), name='app_config'), path('API/App/Config/Copy', gerp_views.AppConfigCopyView.as_view(), name='app_config_copy'), + # 官网社区(discuss.gvsds.com 前端调用) + path('API/Community/', include('gvsds.apps.community.urls')), + # 角色权限分配(权限树勾选) path('API/Role/PermissionsTree', gerp_views.RolePermissionsTreeView.as_view(), name='role_permissions_tree'), path('API/Role/SavePermissions', gerp_views.RoleSavePermissionsView.as_view(), name='role_save_permissions'),