mobile app models chat login notifications themes
This commit is contained in:
174
app.py
174
app.py
@@ -33,6 +33,10 @@ app = Flask(__name__)
|
||||
app.secret_key = os.urandom(24)
|
||||
socketio = SocketIO(app, cors_allowed_origins="*", async_mode='threading')
|
||||
|
||||
# Session配置:设置永久session的有效期为10天
|
||||
from datetime import timedelta
|
||||
app.config['PERMANENT_SESSION_LIFETIME'] = timedelta(days=10)
|
||||
|
||||
# 内存存储(用于临时验证码)
|
||||
captcha_store = {}
|
||||
email_codes = {}
|
||||
@@ -930,11 +934,21 @@ def api_change_name():
|
||||
def apply_contest():
|
||||
if request.method == 'POST':
|
||||
user_id = session['user']['id']
|
||||
# 防止重复提交:如果已有 pending 状态的申请,不允许再提交
|
||||
existing = ContestApplication.query.filter_by(user_id=user_id, status='pending').first()
|
||||
|
||||
# 查找该用户的现有申请(不限状态)
|
||||
existing = ContestApplication.query.filter_by(user_id=user_id).first()
|
||||
|
||||
# 如果存在申请,检查拒绝次数和冷却期
|
||||
if existing:
|
||||
flash('您已有一个待审核的杯赛申请,请等待审核结果后再提交新申请')
|
||||
return redirect(url_for('contest_list'))
|
||||
# 检查是否被拒绝5次且在冷却期内
|
||||
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
|
||||
flash(f'您的申请已被拒绝5次,需等待 {remaining_days} 天后才能再次申请', 'error')
|
||||
return redirect(url_for('contest_list'))
|
||||
|
||||
name = request.form.get('name')
|
||||
organizer = request.form.get('organizer')
|
||||
description = request.form.get('description')
|
||||
@@ -946,6 +960,7 @@ def apply_contest():
|
||||
responsible_phone = request.form.get('responsible_phone')
|
||||
responsible_email = request.form.get('responsible_email')
|
||||
organization = request.form.get('organization')
|
||||
|
||||
if not all([name, organizer, description, contact, start_date, end_date, total_score,
|
||||
responsible_person, responsible_phone, responsible_email, organization]):
|
||||
flash('请填写所有必填项')
|
||||
@@ -953,22 +968,47 @@ def apply_contest():
|
||||
if total_score < 1:
|
||||
flash('满分分数必须大于0')
|
||||
return redirect(url_for('apply_contest'))
|
||||
app = ContestApplication(
|
||||
user_id=session['user']['id'],
|
||||
name=name,
|
||||
organizer=organizer,
|
||||
description=description,
|
||||
contact=contact,
|
||||
start_date=start_date,
|
||||
end_date=end_date,
|
||||
total_score=total_score,
|
||||
responsible_person=responsible_person,
|
||||
responsible_phone=responsible_phone,
|
||||
responsible_email=responsible_email,
|
||||
organization=organization
|
||||
)
|
||||
db.session.add(app)
|
||||
|
||||
# 如果存在申请,更新它;否则创建新申请
|
||||
if existing:
|
||||
# 更新现有申请
|
||||
existing.name = name
|
||||
existing.organizer = organizer
|
||||
existing.description = description
|
||||
existing.contact = contact
|
||||
existing.start_date = start_date
|
||||
existing.end_date = end_date
|
||||
existing.total_score = total_score
|
||||
existing.responsible_person = responsible_person
|
||||
existing.responsible_phone = responsible_phone
|
||||
existing.responsible_email = responsible_email
|
||||
existing.organization = organization
|
||||
existing.status = 'pending'
|
||||
existing.applied_at = datetime.utcnow()
|
||||
existing.reviewed_at = None
|
||||
app = existing
|
||||
flash_msg = '申请已更新,请等待管理员审核'
|
||||
else:
|
||||
# 创建新申请
|
||||
app = ContestApplication(
|
||||
user_id=session['user']['id'],
|
||||
name=name,
|
||||
organizer=organizer,
|
||||
description=description,
|
||||
contact=contact,
|
||||
start_date=start_date,
|
||||
end_date=end_date,
|
||||
total_score=total_score,
|
||||
responsible_person=responsible_person,
|
||||
responsible_phone=responsible_phone,
|
||||
responsible_email=responsible_email,
|
||||
organization=organization
|
||||
)
|
||||
db.session.add(app)
|
||||
flash_msg = '申请已提交,请等待管理员审核'
|
||||
|
||||
db.session.commit()
|
||||
|
||||
# 通知所有管理员有新的杯赛申请
|
||||
admins = User.query.filter_by(role='admin').all()
|
||||
for admin_user in admins:
|
||||
@@ -979,7 +1019,7 @@ def apply_contest():
|
||||
from_user=session['user']['name'],
|
||||
post_id=app.id
|
||||
)
|
||||
flash('申请已提交,请等待管理员审核')
|
||||
flash(flash_msg)
|
||||
return redirect(url_for('contest_list'))
|
||||
return render_template('apply_contest.html')
|
||||
|
||||
@@ -1022,6 +1062,8 @@ def approve_contest_application(app_id):
|
||||
db.session.add(membership)
|
||||
app.status = 'approved'
|
||||
app.reviewed_at = datetime.utcnow()
|
||||
app.rejection_count = 0 # 重置拒绝次数
|
||||
app.last_rejected_at = None # 清除最后拒绝时间
|
||||
# 自动创建杯赛讨论群
|
||||
chatroom = ChatRoom(type='contest', name=contest.name + ' 讨论群',
|
||||
creator_id=app.user_id, contest_id=contest.id)
|
||||
@@ -1044,6 +1086,8 @@ def reject_contest_application(app_id):
|
||||
return redirect(url_for('admin_contest_applications'))
|
||||
app.status = 'rejected'
|
||||
app.reviewed_at = datetime.utcnow()
|
||||
app.rejection_count += 1
|
||||
app.last_rejected_at = datetime.utcnow()
|
||||
# 通知申请人审核未通过
|
||||
add_notification(app.user_id, 'contest_result',
|
||||
f'您申请举办的杯赛「{app.name}」未通过审核。', from_user='系统')
|
||||
@@ -1287,6 +1331,8 @@ def api_approve_contest(app_id):
|
||||
db.session.add(ContestMembership(user_id=ca.user_id, contest_id=contest.id, role='owner'))
|
||||
ca.status = 'approved'
|
||||
ca.reviewed_at = datetime.utcnow()
|
||||
ca.rejection_count = 0 # 重置拒绝次数
|
||||
ca.last_rejected_at = None # 清除最后拒绝时间
|
||||
chatroom = ChatRoom(type='contest', name=contest.name + ' 讨论群',
|
||||
creator_id=ca.user_id, contest_id=contest.id)
|
||||
db.session.add(chatroom)
|
||||
@@ -1298,7 +1344,7 @@ def api_approve_contest(app_id):
|
||||
return jsonify({'success': True, 'message': '已批准'})
|
||||
|
||||
@app.route('/api/teacher-applications/<int:app_id>/approve', methods=['POST'])
|
||||
@teacher_required
|
||||
@login_required
|
||||
def api_approve_teacher(app_id):
|
||||
user = session['user']
|
||||
ta = TeacherApplication.query.get_or_404(app_id)
|
||||
@@ -1328,6 +1374,8 @@ def api_approve_teacher(app_id):
|
||||
ta.status = 'approved'
|
||||
ta.reviewed_at = datetime.utcnow()
|
||||
ta.reviewed_by = user['id']
|
||||
ta.rejection_count = 0 # 重置拒绝次数
|
||||
ta.last_rejected_at = None # 清除最后拒绝时间
|
||||
|
||||
contest = Contest.query.get(ta.contest_id)
|
||||
contest_name = contest.name if contest else ''
|
||||
@@ -1349,7 +1397,7 @@ def api_approve_teacher(app_id):
|
||||
return jsonify({'success': True, 'message': '已批准,邀请码已通过私聊发送给老师'})
|
||||
|
||||
@app.route('/api/teacher-applications/<int:app_id>/reject', methods=['POST'])
|
||||
@teacher_required
|
||||
@login_required
|
||||
def api_reject_teacher(app_id):
|
||||
user = session['user']
|
||||
ta = TeacherApplication.query.get_or_404(app_id)
|
||||
@@ -1362,6 +1410,8 @@ def api_reject_teacher(app_id):
|
||||
ta.status = 'rejected'
|
||||
ta.reviewed_at = datetime.utcnow()
|
||||
ta.reviewed_by = user['id']
|
||||
ta.rejection_count += 1
|
||||
ta.last_rejected_at = datetime.utcnow()
|
||||
contest = Contest.query.get(ta.contest_id)
|
||||
add_notification(ta.user_id, 'teacher_result',
|
||||
f'您申请成为杯赛「{contest.name if contest else ""}」老师未通过审核。',
|
||||
@@ -1377,6 +1427,8 @@ def api_reject_contest(app_id):
|
||||
return jsonify({'success': False, 'message': '该申请已处理'}), 400
|
||||
ca.status = 'rejected'
|
||||
ca.reviewed_at = datetime.utcnow()
|
||||
ca.rejection_count += 1
|
||||
ca.last_rejected_at = datetime.utcnow()
|
||||
add_notification(ca.user_id, 'contest_result',
|
||||
f'您申请举办的杯赛「{ca.name}」未通过审核。', from_user='系统')
|
||||
db.session.commit()
|
||||
@@ -1402,11 +1454,6 @@ def apply_teacher():
|
||||
return redirect(url_for('apply_teacher'))
|
||||
|
||||
user = session['user']
|
||||
# 检查是否已经申请过该杯赛且为pending
|
||||
existing = TeacherApplication.query.filter_by(user_id=user['id'], contest_id=contest_id, status='pending').first()
|
||||
if existing:
|
||||
flash('您已提交过该杯赛的申请,请耐心等待审核', 'error')
|
||||
return redirect(url_for('apply_teacher'))
|
||||
|
||||
# 检查是否已经是该杯赛的老师或负责人
|
||||
membership = ContestMembership.query.filter_by(user_id=user['id'], contest_id=contest_id).first()
|
||||
@@ -1414,15 +1461,44 @@ def apply_teacher():
|
||||
flash('您已经是该杯赛的老师或负责人', 'error')
|
||||
return redirect(url_for('contest_detail', contest_id=contest_id))
|
||||
|
||||
# 创建申请记录
|
||||
appli = TeacherApplication(
|
||||
user_id=user['id'],
|
||||
contest_id=contest_id,
|
||||
name=name,
|
||||
email=email,
|
||||
reason=reason
|
||||
)
|
||||
db.session.add(appli)
|
||||
# 查找该用户对该杯赛的现有申请
|
||||
existing = TeacherApplication.query.filter_by(user_id=user['id'], contest_id=contest_id).first()
|
||||
|
||||
# 如果存在申请,检查拒绝次数和冷却期
|
||||
if existing:
|
||||
# 检查是否被拒绝5次且在冷却期内
|
||||
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
|
||||
flash(f'您对该杯赛的申请已被拒绝5次,需等待 {remaining_days} 天后才能再次申请', 'error')
|
||||
return redirect(url_for('contest_detail', contest_id=contest_id))
|
||||
|
||||
# 如果存在申请,更新它;否则创建新申请
|
||||
if existing:
|
||||
# 更新现有申请
|
||||
existing.name = name
|
||||
existing.email = email
|
||||
existing.reason = reason
|
||||
existing.status = 'pending'
|
||||
existing.applied_at = datetime.utcnow()
|
||||
existing.reviewed_at = None
|
||||
existing.reviewed_by = None
|
||||
appli = existing
|
||||
flash_msg = '申请已更新,管理员或杯赛负责人会尽快审核'
|
||||
else:
|
||||
# 创建新申请
|
||||
appli = TeacherApplication(
|
||||
user_id=user['id'],
|
||||
contest_id=contest_id,
|
||||
name=name,
|
||||
email=email,
|
||||
reason=reason
|
||||
)
|
||||
db.session.add(appli)
|
||||
flash_msg = '申请已提交,管理员或杯赛负责人会尽快审核'
|
||||
|
||||
db.session.commit()
|
||||
|
||||
# 通知杯赛负责人
|
||||
@@ -1438,7 +1514,7 @@ def apply_teacher():
|
||||
f'用户 {name} 申请成为杯赛「{contest.name}」的老师,请审核。',
|
||||
from_user=user['name'], post_id=appli.id)
|
||||
|
||||
flash('申请已提交,管理员或杯赛负责人会尽快审核', 'success')
|
||||
flash(flash_msg, 'success')
|
||||
return redirect(url_for('contest_detail', contest_id=contest_id))
|
||||
|
||||
# GET 请求:显示申请表单,可传入 contest_id 预选
|
||||
@@ -1449,7 +1525,7 @@ def apply_teacher():
|
||||
|
||||
# ========== 管理后台教师申请审核 ==========
|
||||
@app.route('/admin/teacher-applications')
|
||||
@teacher_required
|
||||
@login_required
|
||||
def admin_teacher_applications():
|
||||
user = session['user']
|
||||
# 管理员可见所有申请,杯赛负责人只能看到自己负责杯赛的申请
|
||||
@@ -1468,7 +1544,7 @@ def admin_teacher_applications():
|
||||
return render_template('admin_teacher_applications.html', apps=apps)
|
||||
|
||||
@app.route('/admin/teacher-applications/<int:app_id>/approve', methods=['POST'])
|
||||
@teacher_required
|
||||
@login_required
|
||||
def approve_teacher_application(app_id):
|
||||
user = session['user']
|
||||
app = TeacherApplication.query.get_or_404(app_id)
|
||||
@@ -1504,6 +1580,8 @@ def approve_teacher_application(app_id):
|
||||
app.status = 'approved'
|
||||
app.reviewed_at = datetime.utcnow()
|
||||
app.reviewed_by = user['id']
|
||||
app.rejection_count = 0 # 重置拒绝次数
|
||||
app.last_rejected_at = None # 清除最后拒绝时间
|
||||
|
||||
contest = Contest.query.get(app.contest_id)
|
||||
contest_name = contest.name if contest else ''
|
||||
@@ -1528,7 +1606,7 @@ def approve_teacher_application(app_id):
|
||||
return redirect(url_for('admin_teacher_applications'))
|
||||
|
||||
@app.route('/admin/teacher-applications/<int:app_id>/reject', methods=['POST'])
|
||||
@teacher_required
|
||||
@login_required
|
||||
def reject_teacher_application(app_id):
|
||||
user = session['user']
|
||||
app = TeacherApplication.query.get_or_404(app_id)
|
||||
@@ -1546,6 +1624,8 @@ def reject_teacher_application(app_id):
|
||||
app.status = 'rejected'
|
||||
app.reviewed_at = datetime.utcnow()
|
||||
app.reviewed_by = user['id']
|
||||
app.rejection_count += 1
|
||||
app.last_rejected_at = datetime.utcnow()
|
||||
# 通知申请人
|
||||
contest = Contest.query.get(app.contest_id)
|
||||
add_notification(app.user_id, 'teacher_result',
|
||||
@@ -1760,6 +1840,7 @@ def api_login():
|
||||
return jsonify({'success': False, 'message': '请求数据格式错误'}), 400
|
||||
email = data.get('email', '')
|
||||
password = data.get('password', '')
|
||||
remember = data.get('remember', False)
|
||||
|
||||
if not email or not password:
|
||||
return jsonify({'success': False, 'message': '请输入邮箱和密码'}), 400
|
||||
@@ -1772,6 +1853,11 @@ def api_login():
|
||||
|
||||
user_data = {'name': user.name, 'email': user.email, 'role': user.role, 'id': user.id, 'avatar': user.avatar or ''}
|
||||
session['user'] = user_data
|
||||
|
||||
# 如果勾选了"保持登录",设置session为永久(10天)
|
||||
if remember:
|
||||
session.permanent = True
|
||||
|
||||
return jsonify({'success': True, 'message': '登录成功', 'user': user_data})
|
||||
|
||||
@app.route('/api/send-sms', methods=['POST'])
|
||||
@@ -1837,6 +1923,7 @@ def api_verify_code():
|
||||
data = request.get_json(force=True, silent=True)
|
||||
phone = data.get('phone', '')
|
||||
code = data.get('code', '')
|
||||
remember = data.get('remember', False)
|
||||
|
||||
if not phone or not code:
|
||||
return jsonify({'success': False, 'message': '手机号和验证码不能为空'}), 400
|
||||
@@ -1870,6 +1957,11 @@ def api_verify_code():
|
||||
db.session.commit()
|
||||
user_data = {'name': user.name, 'phone': user.phone, 'role': user.role, 'id': user.id, 'avatar': user.avatar or ''}
|
||||
session['user'] = user_data
|
||||
|
||||
# 如果勾选了"保持登录",设置session为永久(10天)
|
||||
if remember:
|
||||
session.permanent = True
|
||||
|
||||
return jsonify({'success': True, 'message': '验证成功', 'user': user_data})
|
||||
else:
|
||||
msg = '验证码已过期' if model.get('VerifyResult') == 'UNKNOWN' else '验证码错误'
|
||||
@@ -2809,7 +2901,7 @@ def api_add_question_bank(contest_id):
|
||||
@app.route('/api/contests/<int:contest_id>/question-bank/<int:qid>', methods=['DELETE'])
|
||||
@login_required
|
||||
def api_delete_question_bank(contest_id, qid):
|
||||
"""删除题库题目(负责人或题目贡献者)"""
|
||||
"""删除题库题目(负责人可删除任何题目,老师只能删除自己贡献的题目)"""
|
||||
user = session['user']
|
||||
item = QuestionBankItem.query.get(qid)
|
||||
if not item or item.contest_id != contest_id:
|
||||
|
||||
Reference in New Issue
Block a user