Files
zlqy/models.py
2026-02-27 10:37:11 +08:00

470 lines
23 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.
# models.py
from flask_sqlalchemy import SQLAlchemy
from datetime import datetime
import json
db = SQLAlchemy()
# 用户表
class User(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(80), nullable=False)
email = db.Column(db.String(120), unique=True, nullable=True)
phone = db.Column(db.String(20), unique=True, nullable=True)
password = db.Column(db.String(128), nullable=False)
role = db.Column(db.String(20), default='student') # admin, teacher, student
avatar = db.Column(db.String(200), default='') # 头像URL
is_banned = db.Column(db.Boolean, default=False)
completed_exams = db.Column(db.Integer, default=0) # 已完成并批改的考试次数
name_changed_at = db.Column(db.DateTime, nullable=True) # 上次修改用户名时间
created_at = db.Column(db.DateTime, default=datetime.utcnow)
# 关系
exams_created = db.relationship('Exam', back_populates='creator', lazy=True)
submissions = db.relationship('Submission', back_populates='user', lazy=True)
drafts = db.relationship('Draft', back_populates='user', lazy=True)
posts = db.relationship('Post', back_populates='author', lazy=True)
replies = db.relationship('Reply', back_populates='author', lazy=True)
reactions = db.relationship('Reaction', back_populates='user', lazy=True)
bookmarks = db.relationship('Bookmark', back_populates='user', lazy=True)
reports = db.relationship('Report', backref='reporter', lazy=True)
notifications = db.relationship('Notification', backref='user', lazy=True)
contest_memberships = db.relationship('ContestMembership', back_populates='user', lazy=True)
contest_registrations = db.relationship('ContestRegistration', backref='user', lazy=True)
# teacher_applications 将通过 TeacherApplication 的 back_populates 自动创建
# 杯赛成员表(记录用户在特定杯赛中的角色)
class ContestMembership(db.Model):
id = db.Column(db.Integer, primary_key=True)
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
contest_id = db.Column(db.Integer, db.ForeignKey('contest.id'), nullable=False)
role = db.Column(db.String(20), nullable=False) # 'owner' 或 'teacher'
joined_at = db.Column(db.DateTime, default=datetime.utcnow)
user = db.relationship('User', back_populates='contest_memberships')
contest = db.relationship('Contest', back_populates='members')
__table_args__ = (db.UniqueConstraint('user_id', 'contest_id', name='uq_user_contest'),)
# 杯赛申请表(用于用户申请举办新杯赛)
class ContestApplication(db.Model):
id = db.Column(db.Integer, primary_key=True)
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
name = db.Column(db.String(100), nullable=False)
organizer = db.Column(db.String(100))
description = db.Column(db.Text)
contact = db.Column(db.String(100))
status = db.Column(db.String(20), default='pending') # pending, approved, rejected
applied_at = db.Column(db.DateTime, default=datetime.utcnow)
reviewed_at = db.Column(db.DateTime)
total_score = db.Column(db.Integer, default=150) # 杯赛默认满分
start_date = db.Column(db.String(20)) # 申请时填写的开始日期
end_date = db.Column(db.String(20)) # 申请时填写的结束日期
# 报备信息
responsible_person = db.Column(db.String(80)) # 责任人姓名
responsible_phone = db.Column(db.String(20)) # 责任人电话
responsible_email = db.Column(db.String(120)) # 责任人邮箱
organization = db.Column(db.String(100)) # 所属机构/学校
user = db.relationship('User', backref='contest_applications')
# 杯赛报名表(用户报名参加杯赛)
class ContestRegistration(db.Model):
id = db.Column(db.Integer, primary_key=True)
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
contest_id = db.Column(db.Integer, db.ForeignKey('contest.id'), nullable=False)
registered_at = db.Column(db.DateTime, default=datetime.utcnow)
__table_args__ = (db.UniqueConstraint('user_id', 'contest_id', name='uq_user_contest_reg'),)
# 杯赛表
class Contest(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(100), nullable=False)
organizer = db.Column(db.String(100))
description = db.Column(db.Text)
start_date = db.Column(db.String(20))
end_date = db.Column(db.String(20))
status = db.Column(db.String(20), default='upcoming')
participants = db.Column(db.Integer, default=0)
created_by = db.Column(db.Integer, db.ForeignKey('user.id'))
created_at = db.Column(db.DateTime, default=datetime.utcnow)
past_papers = db.Column(db.Text) # JSON 存储历年真题 [{year, title, file}]
total_score = db.Column(db.Integer, default=150) # 杯赛默认考试满分
visible = db.Column(db.Boolean, default=True) # 是否对普通用户可见
# 报备信息
responsible_person = db.Column(db.String(80)) # 责任人姓名
responsible_phone = db.Column(db.String(20)) # 责任人电话
responsible_email = db.Column(db.String(120)) # 责任人邮箱
organization = db.Column(db.String(100)) # 所属机构/学校
creator = db.relationship('User', backref='contests_created')
members = db.relationship('ContestMembership', back_populates='contest', lazy=True, cascade='all, delete-orphan')
exams = db.relationship('Exam', backref='contest', lazy=True)
posts = db.relationship('Post', back_populates='contest', lazy=True, cascade='all, delete-orphan')
teacher_applications = db.relationship('TeacherApplication', back_populates='contest', lazy=True, cascade='all, delete-orphan')
def get_past_papers(self):
return json.loads(self.past_papers) if self.past_papers else []
def set_past_papers(self, papers):
self.past_papers = json.dumps(papers)
# 题库表(杯赛老师和负责人往题库添加题目,负责人选题组卷)
class QuestionBankItem(db.Model):
id = db.Column(db.Integer, primary_key=True)
contest_id = db.Column(db.Integer, db.ForeignKey('contest.id'), nullable=False)
contributor_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
type = db.Column(db.String(20), nullable=False) # 'choice', 'fill', 'essay'
content = db.Column(db.Text, nullable=False) # 题目内容
options = db.Column(db.Text) # JSON选择题选项
answer = db.Column(db.Text) # 答案
score = db.Column(db.Integer, default=10) # 建议分值
created_at = db.Column(db.DateTime, default=datetime.utcnow)
contest = db.relationship('Contest', backref='question_bank_items')
contributor = db.relationship('User', backref='contributed_questions')
# 考试表
class Exam(db.Model):
id = db.Column(db.Integer, primary_key=True)
contest_id = db.Column(db.Integer, db.ForeignKey('contest.id'), nullable=True)
title = db.Column(db.String(200), nullable=False)
subject = db.Column(db.String(50))
duration = db.Column(db.Integer)
total_score = db.Column(db.Integer)
creator_id = db.Column(db.Integer, db.ForeignKey('user.id'))
questions = db.Column(db.Text) # JSON
encrypted_questions = db.Column(db.Text) # 加密后的试卷内容
is_encrypted = db.Column(db.Boolean, default=False) # 是否加密
access_password = db.Column(db.String(128)) # 考试访问密码
scheduled_start = db.Column(db.DateTime, nullable=True) # 预定开始时间
scheduled_end = db.Column(db.DateTime, nullable=True) # 预定结束时间
score_release_time = db.Column(db.DateTime, nullable=True) # 成绩公布时间
created_at = db.Column(db.DateTime, default=datetime.utcnow)
status = db.Column(db.String(20), default='available')
creator = db.relationship('User', back_populates='exams_created')
submissions = db.relationship('Submission', back_populates='exam', lazy=True, cascade='all, delete-orphan')
drafts = db.relationship('Draft', back_populates='exam', lazy=True, cascade='all, delete-orphan')
def get_questions(self):
return json.loads(self.questions) if self.questions else []
def set_questions(self, questions):
self.questions = json.dumps(questions)
# 提交记录表
class Submission(db.Model):
id = db.Column(db.Integer, primary_key=True)
exam_id = db.Column(db.Integer, db.ForeignKey('exam.id'))
user_id = db.Column(db.Integer, db.ForeignKey('user.id'))
answers = db.Column(db.Text) # JSON
score = db.Column(db.Integer, default=0)
question_scores = db.Column(db.Text) # JSON
graded = db.Column(db.Boolean, default=False)
graded_by = db.Column(db.String(80))
submitted_at = db.Column(db.DateTime, default=datetime.utcnow)
exam = db.relationship('Exam', back_populates='submissions')
user = db.relationship('User', back_populates='submissions')
def get_answers(self):
return json.loads(self.answers) if self.answers else {}
def set_answers(self, answers):
self.answers = json.dumps(answers)
def get_question_scores(self):
return json.loads(self.question_scores) if self.question_scores else {}
def set_question_scores(self, scores):
self.question_scores = json.dumps(scores)
# 草稿表
class Draft(db.Model):
id = db.Column(db.Integer, primary_key=True)
exam_id = db.Column(db.Integer, db.ForeignKey('exam.id'))
user_id = db.Column(db.Integer, db.ForeignKey('user.id'))
answers = db.Column(db.Text) # JSON
saved_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
exam = db.relationship('Exam', back_populates='drafts')
user = db.relationship('User', back_populates='drafts')
def get_answers(self):
return json.loads(self.answers) if self.answers else {}
def set_answers(self, answers):
self.answers = json.dumps(answers)
# 论坛帖子表
class Post(db.Model):
id = db.Column(db.Integer, primary_key=True)
title = db.Column(db.String(200), nullable=False)
content = db.Column(db.Text, nullable=False)
author_id = db.Column(db.Integer, db.ForeignKey('user.id'))
tag = db.Column(db.String(50), default='全部')
is_official = db.Column(db.Boolean, default=False)
pinned = db.Column(db.Boolean, default=False)
likes = db.Column(db.Integer, default=0)
replies_count = db.Column(db.Integer, default=0)
views = db.Column(db.Integer, default=0)
has_poll = db.Column(db.Boolean, default=False)
images = db.Column(db.Text) # JSON
contest_id = db.Column(db.Integer, db.ForeignKey('contest.id'), nullable=True)
created_at = db.Column(db.DateTime, default=datetime.utcnow)
updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
author = db.relationship('User', back_populates='posts')
replies = db.relationship('Reply', back_populates='post', lazy=True, cascade='all, delete-orphan')
poll = db.relationship('Poll', back_populates='post', uselist=False, cascade='all, delete-orphan')
reactions = db.relationship('Reaction', back_populates='post', lazy=True, cascade='all, delete-orphan')
bookmarks = db.relationship('Bookmark', back_populates='post', lazy=True, cascade='all, delete-orphan')
contest = db.relationship('Contest', back_populates='posts')
def get_images(self):
return json.loads(self.images) if self.images else []
def set_images(self, images):
self.images = json.dumps(images)
# 回复表
class Reply(db.Model):
id = db.Column(db.Integer, primary_key=True)
post_id = db.Column(db.Integer, db.ForeignKey('post.id'))
author_id = db.Column(db.Integer, db.ForeignKey('user.id'))
content = db.Column(db.Text, nullable=False)
likes = db.Column(db.Integer, default=0)
reply_to = db.Column(db.String(80))
created_at = db.Column(db.DateTime, default=datetime.utcnow)
updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
post = db.relationship('Post', back_populates='replies')
author = db.relationship('User', back_populates='replies')
reactions = db.relationship('Reaction', back_populates='reply', lazy=True, cascade='all, delete-orphan')
# 投票表
class Poll(db.Model):
id = db.Column(db.Integer, primary_key=True)
post_id = db.Column(db.Integer, db.ForeignKey('post.id'))
question = db.Column(db.String(200), nullable=False)
options = db.Column(db.Text) # JSON
voters = db.Column(db.Text) # JSON
multi = db.Column(db.Boolean, default=False)
total_votes = db.Column(db.Integer, default=0)
created_at = db.Column(db.DateTime, default=datetime.utcnow)
post = db.relationship('Post', back_populates='poll')
def get_options(self):
return json.loads(self.options) if self.options else []
def set_options(self, options):
self.options = json.dumps(options)
def get_voters(self):
return json.loads(self.voters) if self.voters else {}
def set_voters(self, voters):
self.voters = json.dumps(voters)
# 表情反应表
class Reaction(db.Model):
id = db.Column(db.Integer, primary_key=True)
user_id = db.Column(db.Integer, db.ForeignKey('user.id'))
post_id = db.Column(db.Integer, db.ForeignKey('post.id'), nullable=True)
reply_id = db.Column(db.Integer, db.ForeignKey('reply.id'), nullable=True)
reaction = db.Column(db.String(20))
created_at = db.Column(db.DateTime, default=datetime.utcnow)
user = db.relationship('User', back_populates='reactions')
post = db.relationship('Post', back_populates='reactions')
reply = db.relationship('Reply', back_populates='reactions')
__table_args__ = (
db.UniqueConstraint('user_id', 'post_id', name='uq_user_post_reaction'),
db.UniqueConstraint('user_id', 'reply_id', name='uq_user_reply_reaction'),
)
# 收藏表
class Bookmark(db.Model):
id = db.Column(db.Integer, primary_key=True)
user_id = db.Column(db.Integer, db.ForeignKey('user.id'))
post_id = db.Column(db.Integer, db.ForeignKey('post.id'))
created_at = db.Column(db.DateTime, default=datetime.utcnow)
user = db.relationship('User', back_populates='bookmarks')
post = db.relationship('Post', back_populates='bookmarks')
__table_args__ = (db.UniqueConstraint('user_id', 'post_id', name='uq_user_post_bookmark'),)
# 举报表
class Report(db.Model):
id = db.Column(db.Integer, primary_key=True)
type = db.Column(db.String(20))
target_id = db.Column(db.Integer)
reporter_id = db.Column(db.Integer, db.ForeignKey('user.id'))
reason = db.Column(db.String(100))
detail = db.Column(db.Text)
status = db.Column(db.String(20), default='pending')
created_at = db.Column(db.DateTime, default=datetime.utcnow)
# reporter 已在 User 中通过 backref 定义
# 通知表
class Notification(db.Model):
id = db.Column(db.Integer, primary_key=True)
user_id = db.Column(db.Integer, db.ForeignKey('user.id'))
type = db.Column(db.String(50))
content = db.Column(db.Text)
from_user = db.Column(db.String(80))
post_id = db.Column(db.Integer, nullable=True)
read = db.Column(db.Boolean, default=False)
created_at = db.Column(db.DateTime, default=datetime.utcnow)
# user 已在 User 中通过 backref 定义
# 系统公告表(管理员发布的全站通知)
class SystemNotification(db.Model):
id = db.Column(db.Integer, primary_key=True)
title = db.Column(db.String(200), nullable=False)
content = db.Column(db.Text, nullable=False)
author_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
pinned = db.Column(db.Boolean, default=False)
created_at = db.Column(db.DateTime, default=datetime.utcnow)
updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
author = db.relationship('User', backref='system_notifications')
# 编辑历史表(可选)
class EditHistory(db.Model):
id = db.Column(db.Integer, primary_key=True)
post_id = db.Column(db.Integer, db.ForeignKey('post.id'))
title = db.Column(db.String(200))
content = db.Column(db.Text)
edited_by = db.Column(db.String(80))
edited_at = db.Column(db.DateTime, default=datetime.utcnow)
post = db.relationship('Post')
# 教师申请表(针对特定杯赛)
class TeacherApplication(db.Model):
"""教师申请记录(针对特定杯赛)"""
id = db.Column(db.Integer, primary_key=True)
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
contest_id = db.Column(db.Integer, db.ForeignKey('contest.id'), nullable=False)
name = db.Column(db.String(80), nullable=False) # 申请人姓名
email = db.Column(db.String(120), nullable=False) # 申请人邮箱
reason = db.Column(db.Text, nullable=False) # 申请理由
status = db.Column(db.String(20), default='pending') # pending, approved, rejected
applied_at = db.Column(db.DateTime, default=datetime.utcnow)
reviewed_at = db.Column(db.DateTime)
reviewed_by = db.Column(db.Integer, db.ForeignKey('user.id'))
# 关系
user = db.relationship('User', foreign_keys=[user_id], backref='teacher_applications')
contest = db.relationship('Contest', back_populates='teacher_applications')
reviewer = db.relationship('User', foreign_keys=[reviewed_by])
__table_args__ = (db.UniqueConstraint('user_id', 'contest_id', name='uq_user_contest_application'),)
# 好友表
class Friend(db.Model):
id = db.Column(db.Integer, primary_key=True)
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
friend_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
status = db.Column(db.String(20), default='pending') # pending, accepted
created_at = db.Column(db.DateTime, default=datetime.utcnow)
user = db.relationship('User', foreign_keys=[user_id], backref=db.backref('friends_initiated', lazy='dynamic'))
friend = db.relationship('User', foreign_keys=[friend_id], backref=db.backref('friends_received', lazy='dynamic'))
# 试卷收藏表
class ExamBookmark(db.Model):
id = db.Column(db.Integer, primary_key=True)
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
exam_id = db.Column(db.Integer, db.ForeignKey('exam.id'), nullable=False)
created_at = db.Column(db.DateTime, default=datetime.utcnow)
user = db.relationship('User', backref=db.backref('exam_bookmarks', lazy='dynamic', cascade='all, delete-orphan'))
exam = db.relationship('Exam', backref=db.backref('bookmarked_by', lazy='dynamic', cascade='all, delete-orphan'))
__table_args__ = (db.UniqueConstraint('user_id', 'exam_id', name='uq_user_exam_bookmark'),)
# 聊天室表
class ChatRoom(db.Model):
id = db.Column(db.Integer, primary_key=True)
type = db.Column(db.String(20), nullable=False) # 'private' | 'group' | 'contest'
name = db.Column(db.String(100), nullable=True)
avatar = db.Column(db.String(200), default='')
creator_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=True)
contest_id = db.Column(db.Integer, db.ForeignKey('contest.id'), unique=True, nullable=True)
announcement = db.Column(db.Text, default='')
announcement_by = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=True)
announcement_at = db.Column(db.DateTime, nullable=True)
created_at = db.Column(db.DateTime, default=datetime.utcnow)
creator = db.relationship('User', foreign_keys=[creator_id], backref='chatrooms_created')
announcement_user = db.relationship('User', foreign_keys=[announcement_by])
contest = db.relationship('Contest', backref=db.backref('chatroom', uselist=False))
members = db.relationship('ChatRoomMember', backref='room', lazy=True, cascade='all, delete-orphan')
messages = db.relationship('Message', backref='room', lazy=True, cascade='all, delete-orphan')
# 聊天室成员表
class ChatRoomMember(db.Model):
id = db.Column(db.Integer, primary_key=True)
room_id = db.Column(db.Integer, db.ForeignKey('chat_room.id'), nullable=False)
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
role = db.Column(db.String(20), default='member') # 'admin' | 'member'
nickname = db.Column(db.String(50), default='')
muted = db.Column(db.Boolean, default=False)
last_read_at = db.Column(db.DateTime, default=datetime.utcnow)
joined_at = db.Column(db.DateTime, default=datetime.utcnow)
user = db.relationship('User', backref='chat_memberships')
__table_args__ = (db.UniqueConstraint('room_id', 'user_id', name='uq_room_user'),)
# 消息表
class Message(db.Model):
id = db.Column(db.Integer, primary_key=True)
room_id = db.Column(db.Integer, db.ForeignKey('chat_room.id'), nullable=False)
sender_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
type = db.Column(db.String(20), default='text') # 'text' | 'image' | 'file' | 'system' | 'voice'
content = db.Column(db.Text, default='')
file_url = db.Column(db.String(300), nullable=True)
file_name = db.Column(db.String(200), nullable=True)
recalled = db.Column(db.Boolean, default=False)
reply_to_id = db.Column(db.Integer, db.ForeignKey('message.id'), nullable=True)
mentions = db.Column(db.Text, default='') # JSON array of user IDs, e.g. "[1,2,3]" or "all"
created_at = db.Column(db.DateTime, default=datetime.utcnow)
sender = db.relationship('User', backref='messages_sent')
reply_to = db.relationship('Message', remote_side='Message.id', backref='replies')
reactions = db.relationship('MessageReaction', backref='message', lazy=True, cascade='all, delete-orphan')
# 消息表情回应表
class MessageReaction(db.Model):
id = db.Column(db.Integer, primary_key=True)
message_id = db.Column(db.Integer, db.ForeignKey('message.id'), nullable=False)
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
emoji = db.Column(db.String(10), nullable=False)
created_at = db.Column(db.DateTime, default=datetime.utcnow)
user = db.relationship('User', backref='message_reactions')
__table_args__ = (db.UniqueConstraint('message_id', 'user_id', 'emoji', name='uq_msg_user_emoji'),)
# 邀请码表(教师申请审批通过后生成,老师输入邀请码激活身份)
class InviteCode(db.Model):
id = db.Column(db.Integer, primary_key=True)
code = db.Column(db.String(32), unique=True, nullable=False)
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
application_id = db.Column(db.Integer, db.ForeignKey('teacher_application.id'), nullable=False)
used = db.Column(db.Boolean, default=False)
created_at = db.Column(db.DateTime, default=datetime.utcnow)
used_at = db.Column(db.DateTime, nullable=True)
user = db.relationship('User', backref='invite_codes')
application = db.relationship('TeacherApplication', backref='invite_code')