From fc5456540df0e140f42e6d9458cfce5b3d44eea2 Mon Sep 17 00:00:00 2001 From: Ba <1072906427@qq.com> Date: Sat, 22 Nov 2025 20:20:57 +0800 Subject: [PATCH] =?UTF-8?q?=E4=B8=8A=E4=BC=A0=E9=A1=B9=E7=9B=AE=EF=BC=9Afr?= =?UTF-8?q?ont=E5=89=8D=E7=AB=AF=20+=20rollcall=5Fbackend=E5=90=8E?= =?UTF-8?q?=E7=AB=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- front/package-lock.json | 7 + front/package.json | 1 + front/src/App.vue | 117 ++--- front/src/components/Leaderboard.vue | 56 +-- front/src/components/RollCall.vue | 135 ++---- front/src/components/StudentManager.vue | 96 ++-- front/src/services/apiService.js | 114 +++-- rollcall_backend/app.py | 432 ++++++++++++++++++ .../exports/积分详单_20251122002433.xlsx | Bin 0 -> 9673 bytes .../exports/积分详单_20251122111015.xlsx | Bin 0 -> 9671 bytes .../exports/积分详单_20251122113527.xlsx | Bin 0 -> 9709 bytes rollcall_backend/requirements.txt | Bin 0 -> 858 bytes rollcall_backend/学生名单.xlsx | Bin 0 -> 14583 bytes 13 files changed, 629 insertions(+), 329 deletions(-) create mode 100644 rollcall_backend/app.py create mode 100644 rollcall_backend/exports/积分详单_20251122002433.xlsx create mode 100644 rollcall_backend/exports/积分详单_20251122111015.xlsx create mode 100644 rollcall_backend/exports/积分详单_20251122113527.xlsx create mode 100644 rollcall_backend/requirements.txt create mode 100644 rollcall_backend/学生名单.xlsx diff --git a/front/package-lock.json b/front/package-lock.json index 732b9a4..8c24cc8 100644 --- a/front/package-lock.json +++ b/front/package-lock.json @@ -11,6 +11,7 @@ "axios": "^1.13.2", "echarts": "^6.0.0", "element-plus": "^2.11.8", + "file-saver": "^2.0.5", "vue": "^3.5.24", "vue-echarts": "^8.0.1", "xlsx": "^0.18.5" @@ -1519,6 +1520,12 @@ } } }, + "node_modules/file-saver": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/file-saver/-/file-saver-2.0.5.tgz", + "integrity": "sha512-P9bmyZ3h/PRG+Nzga+rbdI4OEpNDzAVyy74uVO9ATgzLK6VtAsYybF/+TOCvrc0MO793d6+42lLyZTw7/ArVzA==", + "license": "MIT" + }, "node_modules/follow-redirects": { "version": "1.15.11", "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", diff --git a/front/package.json b/front/package.json index 291251c..57a5608 100644 --- a/front/package.json +++ b/front/package.json @@ -12,6 +12,7 @@ "axios": "^1.13.2", "echarts": "^6.0.0", "element-plus": "^2.11.8", + "file-saver": "^2.0.5", "vue": "^3.5.24", "vue-echarts": "^8.0.1", "xlsx": "^0.18.5" diff --git a/front/src/App.vue b/front/src/App.vue index 8cca2bb..af6b2a2 100644 --- a/front/src/App.vue +++ b/front/src/App.vue @@ -1,105 +1,44 @@ - - - - \ No newline at end of file +onMounted(fetchAllData); + \ No newline at end of file diff --git a/front/src/components/Leaderboard.vue b/front/src/components/Leaderboard.vue index fbb7383..10f552e 100644 --- a/front/src/components/Leaderboard.vue +++ b/front/src/components/Leaderboard.vue @@ -1,6 +1,7 @@ + @@ -9,69 +10,42 @@ import { use } from 'echarts/core'; import { CanvasRenderer } from 'echarts/renderers'; import { BarChart } from 'echarts/charts'; -import { - TitleComponent, - TooltipComponent, - GridComponent, -} from 'echarts/components'; +import { TitleComponent, TooltipComponent, GridComponent } from 'echarts/components'; import VChart from "vue-echarts"; import { computed } from 'vue'; -use([ - CanvasRenderer, - BarChart, - TitleComponent, - TooltipComponent, - GridComponent, -]); +use([CanvasRenderer, BarChart, TitleComponent, TooltipComponent, GridComponent]); const props = defineProps({ - students: { - type: Array, - required: true - } + topStudents: { type: Array, required: true } }); const chartOption = computed(() => { - const sortedStudents = [...props.students].sort((a, b) => b.points - a.points); - const topStudents = sortedStudents.slice(0, 10); - + const studentData = props.topStudents; return { - tooltip: { - trigger: 'axis', - axisPointer: { type: 'shadow' } - }, - grid: { - left: '3%', right: '4%', bottom: '3%', - containLabel: true - }, + tooltip: { trigger: 'axis', axisPointer: { type: 'shadow' } }, + grid: { left: '3%', right: '4%', bottom: '3%', containLabel: true }, xAxis: { type: 'value', boundaryGap: [0, 0.01], - // 【新增点】调整坐标轴样式以适应深色背景 axisLabel: { color: '#fff' }, splitLine: { lineStyle: { color: 'rgba(255, 255, 255, 0.2)' } } }, yAxis: { type: 'category', - data: topStudents.map(s => s.name).reverse(), - axisLabel: { color: '#fff' } // 调整坐标轴样式 + data: studentData.map(s => s.name).reverse(), + axisLabel: { color: '#fff' } }, series: [ { name: '积分', type: 'bar', - data: topStudents.map(s => s.points).reverse(), - // 【修改点】为柱状图添加漂亮的渐变色 + data: studentData.map(s => s.points).reverse(), itemStyle: { borderRadius: [0, 5, 5, 0], color: { - type: 'linear', - x: 0, y: 0, x2: 1, y2: 0, - colorStops: [ - { offset: 0, color: '#23d5ab' }, - { offset: 1, color: '#23a6d5' } - ] + type: 'linear', x: 0, y: 0, x2: 1, y2: 0, + colorStops: [{ offset: 0, color: '#23d5ab' }, { offset: 1, color: '#23a6d5' }] } } } @@ -81,7 +55,5 @@ const chartOption = computed(() => { \ No newline at end of file diff --git a/front/src/components/RollCall.vue b/front/src/components/RollCall.vue index 4253378..ef85456 100644 --- a/front/src/components/RollCall.vue +++ b/front/src/components/RollCall.vue @@ -1,9 +1,10 @@ + - \ No newline at end of file diff --git a/front/src/components/StudentManager.vue b/front/src/components/StudentManager.vue index c270623..f146e0c 100644 --- a/front/src/components/StudentManager.vue +++ b/front/src/components/StudentManager.vue @@ -1,72 +1,68 @@ + \ No newline at end of file diff --git a/front/src/services/apiService.js b/front/src/services/apiService.js index d807429..5aae4e3 100644 --- a/front/src/services/apiService.js +++ b/front/src/services/apiService.js @@ -1,60 +1,74 @@ import axios from 'axios'; +import { ElMessage } from 'element-plus'; +import { saveAs } from 'file-saver'; -// --- MOCK DATA --- -// This data simulates what our backend database would store. -let mockStudents = [ - { id: '2024001', name: '张三', major: '软件工程', points: 5, callCount: 2 }, - { id: '2024002', name: '李四', major: '软件工程', points: 8, callCount: 3 }, - { id: '2024003', name: '王五', major: '计算机科学', points: 2, callCount: 1 }, - { id: '2024004', name: '赵六', major: '网络工程', points: 10, callCount: 4 }, -]; - -/** - * Simulates network delay. - * @param {number} ms - Milliseconds to wait. - */ -const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms)); +// --- 辅助函数:用于命名风格转换(后端snake_case -> 前端camelCase)--- +const toCamel = (s) => s.replace(/([-_][a-z])/ig, ($1) => $1.toUpperCase().replace('_', '')); +const isObject = (o) => o === Object(o) && !Array.isArray(o) && typeof o !== 'function'; +const keysToCamel = (o) => { + if (isObject(o)) { + const n = {}; + Object.keys(o).forEach((k) => { n[toCamel(k)] = keysToCamel(o[k]); }); + return n; + } else if (Array.isArray(o)) { + return o.map((i) => keysToCamel(i)); + } + return o; +}; -// --- MOCK API FUNCTIONS --- -// These functions mimic the behavior of real API calls. +// --- Axios 实例配置 --- +const apiClient = axios.create({ + baseURL: 'http://127.0.0.1:5000/api', + headers: { 'Content-Type': 'application/json' }, +}); -export const studentApi = { - /** - * Fetches the list of all students. - * In the future, this will be: return axios.get('/api/students'); - */ - getStudents: async () => { - console.log("API: Fetching all students..."); - await sleep(500); // Simulate network latency - return { data: [...mockStudents] }; // Return a copy - }, - - /** - * Updates a specific student's data. - * @param {object} studentData - The student object with updated info. - * In the future, this will be: return axios.put(`/api/students/${studentData.id}`, studentData); - */ - updateStudent: async (studentData) => { - console.log(`API: Updating student ${studentData.id}...`, studentData); - await sleep(300); - const index = mockStudents.findIndex(s => s.id === studentData.id); - if (index !== -1) { - mockStudents[index] = studentData; - return { data: { ...studentData } }; // Return a copy +// --- 响应拦截器 --- +apiClient.interceptors.response.use( + (response) => { + if (response.data.code === 200) { + return keysToCamel(response.data.data); } else { - throw new Error("Student not found!"); + ElMessage.error(response.data.msg || '操作失败'); + return Promise.reject(response.data); } }, + (error) => { + const msg = error.response?.data?.msg || '网络请求失败,请检查服务器'; + ElMessage.error(msg); + return Promise.reject(error); + } +); - /** - * Imports a new list of students, replacing the old one. - * @param {Array} newStudents - Array of new student objects. - * In the future, this will be: return axios.post('/api/students/import', newStudents); - */ - importStudents: async (newStudents) => { - console.log("API: Importing new students..."); - await sleep(400); - mockStudents = newStudents; - return { data: [...mockStudents] }; +export const api = { + getStudents: () => apiClient.get('/student/rank'), + getTopNStudents: (n) => apiClient.get(`/student/rank/top/${n}`), + randomRollCall: () => apiClient.get('/rollcall/random'), + sequentialRollCall: () => apiClient.get('/rollcall/order'), + importStudents: (file) => { + const formData = new FormData(); + formData.append('file', file); + return apiClient.post('/student/import', formData, { headers: { 'Content-Type': 'multipart/form-data' } }); + }, + updateStudentPoints: (studentId, payload) => { + const backendPayload = { + student_id: studentId, + is_answer: payload.isAnswer, + repeat_question: payload.repeatQuestion, + answer_correct_level: payload.answerCorrectLevel + }; + return apiClient.post('/student/update-points', backendPayload); + }, + exportStudentDetails: async () => { + try { + const response = await axios({ + url: 'http://127.0.0.1:5000/api/student/export', + method: 'GET', + responseType: 'blob', + }); + return response.data; + } catch (error) { + ElMessage.error('导出失败'); + return Promise.reject(error); + } } }; \ No newline at end of file diff --git a/rollcall_backend/app.py b/rollcall_backend/app.py new file mode 100644 index 0000000..f5e3cf1 --- /dev/null +++ b/rollcall_backend/app.py @@ -0,0 +1,432 @@ +from flask import Flask, request, jsonify, send_file +from flask_sqlalchemy import SQLAlchemy +import pandas as pd +import random +from datetime import datetime +import os +from sqlalchemy import desc +import math +from flask_cors import CORS + +# ---------------------- 1. 应用初始化(核心修改:适配db4free在线MySQL) ---------------------- +app = Flask(__name__) +CORS(app) + +# ---------------------- 关键配置:替换为你的db4free账号信息 ---------------------- +DB_USER = 'bagood' +DB_PASSWORD = 'czb098221' +DB_NAME = 'rollcall_db' +DB_HOST = 'db4free.net' +DB_PORT = 3306 +# 配置db4free在线MySQL连接(核心修改部分) +app.config['SQLALCHEMY_DATABASE_URI'] = f'mysql+pymysql://{DB_USER}:{DB_PASSWORD}@{DB_HOST}:{DB_PORT}/{DB_NAME}' +app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False + +# db4free专属适配配置(解决连接超时/断开问题) +app.config['SQLALCHEMY_ENGINE_OPTIONS'] = { + 'pool_size': 10, # 连接池大小(适配db4free并发限制) + 'pool_recycle': 180, # 3分钟回收连接(db4free空闲连接10分钟自动断开) + 'pool_pre_ping': True, # 执行查询前检测连接,无效则自动重连 + 'connect_args': { + 'charset': 'utf8mb4', # 支持中文和特殊字符 + 'connect_timeout': 10, # 连接超时时间(10秒) + 'read_timeout': 15, # 读取超时时间(15秒) + 'write_timeout': 15 # 写入超时时间(15秒) + } +} + +db = SQLAlchemy(app) + +# 临时文件存储路径(用于Excel导出) +EXPORT_DIR = 'exports' +os.makedirs(EXPORT_DIR, exist_ok=True) + +# ---------------------- 2. 数据库模型设计(无修改,兼容MySQL) ---------------------- +class Student(db.Model): + """学生表:存储学生基础信息与积分""" + __tablename__ = 'students' + id = db.Column(db.Integer, primary_key=True, autoincrement=True) + student_id = db.Column(db.String(20), unique=True, nullable=False, comment='学号') + name = db.Column(db.String(20), nullable=False, comment='姓名') + major = db.Column(db.String(50), nullable=False, comment='专业') + points = db.Column(db.Float, default=0.0, comment='总积分') + call_count = db.Column(db.Integer, default=0, comment='被点名次数') + create_time = db.Column(db.DateTime, default=datetime.now, comment='创建时间') + +class RollCallRecord(db.Model): + """点名记录表:存储每次点名详情""" + __tablename__ = 'roll_call_records' + id = db.Column(db.Integer, primary_key=True, autoincrement=True) + student_id = db.Column(db.String(20), db.ForeignKey('students.student_id'), nullable=False, comment='学号') + call_mode = db.Column(db.String(10), nullable=False, comment='点名模式:random/order') + call_time = db.Column(db.DateTime, default=datetime.now, comment='点名时间') + is_answer = db.Column(db.Boolean, default=False, comment='是否回答问题') + repeat_question = db.Column(db.Boolean, nullable=True, comment='是否准确重复问题') + answer_correct = db.Column(db.Boolean, nullable=True, comment='回答是否正确') + point_change = db.Column(db.Float, default=0.0, comment='本次积分变化') + +class PointRule(db.Model): + """积分规则表:可灵活配置积分规则(避免硬编码)""" + __tablename__ = 'point_rules' + id = db.Column(db.Integer, primary_key=True, autoincrement=True) + rule_type = db.Column(db.String(30), unique=True, nullable=False, comment='规则类型') + point_value = db.Column(db.Float, nullable=False, comment='对应分值') + description = db.Column(db.String(100), comment='规则描述') + +# ---------------------- 3. 初始化数据库表与默认规则(无修改,兼容MySQL) ---------------------- +def init_db(): + with app.app_context(): + db.create_all() # 自动创建所有表(首次运行时执行) + # 插入默认积分规则(若不存在) + default_rules = [ + ('attend_call', 1.0, '到课被点到加分'), + ('repeat_correct', 0.5, '准确重复问题加分'), + ('repeat_incorrect', -1.0, '未准确重复问题扣分'), + ('answer_correct_low', 0.5, '回答正确(基础分)'), + ('answer_correct_mid', 1.5, '回答正确(中等分)'), + ('answer_correct_high', 3.0, '回答正确(高分)') + ] + for rule_type, point_value, desc in default_rules: + if not PointRule.query.filter_by(rule_type=rule_type).first(): + new_rule = PointRule(rule_type=rule_type, point_value=point_value, description=desc) + db.session.add(new_rule) + db.session.commit() + print("数据库初始化完成!数据已存储到db4free在线MySQL中") + +# ---------------------- 4. 工具函数(无修改,逻辑不变) ---------------------- +def get_point_rule(rule_type): + """获取指定类型的积分规则分值""" + rule = PointRule.query.filter_by(rule_type=rule_type).first() + return rule.point_value if rule else 0.0 + +def calculate_random_weight(students): + """计算随机点名权重:积分越高,权重越低(权重=1/(积分+1))""" + total_weight = sum(1/(student.points + 1) for student in students) + weights = [(1/(s.points + 1))/total_weight for s in students] + return weights + +# ---------------------- 5. 核心接口(无修改,所有功能保持不变) ---------------------- +@app.route('/api/student/import', methods=['POST']) +def import_students(): + """导入学生信息(Excel文件)- 支持.xlsx格式,含格式校验""" + if 'file' not in request.files: + return jsonify({'code': 400, 'msg': '未上传文件'}), 400 + + file = request.files['file'] + try: + if not file.filename.endswith('.xlsx'): + return jsonify({'code': 400, 'msg': '仅支持.xlsx格式的Excel文件,请将文件另存为.xlsx后上传'}), 400 + + df = pd.read_excel(file, engine='openpyxl', dtype=str) + df = df.dropna(how='all') + + required_cols = ['学号', '姓名', '专业'] + if not all(col in df.columns for col in required_cols): + missing_cols = [col for col in required_cols if col not in df.columns] + return jsonify({'code': 400, 'msg': f'Excel缺少必填列:{", ".join(missing_cols)}(表头必须严格为「学号、姓名、专业」)'}), 400 + + success_count = 0 + fail_count = 0 + fail_msg = [] + + existing_student_ids = [s.student_id for s in Student.query.with_entities(Student.student_id).all()] + + for idx, row in df.iterrows(): + student_id = str(row['学号']).strip() if pd.notna(row['学号']) else '' + name = str(row['姓名']).strip() if pd.notna(row['姓名']) else '' + major = str(row['专业']).strip() if pd.notna(row['专业']) else '' + + if not student_id or not name or not major: + fail_count += 1 + fail_msg.append(f'第{idx+1}行:存在空数据,跳过') + continue + + if not student_id.isdigit() or len(student_id) != 9: + fail_count += 1 + fail_msg.append(f'第{idx+1}行:学号{student_id}格式错误(需9位数字),跳过') + continue + + if student_id in existing_student_ids: + fail_count += 1 + fail_msg.append(f'第{idx+1}行:学号{student_id}已存在,跳过') + continue + + new_student = Student(student_id=student_id, name=name, major=major) + db.session.add(new_student) + success_count += 1 + existing_student_ids.append(student_id) + + db.session.commit() + + return jsonify({ + 'code': 200, + 'msg': f'导入完成!成功导入{success_count}条,失败{fail_count}条', + 'data': { + 'success_count': success_count, + 'fail_count': fail_count, + 'fail_msg': fail_msg + } + }), 200 + + except Exception as e: + db.session.rollback() + error_msg = str(e) + # db4free专属错误提示 + if 'Lost connection' in error_msg or '10060' in error_msg or '10061' in error_msg: + return jsonify({'code': 500, 'msg': 'db4free连接超时/失败!建议:1. 检查账号密码/数据库名是否正确;2. 1分钟后重试;3. 检查网络是否稳定'}), 500 + return jsonify({'code': 500, 'msg': f'导入失败:{error_msg}'}), 500 + +@app.route('/api/rollcall/random', methods=['GET']) +def random_rollcall(): + """随机点名(按积分权重)""" + try: + students = Student.query.all() + if not students: + return jsonify({'code': 400, 'msg': '暂无学生信息,请先导入'}), 400 + + weights = calculate_random_weight(students) + selected_student = random.choices(students, weights=weights, k=1)[0] + + new_record = RollCallRecord( + student_id=selected_student.student_id, + call_mode='random', + is_answer=False, + point_change=get_point_rule('attend_call') + ) + + selected_student.call_count += 1 + selected_student.points = round(selected_student.points + new_record.point_change, 1) + + db.session.add(new_record) + db.session.commit() + + return jsonify({ + 'code': 200, + 'msg': '随机点名成功', + 'data': { + 'student_id': selected_student.student_id, + 'name': selected_student.name, + 'major': selected_student.major, + 'current_points': selected_student.points, + 'call_count': selected_student.call_count, + 'call_time': new_record.call_time.strftime('%Y-%m-%d %H:%M:%S') + } + }), 200 + + except Exception as e: + db.session.rollback() + error_msg = str(e) + if 'Lost connection' in error_msg: + return jsonify({'code': 500, 'msg': 'db4free连接超时,请1分钟后重试'}), 500 + return jsonify({'code': 500, 'msg': f'点名失败:{error_msg}'}), 500 + +@app.route('/api/rollcall/order', methods=['GET']) +def order_rollcall(): + """顺序点名(按学号升序循环)""" + try: + last_record = RollCallRecord.query.filter_by(call_mode='order').order_by(desc('call_time')).first() + + students = Student.query.order_by(Student.student_id).all() + if not students: + return jsonify({'code': 400, 'msg': '暂无学生信息,请先导入'}), 400 + + if not last_record: + selected_student = students[0] + else: + last_index = next((i for i, s in enumerate(students) if s.student_id == last_record.student_id), -1) + selected_student = students[(last_index + 1) % len(students)] + + new_record = RollCallRecord( + student_id=selected_student.student_id, + call_mode='order', + is_answer=False, + point_change=get_point_rule('attend_call') + ) + + selected_student.call_count += 1 + selected_student.points = round(selected_student.points + new_record.point_change, 1) + + db.session.add(new_record) + db.session.commit() + + return jsonify({ + 'code': 200, + 'msg': '顺序点名成功', + 'data': { + 'student_id': selected_student.student_id, + 'name': selected_student.name, + 'major': selected_student.major, + 'current_points': selected_student.points, + 'call_count': selected_student.call_count, + 'call_time': new_record.call_time.strftime('%Y-%m-%d %H:%M:%S') + } + }), 200 + + except Exception as e: + db.session.rollback() + error_msg = str(e) + if 'Lost connection' in error_msg: + return jsonify({'code': 500, 'msg': 'db4free连接超时,请1分钟后重试'}), 500 + return jsonify({'code': 500, 'msg': f'点名失败:{error_msg}'}), 500 + +@app.route('/api/student/update-points', methods=['POST']) +def update_student_points(): + """更新学生积分(回答问题后)""" + data = request.json + required_fields = ['student_id', 'is_answer', 'repeat_question', 'answer_correct_level'] + if not all(field in data for field in required_fields): + return jsonify({'code': 400, 'msg': '缺少必填参数(student_id/is_answer/repeat_question/answer_correct_level)'}), 400 + + try: + student = Student.query.filter_by(student_id=data['student_id']).first() + if not student: + return jsonify({'code': 404, 'msg': '学生不存在'}), 404 + + last_record = RollCallRecord.query.filter_by( + student_id=data['student_id'], + is_answer=False + ).order_by(desc('call_time')).first() + + if not last_record: + return jsonify({'code': 400, 'msg': '无未完成的点名记录(需先点名再更新积分)'}), 400 + + point_change = last_record.point_change + if data['is_answer']: + if data['repeat_question']: + point_change += get_point_rule('repeat_correct') + else: + point_change += get_point_rule('repeat_incorrect') + + level_map = { + 'low': 'answer_correct_low', + 'mid': 'answer_correct_mid', + 'high': 'answer_correct_high' + } + rule_type = level_map.get(data['answer_correct_level'], 'answer_correct_low') + point_change += get_point_rule(rule_type) + + points_to_add = point_change - last_record.point_change + student.points = round(student.points + points_to_add, 1) + + last_record.is_answer = data['is_answer'] + last_record.repeat_question = data['repeat_question'] + last_record.answer_correct = (data['answer_correct_level'] != 'none') if data['is_answer'] else None + last_record.point_change = point_change + + db.session.commit() + + return jsonify({ + 'code': 200, + 'msg': '积分更新成功', + 'data': { + 'student_id': student.student_id, + 'name': student.name, + 'current_points': student.points, + 'total_point_change': round(point_change, 1) + } + }), 200 + + except Exception as e: + db.session.rollback() + error_msg = str(e) + if 'Lost connection' in error_msg: + return jsonify({'code': 500, 'msg': 'db4free连接超时,请1分钟后重试'}), 500 + return jsonify({'code': 500, 'msg': f'积分更新失败:{error_msg}'}), 500 + +@app.route('/api/student/rank', methods=['GET']) +def get_student_rank(): + """获取学生积分排名(全量)""" + try: + students = Student.query.order_by(desc(Student.points)).all() + rank_list = [ + { + 'rank': i+1, + 'student_id': s.student_id, + 'name': s.name, + 'major': s.major, + 'points': round(s.points, 1), + 'call_count': s.call_count + } for i, s in enumerate(students) + ] + return jsonify({ + 'code': 200, + 'msg': '排名查询成功', + 'data': rank_list + }), 200 + except Exception as e: + error_msg = str(e) + if 'Lost connection' in error_msg: + return jsonify({'code': 500, 'msg': 'db4free连接超时,请1分钟后重试'}), 500 + return jsonify({'code': 500, 'msg': f'排名查询失败:{error_msg}'}), 500 + +@app.route('/api/student/rank/top/', methods=['GET']) +def get_top_student_rank(top_n): + """获取积分最高的top_n名学生(前端可视化专用)""" + try: + top_students = Student.query.order_by(desc(Student.points)).limit(top_n).all() + + if not top_students: + return jsonify({'code': 400, 'msg': '暂无学生信息,请先导入'}), 400 + + rank_list = [] + for idx, student in enumerate(top_students): + random_call_count = RollCallRecord.query.filter_by( + student_id=student.student_id, + call_mode='random' + ).count() + + rank_list.append({ + 'rank': idx + 1, + 'student_id': student.student_id, + 'name': student.name, + 'major': student.major, + 'points': round(student.points, 1), + 'call_count': student.call_count, + 'random_call_count': random_call_count + }) + + return jsonify({ + 'code': 200, + 'msg': f'获取Top{top_n}积分排名成功', + 'data': rank_list + }), 200 + + except Exception as e: + error_msg = str(e) + if 'Lost connection' in error_msg: + return jsonify({'code': 500, 'msg': 'db4free连接超时,请1分钟后重试'}), 500 + return jsonify({'code': 500, 'msg': f'Top{top_n}排名查询失败:{error_msg}'}), 500 + +@app.route('/api/student/export', methods=['GET']) +def export_student_points(): + """导出积分详单(Excel)""" + try: + students = Student.query.order_by(desc(Student.points)).all() + export_data = [ + { + '学号': s.student_id, + '姓名': s.name, + '专业': s.major, + '随机点名次数': len(RollCallRecord.query.filter_by(student_id=s.student_id, call_mode='random').all()), + '总点名次数': s.call_count, + '总积分': round(s.points, 1) + } for s in students + ] + + df = pd.DataFrame(export_data) + export_filename = f'积分详单_{datetime.now().strftime("%Y%m%d%H%M%S")}.xlsx' + export_path = os.path.join(EXPORT_DIR, export_filename) + df.to_excel(export_path, index=False, engine='openpyxl') + + return send_file(export_path, as_attachment=True, download_name=export_filename) + + except Exception as e: + error_msg = str(e) + if 'Lost connection' in error_msg: + return jsonify({'code': 500, 'msg': 'db4free连接超时,请1分钟后重试'}), 500 + return jsonify({'code': 500, 'msg': f'导出失败:{error_msg}'}), 500 + +# ---------------------- 6. 启动入口(无修改) ---------------------- +if __name__ == '__main__': + init_db() # 首次运行自动创建表和默认规则(db4free云端) + app.run(host='0.0.0.0', port=5000, debug=True) \ No newline at end of file diff --git a/rollcall_backend/exports/积分详单_20251122002433.xlsx b/rollcall_backend/exports/积分详单_20251122002433.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..f420dd701f81e3f1ceb2126a20e113f536743aec GIT binary patch literal 9673 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 literal 0 HcmV?d00001 diff --git a/rollcall_backend/exports/积分详单_20251122111015.xlsx b/rollcall_backend/exports/积分详单_20251122111015.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..b3b24b26a9cefef528087d1b1de98d84215c8802 GIT binary patch literal 9671 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 KJe3~#?f(G*`N`4% literal 0 HcmV?d00001 diff --git a/rollcall_backend/requirements.txt b/rollcall_backend/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..b35a1d27174ff828783bc72d07c971f41a6265cc GIT binary patch literal 858 zcmZ{iPfNp45XAQ^_)%KgSpOY7c<~?>lpZ`vjcJW-lhCA4`{C8!>?Vc^5*~!z*_qjS z@B8c3)|OjgE6Z$aH+*9o+BIinr*>kgE%eTuS3i=xm;Z@vnbM8(`>T;^?9>3kRGy+k%OL^`6X+HAR zT*|b3)V@i-Lm(bOOgWLAy5iP}e8td^W{Hb|XVSp4qn5XRCdvMnY zwFJ){Z_WJPJE3<;C*I|Mw-~AGj^dUYoK>Fxk~fsWCT9iuxx0-#^e)G)8|;l8+AZSk S{`K|jDXl*30qq^ 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 t^IypS