from django.db import models from shop.models import WeChatUser, ESP32Config, Order, Service, VCCourse, ServiceOrder class Activity(models.Model): """ 社区活动模型 """ title = models.CharField(max_length=100, verbose_name="活动标题") description = models.TextField(verbose_name="活动详情") banner = models.ImageField(upload_to='activities/banners/', verbose_name="活动Banner图", null=True, blank=True) banner_url = models.URLField(verbose_name="活动Banner链接", null=True, blank=True, help_text="可直接填写图片链接,若同时上传图片,将优先显示上传的图片") start_time = models.DateTimeField(verbose_name="开始时间") end_time = models.DateTimeField(verbose_name="结束时间") location = models.CharField(max_length=100, verbose_name="活动地点") max_participants = models.IntegerField(default=50, verbose_name="最大报名人数") is_active = models.BooleanField(default=True, verbose_name="是否启用") created_at = models.DateTimeField(auto_now_add=True, verbose_name="创建时间") def clean(self): from django.core.exceptions import ValidationError if not self.banner and not self.banner_url: raise ValidationError("Banner图片和Banner链接必须至少填写一项") def save(self, *args, **kwargs): self.clean() super().save(*args, **kwargs) @property def display_banner_url(self): """ 获取Banner显示的URL,优先使用上传的图片 """ if self.banner: return self.banner.url return self.banner_url def __str__(self): return self.title class Meta: verbose_name = "社区活动" verbose_name_plural = "社区活动管理" class ActivitySignup(models.Model): """ 活动报名记录 """ STATUS_CHOICES = ( ('pending', '审核中'), ('confirmed', '报名成功'), ('cancelled', '已取消'), ) activity = models.ForeignKey(Activity, on_delete=models.CASCADE, related_name='signups', verbose_name="活动") user = models.ForeignKey(WeChatUser, on_delete=models.CASCADE, related_name='activity_signups', verbose_name="报名用户") signup_time = models.DateTimeField(auto_now_add=True, verbose_name="报名时间") status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='confirmed', verbose_name="状态") def __str__(self): return f"{self.user.nickname} - {self.activity.title}" class Meta: verbose_name = "活动报名" verbose_name_plural = "活动报名管理" unique_together = ('activity', 'user') class Topic(models.Model): """ 论坛帖子/主题 """ title = models.CharField(max_length=200, verbose_name="标题") CATEGORY_CHOICES = ( ('discussion', '技术讨论'), ('help', '求助问答'), ('share', '经验分享'), ('notice', '官方公告'), ) category = models.CharField(max_length=20, choices=CATEGORY_CHOICES, default='discussion', verbose_name="分类") content = models.TextField(verbose_name="内容", help_text="支持Markdown格式,支持插入图片") author = models.ForeignKey(WeChatUser, on_delete=models.CASCADE, related_name='topics', verbose_name="作者") # 关联对象:硬件、服务、课程 related_product = models.ForeignKey(ESP32Config, on_delete=models.SET_NULL, null=True, blank=True, verbose_name="关联硬件") related_service = models.ForeignKey(Service, on_delete=models.SET_NULL, null=True, blank=True, verbose_name="关联服务") related_course = models.ForeignKey(VCCourse, on_delete=models.SET_NULL, null=True, blank=True, verbose_name="关联课程") view_count = models.IntegerField(default=0, verbose_name="浏览量") is_pinned = models.BooleanField(default=False, verbose_name="置顶") created_at = models.DateTimeField(auto_now_add=True, verbose_name="发布时间") updated_at = models.DateTimeField(auto_now=True, verbose_name="更新时间") def __str__(self): return self.title @property def is_verified_owner(self): """ 判断作者是否为关联项目(硬件/服务/课程)的已购用户(Verified Owner) """ # 1. 验证硬件 if self.related_product: if Order.objects.filter( wechat_user=self.author, config=self.related_product, status__in=['paid', 'shipped'] ).exists(): return True # 2. 验证课程 if self.related_course: if Order.objects.filter( wechat_user=self.author, course=self.related_course, status__in=['paid', 'shipped'] ).exists(): return True # 3. 验证服务 if self.related_service: # ServiceOrder 模型中没有 direct link to WeChatUser (only phone/name), # 但我们假设通过手机号或未来关联来验证,目前先检查 ServiceOrder 是否有对应记录。 # 由于 ServiceOrder 目前设计没有直接关联 WeChatUser 字段,我们暂时尝试通过名字或后续改进。 # 经检查 shop/models.py, ServiceOrder 确实只有 customer_name/phone_number. # 这里为了严谨,我们暂时仅对有关联的进行检查,或者需要改进 ServiceOrder。 # 鉴于当前任务范围,如果 ServiceOrder 没有 user 字段,我们可能无法精确验证, # 除非我们假设用户填写的手机号与微信用户关联。 # *修正*: 为了快速实现,我们先跳过 ServiceOrder 的精确验证,或者仅仅返回 False, # 等待后续 ServiceOrder 添加 wechat_user 字段。 pass return False class Meta: verbose_name = "论坛帖子" verbose_name_plural = "论坛帖子管理" ordering = ['-is_pinned', '-created_at'] class Reply(models.Model): """ 帖子回复 """ topic = models.ForeignKey(Topic, on_delete=models.CASCADE, related_name='replies', verbose_name="所属帖子") content = models.TextField(verbose_name="回复内容", help_text="支持Markdown格式") author = models.ForeignKey(WeChatUser, on_delete=models.CASCADE, related_name='replies', verbose_name="回复者") reply_to = models.ForeignKey('self', on_delete=models.SET_NULL, null=True, blank=True, related_name='children', verbose_name="回复楼层") created_at = models.DateTimeField(auto_now_add=True, verbose_name="回复时间") def __str__(self): return f"回复: {self.topic.title}" class Meta: verbose_name = "帖子回复" verbose_name_plural = "帖子回复管理" ordering = ['created_at'] class TopicMedia(models.Model): """ 论坛多媒体资源(图片/视频/文件) """ MEDIA_TYPE_CHOICES = ( ('image', '图片'), ('video', '视频'), ('file', '文件'), ) topic = models.ForeignKey(Topic, on_delete=models.CASCADE, related_name='media', verbose_name="所属帖子", null=True, blank=True) reply = models.ForeignKey(Reply, on_delete=models.CASCADE, related_name='media', verbose_name="所属回复", null=True, blank=True) file = models.FileField(upload_to='community/media/', verbose_name="文件") media_type = models.CharField(max_length=10, choices=MEDIA_TYPE_CHOICES, default='image', verbose_name="媒体类型") created_at = models.DateTimeField(auto_now_add=True, verbose_name="上传时间") def __str__(self): return f"{self.media_type} - {self.file.name}" class Meta: verbose_name = "论坛媒体资源" verbose_name_plural = "论坛媒体资源管理"