From 5e5cfe14ae083f2998fe169a778a968ef4806b5b Mon Sep 17 00:00:00 2001 From: darkclip Date: Thu, 28 May 2026 18:32:56 +0800 Subject: [PATCH] update --- .gitignore | 10 + .python-version | 1 + README.md | 0 main.py | 1834 +++++++++++++++++++++++++++++++++++++++++++++++ pyproject.toml | 13 + score_system.db | Bin 0 -> 65536 bytes 6 files changed, 1858 insertions(+) create mode 100644 .gitignore create mode 100644 .python-version create mode 100644 README.md create mode 100644 main.py create mode 100644 pyproject.toml create mode 100644 score_system.db diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..505a3b1 --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +# Python-generated files +__pycache__/ +*.py[oc] +build/ +dist/ +wheels/ +*.egg-info + +# Virtual environments +.venv diff --git a/.python-version b/.python-version new file mode 100644 index 0000000..e4fba21 --- /dev/null +++ b/.python-version @@ -0,0 +1 @@ +3.12 diff --git a/README.md b/README.md new file mode 100644 index 0000000..e69de29 diff --git a/main.py b/main.py new file mode 100644 index 0000000..89f5f8d --- /dev/null +++ b/main.py @@ -0,0 +1,1834 @@ +import sqlite3 +from contextlib import contextmanager +from datetime import datetime +from io import BytesIO +from typing import cast + +import pandas as pd +from fastapi import FastAPI, File, HTTPException, Query, UploadFile +from fastapi.middleware.cors import CORSMiddleware +from fastapi.responses import HTMLResponse, StreamingResponse +from pydantic import BaseModel + +app = FastAPI(title='文艺汇演评分系统', description='乌鲁木齐群众文化艺术展评') + +app.add_middleware( + CORSMiddleware, + allow_origins=['*'], + allow_credentials=True, + allow_methods=['*'], + allow_headers=['*'], +) + +# ==================== 数据库配置 ==================== +DB_PATH = 'score_system.db' + + +@contextmanager +def get_db(): + conn = sqlite3.connect(DB_PATH) + conn.row_factory = sqlite3.Row + try: + yield conn + conn.commit() + finally: + conn.close() + + +def init_db(): + with get_db() as conn: + cursor = conn.cursor() + + cursor.execute( + """CREATE TABLE IF NOT EXISTS batches (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, created_at TIMESTAMP)""" + ) + cursor.execute( + """CREATE TABLE IF NOT EXISTS programs (sn TEXT PRIMARY KEY, name TEXT NOT NULL, unit TEXT, performer TEXT, category TEXT, batch_id INTEGER, order_num INTEGER, created_at TIMESTAMP)""" + ) + cursor.execute( + """CREATE TABLE IF NOT EXISTS judges (id TEXT PRIMARY KEY, name TEXT NOT NULL, order_num INTEGER, created_at TIMESTAMP)""" + ) + cursor.execute( + """CREATE TABLE IF NOT EXISTS scores (id INTEGER PRIMARY KEY AUTOINCREMENT, judge_id TEXT NOT NULL, program_sn TEXT NOT NULL, content_score REAL, stage_score REAL, art_score REAL, overall_score REAL, comment TEXT, updated_at TIMESTAMP, UNIQUE(judge_id, program_sn))""" + ) + cursor.execute( + """CREATE TABLE IF NOT EXISTS admins (id TEXT PRIMARY KEY, username TEXT UNIQUE NOT NULL, password TEXT NOT NULL, created_at TIMESTAMP)""" + ) + cursor.execute( + """CREATE TABLE IF NOT EXISTS settings (key TEXT PRIMARY KEY, value TEXT)""" + ) + + cursor.execute('SELECT COUNT(*) FROM admins') + if cursor.fetchone()[0] == 0: + cursor.execute( + 'INSERT INTO admins (id, username, password, created_at) VALUES (?, ?, ?, ?)', + ('admin_001', 'admin', 'admin123', datetime.now()), + ) + + cursor.execute("SELECT COUNT(*) FROM settings WHERE key = 'award_first'") + if cursor.fetchone()[0] == 0: + cursor.execute( + "INSERT INTO settings (key, value) VALUES ('award_first', '3'), ('award_second', '6'), ('award_third', '10')" + ) + + cursor.execute("SELECT COUNT(*) FROM settings WHERE key = 'award_first_name'") + if cursor.fetchone()[0] == 0: + cursor.execute( + "INSERT INTO settings (key, value) VALUES ('award_first_name', '一等奖'), ('award_second_name', '二等奖'), ('award_third_name', '三等奖')" + ) + + cursor.execute("SELECT COUNT(*) FROM settings WHERE key = 'main_title'") + if cursor.fetchone()[0] == 0: + cursor.execute( + "INSERT INTO settings (key, value) VALUES ('main_title', '乌鲁木齐群众文化艺术展评'), ('sub_title', '“建农杯”文艺汇演')" + ) + + cursor.execute('SELECT COUNT(*) FROM batches') + if cursor.fetchone()[0] == 0: + cursor.execute( + 'INSERT INTO batches (name, created_at) VALUES (?, ?)', + ('第一批次', datetime.now()), + ) + cursor.execute( + 'INSERT INTO batches (name, created_at) VALUES (?, ?)', + ('第二批次', datetime.now()), + ) + + cursor.execute('SELECT COUNT(*) FROM judges') + if cursor.fetchone()[0] == 0: + judges_data = [ + ('P.01', '王飞翔', 1, datetime.now()), + ('P.02', '冯建农', 2, datetime.now()), + ('P.03', '张宁', 3, datetime.now()), + ('P.04', '沈澜涛', 4, datetime.now()), + ('P.05', '龚楠', 5, datetime.now()), + ] + cursor.executemany( + 'INSERT INTO judges (id, name, order_num, created_at) VALUES (?, ?, ?, ?)', + judges_data, + ) + + +init_db() + + +# ==================== Pydantic模型 ==================== +class AdminLogin(BaseModel): + username: str + password: str + + +class JudgeLogin(BaseModel): + judge_id: str + judge_name: str + + +class JudgeCreate(BaseModel): + id: str + name: str + + +class JudgeUpdate(BaseModel): + name: str + + +class ProgramCreate(BaseModel): + sn: str + name: str + unit: str + performer: str + category: str + batch_id: int + + +class ProgramUpdate(BaseModel): + name: str + unit: str + performer: str + category: str + batch_id: int + + +class BatchCreate(BaseModel): + name: str + + +class ScoreDetailUpdate(BaseModel): + program_sn: str + field: str + value: float | None = None + + +class ScoreBatchUpdate(BaseModel): + program_sn: str + judge_scores: dict[str, float | None] + + +class CommentUpdate(BaseModel): + program_sn: str + comment: str + + +class AwardSettings(BaseModel): + first: int + second: int + third: int + + +class AwardNamesSettings(BaseModel): + first_name: str + second_name: str + third_name: str + + +class TitleSettings(BaseModel): + main_title: str + sub_title: str + + +# ==================== 根路径 ==================== +@app.get('/') +async def root(): + return HTMLResponse(content=HTML_TEMPLATE) + + +# ==================== 管理员登录API ==================== +@app.post('/api/admin/login') +async def admin_login(login_data: AdminLogin): + with get_db() as conn: + cursor = conn.cursor() + cursor.execute( + 'SELECT id, username FROM admins WHERE username = ? AND password = ?', + (login_data.username, login_data.password), + ) + admin = cursor.fetchone() + if not admin: + raise HTTPException(status_code=401, detail='管理员用户名或密码错误') + return {'success': True, 'admin_id': admin['id'], 'username': admin['username']} + + +@app.post('/api/judge/login') +async def judge_login(login_data: JudgeLogin): + with get_db() as conn: + cursor = conn.cursor() + cursor.execute( + 'SELECT id, name FROM judges WHERE id = ? AND name = ?', + (login_data.judge_id, login_data.judge_name), + ) + judge = cursor.fetchone() + if not judge: + raise HTTPException(status_code=401, detail='评委编号或姓名错误') + return {'success': True, 'judge_id': judge['id'], 'judge_name': judge['name']} + + +# ==================== 设置API ==================== +@app.get('/api/settings/titles') +async def get_title_settings(): + with get_db() as conn: + cursor = conn.cursor() + cursor.execute( + "SELECT key, value FROM settings WHERE key IN ('main_title', 'sub_title')" + ) + titles = {row['key']: row['value'] for row in cursor.fetchall()} + return { + 'main_title': titles.get('main_title', '乌鲁木齐群众文化艺术展评'), + 'sub_title': titles.get('sub_title', '“建农杯”文艺汇演'), + } + + +@app.post('/api/settings/titles') +async def update_title_settings(titles: TitleSettings): + with get_db() as conn: + cursor = conn.cursor() + cursor.execute( + "UPDATE settings SET value = ? WHERE key = 'main_title'", + (titles.main_title,), + ) + cursor.execute( + "UPDATE settings SET value = ? WHERE key = 'sub_title'", (titles.sub_title,) + ) + return {'success': True} + + +@app.get('/api/settings/awards') +async def get_award_settings(): + with get_db() as conn: + cursor = conn.cursor() + cursor.execute( + "SELECT key, value FROM settings WHERE key IN ('award_first', 'award_second', 'award_third')" + ) + counts = {row['key']: int(row['value']) for row in cursor.fetchall()} + cursor.execute( + "SELECT key, value FROM settings WHERE key IN ('award_first_name', 'award_second_name', 'award_third_name')" + ) + names = {row['key']: row['value'] for row in cursor.fetchall()} + return { + 'first': counts.get('award_first', 3), + 'second': counts.get('award_second', 6), + 'third': counts.get('award_third', 10), + 'first_name': names.get('award_first_name', '一等奖'), + 'second_name': names.get('award_second_name', '二等奖'), + 'third_name': names.get('award_third_name', '三等奖'), + } + + +@app.post('/api/settings/awards') +async def update_award_settings(settings: AwardSettings): + with get_db() as conn: + cursor = conn.cursor() + cursor.execute( + "UPDATE settings SET value = ? WHERE key = 'award_first'", + (str(settings.first),), + ) + cursor.execute( + "UPDATE settings SET value = ? WHERE key = 'award_second'", + (str(settings.second),), + ) + cursor.execute( + "UPDATE settings SET value = ? WHERE key = 'award_third'", + (str(settings.third),), + ) + return {'success': True} + + +@app.post('/api/settings/award_names') +async def update_award_names(settings: AwardNamesSettings): + with get_db() as conn: + cursor = conn.cursor() + cursor.execute( + "UPDATE settings SET value = ? WHERE key = 'award_first_name'", + (settings.first_name,), + ) + cursor.execute( + "UPDATE settings SET value = ? WHERE key = 'award_second_name'", + (settings.second_name,), + ) + cursor.execute( + "UPDATE settings SET value = ? WHERE key = 'award_third_name'", + (settings.third_name,), + ) + return {'success': True} + + +@app.get('/api/criteria') +async def get_criteria(): + return { + 'content': { + 'label': '内容取材', + 'max': 20, + 'description': '主题鲜明,健康向上,编排新颖,原创性加分', + }, + 'stage': { + 'label': '舞台风度', + 'max': 10, + 'description': '服装统一整洁,精神饱满,出场退场从容得体', + }, + 'art': { + 'label': '表演艺术水平', + 'max': 60, + 'description': '动作流畅协调,节奏感强,技巧娴熟,富有感染力', + }, + 'overall': { + 'label': '综合评价', + 'max': 10, + 'description': '整体效果印象深刻,现场驾驭能力强', + }, + } + + +# ==================== 批次管理API ==================== +@app.get('/api/batches') +async def get_batches(): + with get_db() as conn: + cursor = conn.cursor() + cursor.execute('SELECT id, name, created_at FROM batches ORDER BY id') + batches = [{'id': row['id'], 'name': row['name']} for row in cursor.fetchall()] + return batches + + +@app.post('/api/batches') +async def create_batch(batch: BatchCreate): + with get_db() as conn: + cursor = conn.cursor() + cursor.execute( + 'INSERT INTO batches (name, created_at) VALUES (?, ?)', + (batch.name, datetime.now()), + ) + new_id = cursor.lastrowid + return {'success': True, 'id': new_id, 'name': batch.name} + + +@app.delete('/api/batches/{batch_id}') +async def delete_batch(batch_id: int): + with get_db() as conn: + cursor = conn.cursor() + cursor.execute('SELECT id FROM batches WHERE id = ?', (batch_id,)) + if not cursor.fetchone(): + raise HTTPException(status_code=404, detail='批次不存在') + cursor.execute( + 'UPDATE programs SET batch_id = NULL WHERE batch_id = ?', (batch_id,) + ) + cursor.execute('DELETE FROM batches WHERE id = ?', (batch_id,)) + return {'success': True} + + +@app.delete('/api/batches/{batch_id}/programs') +async def clear_batch_programs(batch_id: int): + with get_db() as conn: + cursor = conn.cursor() + cursor.execute('SELECT id FROM batches WHERE id = ?', (batch_id,)) + if not cursor.fetchone(): + raise HTTPException(status_code=404, detail='批次不存在') + cursor.execute('SELECT sn FROM programs WHERE batch_id = ?', (batch_id,)) + programs = cursor.fetchall() + for prog in programs: + cursor.execute('DELETE FROM scores WHERE program_sn = ?', (prog['sn'],)) + cursor.execute('DELETE FROM programs WHERE batch_id = ?', (batch_id,)) + return {'success': True} + + +# ==================== 评委管理API ==================== +@app.get('/api/judges') +async def get_judges(): + with get_db() as conn: + cursor = conn.cursor() + cursor.execute('SELECT id, name, order_num FROM judges ORDER BY order_num') + judges = [ + {'id': row['id'], 'name': row['name'], 'order_num': row['order_num']} + for row in cursor.fetchall() + ] + return judges + + +@app.post('/api/judges') +async def create_judge(judge: JudgeCreate): + with get_db() as conn: + cursor = conn.cursor() + cursor.execute('SELECT id FROM judges WHERE id = ?', (judge.id,)) + if cursor.fetchone(): + raise HTTPException(status_code=400, detail='评委编号已存在') + cursor.execute('SELECT MAX(order_num) FROM judges') + max_order = cursor.fetchone()[0] or 0 + cursor.execute( + 'INSERT INTO judges (id, name, order_num, created_at) VALUES (?, ?, ?, ?)', + (judge.id, judge.name, max_order + 1, datetime.now()), + ) + cursor.execute('SELECT sn FROM programs') + for prog in cursor.fetchall(): + cursor.execute( + 'INSERT OR IGNORE INTO scores (judge_id, program_sn, updated_at) VALUES (?, ?, ?)', + (judge.id, prog['sn'], datetime.now()), + ) + return {'success': True} + + +@app.put('/api/judges/{judge_id}') +async def update_judge(judge_id: str, judge: JudgeUpdate): + with get_db() as conn: + cursor = conn.cursor() + cursor.execute('SELECT id FROM judges WHERE id = ?', (judge_id,)) + if not cursor.fetchone(): + raise HTTPException(status_code=404, detail='评委不存在') + cursor.execute( + 'UPDATE judges SET name = ? WHERE id = ?', (judge.name, judge_id) + ) + return {'success': True} + + +@app.delete('/api/judges/{judge_id}') +async def delete_judge(judge_id: str): + with get_db() as conn: + cursor = conn.cursor() + cursor.execute('SELECT id FROM judges WHERE id = ?', (judge_id,)) + if not cursor.fetchone(): + raise HTTPException(status_code=404, detail='评委不存在') + cursor.execute('DELETE FROM scores WHERE judge_id = ?', (judge_id,)) + cursor.execute('DELETE FROM judges WHERE id = ?', (judge_id,)) + return {'success': True} + + +# ==================== 节目管理API ==================== +@app.get('/api/programs') +async def get_programs(batch_id: int | None = Query(None)): + with get_db() as conn: + cursor = conn.cursor() + if batch_id: + cursor.execute( + 'SELECT sn, name, unit, performer, category, batch_id, order_num FROM programs WHERE batch_id = ? ORDER BY order_num', + (batch_id,), + ) + else: + cursor.execute( + 'SELECT sn, name, unit, performer, category, batch_id, order_num FROM programs ORDER BY batch_id, order_num' + ) + programs = [ + { + 'sn': row['sn'], + 'name': row['name'], + 'unit': row['unit'], + 'performer': row['performer'], + 'category': row['category'], + 'batch_id': row['batch_id'], + 'order_num': row['order_num'], + } + for row in cursor.fetchall() + ] + return programs + + +@app.post('/api/programs') +async def create_program(program: ProgramCreate): + with get_db() as conn: + cursor = conn.cursor() + cursor.execute('SELECT sn FROM programs WHERE sn = ?', (program.sn,)) + if cursor.fetchone(): + raise HTTPException(status_code=400, detail='节目序号已存在') + cursor.execute( + 'SELECT MAX(order_num) FROM programs WHERE batch_id = ?', + (program.batch_id,), + ) + max_order = cursor.fetchone()[0] + if max_order is None: + max_order = 0 + cursor.execute( + 'INSERT INTO programs (sn, name, unit, performer, category, batch_id, order_num, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?)', + ( + program.sn, + program.name, + program.unit, + program.performer, + program.category, + program.batch_id, + max_order + 1, + datetime.now(), + ), + ) + cursor.execute('SELECT id FROM judges') + for judge in cursor.fetchall(): + cursor.execute( + 'INSERT OR IGNORE INTO scores (judge_id, program_sn, updated_at) VALUES (?, ?, ?)', + (judge['id'], program.sn, datetime.now()), + ) + return {'success': True} + + +@app.post('/api/programs/import') +async def import_programs(batch_id: int = Query(...), file: UploadFile = File(...)): + try: + contents = await file.read() + df = pd.read_excel(BytesIO(contents)) + + required_columns = ['节目序号', '节目名称', '演出单位', '表演者', '节目类别'] + for col in required_columns: + if col not in df.columns: + raise HTTPException(status_code=400, detail=f'Excel缺少必要列:{col}') + + with get_db() as conn: + cursor = conn.cursor() + cursor.execute( + 'SELECT MAX(order_num) FROM programs WHERE batch_id = ?', (batch_id,) + ) + max_order_result = cursor.fetchone()[0] + start_order = max_order_result if max_order_result is not None else 0 + + for idx, row in df.iterrows(): + sn = str(row['节目序号']).strip() + name = str(row['节目名称']).strip() + unit = str(row['演出单位']).strip() if pd.notna(row['演出单位']) else '' + performer = ( + str(row['表演者']).strip() if pd.notna(row['表演者']) else '' + ) + category = ( + str(row['节目类别']).strip() + if pd.notna(row['节目类别']) + else '舞蹈' + ) + + if not sn or not name: + continue + + idx = cast(int, idx) + current_order = start_order + idx + 1 + cursor.execute('SELECT sn FROM programs WHERE sn = ?', (sn,)) + if cursor.fetchone(): + cursor.execute( + 'UPDATE programs SET name=?, unit=?, performer=?, category=?, batch_id=?, order_num=? WHERE sn=?', + (name, unit, performer, category, batch_id, current_order, sn), + ) + else: + cursor.execute( + 'INSERT INTO programs (sn, name, unit, performer, category, batch_id, order_num, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?)', + ( + sn, + name, + unit, + performer, + category, + batch_id, + current_order, + datetime.now(), + ), + ) + cursor.execute('SELECT id FROM judges') + for judge in cursor.fetchall(): + cursor.execute( + 'INSERT OR IGNORE INTO scores (judge_id, program_sn, updated_at) VALUES (?, ?, ?)', + (judge['id'], sn, datetime.now()), + ) + + return {'success': True, 'message': f'成功导入/更新 {df.shape[0]} 个节目'} + except Exception as e: + raise HTTPException(status_code=400, detail=f'导入失败:{str(e)}') from e + + +@app.put('/api/programs/{program_sn}') +async def update_program(program_sn: str, program: ProgramUpdate): + with get_db() as conn: + cursor = conn.cursor() + cursor.execute('SELECT sn FROM programs WHERE sn = ?', (program_sn,)) + if not cursor.fetchone(): + raise HTTPException(status_code=404, detail='节目不存在') + cursor.execute( + 'UPDATE programs SET name = ?, unit = ?, performer = ?, category = ?, batch_id = ? WHERE sn = ?', + ( + program.name, + program.unit, + program.performer, + program.category, + program.batch_id, + program_sn, + ), + ) + return {'success': True} + + +@app.delete('/api/programs/{program_sn}') +async def delete_program(program_sn: str): + with get_db() as conn: + cursor = conn.cursor() + cursor.execute('SELECT sn FROM programs WHERE sn = ?', (program_sn,)) + if not cursor.fetchone(): + raise HTTPException(status_code=404, detail='节目不存在') + cursor.execute('DELETE FROM scores WHERE program_sn = ?', (program_sn,)) + cursor.execute('DELETE FROM programs WHERE sn = ?', (program_sn,)) + return {'success': True} + + +# ==================== 评分API ==================== +@app.get('/api/scores/{judge_id}') +async def get_judge_scores(judge_id: str): + with get_db() as conn: + cursor = conn.cursor() + cursor.execute( + 'SELECT program_sn, content_score, stage_score, art_score, overall_score, comment FROM scores WHERE judge_id = ?', + (judge_id,), + ) + rows = cursor.fetchall() + scores = {} + for row in rows: + scores[row['program_sn']] = { + 'content': row['content_score'], + 'stage': row['stage_score'], + 'art': row['art_score'], + 'overall': row['overall_score'], + 'comment': row['comment'] or '', + } + cursor.execute('SELECT sn FROM programs') + for row in cursor.fetchall(): + if row['sn'] not in scores: + scores[row['sn']] = { + 'content': None, + 'stage': None, + 'art': None, + 'overall': None, + 'comment': '', + } + return scores + + +@app.post('/api/scores/{judge_id}') +async def update_score(judge_id: str, score_data: ScoreDetailUpdate): + max_scores = {'content': 20, 'stage': 10, 'art': 60, 'overall': 10} + if score_data.field not in max_scores: + raise HTTPException(status_code=400, detail='无效评分项') + val = score_data.value + if val is not None and (val < 0 or val > max_scores[score_data.field]): + raise HTTPException( + status_code=400, detail=f'分值必须在0-{max_scores[score_data.field]}之间' + ) + field_map = { + 'content': 'content_score', + 'stage': 'stage_score', + 'art': 'art_score', + 'overall': 'overall_score', + } + with get_db() as conn: + cursor = conn.cursor() + cursor.execute( + f""" + INSERT INTO scores (judge_id, program_sn, {field_map[score_data.field]}, updated_at) + VALUES (?, ?, ?, ?) + ON CONFLICT(judge_id, program_sn) DO UPDATE SET + {field_map[score_data.field]} = excluded.{field_map[score_data.field]}, + updated_at = excluded.updated_at + """, + (judge_id, score_data.program_sn, val, datetime.now()), + ) + cursor.execute( + 'SELECT content_score, stage_score, art_score, overall_score FROM scores WHERE judge_id = ? AND program_sn = ?', + (judge_id, score_data.program_sn), + ) + row = cursor.fetchone() + total = 0 + if row: + for v in [ + row['content_score'], + row['stage_score'], + row['art_score'], + row['overall_score'], + ]: + if v is not None: + total += v + return {'success': True, 'total': total} + + +@app.post('/api/comment/{judge_id}') +async def update_comment(judge_id: str, comment_data: CommentUpdate): + with get_db() as conn: + cursor = conn.cursor() + cursor.execute( + 'INSERT INTO scores (judge_id, program_sn, comment, updated_at) VALUES (?, ?, ?, ?) ON CONFLICT(judge_id, program_sn) DO UPDATE SET comment = excluded.comment, updated_at = excluded.updated_at', + (judge_id, comment_data.program_sn, comment_data.comment, datetime.now()), + ) + return {'success': True} + + +@app.post('/api/scores/admin/batch') +async def batch_update_scores(batch_data: ScoreBatchUpdate): + """管理员批量更新总分""" + with get_db() as conn: + cursor = conn.cursor() + for judge_id, total_score in batch_data.judge_scores.items(): + if total_score is not None and (total_score < 0 or total_score > 100): + raise HTTPException(status_code=400, detail='总分必须在0-100之间') + + if total_score is None: + cursor.execute( + """ + UPDATE scores SET content_score = NULL, stage_score = NULL, art_score = NULL, overall_score = NULL, updated_at = ? + WHERE judge_id = ? AND program_sn = ? + """, + (datetime.now(), judge_id, batch_data.program_sn), + ) + else: + cursor.execute( + """ + INSERT INTO scores (judge_id, program_sn, content_score, stage_score, art_score, overall_score, comment, updated_at) + VALUES (?, ?, ?, ?, ?, ?, ?, ?) + ON CONFLICT(judge_id, program_sn) DO UPDATE SET + content_score = excluded.content_score, + stage_score = excluded.stage_score, + art_score = excluded.art_score, + overall_score = excluded.overall_score, + comment = excluded.comment, + updated_at = excluded.updated_at + """, + ( + judge_id, + batch_data.program_sn, + total_score, + None, + None, + None, + '', + datetime.now(), + ), + ) + return {'success': True} + + +@app.get('/api/scores/program/{program_sn}') +async def get_program_scores(program_sn: str): + with get_db() as conn: + cursor = conn.cursor() + cursor.execute( + """ + SELECT j.id, j.name, + s.content_score, s.stage_score, s.art_score, s.overall_score, + s.comment + FROM judges j + LEFT JOIN scores s ON j.id = s.judge_id AND s.program_sn = ? + ORDER BY j.order_num + """, + (program_sn,), + ) + scores = [] + for row in cursor.fetchall(): + total = None + if ( + row['content_score'] is not None + or row['stage_score'] is not None + or row['art_score'] is not None + or row['overall_score'] is not None + ): + total = 0 + if row['content_score'] is not None: + total += row['content_score'] + if row['stage_score'] is not None: + total += row['stage_score'] + if row['art_score'] is not None: + total += row['art_score'] + if row['overall_score'] is not None: + total += row['overall_score'] + if total == 0 and row['content_score'] is not None: + total = row['content_score'] + scores.append( + {'judge_id': row['id'], 'judge_name': row['name'], 'total_score': total} + ) + return scores + + +# ==================== 汇总统计API ==================== +@app.get('/api/aggregate') +async def get_aggregate(batch_id: int | None = Query(None)): + with get_db() as conn: + cursor = conn.cursor() + if batch_id: + cursor.execute( + 'SELECT sn, name, unit, performer, category FROM programs WHERE batch_id = ? ORDER BY order_num', + (batch_id,), + ) + else: + cursor.execute( + 'SELECT sn, name, unit, performer, category FROM programs ORDER BY batch_id, order_num' + ) + programs = cursor.fetchall() + results = [] + for prog in programs: + cursor.execute( + """ + SELECT content_score, stage_score, art_score, overall_score + FROM scores WHERE program_sn = ? + """, + (prog['sn'],), + ) + scores_list = [] + for row in cursor.fetchall(): + total = 0 + if row['content_score'] is not None: + total += row['content_score'] + if row['stage_score'] is not None: + total += row['stage_score'] + if row['art_score'] is not None: + total += row['art_score'] + if row['overall_score'] is not None: + total += row['overall_score'] + if total != 0 or (total == 0 and row['content_score'] is not None): + scores_list.append(total) + if scores_list: + avg_score = round(sum(scores_list) / len(scores_list), 3) + max_score = max(scores_list) + min_score = min(scores_list) + else: + avg_score, max_score, min_score = 0, 0, 0 + results.append( + { + 'sn': prog['sn'], + 'name': prog['name'], + 'unit': prog['unit'], + 'performer': prog['performer'], + 'category': prog['category'], + 'avg_score': avg_score, + 'max_score': max_score, + 'min_score': min_score, + 'scored_count': len(scores_list), + } + ) + results.sort(key=lambda x: x['avg_score'], reverse=True) + for idx, r in enumerate(results): + r['rank'] = idx + 1 + return results + + +@app.get('/api/award_winners') +async def get_award_winners(batch_id: int | None = Query(None)): + aggregate = await get_aggregate(batch_id) + settings = await get_award_settings() + first_count, second_count, third_count = ( + settings['first'], + settings['second'], + settings['third'], + ) + winners = {'first': [], 'second': [], 'third': []} + for idx, prog in enumerate(aggregate): + if idx < first_count: + winners['first'].append(prog) + elif idx < first_count + second_count: + winners['second'].append(prog) + elif idx < first_count + second_count + third_count: + winners['third'].append(prog) + return winners + + +@app.get('/api/progress') +async def get_progress(): + with get_db() as conn: + cursor = conn.cursor() + total_programs = cursor.execute('SELECT COUNT(*) FROM programs').fetchone()[0] + total_judges = cursor.execute('SELECT COUNT(*) FROM judges').fetchone()[0] + total_cells = total_programs * total_judges + scored_cells = cursor.execute( + 'SELECT COUNT(*) FROM scores WHERE content_score IS NOT NULL' + ).fetchone()[0] + progress = round(scored_cells / total_cells * 100, 1) if total_cells > 0 else 0 + return { + 'total_programs': total_programs, + 'total_judges': total_judges, + 'scored_programs': scored_cells, + 'total_cells': total_cells, + 'progress': progress, + } + + +@app.get('/api/export_excel') +async def export_excel(batch_id: int | None = Query(None)): + aggregate = await get_aggregate(batch_id) + data = [ + { + '名次': item['rank'], + '节目序号': item['sn'], + '节目名称': item['name'], + '演出单位': item['unit'], + '表演者': item['performer'], + '节目类别': item['category'], + '平均分': item['avg_score'], + '最高分': item['max_score'], + '最低分': item['min_score'], + '评分评委数': item['scored_count'], + } + for item in aggregate + ] + df = pd.DataFrame(data) + output = BytesIO() + with pd.ExcelWriter(output, engine='openpyxl') as writer: + df.to_excel(writer, sheet_name='评分汇总表', index=False) + output.seek(0) + filename = f'{"batch_" + str(batch_id) if batch_id else "all"}_scores.xlsx' + return StreamingResponse( + output, + media_type='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', + headers={'Content-Disposition': f'attachment; filename={filename}'}, + ) + + +# ==================== 打印页面API ==================== +@app.get('/print/program/{program_sn}') +async def print_program_score(program_sn: str): + with get_db() as conn: + prog = conn.execute( + 'SELECT sn, name, unit, performer, category FROM programs WHERE sn = ?', + (program_sn,), + ).fetchone() + if not prog: + raise HTTPException(status_code=404, detail='节目不存在') + judges = conn.execute( + 'SELECT id, name FROM judges ORDER BY order_num' + ).fetchall() + scores_data, all_scores = {}, [] + for judge in judges: + score = conn.execute( + 'SELECT content_score, stage_score, art_score, overall_score, comment FROM scores WHERE judge_id = ? AND program_sn = ?', + (judge['id'], program_sn), + ).fetchone() + total = 0 + comment = '' + if score: + total = 0 + if score['content_score'] is not None: + total += score['content_score'] + if score['stage_score'] is not None: + total += score['stage_score'] + if score['art_score'] is not None: + total += score['art_score'] + if score['overall_score'] is not None: + total += score['overall_score'] + if total == 0 and score['content_score'] is not None: + total = score['content_score'] + comment = score['comment'] or '' + if total > 0: + all_scores.append(total) + scores_data[judge['name']] = {'total': total, 'comment': comment} + max_score = max(all_scores) if all_scores else 0 + min_score = min(all_scores) if all_scores else 0 + avg_score = round(sum(all_scores) / len(all_scores), 3) if all_scores else 0 + titles = await get_title_settings() + return HTMLResponse( + content=render_print_program_template( + prog, judges, scores_data, max_score, min_score, avg_score, titles + ) + ) + + +@app.get('/print/blank/{program_sn}') +async def print_blank_score(program_sn: str): + with get_db() as conn: + prog = conn.execute( + 'SELECT sn, name, unit, performer, category FROM programs WHERE sn = ?', + (program_sn,), + ).fetchone() + if not prog: + raise HTTPException(status_code=404, detail='节目不存在') + judges = conn.execute( + 'SELECT id, name FROM judges ORDER BY order_num' + ).fetchall() + criteria = await get_criteria() + titles = await get_title_settings() + return HTMLResponse( + content=render_print_blank_template(prog, judges, criteria, titles) + ) + + +@app.get('/print/award/{award_type}') +async def print_award_list(award_type: str, batch_id: int | None = Query(None)): + winners = await get_award_winners(batch_id) + if award_type not in winners: + raise HTTPException(status_code=404, detail='奖项类型不存在') + award_settings = await get_award_settings() + award_names = { + 'first': award_settings['first_name'], + 'second': award_settings['second_name'], + 'third': award_settings['third_name'], + } + titles = await get_title_settings() + batch_name = '' + if batch_id: + with get_db() as conn: + batch = conn.execute( + 'SELECT name FROM batches WHERE id = ?', (batch_id,) + ).fetchone() + if batch: + batch_name = f'({batch["name"]})' + return HTMLResponse( + content=render_print_award_template( + award_type, award_names[award_type], winners[award_type], titles, batch_name + ) + ) + + +def render_print_program_template( + program, judges, scores_data, max_score, min_score, avg_score, titles +): + html = f"""{program['name']} - 评分详情 + +

{titles['main_title']}

{titles['sub_title']} · 评分详情表
+

节目序号: {program['sn']}

节目名称: {program['name']}

演出单位: {program['unit']}

表演者: {program['performer']}

节目类别: {program['category']}

+
📊 平均分
{avg_score}
🎯 最高分
{max_score}
⭐ 最低分
{min_score}
+ """ + for judge in judges: + s = scores_data.get(judge['name'], {}) + html += f"""""" + html += f"""
评委姓名总分(100分)评委点评
{judge['name']}{s.get('total') if s.get('total') > 0 else '-'}{s.get('comment', '')}

评委组签名:____________________

日期:{datetime.now().strftime('%Y年%m月%d日')}

+
""" + return html + + +def render_print_blank_template(program, judges, criteria, titles): + html = f"""{program['name']} - 空白评分表 + +""" + for _judge in judges: + html += f"""
+

{titles['main_title']}

{titles['sub_title']} · 节目评分表
+

节目序号: {program['sn']} | 节目名称: {program['name']}

+

演出单位: {program['unit']} | 表演者: {program['performer']} | 节目类别: {program['category']}

+ """ + for _key, cri in criteria.items(): + html += f"""""" + html += """ +
评分项目满分得分评分要点
{cri['label']}{cri['max']}分______{cri['description']}
合计100分______
+
评委签名:____________________日期:____年____月____日
+
""" + html += ( + '
' + '' + ) + return html + + +def render_print_award_template(award_type, award_name, winners, titles, batch_name=''): + html = f"""{award_name}{batch_name}获奖名单 + +

{titles['main_title']}

{titles['sub_title']}{batch_name}
+
{award_name}
+ + + + + """ + for idx, prog in enumerate(winners, 1): + html += f"""""" + html += f""" +
名次节目序号节目名称演出单位表演者节目类别平均分
{idx}{prog['sn']}{prog['name']}{prog['unit']}{prog['performer']}{prog['category']}{prog['avg_score']}
+
评委组签名:____________________日期:{datetime.now().strftime('%Y年%m月%d日')}
+
""" + return html + + +# ==================== HTML模板(前端部分) ==================== +HTML_TEMPLATE = """ + + + + + + 文艺汇演评分系统 + + + +
+
+

加载中...

+
+ +
+
+
+ +
+
🎯 评分打分
+ + +
🖨️ 打印中心
+
+ + +
+
+
+
请先登录选择节目评分
+
+
+ + + + + + + + + + + + + + +
+

📊 评分汇总表

+
名次节目序号节目名称演出单位表演者类别平均分最高分最低分评委数操作
+
+
+ + + + + + + + + + + +""" diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..8e61c36 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,13 @@ +[project] +name = "judge-online" +version = "0.1.0" +description = "Add your description here" +readme = "README.md" +requires-python = ">=3.12" +dependencies = [ + 'hypercorn', + 'fastapi', + 'python-multipart', + 'pandas', + 'openpyxl', +] diff --git a/score_system.db b/score_system.db new file mode 100644 index 0000000000000000000000000000000000000000..6b5a954204bfd971bfc1367413ad7d5dea56b5c7 GIT binary patch literal 65536 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 literal 0 HcmV?d00001