From 9a0814062387c2b36717656042100aa309071585 Mon Sep 17 00:00:00 2001 From: unknown <1251316345@qq.com> Date: Tue, 3 Mar 2026 16:02:23 +0800 Subject: [PATCH] mobile app database models admin_contests admin_dashboard admin_exams admin_posts admin_teacher_applications admin_users apply_contest apply_teacher base chat contest_detail contest_list exam_result forum notifications profile themes --- .claude/settings.local.json | 3 +- add_visibility_column.py | 40 +++ app.py | 85 ++++++- create_admin.py | 45 ++++ database.db | 0 fix_question_ids.py | 66 +++++ instance/database.db | Bin 176128 -> 176128 bytes models.py | 3 + static/css/style.css | 291 ++++++++++++++++++++++ templates/admin_contests.html | 64 ++--- templates/admin_dashboard.html | 189 +++++++------- templates/admin_exams.html | 68 ++--- templates/admin_posts.html | 76 +++--- templates/admin_teacher_applications.html | 124 ++++++--- templates/admin_users.html | 76 +++--- templates/apply_contest.html | 99 +++++--- templates/apply_teacher.html | 36 +-- templates/base.html | 104 +++++++- templates/chat.html | 142 +++++------ templates/contest_detail.html | 128 +++++----- templates/contest_list.html | 70 +++--- templates/exam_create.html | 18 +- templates/exam_detail.html | 64 +++-- templates/exam_list.html | 132 +++++----- templates/exam_result.html | 12 +- templates/forum.html | 82 +++--- templates/login.html | 30 +-- templates/notifications.html | 92 +++---- templates/profile.html | 170 ++++++------- templates/register.html | 56 ++--- 30 files changed, 1531 insertions(+), 834 deletions(-) create mode 100644 add_visibility_column.py create mode 100644 create_admin.py create mode 100644 database.db create mode 100644 fix_question_ids.py diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 229ba16..57ab03a 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -22,7 +22,8 @@ "Bash(cd \"D:/360MoveData/Users/HEIHAHA/Documents/WeChat Files/wxid_k0iaj5miuryq22/FileStorage/File/2026-02/超级大网站2月23号\" && py -3 -c \"\nimport sys, os\nsys.path.insert\\(0, os.path.dirname\\(os.path.abspath\\('app.py'\\)\\)\\)\nfrom app import app\nfrom models import db, User\nwith app.app_context\\(\\):\n users = User.query.all\\(\\)\n for u in users:\n print\\(f'id={u.id}, name={u.name}, email={u.email}, phone={u.phone}, role={u.role}'\\)\n if not users:\n print\\('No users in database'\\)\n\" 2>&1)", "Bash(cd \"D:/360MoveData/Users/HEIHAHA/Documents/WeChat Files/wxid_k0iaj5miuryq22/FileStorage/File/2026-02/超级大网站2月23号\" && py -3 -c \"\nimport os, glob\ntemplates = glob.glob\\('templates/*.html'\\)\ncount = 0\nfor f in templates:\n with open\\(f, 'r', encoding='utf-8'\\) as fh:\n content = fh.read\\(\\)\n if '联考平台' in content:\n new_content = content.replace\\('联考平台', '智联青云'\\)\n with open\\(f, 'w', encoding='utf-8'\\) as fh:\n fh.write\\(new_content\\)\n count += 1\n print\\(f' replaced in {f}'\\)\nprint\\(f'Done: {count} files updated'\\)\n\" 2>&1)", "Bash(git stash:*)", - "Bash(git pull:*)" + "Bash(git pull:*)", + "Bash(iconv:*)" ] } } diff --git a/add_visibility_column.py b/add_visibility_column.py new file mode 100644 index 0000000..c3d14ff --- /dev/null +++ b/add_visibility_column.py @@ -0,0 +1,40 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" +添加 exam.visibility 字段的数据库迁移脚本 +""" + +from app import app, db +from models import Exam + +def add_visibility_column(): + """为 Exam 表添加 visibility 字段""" + with app.app_context(): + try: + # 检查字段是否已存在 + inspector = db.inspect(db.engine) + columns = [col['name'] for col in inspector.get_columns('exam')] + + if 'visibility' in columns: + print("visibility 字段已存在,无需添加") + return + + # 添加字段 + with db.engine.connect() as conn: + conn.execute(db.text("ALTER TABLE exam ADD COLUMN visibility VARCHAR(20) DEFAULT 'private'")) + conn.commit() + print("成功添加 visibility 字段") + + # 将所有现有考试设置为公开(保持向后兼容) + conn.execute(db.text("UPDATE exam SET visibility = 'public' WHERE visibility IS NULL")) + conn.commit() + print("已将现有考试设置为公开") + + except Exception as e: + print(f"添加字段时出错: {e}") + raise + +if __name__ == '__main__': + print("开始添加 visibility 字段...") + add_visibility_column() + print("完成!") diff --git a/app.py b/app.py index 075eea9..9255c31 100644 --- a/app.py +++ b/app.py @@ -554,6 +554,16 @@ def exam_list(): subject_filter = request.args.get('subject', '').strip() query = Exam.query + + # 只显示公开考试或用户自己创建的私有考试 + from sqlalchemy import or_ + query = query.filter( + or_( + Exam.visibility == 'public', + Exam.creator_id == user.get('id') + ) + ) + if subject_filter: query = query.filter_by(subject=subject_filter) if search_query: @@ -1059,6 +1069,23 @@ def apply_contest(): flash('满分分数必须大于0') return redirect(url_for('apply_contest')) + # 处理图片上传 + image_url = None + if 'image' in request.files: + file = request.files['image'] + if file and file.filename: + if allowed_file(file.filename): + filename = secure_filename(file.filename) + # 生成唯一文件名 + ext = filename.rsplit('.', 1)[1].lower() + unique_filename = f"contest_{user_id}_{int(time.time())}.{ext}" + filepath = os.path.join(UPLOAD_FOLDER, unique_filename) + file.save(filepath) + image_url = f'/static/uploads/{unique_filename}' + else: + flash('不支持的图片格式,请上传 JPG、PNG 或 GIF 格式的图片') + return redirect(url_for('apply_contest')) + # 如果存在申请,更新它;否则创建新申请 if existing: # 更新现有申请 @@ -1073,6 +1100,8 @@ def apply_contest(): existing.responsible_phone = responsible_phone existing.responsible_email = responsible_email existing.organization = organization + if image_url: + existing.image = image_url existing.status = 'pending' existing.applied_at = datetime.utcnow() existing.reviewed_at = None @@ -1092,7 +1121,8 @@ def apply_contest(): responsible_person=responsible_person, responsible_phone=responsible_phone, responsible_email=responsible_email, - organization=organization + organization=organization, + image=image_url ) db.session.add(app) flash_msg = '申请已提交,请等待管理员审核' @@ -2501,20 +2531,26 @@ def api_create_exam(): return jsonify({'success': False, 'message': '请先登录'}), 401 data = request.get_json(force=True, silent=True) contest_id = data.get('contest_id') + visibility = data.get('visibility', 'private') # 默认私有 - # 杯赛考试:只有杯赛负责人或系统管理员可以创建 + # 杯赛考试:只有杯赛负责人、老师或系统管理员可以创建 if contest_id: contest = Contest.query.get(contest_id) if not contest or contest.status == 'abolished': return jsonify({'success': False, 'message': '杯赛不存在或已废止'}), 400 membership = ContestMembership.query.filter_by( - user_id=user['id'], contest_id=contest_id, role='owner').first() + user_id=user['id'], contest_id=contest_id).first() + # 检查是否是负责人、老师或管理员 if not membership and user.get('role') != 'admin': - return jsonify({'success': False, 'message': '只有杯赛负责人才能组织考试'}), 403 + return jsonify({'success': False, 'message': '只有杯赛成员才能创建杯赛考试'}), 403 + # 杯赛考试默认为公开(杯赛内可见) + if visibility == 'private': + visibility = 'public' else: - # 普通考试:需要 teacher 或 admin 角色 - if user.get('role') not in ('teacher', 'admin'): - return jsonify({'success': False, 'message': '无权限'}), 403 + # 普通考试:所有登录用户都可以创建私有考试 + # 只有 teacher 或 admin 可以创建公开考试 + if visibility == 'public' and user.get('role') not in ('teacher', 'admin'): + return jsonify({'success': False, 'message': '只有教师和管理员可以创建公开考试'}), 403 title = data.get('title', '') subject = data.get('subject', '') @@ -2532,7 +2568,8 @@ def api_create_exam(): duration=duration, total_score=total_score, creator_id=user.get('id'), - contest_id=contest_id + contest_id=contest_id, + visibility=visibility ) # 解析预定时间 if scheduled_start: @@ -2708,6 +2745,30 @@ def api_update_exam_status(exam_id): db.session.commit() return jsonify({'success': True, 'message': f'试卷已{"发布" if new_status == "available" else "关闭"}'}) +@app.route('/api/exams//visibility', methods=['POST']) +@login_required +def api_update_exam_visibility(exam_id): + user = session.get('user') + exam = Exam.query.get(exam_id) + if not exam: + return jsonify({'success': False, 'message': '试卷不存在'}), 404 + if exam.creator_id != user.get('id'): + return jsonify({'success': False, 'message': '只能修改自己创建的试卷'}), 403 + + data = request.get_json(force=True, silent=True) + new_visibility = data.get('visibility', '') + + if new_visibility not in ('private', 'public'): + return jsonify({'success': False, 'message': '无效的可见性设置'}), 400 + + # 检查权限:只有教师和管理员可以设置为公开 + if new_visibility == 'public' and user.get('role') not in ('teacher', 'admin'): + return jsonify({'success': False, 'message': '只有教师和管理员可以创建公开考试'}), 403 + + exam.visibility = new_visibility + db.session.commit() + return jsonify({'success': True, 'message': f'试卷已设置为{"公开" if new_visibility == "public" else "私有"}'}) + @app.route('/api/exams/', methods=['DELETE']) def api_delete_exam(exam_id): user = session.get('user') @@ -3229,12 +3290,20 @@ def api_search_posts(): uid = user.get('id') if user else None data = [] for p in posts: + # 计算作者等级 + author_post_count = Post.query.filter_by(author_id=p.author_id).count() + author_reply_count = Reply.query.filter_by(author_id=p.author_id).count() + author_likes = db.session.query(db.func.sum(Post.likes)).filter_by(author_id=p.author_id).scalar() or 0 + author_points = author_post_count * 10 + author_reply_count * 3 + author_likes * 2 + author_level = calc_level(author_points) + p_dict = { 'id': p.id, 'title': p.title, 'content': p.content[:200] + ('...' if len(p.content) > 200 else ''), 'author': p.author.name, 'author_id': p.author_id, + 'author_level': author_level, 'tag': p.tag, 'is_official': p.is_official, 'pinned': p.pinned, diff --git a/create_admin.py b/create_admin.py new file mode 100644 index 0000000..acf42a7 --- /dev/null +++ b/create_admin.py @@ -0,0 +1,45 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +"""创建管理员账号脚本""" + +from app import app, db +from models import User + +def create_admin(): + with app.app_context(): + # 检查邮箱是否已存在 + existing_user = User.query.filter_by(email='1512344589@qq.com').first() + if existing_user: + print(f'邮箱 1512344589@qq.com 已存在') + print(f'用户名: {existing_user.name}') + print(f'角色: {existing_user.role}') + + # 更新为管理员 + if existing_user.role != 'admin': + existing_user.role = 'admin' + existing_user.password = '1' + db.session.commit() + print('已将该用户更新为管理员') + else: + print('该用户已经是管理员') + return + + # 创建新管理员 + admin = User( + name='管理员', + email='1512344589@qq.com', + password='1', + role='admin' + ) + + db.session.add(admin) + db.session.commit() + + print('管理员账号创建成功!') + print(f'邮箱: {admin.email}') + print(f'密码: 1') + print(f'角色: {admin.role}') + print(f'用户ID: {admin.id}') + +if __name__ == '__main__': + create_admin() diff --git a/database.db b/database.db new file mode 100644 index 0000000..e69de29 diff --git a/fix_question_ids.py b/fix_question_ids.py new file mode 100644 index 0000000..6591a37 --- /dev/null +++ b/fix_question_ids.py @@ -0,0 +1,66 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" +修复考试题目ID不连续的问题 +重新为所有考试的题目分配连续的ID +""" + +from app import app, db +from models import Exam +import json + +def fix_question_ids(): + """修复所有考试的题目ID,使其连续""" + with app.app_context(): + exams = Exam.query.all() + fixed_count = 0 + + for exam in exams: + try: + # 获取题目列表 + if exam.is_encrypted and exam.encrypted_questions: + from app import decrypt_questions + questions = json.loads(decrypt_questions(exam.encrypted_questions)) + else: + questions = exam.get_questions() + + if not questions: + continue + + # 检查ID是否连续 + needs_fix = False + for idx, q in enumerate(questions, 1): + if q.get('id') != idx: + needs_fix = True + break + + if needs_fix: + # 重新分配连续的ID + for idx, q in enumerate(questions, 1): + q['id'] = idx + + # 保存修复后的题目 + questions_json = json.dumps(questions) + exam.set_questions(questions) + + if exam.is_encrypted: + from app import encrypt_questions + exam.encrypted_questions = encrypt_questions(questions_json) + + fixed_count += 1 + print(f"修复考试 #{exam.id}: {exam.title}") + + except Exception as e: + print(f"处理考试 #{exam.id} 时出错: {e}") + continue + + if fixed_count > 0: + db.session.commit() + print(f"\n总共修复了 {fixed_count} 份考试") + else: + print("\n没有需要修复的考试") + +if __name__ == '__main__': + print("开始修复考试题目ID...") + fix_question_ids() + print("修复完成!") diff --git a/instance/database.db b/instance/database.db index ccb214de818c6d9ca5bb35393209e37a4d2dd62f..3b658c2a09a63a44ab921e25806c4c74a87cf3c0 100644 GIT binary patch delta 639 zcmZutO-mGE0G@HJT{manE!Y;ZbVEa<)c5;i4(XzOfd!gHd5~t_nb9HL)Ljh;Y6~f( zi?)ZN5(@Gf$O=LT{Q(c1?c&At&>=fUm)4*N>VfC@d=|Ik#qIcFY1AA)ORNGdU|fMU=u93w55$&$A>QjS2c2a@Akqgwl$Xp*{!d#Cls^vJAL@FeN)*3UPyDZ=V!WuHFs6$+rFy*=uaTTy2KwM%? zi1r94Li^06SPRcZf(62Zf;iqpn8OhB4oEs6&Ol{jWFy(h6=xEaMw;%K^QEn(;_!`F zy>XJXfW}{=E6?9lUVo@87b^=-$7Y<|XnrQ==SF)FC;Q705r`@z6`?3!nDP8v;ZF$3 zKxQ*(L*B_TqXL)KnlFRu^|fHd1V6wISOJ^mWM%{Or6a`rk9({q73nhnxjR}LG0cN; zuvvB5Fu)hE0rtS>auR+7Cjep!QCO`fIf%5Fe-(`F%cAAI?xUZMbsLGr#AxDd&8q`x ztLf19_*UIOU48Uf^perNlsz7wziCC&sg${pTK{^)Ts7Ng!+hw5<6&Vgb3HrQdoerM sj^U|H-?{VIYyCr+w#n&m)+zXHrEihB^)PAPuX)mtGS?eYHM=i=0;2Y?T>t<8 delta 435 zcmZp8z}4`8Yl5`kOa=x9Z6JmLlZiUUj59YTEaB&2;)`S8i{szI7snU3SwJ9}cd~)N zEOUQG9tKri21#jNP7X(gXIr~pte^UP%8F+l+e=c5OB_-w5_1c3QuUJaa}AA*O-#*- zOG;Bx^Gc>qsACjmH88a>HJThJSIu~D@-4ZBC=*i?pv8-Y^_6ACc{w;74S~SO)X><_ z45-ziun=sFvALm%rJ;qTu}My*fu(_&k!fN|Zf2g5fsvW6fsw9}v4WwIm9e3fv5B6s zk&&sHA<)Qx$y?-o`T5T -

杯赛管理

+

杯赛管理

-
- - - - - - - - - - - - - -
ID名称主办方状态开始日期操作
- +
+
+ + + + + + + + + + + + + +
ID名称主办方状态开始日期操作
+
+
{% endblock %} @@ -29,11 +31,11 @@ {% block scripts %} {% endblock %} + diff --git a/templates/admin_exams.html b/templates/admin_exams.html index 3fe7974..330c904 100644 --- a/templates/admin_exams.html +++ b/templates/admin_exams.html @@ -4,41 +4,43 @@ {% block admin_content %}
-

考试管理

+

考试管理

-
- - - - - - - - - - - - - -
ID标题科目出题人状态创建时间操作
- {% endblock %} {% block scripts %} - + + +
+ +
{% block navbar %} -