aifeilong 13b1ca2b9b Namespace API routes by app and flatten package layout.
Move Django apps out of apps/ and mount them under auth/, common/, content/, and cms/ so endpoint paths map directly to the owning app.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-15 08:42:37 +08:00

562 lines
18 KiB
Python
Raw 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 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}"