Compare commits

..

1 Commits

Author SHA1 Message Date
dd9609165c mobile . themes 2026-03-16 14:00:50 +08:00
6 changed files with 110 additions and 78 deletions

90
app.py
View File

@@ -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',

View File

@@ -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': {}

View File

@@ -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')

View File

@@ -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">

View File

@@ -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>

View File

@@ -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));
});
// ===== 图片上传 =====