Compare commits
1 Commits
33763d279e
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| dd9609165c |
90
app.py
90
app.py
@@ -6,7 +6,7 @@ from io import BytesIO
|
||||
import random
|
||||
import string
|
||||
import smtplib
|
||||
from datetime import datetime, timedelta
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from email.mime.text import MIMEText
|
||||
from email.mime.multipart import MIMEMultipart
|
||||
from email.utils import formatdate
|
||||
@@ -26,7 +26,7 @@ import fitz # PyMuPDF
|
||||
from openai import OpenAI as DashScopeClient
|
||||
|
||||
# 引入数据库模型(添加 TeacherApplication)
|
||||
from models import db, User, Exam, Submission, Draft, Contest, ContestMembership, ContestApplication, Post, Reply, Poll, Report, Bookmark, Reaction, Notification, EditHistory, ContestRegistration, TeacherApplication, Friend, ExamBookmark, ChatRoom, ChatRoomMember, Message, MessageReaction, QuestionBankItem, InviteCode, SystemNotification
|
||||
from models import db, User, Exam, Submission, Draft, Contest, ContestMembership, ContestApplication, Post, Reply, Poll, Report, Bookmark, Reaction, Notification, EditHistory, ContestRegistration, TeacherApplication, Friend, ExamBookmark, ChatRoom, ChatRoomMember, Message, MessageReaction, QuestionBankItem, InviteCode, SystemNotification, beijing_now
|
||||
from flask_socketio import SocketIO, emit, join_room as sio_join, leave_room as sio_leave
|
||||
from excel_export import export_users_to_excel, export_exams_to_excel, export_submissions_to_excel, export_contests_to_excel, export_posts_to_excel
|
||||
from backup_utils import create_backup_zip, export_all_data
|
||||
@@ -602,7 +602,7 @@ def exam_list():
|
||||
)
|
||||
all_exams = query.all()
|
||||
|
||||
now = datetime.now()
|
||||
now = beijing_now()
|
||||
changed = False
|
||||
for exam in all_exams:
|
||||
if exam.status == 'available' and exam.scheduled_end and now > exam.scheduled_end:
|
||||
@@ -673,7 +673,7 @@ def exam_detail(exam_id):
|
||||
draft = Draft.query.filter_by(exam_id=exam_id, user_id=user.get('id')).first()
|
||||
questions = get_exam_questions(exam)
|
||||
# 检查预定时间
|
||||
now = datetime.now()
|
||||
now = beijing_now()
|
||||
schedule_status = 'available' # available, not_started, ended
|
||||
if exam.scheduled_start and now < exam.scheduled_start:
|
||||
schedule_status = 'not_started'
|
||||
@@ -698,7 +698,7 @@ def exam_result(exam_id):
|
||||
return redirect(url_for('exam_detail', exam_id=exam_id))
|
||||
# 检查成绩公布时间
|
||||
score_hidden = False
|
||||
if exam.score_release_time and datetime.now() < exam.score_release_time:
|
||||
if exam.score_release_time and beijing_now() < exam.score_release_time:
|
||||
if user.get('role') != 'teacher' and user.get('role') != 'admin':
|
||||
score_hidden = True
|
||||
questions = get_exam_questions(exam)
|
||||
@@ -836,7 +836,7 @@ def api_add_friend():
|
||||
# 确保是当前用户发起的请求才能更新
|
||||
if existing.user_id == user_id:
|
||||
existing.status = 'pending'
|
||||
existing.created_at = datetime.utcnow()
|
||||
existing.created_at = beijing_now()
|
||||
db.session.flush()
|
||||
|
||||
# 重新发送通知给对方
|
||||
@@ -973,7 +973,7 @@ def api_user_exam_bookmarks():
|
||||
def api_user_exam_history():
|
||||
user = session['user']
|
||||
subs = Submission.query.filter_by(user_id=user['id']).order_by(Submission.submitted_at.desc()).all()
|
||||
now = datetime.utcnow()
|
||||
now = beijing_now()
|
||||
result = []
|
||||
for s in subs:
|
||||
exam = s.exam
|
||||
@@ -1056,8 +1056,8 @@ def api_change_name():
|
||||
# 每月仅可修改一次
|
||||
if user.name_changed_at:
|
||||
from datetime import timedelta
|
||||
if datetime.utcnow() - user.name_changed_at < timedelta(days=30):
|
||||
remaining = 30 - (datetime.utcnow() - user.name_changed_at).days
|
||||
if beijing_now() - user.name_changed_at < timedelta(days=30):
|
||||
remaining = 30 - (beijing_now() - user.name_changed_at).days
|
||||
return jsonify({'success': False, 'message': f'每月仅可修改一次用户名,还需等待{remaining}天'}), 400
|
||||
data = request.get_json(force=True, silent=True)
|
||||
new_name = data.get('name', '').strip()
|
||||
@@ -1069,7 +1069,7 @@ def api_change_name():
|
||||
if existing:
|
||||
return jsonify({'success': False, 'message': '该用户名已被使用'}), 400
|
||||
user.name = new_name
|
||||
user.name_changed_at = datetime.utcnow()
|
||||
user.name_changed_at = beijing_now()
|
||||
db.session.commit()
|
||||
session['user'] = {**user_data, 'name': new_name}
|
||||
return jsonify({'success': True, 'message': '用户名修改成功', 'name': new_name})
|
||||
@@ -1090,8 +1090,8 @@ def apply_contest():
|
||||
if existing.rejection_count >= 5 and existing.last_rejected_at:
|
||||
from datetime import timedelta
|
||||
cooldown_end = existing.last_rejected_at + timedelta(days=30)
|
||||
if datetime.utcnow() < cooldown_end:
|
||||
remaining_days = (cooldown_end - datetime.utcnow()).days + 1
|
||||
if beijing_now() < cooldown_end:
|
||||
remaining_days = (cooldown_end - beijing_now()).days + 1
|
||||
flash(f'您的申请已被拒绝5次,需等待 {remaining_days} 天后才能再次申请', 'error')
|
||||
return redirect(url_for('contest_list'))
|
||||
|
||||
@@ -1149,7 +1149,7 @@ def apply_contest():
|
||||
if image_url:
|
||||
existing.image = image_url
|
||||
existing.status = 'pending'
|
||||
existing.applied_at = datetime.utcnow()
|
||||
existing.applied_at = beijing_now()
|
||||
existing.reviewed_at = None
|
||||
app = existing
|
||||
flash_msg = '申请已更新,请等待管理员审核'
|
||||
@@ -1227,7 +1227,7 @@ def approve_contest_application(app_id):
|
||||
)
|
||||
db.session.add(membership)
|
||||
app.status = 'approved'
|
||||
app.reviewed_at = datetime.utcnow()
|
||||
app.reviewed_at = beijing_now()
|
||||
app.rejection_count = 0 # 重置拒绝次数
|
||||
app.last_rejected_at = None # 清除最后拒绝时间
|
||||
# 自动创建杯赛讨论群
|
||||
@@ -1251,9 +1251,9 @@ def reject_contest_application(app_id):
|
||||
flash('该申请已处理')
|
||||
return redirect(url_for('admin_contest_applications'))
|
||||
app.status = 'rejected'
|
||||
app.reviewed_at = datetime.utcnow()
|
||||
app.reviewed_at = beijing_now()
|
||||
app.rejection_count += 1
|
||||
app.last_rejected_at = datetime.utcnow()
|
||||
app.last_rejected_at = beijing_now()
|
||||
# 通知申请人审核未通过
|
||||
add_notification(app.user_id, 'contest_result',
|
||||
f'您申请举办的杯赛「{app.name}」未通过审核。', from_user='系统')
|
||||
@@ -1501,7 +1501,7 @@ def api_approve_contest(app_id):
|
||||
db.session.flush()
|
||||
db.session.add(ContestMembership(user_id=ca.user_id, contest_id=contest.id, role='owner'))
|
||||
ca.status = 'approved'
|
||||
ca.reviewed_at = datetime.utcnow()
|
||||
ca.reviewed_at = beijing_now()
|
||||
ca.rejection_count = 0 # 重置拒绝次数
|
||||
ca.last_rejected_at = None # 清除最后拒绝时间
|
||||
chatroom = ChatRoom(type='contest', name=contest.name + ' 讨论群',
|
||||
@@ -1529,7 +1529,7 @@ def api_approve_teacher(app_id):
|
||||
existing = ContestMembership.query.filter_by(user_id=ta.user_id, contest_id=ta.contest_id).first()
|
||||
if existing:
|
||||
ta.status = 'rejected'
|
||||
ta.reviewed_at = datetime.utcnow()
|
||||
ta.reviewed_at = beijing_now()
|
||||
ta.reviewed_by = user['id']
|
||||
db.session.commit()
|
||||
return jsonify({'success': False, 'message': '用户已是杯赛成员'}), 400
|
||||
@@ -1543,7 +1543,7 @@ def api_approve_teacher(app_id):
|
||||
db.session.add(invite)
|
||||
|
||||
ta.status = 'approved'
|
||||
ta.reviewed_at = datetime.utcnow()
|
||||
ta.reviewed_at = beijing_now()
|
||||
ta.reviewed_by = user['id']
|
||||
ta.rejection_count = 0 # 重置拒绝次数
|
||||
ta.last_rejected_at = None # 清除最后拒绝时间
|
||||
@@ -1579,10 +1579,10 @@ def api_reject_teacher(app_id):
|
||||
if not membership:
|
||||
return jsonify({'success': False, 'message': '无权限'}), 403
|
||||
ta.status = 'rejected'
|
||||
ta.reviewed_at = datetime.utcnow()
|
||||
ta.reviewed_at = beijing_now()
|
||||
ta.reviewed_by = user['id']
|
||||
ta.rejection_count += 1
|
||||
ta.last_rejected_at = datetime.utcnow()
|
||||
ta.last_rejected_at = beijing_now()
|
||||
contest = Contest.query.get(ta.contest_id)
|
||||
add_notification(ta.user_id, 'teacher_result',
|
||||
f'您申请成为杯赛「{contest.name if contest else ""}」老师未通过审核。',
|
||||
@@ -1597,9 +1597,9 @@ def api_reject_contest(app_id):
|
||||
if ca.status != 'pending':
|
||||
return jsonify({'success': False, 'message': '该申请已处理'}), 400
|
||||
ca.status = 'rejected'
|
||||
ca.reviewed_at = datetime.utcnow()
|
||||
ca.reviewed_at = beijing_now()
|
||||
ca.rejection_count += 1
|
||||
ca.last_rejected_at = datetime.utcnow()
|
||||
ca.last_rejected_at = beijing_now()
|
||||
add_notification(ca.user_id, 'contest_result',
|
||||
f'您申请举办的杯赛「{ca.name}」未通过审核。', from_user='系统')
|
||||
db.session.commit()
|
||||
@@ -1641,8 +1641,8 @@ def apply_teacher():
|
||||
if existing.rejection_count >= 3 and existing.last_rejected_at:
|
||||
from datetime import timedelta
|
||||
cooldown_end = existing.last_rejected_at + timedelta(days=10)
|
||||
if datetime.utcnow() < cooldown_end:
|
||||
remaining_days = (cooldown_end - datetime.utcnow()).days + 1
|
||||
if beijing_now() < cooldown_end:
|
||||
remaining_days = (cooldown_end - beijing_now()).days + 1
|
||||
flash(f'您对该杯赛的申请已被拒绝3次,需等待 {remaining_days} 天后才能再次申请', 'error')
|
||||
return redirect(url_for('contest_detail', contest_id=contest_id))
|
||||
|
||||
@@ -1653,7 +1653,7 @@ def apply_teacher():
|
||||
existing.email = email
|
||||
existing.reason = reason
|
||||
existing.status = 'pending'
|
||||
existing.applied_at = datetime.utcnow()
|
||||
existing.applied_at = beijing_now()
|
||||
existing.reviewed_at = None
|
||||
existing.reviewed_by = None
|
||||
appli = existing
|
||||
@@ -1734,7 +1734,7 @@ def approve_teacher_application(app_id):
|
||||
existing = ContestMembership.query.filter_by(user_id=app.user_id, contest_id=app.contest_id).first()
|
||||
if existing:
|
||||
app.status = 'rejected' # 已存在,无法再次添加
|
||||
app.reviewed_at = datetime.utcnow()
|
||||
app.reviewed_at = beijing_now()
|
||||
app.reviewed_by = user['id']
|
||||
db.session.commit()
|
||||
flash('用户已是杯赛成员,申请已拒绝', 'error')
|
||||
@@ -1749,7 +1749,7 @@ def approve_teacher_application(app_id):
|
||||
db.session.add(invite)
|
||||
|
||||
app.status = 'approved'
|
||||
app.reviewed_at = datetime.utcnow()
|
||||
app.reviewed_at = beijing_now()
|
||||
app.reviewed_by = user['id']
|
||||
app.rejection_count = 0 # 重置拒绝次数
|
||||
app.last_rejected_at = None # 清除最后拒绝时间
|
||||
@@ -1793,10 +1793,10 @@ def reject_teacher_application(app_id):
|
||||
return redirect(url_for('admin_teacher_applications'))
|
||||
|
||||
app.status = 'rejected'
|
||||
app.reviewed_at = datetime.utcnow()
|
||||
app.reviewed_at = beijing_now()
|
||||
app.reviewed_by = user['id']
|
||||
app.rejection_count += 1
|
||||
app.last_rejected_at = datetime.utcnow()
|
||||
app.last_rejected_at = beijing_now()
|
||||
# 通知申请人
|
||||
contest = Contest.query.get(app.contest_id)
|
||||
add_notification(app.user_id, 'teacher_result',
|
||||
@@ -1849,7 +1849,7 @@ def api_activate_invite_code():
|
||||
|
||||
# 标记邀请码已使用
|
||||
invite.used = True
|
||||
invite.used_at = datetime.utcnow()
|
||||
invite.used_at = beijing_now()
|
||||
db.session.commit()
|
||||
|
||||
contest = Contest.query.get(ta.contest_id)
|
||||
@@ -2779,7 +2779,7 @@ def api_submit_exam(exam_id):
|
||||
if exam.status == 'closed':
|
||||
return jsonify({'success': False, 'message': '该考试已关闭'}), 400
|
||||
# 检查预定时间
|
||||
now = datetime.now()
|
||||
now = beijing_now()
|
||||
if exam.scheduled_start and now < exam.scheduled_start:
|
||||
return jsonify({'success': False, 'message': '考试尚未开始'}), 400
|
||||
if exam.scheduled_end and now > exam.scheduled_end:
|
||||
@@ -3044,7 +3044,7 @@ def api_contest_exams(contest_id):
|
||||
if user:
|
||||
membership = ContestMembership.query.filter_by(user_id=user['id'], contest_id=contest_id).first()
|
||||
is_owner = (membership and membership.role == 'owner') or user.get('role') == 'admin'
|
||||
now = datetime.now()
|
||||
now = beijing_now()
|
||||
changed = False
|
||||
for e in exams:
|
||||
if e.status == 'available' and e.scheduled_end and now > e.scheduled_end:
|
||||
@@ -3983,7 +3983,7 @@ def api_forum_stats():
|
||||
total_posts = Post.query.count()
|
||||
total_replies = Reply.query.count()
|
||||
total_users = User.query.count()
|
||||
today = datetime.utcnow().date()
|
||||
today = beijing_now().date()
|
||||
today_posts = Post.query.filter(db.func.date(Post.created_at) == today).count()
|
||||
today_replies = Reply.query.filter(db.func.date(Reply.created_at) == today).count()
|
||||
online_count = get_online_count()
|
||||
@@ -4353,7 +4353,7 @@ def api_mark_chat_read(room_id):
|
||||
user_id = session['user']['id']
|
||||
member = ChatRoomMember.query.filter_by(room_id=room_id, user_id=user_id).first()
|
||||
if member:
|
||||
member.last_read_at = datetime.utcnow()
|
||||
member.last_read_at = beijing_now()
|
||||
db.session.commit()
|
||||
socketio.emit('read_update', {
|
||||
'room_id': room_id, 'user_id': user_id,
|
||||
@@ -4385,7 +4385,7 @@ def api_recall_message(msg_id):
|
||||
msg = Message.query.get_or_404(msg_id)
|
||||
if msg.sender_id != user_id:
|
||||
return jsonify({'success': False, 'message': '只能撤回自己的消息'}), 403
|
||||
if (datetime.utcnow() - msg.created_at).total_seconds() > 120:
|
||||
if (beijing_now() - msg.created_at).total_seconds() > 120:
|
||||
return jsonify({'success': False, 'message': '超过2分钟无法撤回'}), 400
|
||||
msg.recalled = True
|
||||
msg.content = ''
|
||||
@@ -4497,7 +4497,7 @@ def handle_mark_read(data):
|
||||
room_id = data.get('room_id')
|
||||
member = ChatRoomMember.query.filter_by(room_id=room_id, user_id=user['id']).first()
|
||||
if member:
|
||||
member.last_read_at = datetime.utcnow()
|
||||
member.last_read_at = beijing_now()
|
||||
db.session.commit()
|
||||
emit('read_update', {
|
||||
'room_id': room_id, 'user_id': user['id'],
|
||||
@@ -4531,7 +4531,7 @@ def api_set_announcement(room_id):
|
||||
data = request.get_json(force=True, silent=True)
|
||||
room.announcement = data.get('content', '').strip()
|
||||
room.announcement_by = user_id
|
||||
room.announcement_at = datetime.utcnow()
|
||||
room.announcement_at = beijing_now()
|
||||
db.session.add(Message(room_id=room_id, sender_id=user_id, type='system',
|
||||
content=f'{session["user"]["name"]} 更新了群公告'))
|
||||
db.session.commit()
|
||||
@@ -5194,7 +5194,7 @@ def admin_export_users():
|
||||
users = User.query.order_by(User.created_at.desc()).all()
|
||||
output = export_users_to_excel(users)
|
||||
|
||||
filename = f"用户数据_{datetime.now().strftime('%Y%m%d_%H%M%S')}.csv"
|
||||
filename = f"用户数据_{beijing_now().strftime('%Y%m%d_%H%M%S')}.csv"
|
||||
return send_file(
|
||||
output,
|
||||
mimetype='text/csv; charset=utf-8',
|
||||
@@ -5212,7 +5212,7 @@ def admin_export_exams():
|
||||
exams = Exam.query.order_by(Exam.created_at.desc()).all()
|
||||
output = export_exams_to_excel(exams)
|
||||
|
||||
filename = f"考试数据_{datetime.now().strftime('%Y%m%d_%H%M%S')}.csv"
|
||||
filename = f"考试数据_{beijing_now().strftime('%Y%m%d_%H%M%S')}.csv"
|
||||
return send_file(
|
||||
output,
|
||||
mimetype='text/csv; charset=utf-8',
|
||||
@@ -5230,7 +5230,7 @@ def admin_export_submissions():
|
||||
submissions = Submission.query.order_by(Submission.submitted_at.desc()).all()
|
||||
output = export_submissions_to_excel(submissions)
|
||||
|
||||
filename = f"提交记录_{datetime.now().strftime('%Y%m%d_%H%M%S')}.csv"
|
||||
filename = f"提交记录_{beijing_now().strftime('%Y%m%d_%H%M%S')}.csv"
|
||||
return send_file(
|
||||
output,
|
||||
mimetype='text/csv; charset=utf-8',
|
||||
@@ -5248,7 +5248,7 @@ def admin_export_contests():
|
||||
contests = Contest.query.order_by(Contest.created_at.desc()).all()
|
||||
output = export_contests_to_excel(contests)
|
||||
|
||||
filename = f"杯赛数据_{datetime.now().strftime('%Y%m%d_%H%M%S')}.csv"
|
||||
filename = f"杯赛数据_{beijing_now().strftime('%Y%m%d_%H%M%S')}.csv"
|
||||
return send_file(
|
||||
output,
|
||||
mimetype='text/csv; charset=utf-8',
|
||||
@@ -5266,7 +5266,7 @@ def admin_export_posts():
|
||||
posts = Post.query.order_by(Post.created_at.desc()).all()
|
||||
output = export_posts_to_excel(posts)
|
||||
|
||||
filename = f"论坛帖子_{datetime.now().strftime('%Y%m%d_%H%M%S')}.csv"
|
||||
filename = f"论坛帖子_{beijing_now().strftime('%Y%m%d_%H%M%S')}.csv"
|
||||
return send_file(
|
||||
output,
|
||||
mimetype='text/csv; charset=utf-8',
|
||||
@@ -5290,7 +5290,7 @@ def admin_backup_download():
|
||||
full = request.args.get('full', '1') == '1'
|
||||
if full:
|
||||
zip_buf = create_backup_zip(include_uploads=True, upload_folder=UPLOAD_FOLDER_BACKUP)
|
||||
filename = f"智联青云_完整备份_{datetime.now().strftime('%Y%m%d_%H%M%S')}.zip"
|
||||
filename = f"智联青云_完整备份_{beijing_now().strftime('%Y%m%d_%H%M%S')}.zip"
|
||||
return send_file(
|
||||
zip_buf,
|
||||
mimetype='application/zip',
|
||||
@@ -5301,7 +5301,7 @@ def admin_backup_download():
|
||||
data = export_all_data()
|
||||
buf = BytesIO(json.dumps(data, ensure_ascii=False, indent=2).encode('utf-8'))
|
||||
buf.seek(0)
|
||||
filename = f"智联青云_数据备份_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json"
|
||||
filename = f"智联青云_数据备份_{beijing_now().strftime('%Y%m%d_%H%M%S')}.json"
|
||||
return send_file(
|
||||
buf,
|
||||
mimetype='application/json; charset=utf-8',
|
||||
|
||||
@@ -4,6 +4,7 @@ import json
|
||||
import os
|
||||
import zipfile
|
||||
from datetime import datetime
|
||||
from models import beijing_now
|
||||
from io import BytesIO
|
||||
|
||||
from models import (
|
||||
@@ -76,7 +77,7 @@ def export_all_data():
|
||||
"""导出所有表数据为 JSON 字典"""
|
||||
data = {
|
||||
'meta': {
|
||||
'exported_at': datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
|
||||
'exported_at': beijing_now().strftime('%Y-%m-%d %H:%M:%S'),
|
||||
'version': '1.0',
|
||||
},
|
||||
'tables': {}
|
||||
|
||||
67
models.py
67
models.py
@@ -1,6 +1,11 @@
|
||||
# models.py
|
||||
from flask_sqlalchemy import SQLAlchemy
|
||||
from datetime import datetime
|
||||
from datetime import datetime, timezone, timedelta
|
||||
|
||||
_beijing_tz = timezone(timedelta(hours=8))
|
||||
|
||||
def beijing_now():
|
||||
return datetime.now(_beijing_tz).replace(tzinfo=None)
|
||||
import json
|
||||
|
||||
db = SQLAlchemy()
|
||||
@@ -17,7 +22,7 @@ class User(db.Model):
|
||||
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)
|
||||
created_at = db.Column(db.DateTime, default=beijing_now)
|
||||
|
||||
# 关系
|
||||
exams_created = db.relationship('Exam', back_populates='creator', lazy=True)
|
||||
@@ -39,7 +44,7 @@ class ContestMembership(db.Model):
|
||||
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)
|
||||
joined_at = db.Column(db.DateTime, default=beijing_now)
|
||||
|
||||
user = db.relationship('User', back_populates='contest_memberships')
|
||||
contest = db.relationship('Contest', back_populates='members')
|
||||
@@ -55,7 +60,7 @@ class ContestApplication(db.Model):
|
||||
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)
|
||||
applied_at = db.Column(db.DateTime, default=beijing_now)
|
||||
reviewed_at = db.Column(db.DateTime)
|
||||
total_score = db.Column(db.Integer, default=150) # 杯赛默认满分
|
||||
start_date = db.Column(db.String(20)) # 申请时填写的开始日期
|
||||
@@ -78,7 +83,7 @@ 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)
|
||||
registered_at = db.Column(db.DateTime, default=beijing_now)
|
||||
|
||||
__table_args__ = (db.UniqueConstraint('user_id', 'contest_id', name='uq_user_contest_reg'),)
|
||||
|
||||
@@ -93,7 +98,7 @@ class Contest(db.Model):
|
||||
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)
|
||||
created_at = db.Column(db.DateTime, default=beijing_now)
|
||||
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) # 是否对普通用户可见
|
||||
@@ -125,7 +130,7 @@ class QuestionBankItem(db.Model):
|
||||
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)
|
||||
created_at = db.Column(db.DateTime, default=beijing_now)
|
||||
|
||||
contest = db.relationship('Contest', backref='question_bank_items')
|
||||
contributor = db.relationship('User', backref='contributed_questions')
|
||||
@@ -146,7 +151,7 @@ class Exam(db.Model):
|
||||
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)
|
||||
created_at = db.Column(db.DateTime, default=beijing_now)
|
||||
status = db.Column(db.String(20), default='available')
|
||||
visibility = db.Column(db.String(20), default='private') # 'private' 或 'public'
|
||||
|
||||
@@ -170,7 +175,7 @@ class Submission(db.Model):
|
||||
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)
|
||||
submitted_at = db.Column(db.DateTime, default=beijing_now)
|
||||
|
||||
exam = db.relationship('Exam', back_populates='submissions')
|
||||
user = db.relationship('User', back_populates='submissions')
|
||||
@@ -193,7 +198,7 @@ class Draft(db.Model):
|
||||
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)
|
||||
saved_at = db.Column(db.DateTime, default=beijing_now, onupdate=beijing_now)
|
||||
|
||||
exam = db.relationship('Exam', back_populates='drafts')
|
||||
user = db.relationship('User', back_populates='drafts')
|
||||
@@ -219,8 +224,8 @@ class Post(db.Model):
|
||||
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)
|
||||
created_at = db.Column(db.DateTime, default=beijing_now)
|
||||
updated_at = db.Column(db.DateTime, default=beijing_now, onupdate=beijing_now)
|
||||
|
||||
author = db.relationship('User', back_populates='posts')
|
||||
replies = db.relationship('Reply', back_populates='post', lazy=True, cascade='all, delete-orphan')
|
||||
@@ -243,8 +248,8 @@ class Reply(db.Model):
|
||||
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)
|
||||
created_at = db.Column(db.DateTime, default=beijing_now)
|
||||
updated_at = db.Column(db.DateTime, default=beijing_now, onupdate=beijing_now)
|
||||
|
||||
post = db.relationship('Post', back_populates='replies')
|
||||
author = db.relationship('User', back_populates='replies')
|
||||
@@ -259,7 +264,7 @@ class Poll(db.Model):
|
||||
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)
|
||||
created_at = db.Column(db.DateTime, default=beijing_now)
|
||||
|
||||
post = db.relationship('Post', back_populates='poll')
|
||||
|
||||
@@ -282,7 +287,7 @@ class Reaction(db.Model):
|
||||
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)
|
||||
created_at = db.Column(db.DateTime, default=beijing_now)
|
||||
|
||||
user = db.relationship('User', back_populates='reactions')
|
||||
post = db.relationship('Post', back_populates='reactions')
|
||||
@@ -298,7 +303,7 @@ 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)
|
||||
created_at = db.Column(db.DateTime, default=beijing_now)
|
||||
|
||||
user = db.relationship('User', back_populates='bookmarks')
|
||||
post = db.relationship('Post', back_populates='bookmarks')
|
||||
@@ -314,7 +319,7 @@ class Report(db.Model):
|
||||
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)
|
||||
created_at = db.Column(db.DateTime, default=beijing_now)
|
||||
|
||||
# reporter 已在 User 中通过 backref 定义
|
||||
|
||||
@@ -327,7 +332,7 @@ class Notification(db.Model):
|
||||
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)
|
||||
created_at = db.Column(db.DateTime, default=beijing_now)
|
||||
|
||||
# user 已在 User 中通过 backref 定义
|
||||
|
||||
@@ -338,8 +343,8 @@ class SystemNotification(db.Model):
|
||||
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)
|
||||
created_at = db.Column(db.DateTime, default=beijing_now)
|
||||
updated_at = db.Column(db.DateTime, default=beijing_now, onupdate=beijing_now)
|
||||
|
||||
author = db.relationship('User', backref='system_notifications')
|
||||
|
||||
@@ -350,7 +355,7 @@ class EditHistory(db.Model):
|
||||
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)
|
||||
edited_at = db.Column(db.DateTime, default=beijing_now)
|
||||
|
||||
post = db.relationship('Post')
|
||||
|
||||
@@ -364,7 +369,7 @@ class TeacherApplication(db.Model):
|
||||
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)
|
||||
applied_at = db.Column(db.DateTime, default=beijing_now)
|
||||
reviewed_at = db.Column(db.DateTime)
|
||||
reviewed_by = db.Column(db.Integer, db.ForeignKey('user.id'))
|
||||
# 拒绝次数控制
|
||||
@@ -384,7 +389,7 @@ class Friend(db.Model):
|
||||
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)
|
||||
created_at = db.Column(db.DateTime, default=beijing_now)
|
||||
|
||||
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'))
|
||||
@@ -394,7 +399,7 @@ 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)
|
||||
created_at = db.Column(db.DateTime, default=beijing_now)
|
||||
|
||||
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'))
|
||||
@@ -412,7 +417,7 @@ class ChatRoom(db.Model):
|
||||
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)
|
||||
created_at = db.Column(db.DateTime, default=beijing_now)
|
||||
|
||||
creator = db.relationship('User', foreign_keys=[creator_id], backref='chatrooms_created')
|
||||
announcement_user = db.relationship('User', foreign_keys=[announcement_by])
|
||||
@@ -428,8 +433,8 @@ class ChatRoomMember(db.Model):
|
||||
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)
|
||||
last_read_at = db.Column(db.DateTime, default=beijing_now)
|
||||
joined_at = db.Column(db.DateTime, default=beijing_now)
|
||||
|
||||
user = db.relationship('User', backref='chat_memberships')
|
||||
|
||||
@@ -447,7 +452,7 @@ class Message(db.Model):
|
||||
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)
|
||||
created_at = db.Column(db.DateTime, default=beijing_now)
|
||||
|
||||
sender = db.relationship('User', backref='messages_sent')
|
||||
reply_to = db.relationship('Message', remote_side='Message.id', backref='replies')
|
||||
@@ -459,7 +464,7 @@ class MessageReaction(db.Model):
|
||||
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)
|
||||
created_at = db.Column(db.DateTime, default=beijing_now)
|
||||
|
||||
user = db.relationship('User', backref='message_reactions')
|
||||
|
||||
@@ -472,7 +477,7 @@ class InviteCode(db.Model):
|
||||
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)
|
||||
created_at = db.Column(db.DateTime, default=beijing_now)
|
||||
used_at = db.Column(db.DateTime, nullable=True)
|
||||
|
||||
user = db.relationship('User', backref='invite_codes')
|
||||
|
||||
@@ -283,7 +283,7 @@ async function loadPosts() {
|
||||
let html = '';
|
||||
data.data.forEach(p => {
|
||||
html += `
|
||||
<div class="border border-slate-200 rounded-lg p-4 hover:shadow-sm transition-shadow">
|
||||
<div class="border border-slate-200 rounded-lg p-4 hover:shadow-sm transition-shadow cursor-pointer" onclick="window.location.href='/forum?post=${p.id}'">
|
||||
<h3 class="text-base font-semibold bg-gradient-to-r from-cyan-400 to-blue-400 bg-clip-text text-transparent mb-1">${escapeHtml(p.title)}</h3>
|
||||
<p class="text-sm text-slate-300 mb-2">${escapeHtml(p.content)}</p>
|
||||
<div class="flex items-center text-xs text-slate-400 space-x-3">
|
||||
|
||||
@@ -25,6 +25,8 @@
|
||||
.footer { text-align: center; margin-top: 30px; font-size: 12px; color: #666; border-top: 1px solid #ccc; padding-top: 10px; }
|
||||
.print-btn { position: fixed; top: 20px; right: 20px; padding: 10px 24px; background: #3b82f6; color: #fff; border: none; border-radius: 6px; cursor: pointer; font-size: 14px; z-index: 100; }
|
||||
.print-btn:hover { background: #2563eb; }
|
||||
.q-images { margin: 8px 0 4px 25px; }
|
||||
.q-images img { max-height: 200px; max-width: 100%; border: 1px solid #ccc; margin: 4px 4px 4px 0; }
|
||||
</style>
|
||||
<!-- KaTeX 数学公式渲染 -->
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.16.11/dist/katex.min.css">
|
||||
@@ -66,6 +68,13 @@
|
||||
<div class="question">
|
||||
<span class="q-num">{{ loop.index }}.</span> {{ q.content }}
|
||||
<span class="q-score">({{ q.score }}分)</span>
|
||||
{% if q.get('images') %}
|
||||
<div class="q-images">
|
||||
{% for img in q.images %}
|
||||
<img src="{{ img }}" alt="题目图片">
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if q.options %}
|
||||
<div class="options">
|
||||
{% for opt in q.options %}
|
||||
@@ -83,6 +92,13 @@
|
||||
<div class="question">
|
||||
<span class="q-num">{{ loop.index }}.</span> {{ q.content }}
|
||||
<span class="q-score">({{ q.score }}分)</span>
|
||||
{% if q.get('images') %}
|
||||
<div class="q-images">
|
||||
{% for img in q.images %}
|
||||
<img src="{{ img }}" alt="题目图片">
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="answer-area">
|
||||
<div class="answer-line"></div>
|
||||
</div>
|
||||
@@ -96,6 +112,13 @@
|
||||
<div class="question">
|
||||
<span class="q-num">{{ loop.index }}.</span> {{ q.content }}
|
||||
<span class="q-score">({{ q.score }}分)</span>
|
||||
{% if q.get('images') %}
|
||||
<div class="q-images">
|
||||
{% for img in q.images %}
|
||||
<img src="{{ img }}" alt="题目图片">
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="answer-area">
|
||||
{% for i in range(8) %}
|
||||
<div class="answer-line"></div>
|
||||
|
||||
@@ -347,6 +347,9 @@ function insEmoji(em) {
|
||||
// 初始化富文本编辑器
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
new RichEditor('new-content');
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
const postId = params.get('post');
|
||||
if (postId) openPost(parseInt(postId));
|
||||
});
|
||||
|
||||
// ===== 图片上传 =====
|
||||
|
||||
Reference in New Issue
Block a user