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_paid = models.BooleanField(default=False, verbose_name="是否收费") price = models.DecimalField(max_digits=10, decimal_places=2, default=0, verbose_name="报名费用") author = models.ForeignKey(WeChatUser, to_field='phone_number', on_delete=models.SET_NULL, related_name='activities', verbose_name="发布者", null=True, blank=True) is_active = models.BooleanField(default=True, verbose_name="是否启用") is_visible = models.BooleanField(default=True, verbose_name="是否显示", help_text="关闭后将不在前端列表页显示") auto_confirm = models.BooleanField(default=False, verbose_name="无需审核", help_text="开启后,付费活动支付成功或免费活动报名后直接显示报名成功,不需要审核") # 常用报名信息开关 ask_name = models.BooleanField(default=False, verbose_name="收集姓名") ask_phone = models.BooleanField(default=False, verbose_name="收集手机号") ask_wechat = models.BooleanField(default=False, verbose_name="收集微信号") ask_company = models.BooleanField(default=False, verbose_name="收集公司/机构") signup_form_config = models.JSONField( default=list, verbose_name="自定义报名配置", blank=True, help_text='JSON格式的高级配置,若填写则优先于上方开关。例如:[{"name": "job", "label": "职位", "type": "text", "required": true}]' ) 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 @property def current_signups(self): """ 当前有效报名人数(仅统计已确认/已支付的报名) """ return self.signups.filter(status='confirmed').count() def __str__(self): return self.title class Meta: verbose_name = "社区活动" verbose_name_plural = "社区活动管理" class ActivitySignup(models.Model): """ 活动报名记录 """ STATUS_CHOICES = ( ('unpaid', '待支付'), ('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="报名时间") signup_info = models.JSONField( default=dict, verbose_name="报名信息", blank=True ) status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='confirmed', verbose_name="状态") # 关联订单(针对付费活动) order = models.ForeignKey('shop.Order', on_delete=models.SET_NULL, null=True, blank=True, verbose_name="关联订单", related_name='activity_signups') def __str__(self): return f"{self.user.nickname} - {self.activity.title}" def check_payment_status(self): """ 检查并同步关联订单的支付状态 """ if self.status == 'unpaid' and self.order: if self.order.status == 'paid': self.status = 'confirmed' if self.activity.auto_confirm else 'pending' self.save() return True return False 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="分类") STATUS_CHOICES = ( ('pending', '待审核'), ('published', '已发布'), ('rejected', '已拒绝'), ) status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='published', 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="浏览量") likes = models.ManyToManyField(WeChatUser, related_name='liked_topics', blank=True, 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="更新时间") order = models.IntegerField(default=0, verbose_name="排序") def save(self, *args, **kwargs): # 记录是否为新对象,因为super().save后pk就有了 is_new = self.pk is None # 第一次保存,先入库 super().save(*args, **kwargs) # 如果是新创建,且 order 默认为 0(未指定) if is_new and getattr(self, 'order', 0) == 0: # 将所有其他帖子的 order + 1,腾出 0 的位置 Topic.objects.exclude(pk=self.pk).filter(order__gte=0).update(order=models.F('order') + 1) # 确保自己是 0 Topic.objects.filter(pk=self.pk).update(order=0) self.order = 0 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: pass return False class Meta: verbose_name = "论坛帖子" verbose_name_plural = "论坛帖子管理" ordering = ['order', '-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="回复楼层") likes = models.ManyToManyField(WeChatUser, related_name='liked_replies', blank=True, verbose_name="点赞用户") is_pinned = models.BooleanField(default=False, 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 = ['-is_pinned', '-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="文件", null=True, blank=True) file_url = models.URLField(max_length=500, verbose_name="文件链接", null=True, blank=True) 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 = "论坛媒体资源管理" class Announcement(models.Model): """ 社区公告模型 """ title = models.CharField(max_length=100, verbose_name="公告标题") content = models.TextField(verbose_name="公告内容") image = models.ImageField(upload_to='announcements/', verbose_name="公告图片", null=True, blank=True) image_url = models.URLField(verbose_name="图片链接", null=True, blank=True, help_text="优先使用上传的图片") link_url = models.URLField(verbose_name="跳转链接", null=True, blank=True) is_active = models.BooleanField(default=True, verbose_name="是否启用") is_pinned = models.BooleanField(default=False, verbose_name="是否置顶") priority = models.IntegerField(default=0, verbose_name="排序权重", help_text="数字越大越靠前") start_time = models.DateTimeField(verbose_name="开始展示时间", null=True, blank=True) end_time = models.DateTimeField(verbose_name="结束展示时间", null=True, blank=True) created_at = models.DateTimeField(auto_now_add=True, verbose_name="创建时间") updated_at = models.DateTimeField(auto_now=True, verbose_name="更新时间") @property def display_image_url(self): if self.image: return self.image.url return self.image_url def __str__(self): return self.title class Meta: verbose_name = "社区公告" verbose_name_plural = "社区公告管理" ordering = ['-is_pinned', '-priority', '-created_at']