370 lines
16 KiB
Python
370 lines
16 KiB
Python
import os
|
||
import sqlite3
|
||
import random
|
||
import string
|
||
from datetime import datetime
|
||
from flask import Flask, render_template, request, redirect, url_for, g, flash, jsonify
|
||
|
||
app = Flask(__name__)
|
||
app.secret_key = 'admin-secret-key-2026'
|
||
|
||
# 连接主应用的数据库
|
||
DATABASE = os.path.join(os.path.dirname(os.path.abspath(__file__)), '..', 'instance', 'database.db')
|
||
|
||
def get_db():
|
||
db = getattr(g, '_database', None)
|
||
if db is None:
|
||
db = g._database = sqlite3.connect(DATABASE)
|
||
db.row_factory = sqlite3.Row
|
||
return db
|
||
|
||
@app.teardown_appcontext
|
||
def close_connection(exception):
|
||
db = getattr(g, '_database', None)
|
||
if db is not None:
|
||
db.close()
|
||
|
||
def init_db():
|
||
"""确保 system_notification 和 invite_code 表存在"""
|
||
with app.app_context():
|
||
db = get_db()
|
||
db.executescript('''
|
||
CREATE TABLE IF NOT EXISTS system_notification (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
title TEXT NOT NULL,
|
||
content TEXT NOT NULL,
|
||
author_id INTEGER NOT NULL,
|
||
pinned INTEGER DEFAULT 0,
|
||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||
);
|
||
CREATE TABLE IF NOT EXISTS invite_code (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
code TEXT UNIQUE NOT NULL,
|
||
user_id INTEGER NOT NULL,
|
||
application_id INTEGER NOT NULL,
|
||
used INTEGER DEFAULT 0,
|
||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||
used_at TIMESTAMP
|
||
);
|
||
''')
|
||
db.commit()
|
||
|
||
# PLACEHOLDER_ROUTES
|
||
|
||
def generate_invite_code():
|
||
return 'TC-' + ''.join(random.choices(string.ascii_uppercase + string.digits, k=8))
|
||
|
||
def add_notification(db, user_id, ntype, content, from_user='', post_id=0):
|
||
db.execute('INSERT INTO notification (user_id, type, content, from_user, post_id, read) VALUES (?,?,?,?,?,0)',
|
||
(user_id, ntype, content, from_user, post_id))
|
||
|
||
# ========== 后台首页 ==========
|
||
@app.route('/admin')
|
||
def dashboard():
|
||
db = get_db()
|
||
stats = {
|
||
'users': db.execute('SELECT COUNT(*) as c FROM user').fetchone()['c'],
|
||
'contests': db.execute('SELECT COUNT(*) as c FROM contest').fetchone()['c'],
|
||
'exams': db.execute('SELECT COUNT(*) as c FROM exam').fetchone()['c'],
|
||
'posts': db.execute('SELECT COUNT(*) as c FROM post').fetchone()['c'],
|
||
'pending_teacher': db.execute("SELECT COUNT(*) as c FROM teacher_application WHERE status='pending'").fetchone()['c'],
|
||
'pending_contest': db.execute("SELECT COUNT(*) as c FROM contest_application WHERE status='pending'").fetchone()['c'],
|
||
}
|
||
return render_template('dashboard.html', stats=stats)
|
||
|
||
# ========== 杯赛管理 ==========
|
||
@app.route('/admin/competitions')
|
||
def competitions():
|
||
db = get_db()
|
||
contests = db.execute('SELECT * FROM contest ORDER BY created_at DESC').fetchall()
|
||
return render_template('competitions.html', competitions=contests)
|
||
|
||
@app.route('/admin/competitions/stop/<int:id>', methods=['POST'])
|
||
def stop_competition(id):
|
||
db = get_db()
|
||
db.execute("UPDATE contest SET status = 'ended' WHERE id = ?", (id,))
|
||
db.commit()
|
||
return redirect(url_for('competitions'))
|
||
|
||
@app.route('/admin/competitions/activate/<int:id>', methods=['POST'])
|
||
def activate_competition(id):
|
||
db = get_db()
|
||
db.execute("UPDATE contest SET status = 'upcoming' WHERE id = ?", (id,))
|
||
db.commit()
|
||
return redirect(url_for('competitions'))
|
||
|
||
# ========== 杯赛申请 ==========
|
||
@app.route('/admin/competition_apps')
|
||
def competition_apps():
|
||
db = get_db()
|
||
apps = db.execute('''
|
||
SELECT ca.*, u.name as applicant_name FROM contest_application ca
|
||
JOIN user u ON ca.user_id = u.id
|
||
ORDER BY ca.applied_at DESC
|
||
''').fetchall()
|
||
return render_template('competition_apps.html', apps=apps)
|
||
|
||
@app.route('/admin/competition_apps/approve/<int:id>', methods=['POST'])
|
||
def approve_competition_app(id):
|
||
db = get_db()
|
||
ca = db.execute('SELECT * FROM contest_application WHERE id = ?', (id,)).fetchone()
|
||
if not ca or ca['status'] != 'pending':
|
||
flash('该申请已处理', 'error')
|
||
return redirect(url_for('competition_apps'))
|
||
# 创建杯赛
|
||
db.execute('INSERT INTO contest (name, organizer, description, start_date, end_date, status, participants, created_by) VALUES (?,?,?,?,?,?,?,?)',
|
||
(ca['name'], ca['organizer'], ca['description'], '待定', '待定', 'upcoming', 0, ca['user_id']))
|
||
contest_id = db.execute('SELECT last_insert_rowid()').fetchone()[0]
|
||
# 创建成员关系(owner)
|
||
db.execute('INSERT INTO contest_membership (user_id, contest_id, role) VALUES (?,?,?)', (ca['user_id'], contest_id, 'owner'))
|
||
# 创建杯赛讨论群
|
||
db.execute('INSERT INTO chat_room (type, name, creator_id, contest_id) VALUES (?,?,?,?)',
|
||
('contest', ca['name'] + ' 讨论群', ca['user_id'], contest_id))
|
||
room_id = db.execute('SELECT last_insert_rowid()').fetchone()[0]
|
||
db.execute('INSERT INTO chat_room_member (room_id, user_id, role) VALUES (?,?,?)', (room_id, ca['user_id'], 'admin'))
|
||
# 更新申请状态
|
||
db.execute("UPDATE contest_application SET status = 'approved', reviewed_at = ? WHERE id = ?", (datetime.utcnow(), id))
|
||
add_notification(db, ca['user_id'], 'contest_result', f'您申请举办的杯赛「{ca["name"]}」已通过审核!', from_user='系统')
|
||
db.commit()
|
||
flash('申请已批准,杯赛已创建', 'success')
|
||
return redirect(url_for('competition_apps'))
|
||
|
||
@app.route('/admin/competition_apps/reject/<int:id>', methods=['POST'])
|
||
def reject_competition_app(id):
|
||
db = get_db()
|
||
ca = db.execute('SELECT * FROM contest_application WHERE id = ?', (id,)).fetchone()
|
||
if not ca or ca['status'] != 'pending':
|
||
flash('该申请已处理', 'error')
|
||
return redirect(url_for('competition_apps'))
|
||
db.execute("UPDATE contest_application SET status = 'rejected', reviewed_at = ? WHERE id = ?", (datetime.utcnow(), id))
|
||
add_notification(db, ca['user_id'], 'contest_result', f'您申请举办的杯赛「{ca["name"]}」未通过审核。', from_user='系统')
|
||
db.commit()
|
||
flash('申请已拒绝', 'success')
|
||
return redirect(url_for('competition_apps'))
|
||
|
||
# PLACEHOLDER_TEACHER_ROUTES
|
||
|
||
# ========== 教师申请(邀请码流程) ==========
|
||
@app.route('/admin/teacher_apps')
|
||
def teacher_apps():
|
||
db = get_db()
|
||
apps = db.execute('''
|
||
SELECT ta.*, u.name as username, c.name as contest_name
|
||
FROM teacher_application ta
|
||
JOIN user u ON ta.user_id = u.id
|
||
JOIN contest c ON ta.contest_id = c.id
|
||
ORDER BY ta.applied_at DESC
|
||
''').fetchall()
|
||
return render_template('teacher_apps.html', apps=apps)
|
||
|
||
@app.route('/admin/teacher_apps/approve/<int:id>', methods=['POST'])
|
||
def approve_teacher_app(id):
|
||
db = get_db()
|
||
ta = db.execute('SELECT * FROM teacher_application WHERE id = ?', (id,)).fetchone()
|
||
if not ta or ta['status'] != 'pending':
|
||
flash('该申请已处理', 'error')
|
||
return redirect(url_for('teacher_apps'))
|
||
# 检查是否已是杯赛成员
|
||
existing = db.execute('SELECT id FROM contest_membership WHERE user_id = ? AND contest_id = ?',
|
||
(ta['user_id'], ta['contest_id'])).fetchone()
|
||
if existing:
|
||
db.execute("UPDATE teacher_application SET status = 'rejected', reviewed_at = ? WHERE id = ?", (datetime.utcnow(), id))
|
||
db.commit()
|
||
flash('用户已是杯赛成员,申请已拒绝', 'error')
|
||
return redirect(url_for('teacher_apps'))
|
||
# 生成邀请码
|
||
code = generate_invite_code()
|
||
while db.execute('SELECT id FROM invite_code WHERE code = ?', (code,)).fetchone():
|
||
code = generate_invite_code()
|
||
db.execute('INSERT INTO invite_code (code, user_id, application_id) VALUES (?,?,?)', (code, ta['user_id'], ta['id']))
|
||
db.execute("UPDATE teacher_application SET status = 'approved', reviewed_at = ? WHERE id = ?", (datetime.utcnow(), id))
|
||
# 获取杯赛名称
|
||
contest = db.execute('SELECT name FROM contest WHERE id = ?', (ta['contest_id'],)).fetchone()
|
||
contest_name = contest['name'] if contest else ''
|
||
# 查找或创建私聊室,发送邀请码消息
|
||
admin_id = db.execute("SELECT id FROM user WHERE role = 'admin' LIMIT 1").fetchone()
|
||
sender_id = admin_id['id'] if admin_id else 1
|
||
# 查找已有私聊
|
||
room = db.execute('''
|
||
SELECT cr.id FROM chat_room cr
|
||
WHERE cr.type = 'private'
|
||
AND cr.id IN (SELECT room_id FROM chat_room_member WHERE user_id = ?)
|
||
AND cr.id IN (SELECT room_id FROM chat_room_member WHERE user_id = ?)
|
||
''', (sender_id, ta['user_id'])).fetchone()
|
||
if room:
|
||
room_id = room['id']
|
||
else:
|
||
db.execute('INSERT INTO chat_room (type, creator_id) VALUES (?,?)', ('private', sender_id))
|
||
room_id = db.execute('SELECT last_insert_rowid()').fetchone()[0]
|
||
db.execute('INSERT INTO chat_room_member (room_id, user_id, role) VALUES (?,?,?)', (room_id, sender_id, 'member'))
|
||
db.execute('INSERT INTO chat_room_member (room_id, user_id, role) VALUES (?,?,?)', (room_id, ta['user_id'], 'member'))
|
||
# 发送私聊消息
|
||
msg = f'恭喜!您申请成为杯赛「{contest_name}」老师已通过审核。\n请使用以下邀请码激活您的教师身份:\n\n🎫 邀请码:{code}\n\n请前往「申请成为老师」页面,在邀请码输入框中输入此码完成激活。\n注意:此邀请码仅限您本人使用,且仅限一次。'
|
||
db.execute('INSERT INTO message (room_id, sender_id, type, content) VALUES (?,?,?,?)', (room_id, sender_id, 'system', msg))
|
||
add_notification(db, ta['user_id'], 'teacher_result',
|
||
f'您申请成为杯赛「{contest_name}」老师已通过审核,请查看私聊消息获取邀请码。', from_user='系统')
|
||
db.commit()
|
||
flash(f'申请已批准,邀请码 {code} 已通过私聊发送给老师', 'success')
|
||
return redirect(url_for('teacher_apps'))
|
||
|
||
@app.route('/admin/teacher_apps/reject/<int:id>', methods=['POST'])
|
||
def reject_teacher_app(id):
|
||
db = get_db()
|
||
ta = db.execute('SELECT * FROM teacher_application WHERE id = ?', (id,)).fetchone()
|
||
if not ta or ta['status'] != 'pending':
|
||
flash('该申请已处理', 'error')
|
||
return redirect(url_for('teacher_apps'))
|
||
db.execute("UPDATE teacher_application SET status = 'rejected', reviewed_at = ? WHERE id = ?", (datetime.utcnow(), id))
|
||
contest = db.execute('SELECT name FROM contest WHERE id = ?', (ta['contest_id'],)).fetchone()
|
||
contest_name = contest['name'] if contest else ''
|
||
add_notification(db, ta['user_id'], 'teacher_result',
|
||
f'您申请成为杯赛「{contest_name}」老师未通过审核。', from_user='系统')
|
||
db.commit()
|
||
flash('申请已拒绝', 'success')
|
||
return redirect(url_for('teacher_apps'))
|
||
|
||
# PLACEHOLDER_REMAINING_ROUTES
|
||
|
||
# ========== 考试管理 ==========
|
||
@app.route('/admin/exams')
|
||
def exams():
|
||
db = get_db()
|
||
exams = db.execute('''
|
||
SELECT e.*, c.name as contest_name, u.name as creator_name
|
||
FROM exam e
|
||
LEFT JOIN contest c ON e.contest_id = c.id
|
||
LEFT JOIN user u ON e.creator_id = u.id
|
||
ORDER BY e.created_at DESC
|
||
''').fetchall()
|
||
return render_template('exams.html', exams=exams)
|
||
|
||
@app.route('/admin/exams/stop/<int:id>', methods=['POST'])
|
||
def stop_exam(id):
|
||
db = get_db()
|
||
db.execute("UPDATE exam SET status = 'closed' WHERE id = ?", (id,))
|
||
db.commit()
|
||
flash('考试已停止', 'success')
|
||
return redirect(url_for('exams'))
|
||
|
||
@app.route('/admin/exams/activate/<int:id>', methods=['POST'])
|
||
def activate_exam(id):
|
||
db = get_db()
|
||
db.execute("UPDATE exam SET status = 'available' WHERE id = ?", (id,))
|
||
db.commit()
|
||
flash('考试已恢复', 'success')
|
||
return redirect(url_for('exams'))
|
||
|
||
# ========== 用户管理 ==========
|
||
@app.route('/admin/users')
|
||
def users():
|
||
db = get_db()
|
||
all_users = db.execute('SELECT * FROM user ORDER BY created_at DESC').fetchall()
|
||
return render_template('users.html', users=all_users)
|
||
|
||
@app.route('/admin/users/toggle_ban/<int:id>', methods=['POST'])
|
||
def toggle_ban_user(id):
|
||
db = get_db()
|
||
cur = db.execute('SELECT is_banned FROM user WHERE id = ?', (id,)).fetchone()
|
||
new_val = 0 if cur['is_banned'] else 1
|
||
db.execute('UPDATE user SET is_banned = ? WHERE id = ?', (new_val, id))
|
||
db.commit()
|
||
flash('用户状态已更新', 'success')
|
||
return redirect(url_for('users'))
|
||
|
||
@app.route('/admin/users/set_role/<int:id>', methods=['POST'])
|
||
def set_user_role(id):
|
||
role = request.form.get('role', 'student')
|
||
db = get_db()
|
||
db.execute('UPDATE user SET role = ? WHERE id = ?', (role, id))
|
||
db.commit()
|
||
flash('角色已更新', 'success')
|
||
return redirect(url_for('users'))
|
||
|
||
# ========== 帖子管理 ==========
|
||
@app.route('/admin/posts')
|
||
def posts():
|
||
db = get_db()
|
||
all_posts = db.execute('''
|
||
SELECT p.*, u.name as author_name FROM post p
|
||
LEFT JOIN user u ON p.author_id = u.id
|
||
ORDER BY p.created_at DESC
|
||
''').fetchall()
|
||
return render_template('posts.html', posts=all_posts)
|
||
|
||
@app.route('/admin/posts/delete/<int:id>', methods=['POST'])
|
||
def delete_post(id):
|
||
db = get_db()
|
||
db.execute('DELETE FROM post WHERE id = ?', (id,))
|
||
db.commit()
|
||
flash('帖子已删除', 'success')
|
||
return redirect(url_for('posts'))
|
||
|
||
@app.route('/admin/posts/toggle_pin/<int:id>', methods=['POST'])
|
||
def toggle_pin_post(id):
|
||
db = get_db()
|
||
cur = db.execute('SELECT pinned FROM post WHERE id = ?', (id,)).fetchone()
|
||
new_val = 0 if cur['pinned'] else 1
|
||
db.execute('UPDATE post SET pinned = ? WHERE id = ?', (new_val, id))
|
||
db.commit()
|
||
flash('帖子置顶状态已更新', 'success')
|
||
return redirect(url_for('posts'))
|
||
|
||
# PLACEHOLDER_NOTIFICATION_ROUTES
|
||
|
||
# ========== 公告管理 ==========
|
||
@app.route('/admin/notifications')
|
||
def notifications():
|
||
db = get_db()
|
||
anns = db.execute('''
|
||
SELECT sn.*, u.name as author_name FROM system_notification sn
|
||
LEFT JOIN user u ON sn.author_id = u.id
|
||
ORDER BY sn.pinned DESC, sn.created_at DESC
|
||
''').fetchall()
|
||
return render_template('notifications.html', announcements=anns)
|
||
|
||
@app.route('/admin/notifications/create', methods=['POST'])
|
||
def create_notification():
|
||
title = request.form.get('title', '').strip()
|
||
content = request.form.get('content', '').strip()
|
||
pinned = 1 if request.form.get('pinned') else 0
|
||
if not title or not content:
|
||
flash('标题和内容不能为空', 'error')
|
||
return redirect(url_for('notifications'))
|
||
db = get_db()
|
||
admin = db.execute("SELECT id FROM user WHERE role = 'admin' LIMIT 1").fetchone()
|
||
author_id = admin['id'] if admin else 1
|
||
now = datetime.utcnow()
|
||
db.execute('INSERT INTO system_notification (title, content, author_id, pinned, created_at, updated_at) VALUES (?,?,?,?,?,?)',
|
||
(title, content, author_id, pinned, now, now))
|
||
db.commit()
|
||
flash('公告已发布', 'success')
|
||
return redirect(url_for('notifications'))
|
||
|
||
@app.route('/admin/notifications/edit/<int:id>', methods=['POST'])
|
||
def edit_notification(id):
|
||
title = request.form.get('title', '').strip()
|
||
content = request.form.get('content', '').strip()
|
||
pinned = 1 if request.form.get('pinned') else 0
|
||
if not title or not content:
|
||
flash('标题和内容不能为空', 'error')
|
||
return redirect(url_for('notifications'))
|
||
db = get_db()
|
||
db.execute('UPDATE system_notification SET title=?, content=?, pinned=?, updated_at=? WHERE id=?',
|
||
(title, content, pinned, datetime.utcnow(), id))
|
||
db.commit()
|
||
flash('公告已更新', 'success')
|
||
return redirect(url_for('notifications'))
|
||
|
||
@app.route('/admin/notifications/delete/<int:id>', methods=['POST'])
|
||
def delete_notification(id):
|
||
db = get_db()
|
||
db.execute('DELETE FROM system_notification WHERE id = ?', (id,))
|
||
db.commit()
|
||
flash('公告已删除', 'success')
|
||
return redirect(url_for('notifications'))
|
||
|
||
if __name__ == '__main__':
|
||
init_db()
|
||
app.run(debug=True, port=5001)
|