222 lines
8.8 KiB
Python
222 lines
8.8 KiB
Python
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="是否启用")
|
||
signup_form_config = models.JSONField(
|
||
default=list,
|
||
verbose_name="报名表单配置",
|
||
blank=True,
|
||
help_text='配置报名时需要收集的信息,JSON格式,例如:[{"name": "phone", "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
|
||
|
||
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="报名时间")
|
||
signup_info = models.JSONField(
|
||
default=dict,
|
||
verbose_name="报名信息",
|
||
blank=True
|
||
)
|
||
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:
|
||
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="文件", 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']
|