from django.db import models from common.models import SoftDeleteModel, TimeStampedModel class NavPosition(models.IntegerChoices): TOP = 1, "顶部" MIDDLE = 2, "中部" BOTTOM = 3, "底部" class Column(SoftDeleteModel): """导航栏目。""" uuid = models.CharField(max_length=64, blank=True, default="", verbose_name="栏目 UUID") name = models.CharField(max_length=200, verbose_name="名称") description = models.TextField(blank=True, default="", verbose_name="描述") note = models.TextField(blank=True, default="", verbose_name="备注") template_id = models.IntegerField(null=True, blank=True, verbose_name="页面模板") position = models.IntegerField( choices=NavPosition.choices, default=NavPosition.TOP, verbose_name="位置" ) parent = models.ForeignKey( "self", null=True, blank=True, on_delete=models.SET_NULL, related_name="children", verbose_name="父栏目", ) sort = models.IntegerField(default=0, verbose_name="排序") is_disabled = models.BooleanField(default=False, verbose_name="禁用") path = models.CharField(max_length=200, blank=True, default="", verbose_name="前端路径") class Meta: ordering = ["sort", "id"] verbose_name = "栏目" verbose_name_plural = verbose_name def __str__(self): return self.name class TaxonomyBase(SoftDeleteModel): """分类字典公共字段。""" serial_num = models.CharField(max_length=64, blank=True, default="", verbose_name="编号") name = models.CharField(max_length=200, verbose_name="名称") sort = models.IntegerField(default=0, verbose_name="排序") is_disabled = models.BooleanField(default=False, verbose_name="禁用") class Meta: abstract = True def __str__(self): return self.name class Subject(TaxonomyBase): """学科。""" parent = models.ForeignKey( "self", null=True, blank=True, on_delete=models.SET_NULL, related_name="children", verbose_name="父学科", ) class Meta: ordering = ["sort", "id"] verbose_name = "学科" verbose_name_plural = verbose_name class OrganizeCategory(TaxonomyBase): """机构类别。""" parent = models.ForeignKey( "self", null=True, blank=True, on_delete=models.SET_NULL, related_name="children", verbose_name="父类别", ) class Meta: ordering = ["sort", "id"] verbose_name = "机构类别" verbose_name_plural = verbose_name class MagazineCategory(TaxonomyBase): """杂志类别。""" parent = models.ForeignKey( "self", null=True, blank=True, on_delete=models.SET_NULL, related_name="children", verbose_name="父类别", ) class Meta: ordering = ["sort", "id"] verbose_name = "杂志类别" verbose_name_plural = verbose_name class ArticleType(TaxonomyBase): """文献类型(指南、共识等,可后台维护)。""" code = models.CharField( max_length=8, unique=True, verbose_name="字母标识", help_text="用于文献编号前缀,如 G、C", ) note = models.TextField(blank=True, default="", verbose_name="备注") class Meta: ordering = ["sort", "id"] verbose_name = "文献类型" verbose_name_plural = verbose_name class Organize(SoftDeleteModel): """制订机构。""" serial_num = models.CharField(max_length=64, blank=True, default="", verbose_name="编号") name = models.CharField(max_length=200, verbose_name="名称") logo = models.URLField(blank=True, default="", verbose_name="Logo") description = models.TextField(blank=True, default="", verbose_name="描述") url = models.URLField(blank=True, default="", verbose_name="官网") category = models.ForeignKey( OrganizeCategory, null=True, blank=True, on_delete=models.SET_NULL, related_name="organizes", verbose_name="机构类别", ) flags = models.JSONField(default=list, blank=True, verbose_name="标签") collect_num = models.PositiveIntegerField(default=0, verbose_name="收藏数") like_num = models.PositiveIntegerField(default=0, verbose_name="点赞数") share_num = models.PositiveIntegerField(default=0, verbose_name="分享数") rank = models.IntegerField(null=True, blank=True, verbose_name="首页排序") is_disabled = models.BooleanField(default=False, verbose_name="禁用") class Meta: ordering = ["rank", "id"] verbose_name = "机构" verbose_name_plural = verbose_name def __str__(self): return self.name class Magazine(SoftDeleteModel): """杂志。""" serial_num = models.CharField(max_length=64, blank=True, default="", verbose_name="编号") name = models.CharField(max_length=200, verbose_name="名称") logo = models.URLField(blank=True, default="", verbose_name="Logo") category = models.ForeignKey( MagazineCategory, null=True, blank=True, on_delete=models.SET_NULL, related_name="magazines", verbose_name="杂志类别", ) subjects = models.ManyToManyField( Subject, blank=True, related_name="magazines", verbose_name="学科", ) is_disabled = models.BooleanField(default=False, verbose_name="禁用") class Meta: ordering = ["id"] verbose_name = "杂志" verbose_name_plural = verbose_name def __str__(self): return self.name class Specialty(SoftDeleteModel): """专科/专长。""" name = models.CharField(max_length=200, verbose_name="名称") type = models.IntegerField(null=True, blank=True, verbose_name="类型") description = models.TextField(blank=True, default="", verbose_name="描述") logo = models.URLField(blank=True, default="", verbose_name="Logo") content = models.JSONField(default=list, blank=True, verbose_name="内容块") position = models.IntegerField(default=0, verbose_name="排序") is_disabled = models.BooleanField(default=False, verbose_name="禁用") class Meta: ordering = ["position", "id"] verbose_name = "专科" verbose_name_plural = verbose_name def __str__(self): return self.name class WorkGroup(SoftDeleteModel): """工作组模板(可含分委会层级)。""" name = models.CharField(max_length=200, verbose_name="名称") description = models.TextField(blank=True, default="", verbose_name="描述") parent = models.ForeignKey( "self", null=True, blank=True, on_delete=models.SET_NULL, related_name="children", verbose_name="上级工作组", ) sort = models.IntegerField(default=0, verbose_name="排序") is_disabled = models.BooleanField(default=False, verbose_name="禁用") class Meta: ordering = ["sort", "id"] verbose_name = "工作组" verbose_name_plural = verbose_name def __str__(self): return self.name class WorkGroupTerm(SoftDeleteModel): """工作组某一届;parent 表示同届内的分委会层级。""" workgroup = models.ForeignKey( WorkGroup, on_delete=models.CASCADE, related_name="terms", verbose_name="工作组", ) parent = models.ForeignKey( "self", null=True, blank=True, on_delete=models.SET_NULL, related_name="children", verbose_name="同届上级", ) name = models.CharField(max_length=200, verbose_name="届次名称") start_year = models.IntegerField(null=True, blank=True, verbose_name="起始年") end_year = models.IntegerField(null=True, blank=True, verbose_name="结束年") is_current = models.BooleanField(default=False, verbose_name="现任届") sort = models.IntegerField(default=0, verbose_name="排序") is_disabled = models.BooleanField(default=False, verbose_name="禁用") class Meta: ordering = ["-is_current", "-start_year", "sort", "id"] verbose_name = "工作组届次" verbose_name_plural = verbose_name def __str__(self): return f"{self.workgroup.name} · {self.name}" class WorkGroupMembership(models.Model): """专家在某一届/分委会中的任职。""" professor = models.ForeignKey( "Professor", on_delete=models.CASCADE, related_name="memberships", verbose_name="专家", ) term = models.ForeignKey( WorkGroupTerm, on_delete=models.CASCADE, related_name="memberships", verbose_name="届次", ) role = models.CharField(max_length=200, blank=True, default="", verbose_name="组内职务") class Meta: unique_together = [("professor", "term")] verbose_name = "工作组任职" verbose_name_plural = verbose_name def __str__(self): return f"{self.professor_id}@{self.term_id}" class Professor(SoftDeleteModel): """专家。""" serial_num = models.CharField(max_length=64, blank=True, default="", verbose_name="编号") name = models.CharField(max_length=200, verbose_name="名称") type = models.IntegerField(null=True, blank=True, verbose_name="分类") role = models.CharField(max_length=200, blank=True, default="", verbose_name="身份") job = models.CharField(max_length=200, blank=True, default="", verbose_name="职称") description = models.TextField(blank=True, default="", verbose_name="简介") email = models.EmailField(blank=True, default="", verbose_name="邮箱") addr = models.CharField(max_length=500, blank=True, default="", verbose_name="地址") logo = models.URLField(blank=True, default="", verbose_name="头像") phone = models.CharField(max_length=50, blank=True, default="", verbose_name="电话") position = models.IntegerField(default=0, verbose_name="排序") terms = models.ManyToManyField( WorkGroupTerm, through="WorkGroupMembership", blank=True, related_name="professors", verbose_name="所属届次", ) is_disabled = models.BooleanField(default=False, verbose_name="禁用") class Meta: ordering = ["position", "id"] verbose_name = "专家" verbose_name_plural = verbose_name def __str__(self): return self.name class StarRule(SoftDeleteModel): """STAR 评分规则。""" name = models.CharField(max_length=200, verbose_name="名称") year = models.IntegerField(verbose_name="年份") is_disabled = models.BooleanField(default=False, verbose_name="禁用") class Meta: ordering = ["-year", "id"] verbose_name = "STAR 规则" verbose_name_plural = verbose_name def __str__(self): return f"{self.name} ({self.year})" class RuleBand(SoftDeleteModel): """评分分档明细。""" rule = models.ForeignKey( StarRule, on_delete=models.CASCADE, related_name="bands", verbose_name="规则" ) star_low = models.DecimalField(max_digits=10, decimal_places=2, verbose_name="最低分") star_high = models.DecimalField(max_digits=10, decimal_places=2, verbose_name="最高分") star = models.DecimalField(max_digits=10, decimal_places=2, verbose_name="星级") is_disabled = models.BooleanField(default=False, verbose_name="禁用") class Meta: ordering = ["star_low", "id"] verbose_name = "评分分档" verbose_name_plural = verbose_name def __str__(self): return f"{self.rule_id}: {self.star_low}-{self.star_high}" class Article(SoftDeleteModel): """指南 / 共识文献。""" serial_number = models.CharField(max_length=64, blank=True, default="", verbose_name="编号") title = models.CharField(max_length=500, verbose_name="标题") type = models.ForeignKey( ArticleType, on_delete=models.PROTECT, related_name="articles", verbose_name="类型", ) year = models.IntegerField(null=True, blank=True, verbose_name="年份") score = models.DecimalField( max_digits=10, decimal_places=2, null=True, blank=True, verbose_name="STAR 得分" ) url = models.URLField(blank=True, default="", verbose_name="原文地址") flags = models.JSONField(default=list, blank=True, verbose_name="标签") content = models.TextField(blank=True, default="", verbose_name="正文") introduction = models.TextField(blank=True, default="", verbose_name="简介") push_date = models.CharField(max_length=32, blank=True, default="", verbose_name="发布日期") read_num = models.PositiveIntegerField(default=0, verbose_name="阅读数") collect_num = models.PositiveIntegerField(default=0, verbose_name="收藏数") like_num = models.PositiveIntegerField(default=0, verbose_name="点赞数") share_num = models.PositiveIntegerField(default=0, verbose_name="分享数") rank = models.IntegerField(null=True, blank=True, verbose_name="首页排序") is_disabled = models.BooleanField(default=False, verbose_name="禁用") organizes = models.ManyToManyField( Organize, blank=True, related_name="articles", verbose_name="机构" ) magazines = models.ManyToManyField( Magazine, blank=True, related_name="articles", verbose_name="杂志" ) subjects = models.ManyToManyField( Subject, blank=True, related_name="articles", verbose_name="学科", ) class Meta: ordering = ["-year", "-score", "id"] verbose_name = "文献" verbose_name_plural = verbose_name def __str__(self): return self.title class ReviewStatus(models.IntegerChoices): PENDING = 1, "待审核" REJECTED = 2, "不通过" APPROVED = 3, "通过" class ArticleComment(SoftDeleteModel): """指南/共识用户评价。""" article = models.ForeignKey( Article, on_delete=models.CASCADE, related_name="comments", verbose_name="文献", ) member = models.ForeignKey( "accounts.Member", on_delete=models.CASCADE, related_name="comments", verbose_name="会员", ) star = models.DecimalField(max_digits=3, decimal_places=1, verbose_name="评价星级") content = models.TextField(blank=True, default="", verbose_name="评价内容") review_status = models.IntegerField( choices=ReviewStatus.choices, default=ReviewStatus.PENDING, verbose_name="审核状态", ) class Meta: ordering = ["-id"] verbose_name = "文献评价" verbose_name_plural = verbose_name def __str__(self): return f"Comment#{self.pk} on Article#{self.article_id}" class ArticleLike(TimeStampedModel): """会员点赞文献(需登录;取消则硬删除)。""" article = models.ForeignKey( Article, on_delete=models.CASCADE, related_name="likes", verbose_name="文献", ) member = models.ForeignKey( "accounts.Member", on_delete=models.CASCADE, related_name="article_likes", verbose_name="会员", ) class Meta: ordering = ["-id"] verbose_name = "文献点赞" verbose_name_plural = verbose_name constraints = [ models.UniqueConstraint( fields=["article", "member"], name="content_articlelike_article_member_uniq", ) ] def __str__(self): return f"Like member={self.member_id} article={self.article_id}" class ArticleCollect(TimeStampedModel): """会员收藏文献(需登录;取消则硬删除)。""" article = models.ForeignKey( Article, on_delete=models.CASCADE, related_name="collections", verbose_name="文献", ) member = models.ForeignKey( "accounts.Member", on_delete=models.CASCADE, related_name="article_collections", verbose_name="会员", ) class Meta: ordering = ["-id"] verbose_name = "文献收藏" verbose_name_plural = verbose_name constraints = [ models.UniqueConstraint( fields=["article", "member"], name="content_articlecollect_article_member_uniq", ) ] def __str__(self): return f"Collect member={self.member_id} article={self.article_id}" class ArticleShare(TimeStampedModel): """ 文献分享去重记录:同一会员或同一访客对同一文献只计一次。 - 已登录:按 member 去重 - 未登录:按前端 visitor_key(如 localStorage UUID)去重 """ article = models.ForeignKey( Article, on_delete=models.CASCADE, related_name="shares", verbose_name="文献", ) member = models.ForeignKey( "accounts.Member", on_delete=models.CASCADE, null=True, blank=True, related_name="article_shares", verbose_name="会员", ) visitor_key = models.CharField( max_length=64, blank=True, default="", verbose_name="访客标识", ) class Meta: ordering = ["-id"] verbose_name = "文献分享" verbose_name_plural = verbose_name constraints = [ models.UniqueConstraint( fields=["article", "member"], condition=models.Q(member__isnull=False), name="content_articleshare_article_member_uniq", ), models.UniqueConstraint( fields=["article", "visitor_key"], condition=~models.Q(visitor_key=""), name="content_articleshare_article_visitor_uniq", ), ] def __str__(self): who = f"member={self.member_id}" if self.member_id else f"visitor={self.visitor_key}" return f"Share {who} article={self.article_id}"