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'),