diff --git a/app.py b/app.py index 4913765..3061c04 100644 --- a/app.py +++ b/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//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//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//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//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//question-bank/', 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: diff --git a/models.py b/models.py index 4aaeb67..9c68053 100644 --- a/models.py +++ b/models.py @@ -65,6 +65,9 @@ class ContestApplication(db.Model): responsible_phone = db.Column(db.String(20)) # 责任人电话 responsible_email = db.Column(db.String(120)) # 责任人邮箱 organization = db.Column(db.String(100)) # 所属机构/学校 + # 拒绝次数控制 + rejection_count = db.Column(db.Integer, default=0) # 被拒绝次数 + last_rejected_at = db.Column(db.DateTime) # 最后一次被拒绝的时间 user = db.relationship('User', backref='contest_applications') @@ -361,6 +364,9 @@ class TeacherApplication(db.Model): 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')) + # 拒绝次数控制 + rejection_count = db.Column(db.Integer, default=0) # 被拒绝次数 + last_rejected_at = db.Column(db.DateTime) # 最后一次被拒绝的时间 # 关系 user = db.relationship('User', foreign_keys=[user_id], backref='teacher_applications') diff --git a/templates/chat.html b/templates/chat.html index 4b0f2c1..d33d6be 100644 --- a/templates/chat.html +++ b/templates/chat.html @@ -175,7 +175,7 @@ -