Files

286 lines
12 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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']