import re from datetime import date from django.db import transaction from django.db.models import Max from rest_framework import serializers from .models import ( Article, ArticleComment, ArticleType, Column, Magazine, MagazineCategory, Organize, OrganizeCategory, Professor, RuleBand, Specialty, StarRule, Subject, WorkGroup, WorkGroupMembership, WorkGroupTerm, ) class ColumnSerializer(serializers.ModelSerializer): class Meta: model = Column fields = [ "id", "uuid", "name", "description", "note", "template_id", "position", "parent", "sort", "is_disabled", "path", "created_at", "updated_at", ] class TaxonomySerializer(serializers.ModelSerializer): class Meta: fields = [ "id", "serial_num", "name", "parent", "sort", "is_disabled", "created_at", "updated_at", ] class SubjectSerializer(TaxonomySerializer): article_count = serializers.IntegerField(read_only=True, required=False) class Meta(TaxonomySerializer.Meta): model = Subject fields = TaxonomySerializer.Meta.fields + ["article_count"] class OrganizeCategorySerializer(TaxonomySerializer): class Meta(TaxonomySerializer.Meta): model = OrganizeCategory class MagazineCategorySerializer(TaxonomySerializer): class Meta(TaxonomySerializer.Meta): model = MagazineCategory class ArticleTypeSerializer(serializers.ModelSerializer): article_count = serializers.IntegerField(read_only=True, required=False) class Meta: model = ArticleType fields = [ "id", "serial_num", "code", "name", "note", "sort", "is_disabled", "article_count", "created_at", "updated_at", ] def validate_code(self, value): code = (value or "").strip().upper() if not code: raise serializers.ValidationError("请填写字母标识") if not re.fullmatch(r"[A-Z]{1,8}", code): raise serializers.ValidationError("字母标识仅支持 1–8 位大写英文字母") return code class OrganizeListSerializer(serializers.ModelSerializer): article_count = serializers.IntegerField(read_only=True, required=False) avg_score = serializers.DecimalField( max_digits=10, decimal_places=2, read_only=True, required=False ) class Meta: model = Organize fields = [ "id", "serial_num", "name", "logo", "description", "url", "category", "flags", "collect_num", "like_num", "share_num", "rank", "is_disabled", "article_count", "avg_score", "created_at", "updated_at", ] class OrganizeSerializer(OrganizeListSerializer): pass class MagazineSerializer(serializers.ModelSerializer): article_count = serializers.IntegerField(read_only=True, required=False) avg_score = serializers.DecimalField( max_digits=10, decimal_places=2, read_only=True, required=False ) class Meta: model = Magazine fields = [ "id", "serial_num", "name", "logo", "category", "subjects", "is_disabled", "article_count", "avg_score", "created_at", "updated_at", ] class SpecialtySerializer(serializers.ModelSerializer): class Meta: model = Specialty fields = [ "id", "name", "type", "description", "logo", "content", "position", "is_disabled", "created_at", "updated_at", ] class WorkGroupSerializer(serializers.ModelSerializer): class Meta: model = WorkGroup fields = [ "id", "name", "description", "parent", "sort", "is_disabled", "created_at", "updated_at", ] class WorkGroupTermSerializer(serializers.ModelSerializer): workgroup_name = serializers.CharField(source="workgroup.name", read_only=True) class Meta: model = WorkGroupTerm fields = [ "id", "workgroup", "workgroup_name", "parent", "name", "start_year", "end_year", "is_current", "sort", "is_disabled", "created_at", "updated_at", ] class WorkGroupMembershipSerializer(serializers.ModelSerializer): term_detail = WorkGroupTermSerializer(source="term", read_only=True) class Meta: model = WorkGroupMembership fields = ["id", "professor", "term", "term_detail", "role"] class ProfessorSerializer(serializers.ModelSerializer): memberships = WorkGroupMembershipSerializer(many=True, read_only=True) terms = serializers.PrimaryKeyRelatedField(many=True, read_only=True) class Meta: model = Professor fields = [ "id", "serial_num", "name", "type", "role", "job", "description", "email", "addr", "logo", "phone", "position", "terms", "memberships", "is_disabled", "created_at", "updated_at", ] class RuleBandSerializer(serializers.ModelSerializer): class Meta: model = RuleBand fields = [ "id", "rule", "star_low", "star_high", "star", "is_disabled", "created_at", "updated_at", ] def validate_star(self, value): if value is None: return value num = float(value) if num < 0.5 or num > 5: raise serializers.ValidationError("星级须在 0.5~5 之间") # 仅允许 0.5 步进(如 1、1.5、2…) if abs(num * 2 - round(num * 2)) > 1e-9: raise serializers.ValidationError("星级须为 0.5 的倍数(如 1、1.5、2)") return value def validate_star_low(self, value): if value is not None and float(value) < 0: raise serializers.ValidationError("最低分不能为负数") return value def validate_star_high(self, value): if value is not None and float(value) < 0: raise serializers.ValidationError("最高分不能为负数") return value def validate(self, attrs): attrs = super().validate(attrs) instance = getattr(self, "instance", None) low = attrs.get("star_low", getattr(instance, "star_low", None)) high = attrs.get("star_high", getattr(instance, "star_high", None)) star = attrs.get("star", getattr(instance, "star", None)) rule = attrs.get("rule", getattr(instance, "rule", None)) is_disabled = attrs.get( "is_disabled", getattr(instance, "is_disabled", False) if instance else False, ) if low is not None and high is not None and float(low) > float(high): raise serializers.ValidationError({"star_high": "最高分不能低于最低分"}) if rule is None or is_disabled: return attrs rule_id = getattr(rule, "pk", rule) siblings = RuleBand.objects.filter(rule_id=rule_id, is_disabled=False) if instance is not None: siblings = siblings.exclude(pk=instance.pk) if star is not None: if siblings.filter(star=star).exists(): raise serializers.ValidationError( {"star": f"该规则下已存在星级 {star} 的分档"} ) if low is not None and high is not None: low_f, high_f = float(low), float(high) for other in siblings: o_low, o_high = float(other.star_low), float(other.star_high) # 闭区间重叠:[a,b] 与 [c,d] 当 a<=d 且 c<=b if low_f <= o_high and o_low <= high_f: raise serializers.ValidationError( { "star_low": ( f"分数区间与已有分档 {other.star_low}~{other.star_high} " f"({other.star} 星)重叠" ) } ) return attrs class StarRuleSerializer(serializers.ModelSerializer): bands = RuleBandSerializer(many=True, read_only=True) class Meta: model = StarRule fields = [ "id", "name", "year", "is_disabled", "bands", "created_at", "updated_at", ] def validate_year(self, value): if value is None: return value if value < 1900 or value > 2100: raise serializers.ValidationError("请输入合理年份(1900~2100)") return value def validate(self, attrs): attrs = super().validate(attrs) instance = getattr(self, "instance", None) year = attrs.get("year", getattr(instance, "year", None)) is_disabled = attrs.get( "is_disabled", getattr(instance, "is_disabled", False) if instance else False, ) if year is None or is_disabled: return attrs qs = StarRule.objects.filter(year=year, is_disabled=False) if instance is not None: qs = qs.exclude(pk=instance.pk) if qs.exists(): raise serializers.ValidationError( {"year": f"已存在启用中的 {year} 年评分规则,请先禁用或改用其他年份"} ) return attrs class ArticleListSerializer(serializers.ModelSerializer): organizes = OrganizeListSerializer(many=True, read_only=True) magazines = MagazineSerializer(many=True, read_only=True) subjects = SubjectSerializer(many=True, read_only=True) type_name = serializers.CharField(source="type.name", read_only=True) class Meta: model = Article fields = [ "id", "serial_number", "title", "type", "type_name", "year", "score", "url", "flags", "introduction", "push_date", "read_num", "collect_num", "like_num", "share_num", "rank", "is_disabled", "organizes", "magazines", "subjects", "created_at", "updated_at", ] class ArticleSerializer(ArticleListSerializer): class Meta(ArticleListSerializer.Meta): fields = ArticleListSerializer.Meta.fields + ["content"] def _next_article_serial(article_type, year=None): """ 生成文献编号:{类型字母标识}{年份}{5位录入序号} 例:指南(G) + 2024 + 第 1 条 → G202400001 序号按「同类型字母 + 同年」的录入顺序递增。 """ code = (getattr(article_type, "code", None) or "").strip().upper() if not code: raise serializers.ValidationError({"type": "该文献类型未配置字母标识"}) year = year or date.today().year prefix = f"{code}{year}" latest = ( Article.objects.filter(serial_number__startswith=prefix) .aggregate(Max("serial_number")) .get("serial_number__max") ) seq = 1 if latest and latest.startswith(prefix): tail = latest[len(prefix) :] if tail.isdigit(): seq = int(tail) + 1 if seq > 99999: raise serializers.ValidationError( {"serial_number": f"{prefix} 当年序号已满,请联系管理员"} ) return f"{prefix}{seq:05d}" class ArticleWriteSerializer(serializers.ModelSerializer): serial_number = serializers.CharField(read_only=True) class Meta: model = Article fields = [ "serial_number", "title", "type", "year", "score", "url", "flags", "content", "introduction", "push_date", "rank", "is_disabled", "organizes", "magazines", "subjects", ] @transaction.atomic def create(self, validated_data): article_type = validated_data["type"] validated_data["serial_number"] = _next_article_serial( article_type, validated_data.get("year") ) return super().create(validated_data) class ArticleCommentSerializer(serializers.ModelSerializer): member_name = serializers.CharField(source="member.name", read_only=True) member_phone = serializers.CharField(source="member.phone", read_only=True) member_organize = serializers.CharField(source="member.organize", read_only=True) article_title = serializers.CharField(source="article.title", read_only=True) class Meta: model = ArticleComment fields = [ "id", "article", "article_title", "member", "member_name", "member_phone", "member_organize", "star", "content", "review_status", "created_at", "updated_at", ]