Răsfoiți Sursa

Add frontend project structure (Vite + Vue)

0225015 1 lună în urmă
părinte
comite
7d3c30d7c4

+ 54 - 22
tongqu_backend_v2/groups/admin.py

@@ -1,49 +1,40 @@
 from django.contrib import admin
-from .models import Group, Membership, Post # 导入所有需要的模型
+from .models import Group, Membership, Post, Comment, PostLike # <<<< 导入 PostLike
 
-# 为 Membership 模型创建一个内联 Admin 类
 class MembershipInline(admin.TabularInline):
     model = Membership
-    extra = 1 # 默认显示一个空的额外行,方便添加新成员
-    autocomplete_fields = ['user'] # 假设 CustomUserAdmin 中设置了 search_fields
+    extra = 1
+    autocomplete_fields = ['user']
 
-# 为 Group 模型创建一个 Admin 类
 class GroupAdmin(admin.ModelAdmin):
     list_display = ('name', 'creator', 'created_at', 'get_tags_display')
     list_filter = ('created_at', 'tags')
     search_fields = ('name', 'description', 'creator__email', 'creator__nickname')
-    
-    inlines = [MembershipInline] # 将 MembershipInline 嵌入
-    
-    filter_horizontal = ('tags',) # 为 tags 字段使用 filter_horizontal
+    inlines = [MembershipInline]
+    filter_horizontal = ('tags',)
 
     def get_tags_display(self, obj):
         return ", ".join([tag.name for tag in obj.tags.all()])
     get_tags_display.short_description = '相关标签'
 
-# 为 Membership 模型创建一个 Admin 类 (可选,但方便单独管理)
 class MembershipAdmin(admin.ModelAdmin):
-    list_display = ('user_display', 'group', 'date_joined') # 假设 CustomUser 有 display_name
+    list_display = ('user_display', 'group', 'date_joined')
     list_filter = ('group', 'date_joined')
     search_fields = ('user__email', 'user__nickname', 'group__name')
     autocomplete_fields = ['user', 'group']
 
     def user_display(self, obj):
-        # 确保你的 CustomUser 模型有一个 display_name 属性或方法
-        # 如果没有,可以直接用 obj.user.email 或 obj.user.nickname
-        if hasattr(obj.user, 'display_name'):
+        if hasattr(obj.user, 'display_name') and obj.user.display_name:
             return obj.user.display_name
-        return obj.user.email # 或者其他你想显示的字段
+        return obj.user.email
     user_display.short_description = '用户'
     user_display.admin_order_field = 'user__email'
 
-
-# 为 Post 模型创建一个 Admin 类
 class PostAdmin(admin.ModelAdmin):
     list_display = ('title_or_author', 'group', 'author_display', 'created_at', 'short_content')
     list_filter = ('group', 'created_at', 'author')
     search_fields = ('title', 'content', 'author__email', 'author__nickname', 'group__name')
-    autocomplete_fields = ['group', 'author'] # 为外键启用自动完成
+    autocomplete_fields = ['group', 'author']
 
     def title_or_author(self, obj):
         return obj.title if obj.title else f"来自 {self.author_display(obj)} 的动态"
@@ -52,17 +43,58 @@ class PostAdmin(admin.ModelAdmin):
     def author_display(self, obj):
         if hasattr(obj.author, 'display_name'):
             return obj.author.display_name
-        return obj.author.email # 或者其他你想显示的字段
+        return obj.author.email
     author_display.short_description = '作者'
-    author_display.admin_order_field = 'author__email' # 允许按作者邮箱排序
+    author_display.admin_order_field = 'author__email'
 
     def short_content(self, obj):
         return obj.content[:50] + '...' if len(obj.content) > 50 else obj.content
     short_content.short_description = '内容预览'
-# PostAdmin 类定义到此结束
+
+class CommentAdmin(admin.ModelAdmin):
+    list_display = ('author_display_comment', 'post_identifier_comment', 'short_content_comment', 'created_at', 'updated_at')
+    list_filter = ('created_at', 'post', 'author')
+    search_fields = ('content', 'author__email', 'author__nickname', 'post__title')
+    autocomplete_fields = ['post', 'author']
+
+    def author_display_comment(self, obj):
+        if hasattr(obj.author, 'display_name') and obj.author.display_name:
+            return obj.author.display_name
+        return obj.author.email
+    author_display_comment.short_description = '评论作者'
+    author_display_comment.admin_order_field = 'author__email'
+
+    def post_identifier_comment(self, obj):
+        return obj.post.title if obj.post.title else f"帖子ID: {obj.post.id}"
+    post_identifier_comment.short_description = '所属帖子'
+    post_identifier_comment.admin_order_field = 'post__title'
+        
+    def short_content_comment(self, obj):
+        return obj.content[:75] + '...' if len(obj.content) > 75 else obj.content
+    short_content_comment.short_description = '评论内容预览'
+
+class PostLikeAdmin(admin.ModelAdmin): # <<<< 新增 PostLikeAdmin
+    list_display = ('user_display', 'post_identifier', 'created_at')
+    list_filter = ('created_at', 'post__group', 'post', 'user') # 可以按小组、帖子、用户筛选
+    search_fields = ('user__email', 'user__nickname', 'post__title')
+    autocomplete_fields = ['user', 'post'] # 方便选择用户和帖子
+
+    def user_display(self, obj):
+        if hasattr(obj.user, 'display_name') and obj.user.display_name:
+            return obj.user.display_name
+        return obj.user.email
+    user_display.short_description = '点赞用户'
+    user_display.admin_order_field = 'user__email'
+
+    def post_identifier(self, obj):
+        return obj.post.title if obj.post.title else f"帖子ID: {obj.post.id}"
+    post_identifier.short_description = '被点赞的帖子'
+    post_identifier.admin_order_field = 'post__title'
 
 
 # 注册所有模型到Admin站点
 admin.site.register(Group, GroupAdmin)
 admin.site.register(Membership, MembershipAdmin)
-admin.site.register(Post, PostAdmin) # PostAdmin 的注册现在在类定义之后
+admin.site.register(Post, PostAdmin)
+admin.site.register(Comment, CommentAdmin)
+admin.site.register(PostLike, PostLikeAdmin) # <<<< 新增注册 PostLike

+ 84 - 0
tongqu_backend_v2/groups/migrations/0003_alter_membership_options_alter_post_author_and_more.py

@@ -0,0 +1,84 @@
+# Generated by Django 5.2.1 on 2025-06-05 23:14
+
+import django.db.models.deletion
+from django.conf import settings
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ("groups", "0002_alter_membership_options_and_more"),
+        migrations.swappable_dependency(settings.AUTH_USER_MODEL),
+    ]
+
+    operations = [
+        migrations.AlterModelOptions(
+            name="membership",
+            options={
+                "ordering": ["-date_joined"],
+                "verbose_name": "小组成员关系",
+                "verbose_name_plural": "小组成员关系们",
+            },
+        ),
+        migrations.AlterField(
+            model_name="post",
+            name="author",
+            field=models.ForeignKey(
+                on_delete=django.db.models.deletion.CASCADE,
+                related_name="%(app_label)s_posts",
+                to=settings.AUTH_USER_MODEL,
+                verbose_name="作者",
+            ),
+        ),
+        migrations.AlterUniqueTogether(
+            name="membership",
+            unique_together={("user", "group")},
+        ),
+        migrations.CreateModel(
+            name="Comment",
+            fields=[
+                (
+                    "id",
+                    models.BigAutoField(
+                        auto_created=True,
+                        primary_key=True,
+                        serialize=False,
+                        verbose_name="ID",
+                    ),
+                ),
+                ("content", models.TextField(verbose_name="评论内容")),
+                (
+                    "created_at",
+                    models.DateTimeField(auto_now_add=True, verbose_name="评论时间"),
+                ),
+                (
+                    "updated_at",
+                    models.DateTimeField(auto_now=True, verbose_name="更新时间"),
+                ),
+                (
+                    "author",
+                    models.ForeignKey(
+                        on_delete=django.db.models.deletion.CASCADE,
+                        related_name="%(app_label)s_comments",
+                        to=settings.AUTH_USER_MODEL,
+                        verbose_name="评论作者",
+                    ),
+                ),
+                (
+                    "post",
+                    models.ForeignKey(
+                        on_delete=django.db.models.deletion.CASCADE,
+                        related_name="comments",
+                        to="groups.post",
+                        verbose_name="所属帖子",
+                    ),
+                ),
+            ],
+            options={
+                "verbose_name": "评论",
+                "verbose_name_plural": "评论们",
+                "ordering": ["created_at"],
+            },
+        ),
+    ]

+ 58 - 0
tongqu_backend_v2/groups/migrations/0004_postlike.py

@@ -0,0 +1,58 @@
+# Generated by Django 5.2.1 on 2025-06-06 00:12
+
+import django.db.models.deletion
+from django.conf import settings
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ("groups", "0003_alter_membership_options_alter_post_author_and_more"),
+        migrations.swappable_dependency(settings.AUTH_USER_MODEL),
+    ]
+
+    operations = [
+        migrations.CreateModel(
+            name="PostLike",
+            fields=[
+                (
+                    "id",
+                    models.BigAutoField(
+                        auto_created=True,
+                        primary_key=True,
+                        serialize=False,
+                        verbose_name="ID",
+                    ),
+                ),
+                (
+                    "created_at",
+                    models.DateTimeField(auto_now_add=True, verbose_name="点赞时间"),
+                ),
+                (
+                    "post",
+                    models.ForeignKey(
+                        on_delete=django.db.models.deletion.CASCADE,
+                        related_name="likes",
+                        to="groups.post",
+                        verbose_name="被点赞的帖子",
+                    ),
+                ),
+                (
+                    "user",
+                    models.ForeignKey(
+                        on_delete=django.db.models.deletion.CASCADE,
+                        related_name="post_likes",
+                        to=settings.AUTH_USER_MODEL,
+                        verbose_name="点赞用户",
+                    ),
+                ),
+            ],
+            options={
+                "verbose_name": "帖子点赞",
+                "verbose_name_plural": "帖子点赞们",
+                "ordering": ["-created_at"],
+                "unique_together": {("user", "post")},
+            },
+        ),
+    ]

+ 100 - 65
tongqu_backend_v2/groups/models.py

@@ -1,31 +1,29 @@
 from django.db import models
 from django.conf import settings # 用于引用 AUTH_USER_MODEL
 from django.utils.translation import gettext_lazy as _
-# 我们可以让小组也关联到兴趣标签
-# from accounts.models import InterestTag # 如果你想直接引用
+from django.utils import timezone
 
 class Group(models.Model):
     name = models.CharField(_('小组名称'), max_length=100, unique=True)
     description = models.TextField(_('小组描述'), blank=True)
     cover_image = models.ImageField(_('小组封面'), upload_to='group_covers/', null=True, blank=True)
     creator = models.ForeignKey(
-        settings.AUTH_USER_MODEL, # 关联到你的CustomUser模型
+        settings.AUTH_USER_MODEL,
         verbose_name=_('创建者'),
-        on_delete=models.SET_NULL, # 如果创建者被删除,小组可以保留,或者设为CASCADE则一起删除
-        null=True, # 允许创建者为空 (虽然通常不希望这样,但SET_NULL要求)
+        on_delete=models.SET_NULL,
+        null=True,
         related_name='created_groups'
     )
     members = models.ManyToManyField(
         settings.AUTH_USER_MODEL,
-        through='Membership', # 通过我们下面定义的Membership模型来管理成员关系
-        through_fields=('group', 'user'), # 指定Membership模型中关联Group和User的字段名
+        through='Membership',
+        through_fields=('group', 'user'),
         verbose_name=_('小组成员'),
-        blank=True, # 小组刚创建时可以没有成员 (创建者会自动成为第一个成员)
+        blank=True,
         related_name='joined_groups'
     )
-    # 关联到兴趣标签 (可选,但对推荐小组有帮助)
     tags = models.ManyToManyField(
-        'accounts.InterestTag', # 引用 accounts 应用的 InterestTag 模型
+        'accounts.InterestTag',
         verbose_name=_('相关兴趣标签'),
         blank=True,
         related_name='groups_with_tag'
@@ -33,87 +31,124 @@ class Group(models.Model):
     created_at = models.DateTimeField(auto_now_add=True, verbose_name=_('创建时间'))
     updated_at = models.DateTimeField(auto_now=True, verbose_name=_('更新时间'))
 
-    # (可选) 小组类型:公开、私密等
-    # PUBLIC = 'public'
-    # PRIVATE = 'private'
-    # GROUP_TYPE_CHOICES = [
-    #     (PUBLIC, _('公开小组')),
-    #     (PRIVATE, _('私密小组')),
-    # ]
-    # group_type = models.CharField(
-    #     max_length=10,
-    #     choices=GROUP_TYPE_CHOICES,
-    #     default=PUBLIC,
-    #     verbose_name=_('小组类型')
-    # )
-
     class Meta:
         verbose_name = _('小组')
         verbose_name_plural = _('小组们')
-        ordering = ['-created_at'] # 按创建时间降序排列
+        ordering = ['-created_at']
 
     def __str__(self):
         return self.name
 
 class Membership(models.Model):
-    """
-    用户和小组之间的中间模型,用于存储额外的成员关系信息。
-    """
     user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, verbose_name=_('用户'))
     group = models.ForeignKey(Group, on_delete=models.CASCADE, verbose_name=_('小组'))
     date_joined = models.DateTimeField(auto_now_add=True, verbose_name=_('加入日期'))
     
-    # (可选) 成员角色
-    # MEMBER = 'member'
-    # ADMIN = 'admin'
-    # MODERATOR = 'moderator'
-    # ROLE_CHOICES = [
-    #     (MEMBER, _('成员')),
-    #     (ADMIN, _('管理员')),
-    #     (MODERATOR, _('版主')),
-    # ]
-    # role = models.CharField(
-    #     max_length=10,
-    #     choices=ROLE_CHOICES,
-    #     default=MEMBER,
-    #     verbose_name=_('角色')
-    # )
+    class Meta:
+        verbose_name = _('小组成员关系')
+        verbose_name_plural = _('小组成员关系们')
+        unique_together = ('user', 'group')
+        ordering = ['-date_joined']
+
+    def __str__(self):
+        user_identifier = self.user.email
+        if hasattr(self.user, 'display_name') and self.user.display_name:
+            user_identifier = self.user.display_name
+        elif hasattr(self.user, 'nickname') and self.user.nickname:
+            user_identifier = self.user.nickname
+        return f"{user_identifier} 加入了 {self.group.name}"
+
 class Post(models.Model):
     group = models.ForeignKey(
         Group, 
-        on_delete=models.CASCADE, # 如果小组被删除,该小组下的所有帖子也一并删除
-        related_name='posts',     # 允许通过 group_instance.posts.all() 获取小组所有帖子
+        on_delete=models.CASCADE,
+        related_name='posts',
         verbose_name=_('所属小组')
     )
     author = models.ForeignKey(
-        settings.AUTH_USER_MODEL, # 关联到 CustomUser 模型
-        on_delete=models.CASCADE, # 如果发帖用户被删除,其帖子也一并删除 (你也可以考虑SET_NULL)
-        related_name='posts',     # 允许通过 user_instance.posts.all() 获取用户所有帖子
+        settings.AUTH_USER_MODEL,
+        on_delete=models.CASCADE,
+        related_name='%(app_label)s_posts',
         verbose_name=_('作者')
     )
-    title = models.CharField(_('帖子标题'), max_length=200, blank=True, null=True) # 帖子可以有标题,也可以没有(类似动态)
+    title = models.CharField(_('帖子标题'), max_length=200, blank=True, null=True)
     content = models.TextField(_('帖子内容'))
-    # (可选) 帖子图片或附件,可以用 ImageField 或 FileField,或者更复杂的方案如多图片关联
-    # image = models.ImageField(_('帖子图片'), upload_to='post_images/', null=True, blank=True)
-    
     created_at = models.DateTimeField(auto_now_add=True, verbose_name=_('发布时间'))
     updated_at = models.DateTimeField(auto_now=True, verbose_name=_('更新时间'))
 
-    # (可选) 点赞数、评论数等统计字段,可以通过 annotate 动态获取,或在这里冗余存储以提高查询性能
-    # likes_count = models.PositiveIntegerField(default=0, verbose_name=_('点赞数'))
-    # comments_count = models.PositiveIntegerField(default=0, verbose_name=_('评论数'))
-
     class Meta:
         verbose_name = _('帖子/动态')
         verbose_name_plural = _('帖子/动态们')
-        ordering = ['-created_at'] # 默认按发布时间降序排列 (最新的在前面)
+        ordering = ['-created_at']
+
+    def __str__(self):
+        author_identifier = self.author.email
+        if hasattr(self.author, 'display_name') and self.author.display_name:
+            author_identifier = self.author.display_name
+        elif hasattr(self.author, 'nickname') and self.author.nickname:
+            author_identifier = self.author.nickname
+        title_part = f"'{self.title}'" if self.title else f"动态来自 {author_identifier}"
+        return f"{title_part} 在小组 '{self.group.name}'"
+
+class Comment(models.Model):
+    post = models.ForeignKey(
+        Post,
+        on_delete=models.CASCADE,
+        related_name='comments',
+        verbose_name=_('所属帖子')
+    )
+    author = models.ForeignKey(
+        settings.AUTH_USER_MODEL,
+        on_delete=models.CASCADE,
+        related_name='%(app_label)s_comments',
+        verbose_name=_('评论作者')
+    )
+    content = models.TextField(_('评论内容'))
+    created_at = models.DateTimeField(auto_now_add=True, verbose_name=_('评论时间'))
+    updated_at = models.DateTimeField(auto_now=True, verbose_name=_('更新时间'))
+
+    class Meta:
+        verbose_name = _('评论')
+        verbose_name_plural = _('评论们')
+        ordering = ['created_at']
+
+    def __str__(self):
+        author_identifier = self.author.email
+        if hasattr(self.author, 'display_name') and self.author.display_name:
+            author_identifier = self.author.display_name
+        elif hasattr(self.author, 'nickname') and self.author.nickname:
+            author_identifier = self.author.nickname
+        post_identifier = self.post.title if self.post.title else f"帖子ID({self.post.id})"
+        content_preview = (self.content[:30] + '...') if len(self.content) > 30 else self.content
+        return f"{author_identifier} 对 '{post_identifier}' 的评论: \"{content_preview}\""
+
+class PostLike(models.Model): # <<<< 新增 PostLike 模型
+    user = models.ForeignKey(
+        settings.AUTH_USER_MODEL,
+        on_delete=models.CASCADE,
+        related_name='post_likes',
+        verbose_name=_('点赞用户')
+    )
+    post = models.ForeignKey(
+        Post,
+        on_delete=models.CASCADE,
+        related_name='likes', # 允许 post.likes.count()
+        verbose_name=_('被点赞的帖子')
+    )
+    created_at = models.DateTimeField(auto_now_add=True, verbose_name=_('点赞时间'))
+
+    class Meta:
+        verbose_name = _('帖子点赞')
+        verbose_name_plural = _('帖子点赞们')
+        unique_together = ('user', 'post') # 核心:确保用户对帖子只能点赞一次
+        ordering = ['-created_at']
 
     def __str__(self):
-        return self.title if self.title else f"帖子来自 {self.author.display_name} 在 {self.group.name}"
-# 为了上面的 __str__ 方法,我们可能需要在 CustomUser 模型中添加一个辅助方法
-# 打开 accounts/models.py,在 CustomUser 类中添加:
-# def nickname_or_email(self):
-#     return self.nickname if self.nickname else self.email
-# CustomUser.add_to_class("nickname_or_email", nickname_or_email) # 如果想动态添加
-# 或者直接在 __str__ 中写:
-# return f"{self.user.nickname if self.user.nickname else self.user.email} 加入了 {self.group.name}"
+        user_identifier = self.user.email
+        if hasattr(self.user, 'display_name') and self.user.display_name:
+            user_identifier = self.user.display_name
+        elif hasattr(self.user, 'nickname') and self.user.nickname:
+            user_identifier = self.user.nickname
+        
+        post_identifier = self.post.title if self.post.title else f"帖子ID({self.post.id})"
+        return f"{user_identifier} 点赞了 '{post_identifier}'"

+ 61 - 29
tongqu_backend_v2/groups/serializers.py

@@ -1,5 +1,5 @@
 from rest_framework import serializers
-from .models import Group, Membership, Post
+from .models import Group, Membership, Post, Comment, PostLike # <<<< 导入 PostLike 模型
 from accounts.models import CustomUser, InterestTag
 from accounts.serializers import InterestTagSerializer as AccountsInterestTagSerializer
 
@@ -86,41 +86,73 @@ class MembershipSerializer(serializers.ModelSerializer):
         read_only_fields = ('id', 'user', 'group_id', 'group_name', 'date_joined')
 
 
-class PostSerializer(serializers.ModelSerializer): # 经过再次修正和简化的 PostSerializer
-    author = MinimalUserSerializer(read_only=True) # 作者信息只读,在GET时显示
-    group = MinimalGroupSerializer(read_only=True)   # 小组信息只读,在GET时显示
+class PostSerializer(serializers.ModelSerializer): # <<<< 修改 PostSerializer
+    author = MinimalUserSerializer(read_only=True)
+    group = MinimalGroupSerializer(read_only=True)
+    
+    # 新增:点赞相关字段
+    likes_count = serializers.SerializerMethodField(read_only=True)
+    is_liked_by_current_user = serializers.SerializerMethodField(read_only=True)
 
     class Meta:
         model = Post
         fields = (
             'id', 
-            'group',        # 在GET时显示小组信息 (只读)
-            'author',       # 在GET时显示作者信息 (只读)
+            'group',
+            'author',
             'title', 
             'content',
-            # 'image',      # 如果模型中有 image 字段,可以在这里添加用于上传
-            # 'image_url',  # 如果模型中有 image 字段,可以添加 get_image_url 方法
             'created_at', 
             'updated_at',
+            'likes_count',                # 新增
+            'is_liked_by_current_user',   # 新增
         )
-        # author 和 group 将在视图的 serializer.save() 中设置,所以它们对客户端来说是只读的
-        # title 和 content 是客户端在创建帖子时可以发送的
-        read_only_fields = ('id', 'author', 'group', 'created_at', 'updated_at')
-    
-    # create 方法不需要特别修改,因为 author 和 group 会在视图的 save() 中传入
-    # def create(self, validated_data):
-    #     # validated_data 只包含客户端发送的 title, content
-    #     # author 和 group 是通过 serializer.save(author=request.user, group=group) 传入的
-    #     # ModelSerializer的默认create会将这些额外参数设置到模型实例上
-    #     return super().create(validated_data)
-    # 实际上,DRF的ModelSerializer在调用save时传递的额外关键字参数,
-    # 会在模型实例被创建(通过调用模型的objects.create或类似方法)之前,
-    # 被添加到传递给模型构造函数的关键字参数中。
-    # 所以,PostSerializer 的 create 方法可以保持默认,或者明确地像下面这样处理(如果需要更细致控制)
-    # def create(self, validated_data):
-    #     # author 和 group 是从 serializer.save(author=..., group=...) 传递过来的
-    #     # 它们现在应该在 validated_data 中,或者需要从 context 中获取
-    #     # 更准确地说,它们会作为额外参数传递给模型的create方法
-    #     # 保持默认的 super().create() 即可,只要视图的save()调用正确
-    #     return super().create(validated_data)
-    # 我们让 PostSerializer 的 create 方法保持默认,依赖视图的 serializer.save(author=..., group=...)
+        read_only_fields = (
+            'id', 'author', 'group', 'created_at', 'updated_at', 
+            'likes_count', 'is_liked_by_current_user' # 新增的也设为只读
+        )
+
+    def get_likes_count(self, obj: Post) -> int: # obj 是 Post 实例
+        """获取帖子的总点赞数。"""
+        return obj.likes.count() # obj.likes 是 PostLike 的 related_name
+
+    def get_is_liked_by_current_user(self, obj: Post) -> bool: # obj 是 Post 实例
+        """检查当前用户是否已点赞该帖子。"""
+        request = self.context.get('request')
+        if not request or not request.user or not request.user.is_authenticated:
+            return False # 未登录用户或匿名用户视为未点赞
+        
+        # 检查是否存在当前用户对该帖子的点赞记录
+        return PostLike.objects.filter(post=obj, user=request.user).exists()
+
+
+class CommentSerializer(serializers.ModelSerializer):
+    author = MinimalUserSerializer(read_only=True)
+    post_id_display = serializers.IntegerField(source='post.id', read_only=True)
+    post_title_display = serializers.CharField(source='post.title', read_only=True, allow_null=True)
+
+    class Meta:
+        model = Comment
+        fields = (
+            'id',
+            'post_id_display',
+            'post_title_display',
+            'author',
+            'content',
+            'created_at',
+            'updated_at',
+        )
+        read_only_fields = ('id', 'author', 'post_id_display', 'post_title_display', 'created_at', 'updated_at')
+
+
+class PostLikeSerializer(serializers.ModelSerializer): # <<<< (可选) 新增 PostLikeSerializer
+    user = MinimalUserSerializer(read_only=True) 
+    # 如果需要在点赞详情中显示帖子信息,可以添加 post 字段
+    # post = MinimalPostSerializer(read_only=True) # 假设有 MinimalPostSerializer
+    post_id = serializers.IntegerField(source='post.id', read_only=True)
+
+
+    class Meta:
+        model = PostLike
+        fields = ('id', 'user', 'post_id', 'created_at')
+        read_only_fields = ('id', 'user', 'post_id', 'created_at')

+ 5 - 4
tongqu_backend_v2/groups/urls.py

@@ -1,15 +1,16 @@
 from django.urls import path, include
 from rest_framework.routers import DefaultRouter
-from .views import GroupViewSet, PostViewSet # <<<< 确保 PostViewSet 已导入
+from .views import GroupViewSet, PostViewSet, CommentViewSet # <<<< 确保 CommentViewSet 已导入
 
 app_name = 'groups'
 
 router = DefaultRouter()
 router.register(r'groups', GroupViewSet, basename='group')
-router.register(r'posts', PostViewSet, basename='post')   # <<<< 新增注册 PostViewSet
+router.register(r'posts', PostViewSet, basename='post')
+router.register(r'comments', CommentViewSet, basename='comment') # <<<< 新增注册 CommentViewSet
 
 urlpatterns = [
-    # router.urls 会自动包含为 GroupViewSet 和 PostViewSet 生成的所有标准URL
-    # 以及 GroupViewSet 中通过 @action 定义的路由
+    # router.urls 会自动包含为 GroupViewSet, PostViewSet, CommentViewSet 生成的所有标准URL
+    # 以及它们各自通过 @action 定义的路由
     path('', include(router.urls)),
 ]

+ 90 - 20
tongqu_backend_v2/groups/views.py

@@ -1,16 +1,17 @@
 from rest_framework import viewsets, status, permissions
 from rest_framework.decorators import action
 from rest_framework.response import Response
-from rest_framework.exceptions import MethodNotAllowed # <<<< 新增导入
-# from django.shortcuts import get_object_or_404
+from rest_framework.exceptions import MethodNotAllowed
 
-from .models import Group, Membership, Post
+from .models import Group, Membership, Post, Comment, PostLike # <<<< 确保 PostLike 已导入
 from .serializers import (
     GroupSerializer,
     # MembershipSerializer,
-    PostSerializer
+    PostSerializer,
+    CommentSerializer
+    # PostLikeSerializer, # 目前点赞/取消点赞的action直接返回PostSerializer的数据
 )
-from .permissions import IsAuthorOrReadOnly # 从自定义权限文件导入
+from .permissions import IsAuthorOrReadOnly
 
 class GroupViewSet(viewsets.ModelViewSet):
     queryset = Group.objects.all().order_by('-created_at')
@@ -84,34 +85,103 @@ class GroupViewSet(viewsets.ModelViewSet):
 
 class PostViewSet(viewsets.ModelViewSet):
     queryset = Post.objects.all().select_related('author', 'group').order_by('-created_at')
-    serializer_class = PostSerializer
+    serializer_class = PostSerializer # 主序列化器是 PostSerializer
 
     def get_permissions(self):
         if self.action in ['list', 'retrieve']:
-            permission_classes = [permissions.IsAuthenticated] # 或者 IsAuthenticatedOrReadOnly
+            permission_classes = [permissions.IsAuthenticated]
         elif self.action in ['update', 'partial_update', 'destroy']:
             permission_classes = [permissions.IsAuthenticated, IsAuthorOrReadOnly]
+        elif self.action in ['list_comments', 'create_comment', 'like_post', 'unlike_post']: # <<<< 添加了 like_post, unlike_post
+            permission_classes = [permissions.IsAuthenticated]
         elif self.action == 'create':
-            # 即使是Admin,我们也希望通过小组接口创建帖子,以确保group关联正确
-            # 所以这里可以直接禁止,或者只允许特定调试场景
-            permission_classes = [permissions.IsAdminUser] # 示例:只允许Admin(但会被perform_create阻止)
+            permission_classes = [permissions.IsAdminUser]
         else:
             permission_classes = [permissions.IsAuthenticated]
         return [permission() for permission in permission_classes]
 
-    def perform_create(self, serializer): # <<<< 修改这里
-        """
-        通过此独立ViewSet创建帖子是不推荐的。
-        应通过 /api/v1/groups/<group_pk>/create-post/ 创建。
-        """
+    def perform_create(self, serializer):
         raise MethodNotAllowed(
             method=self.request.method,
             detail="不推荐通过此接口创建帖子。请通过小组接口 /api/v1/groups/<group_pk>/create-post/ 创建。"
         )
 
-    # perform_update 和 perform_destroy 由 IsAuthorOrReadOnly 权限类控制,无需显式覆盖默认行为
-    # def perform_update(self, serializer):
-    #     serializer.save() # DRF默认行为
+    @action(detail=True, methods=['get'], url_path='comment-list', url_name='post-comments-list-alt')
+    def list_comments(self, request, pk=None):
+        post = self.get_object()
+        comments_queryset = Comment.objects.filter(post=post).select_related('author').order_by('created_at')
+        page = self.paginate_queryset(comments_queryset)
+        if page is not None:
+            serializer = CommentSerializer(page, many=True, context={'request': request})
+            return self.get_paginated_response(serializer.data)
+        serializer = CommentSerializer(comments_queryset, many=True, context={'request': request})
+        return Response(serializer.data)
+
+    @action(detail=True, methods=['post'], url_path='comments', url_name='post-comment-create')
+    def create_comment(self, request, pk=None):
+        post = self.get_object()
+        serializer = CommentSerializer(data=request.data, context={'request': request})
+        if serializer.is_valid():
+            serializer.save(author=request.user, post=post)
+            return Response(serializer.data, status=status.HTTP_201_CREATED)
+        return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
+
+    # --- 新增:用于帖子点赞/取消点赞的 Action ---
+    @action(detail=True, methods=['post'], url_path='like', url_name='post-like')
+    def like_post(self, request, pk=None):
+        """
+        当前登录用户点赞指定ID的帖子。
+        POST /api/v1/posts/<pk>/like/
+        """
+        post = self.get_object() # 获取pk对应的Post实例
+        user = request.user
+
+        if PostLike.objects.filter(user=user, post=post).exists():
+            return Response({'detail': '您已经点赞过该帖子了。'}, status=status.HTTP_400_BAD_REQUEST)
+
+        PostLike.objects.create(user=user, post=post)
+        
+        # 返回更新后的帖子信息(包含新的点赞数和is_liked状态)
+        # get_serializer() 会使用 PostViewSet 的 self.serializer_class (即 PostSerializer)
+        serializer = self.get_serializer(post, context={'request': request}) 
+        return Response(serializer.data, status=status.HTTP_200_OK)
 
-    # def perform_destroy(self, instance):
-    #     instance.delete() # DRF默认行为
+    @action(detail=True, methods=['post'], url_path='unlike', url_name='post-unlike')
+    def unlike_post(self, request, pk=None):
+        """
+        当前登录用户取消点赞指定ID的帖子。
+        POST /api/v1/posts/<pk>/unlike/
+        """
+        post = self.get_object()
+        user = request.user
+
+        try:
+            like = PostLike.objects.get(user=user, post=post)
+            like.delete()
+            # 返回更新后的帖子信息
+            serializer = self.get_serializer(post, context={'request': request})
+            return Response(serializer.data, status=status.HTTP_200_OK)
+        except PostLike.DoesNotExist:
+            return Response({'detail': '您还没有点赞该帖子。'}, status=status.HTTP_400_BAD_REQUEST)
+
+
+class CommentViewSet(viewsets.ModelViewSet):
+    queryset = Comment.objects.all().select_related('author', 'post').order_by('-created_at')
+    serializer_class = CommentSerializer
+
+    def get_permissions(self):
+        if self.action in ['list', 'retrieve']:
+            permission_classes = [permissions.IsAuthenticated]
+        elif self.action in ['update', 'partial_update', 'destroy']:
+            permission_classes = [permissions.IsAuthenticated, IsAuthorOrReadOnly]
+        elif self.action == 'create':
+            permission_classes = [permissions.IsAdminUser]
+        else:
+            permission_classes = [permissions.IsAuthenticated]
+        return [permission() for permission in permission_classes]
+
+    def perform_create(self, serializer):
+        raise MethodNotAllowed(
+            method=self.request.method,
+            detail="请通过帖子接口 /api/v1/posts/<post_pk>/comments/ 创建评论。"
+        )