Initial commit

This commit is contained in:
luoyu 2025-07-10 13:42:54 +08:00
commit 646a8a3e7a
16 changed files with 2779 additions and 0 deletions

8
.idea/.gitignore generated vendored Normal file
View File

@ -0,0 +1,8 @@
# 默认忽略的文件
/shelf/
/workspace.xml
# 基于编辑器的 HTTP 客户端请求
/httpRequests/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml

8
.idea/RSJselet.iml generated Normal file
View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="PYTHON_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$" />
<orderEntry type="jdk" jdkName="Python 3.10" jdkType="Python SDK" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

View File

@ -0,0 +1,6 @@
<component name="InspectionProjectProfileManager">
<profile version="1.0">
<option name="myName" value="Project Default" />
<inspection_tool class="Eslint" enabled="true" level="WARNING" enabled_by_default="true" />
</profile>
</component>

View File

@ -0,0 +1,6 @@
<component name="InspectionProjectProfileManager">
<settings>
<option name="USE_PROJECT_PROFILE" value="false" />
<version value="1.0" />
</settings>
</component>

6
.idea/misc.xml generated Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Black">
<option name="sdkName" value="Python 3.10" />
</component>
</project>

8
.idea/modules.xml generated Normal file
View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/RSJselet.iml" filepath="$PROJECT_DIR$/.idea/RSJselet.iml" />
</modules>
</component>
</project>

103
README.md Normal file
View File

@ -0,0 +1,103 @@
# 成绩查询系统 (RSJselet)
这是一个高性能的学生成绩查询系统支持12,000人同时在线查询。
## 主要特性
- 支持高并发查询12,000人同时在线
- 使用Redis缓存减轻数据库压力
- 提供等待室机制避免系统过载
- 自动释放数据库连接,避免资源泄漏
- 手机号验证和内网可访问
## 系统架构
- 后端: FastAPI + Uvicorn
- 数据库: MySQL
- 缓存: Redis
- 前端: HTML + CSS
## 快速开始
系统提供了测试数据和快速启动脚本,方便本地开发和测试:
### Windows系统
```bash
# 修改run_with_test_data.bat中的数据库连接信息
# 然后运行批处理文件
run_with_test_data.bat
```
### Linux/Mac系统
```bash
# 修改run_with_test_data.sh中的数据库连接信息
chmod +x run_with_test_data.sh
./run_with_test_data.sh
```
启动后访问 http://localhost:8000 即可使用系统。
### 测试数据
系统提供了约1000条测试数据包括
- 江苏省教育局的语文、数学、英语教师岗位
- 淮安市政府的行政助理、文案策划岗位
- 淮安市医院的内科医生、外科医生、护士岗位
- 江苏省人社厅的人事专员、劳动监察员、社保专员岗位
测试账号示例:
- 准考证号: 101081100101手机号: 13800000001
- 准考证号: 101081100201手机号: 13800000006
- 准考证号: 101081100301手机号: 13800000010
## 部署方式
系统提供多种部署方式:
### Docker部署推荐
使用Docker Compose一键部署所有服务
```bash
# 构建并启动
docker-compose up -d
```
### Linux可执行文件部署
将系统打包为单个可执行文件方便在Linux服务器上部署
```bash
# 构建可执行文件
chmod +x build_executable.sh
./build_executable.sh
# 部署到服务器
scp -r dist/* user@server:/path/to/deploy/
# 在服务器上启动
cd /path/to/deploy/
./start.sh
```
### 更多详情
请查看 [部署指南](DEPLOY.md) 获取更多详细信息。
## 高并发支持
系统已经过优化可以支持12,000人同时查询
1. **连接池优化**: 高效管理数据库和Redis连接
2. **等待室机制**: 控制并发查询数量,防止系统崩溃
3. **多进程支持**: 利用多核CPU提高处理能力
4. **缓存策略**: 自适应缓存热门数据
## 配置和扩展
所有系统参数都可以通过环境变量配置,请参考 [部署指南](DEPLOY.md) 获取完整配置选项。
如需支持更大规模并发,可参考扩展策略进行水平或垂直扩展。

38
config.py Normal file
View File

@ -0,0 +1,38 @@
# 配置文件
import os
# 数据库配置
DB_CONFIG = {
"host": os.environ.get("DB_HOST", "192.140.160.11"),
"port": int(os.environ.get("DB_PORT", "3306")),
"user": os.environ.get("DB_USER", "root"),
"password": os.environ.get("DB_PASSWORD", "Boyue123"),
"db": os.environ.get("DB_NAME", "harsjselect"),
"charset": "utf8mb4",
"minsize": 10, # 最小连接数
"maxsize": int(os.environ.get("DB_MAX_CONN", "200")), # 最大连接数
"pool_recycle": 3600 # 连接回收时间
}
# Redis配置
REDIS_CONFIG = {
"host": os.environ.get("REDIS_HOST", "192.140.160.11"),
"port": int(os.environ.get("REDIS_PORT", "6379")),
"db": int(os.environ.get("REDIS_DB", "0")),
"password": os.environ.get("REDIS_PASSWORD", "boyue123"),
"encoding": "utf-8",
"pool_size": int(os.environ.get("REDIS_POOL_SIZE", "100")) # Redis连接池大小
}
# 缓存过期时间(秒)
CACHE_EXPIRE = int(os.environ.get("CACHE_EXPIRE", "3600")) # 1小时
# 等待室配置
WAITING_ROOM_CAPACITY = int(os.environ.get("WAITING_ROOM_CAPACITY", "15000")) # 等待室容量
CONCURRENT_QUERIES = int(os.environ.get("CONCURRENT_QUERIES", "1000")) # 并发查询数量
# 服务器配置
SERVER_HOST = os.environ.get("SERVER_HOST", "0.0.0.0")
SERVER_PORT = int(os.environ.get("SERVER_PORT", "80"))
WORKERS = int(os.environ.get("WORKERS", "4")) # Uvicorn工作进程数
TIMEOUT = int(os.environ.get("TIMEOUT", "300")) # 请求超时时间

197
cstest.py Normal file
View File

@ -0,0 +1,197 @@
# 文件名: concurrent_test.py
import asyncio
import aiohttp
import time
import json
import random
import argparse
from collections import defaultdict
from tqdm import tqdm
# 测试数据生成
def generate_test_data(count):
test_data = []
# 基于系统中看到的测试数据格式
for i in range(count):
zkzh = random.randint(2000000, 2999999)
sj = f"138{random.randint(10000000, 99999999)}"
test_data.append({"zkzh": str(zkzh), "sj": sj})
return test_data
# 单个查询请求
async def query_score(session, url, data, timeout=30):
start_time = time.time()
try:
async with session.post(url, json=data, timeout=timeout) as response:
result = await response.json()
end_time = time.time()
return {
"status_code": response.status,
"response_time": end_time - start_time,
"success": result.get("success", False),
"message": result.get("message", ""),
"in_queue": "queue_position" in result,
"queue_position": result.get("queue_position", None)
}
except asyncio.TimeoutError:
return {
"status_code": 0,
"response_time": time.time() - start_time,
"success": False,
"message": "请求超时",
"in_queue": False,
"queue_position": None
}
except Exception as e:
return {
"status_code": 0,
"response_time": time.time() - start_time,
"success": False,
"message": f"发生错误: {str(e)}",
"in_queue": False,
"queue_position": None
}
# 轮询状态(如果被放入等待队列)
async def poll_status(session, base_url, task_id, max_attempts=20, interval=1):
status_url = f"{base_url}/api/query_status/{task_id}"
for attempt in range(max_attempts):
try:
async with session.get(status_url) as response:
result = await response.json()
if result.get("success") and "data" in result:
return {"success": True, "data": result["data"], "attempts": attempt + 1}
elif not result.get("in_progress", True):
return {"success": False, "message": result.get("message", "查询失败"), "attempts": attempt + 1}
await asyncio.sleep(interval)
except Exception:
await asyncio.sleep(interval)
return {"success": False, "message": "轮询超时", "attempts": max_attempts}
# 并发测试主函数
async def run_concurrent_test(base_url, concurrency, total_requests, delay=0):
url = f"{base_url}/api/query_score"
test_data = generate_test_data(total_requests)
results = []
# 创建一个共享的 ClientSession
async with aiohttp.ClientSession() as session:
# 分批次发送请求
tasks = []
for i, data in enumerate(test_data):
if i > 0 and i % concurrency == 0:
await asyncio.sleep(delay)
task = asyncio.create_task(query_score(session, url, data))
tasks.append(task)
# 使用tqdm显示进度条
for future in tqdm(asyncio.as_completed(tasks), total=len(tasks), desc="发送请求"):
result = await future
results.append(result)
return results
# 生成测试报告
def generate_report(results):
total = len(results)
success_count = sum(1 for r in results if r["success"])
failure_count = total - success_count
in_queue_count = sum(1 for r in results if r["in_queue"])
response_times = [r["response_time"] for r in results]
avg_response_time = sum(response_times) / len(response_times)
min_response_time = min(response_times)
max_response_time = max(response_times)
# 计算各百分位响应时间
response_times.sort()
p50 = response_times[int(total * 0.5)]
p90 = response_times[int(total * 0.9)]
p95 = response_times[int(total * 0.95)]
p99 = response_times[int(total * 0.99)]
# 状态码分布
status_codes = defaultdict(int)
for r in results:
status_codes[r["status_code"]] += 1
# 错误消息分类
error_messages = defaultdict(int)
for r in results:
if not r["success"]:
error_messages[r["message"]] += 1
report = {
"总请求数": total,
"成功请求": success_count,
"失败请求": failure_count,
"成功率": f"{(success_count/total*100):.2f}%",
"进入等待队列数": in_queue_count,
"响应时间(秒)": {
"平均": f"{avg_response_time:.4f}",
"最小": f"{min_response_time:.4f}",
"最大": f"{max_response_time:.4f}",
"P50": f"{p50:.4f}",
"P90": f"{p90:.4f}",
"P95": f"{p95:.4f}",
"P99": f"{p99:.4f}"
},
"状态码分布": dict(status_codes),
"错误消息分类": dict(error_messages)
}
return report
async def main():
parser = argparse.ArgumentParser(description="成绩查询系统并发测试工具")
parser.add_argument("--url", default="http://localhost:8000", help="API基础URL")
parser.add_argument("--concurrency", type=int, default=100, help="并发请求数")
parser.add_argument("--total", type=int, default=1000, help="总请求数")
parser.add_argument("--delay", type=float, default=0.1, help="批次间延迟(秒)")
parser.add_argument("--output", default="test_report.json", help="报告输出文件")
args = parser.parse_args()
print(f"开始并发测试: URL={args.url}, 并发数={args.concurrency}, 总请求数={args.total}")
start_time = time.time()
results = await run_concurrent_test(
args.url,
args.concurrency,
args.total,
args.delay
)
end_time = time.time()
total_time = end_time - start_time
report = generate_report(results)
report["总测试时间(秒)"] = f"{total_time:.2f}"
report["每秒请求数(RPS)"] = f"{args.total/total_time:.2f}"
# 保存报告到文件
with open(args.output, 'w', encoding='utf-8') as f:
json.dump(report, f, ensure_ascii=False, indent=2)
print(f"测试完成! 总耗时: {total_time:.2f}")
print(f"报告已保存至: {args.output}")
# 打印关键指标
print("\n关键性能指标:")
print(f"总请求数: {report['总请求数']}")
print(f"成功率: {report['成功率']}")
print(f"平均响应时间: {report['响应时间(秒)']['平均']}")
print(f"RPS: {report['每秒请求数(RPS)']}请求/秒")
if __name__ == "__main__":
asyncio.run(main())
# # 默认本地测试
# python concurrent_test.py
# # 指定URL和并发参数
# python concurrent_test.py --url http://192.168.0.46:8000 --concurrency 200 --total 2000

1083
harsjselect.sql Normal file

File diff suppressed because it is too large Load Diff

330
html/index.html Normal file
View File

@ -0,0 +1,330 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>成绩查询系统</title>
<link rel="stylesheet" href="/static/style.css">
</head>
<body>
<div class="container mt-5">
<div class="card mb-4">
<div class="card-header text-center py-3">
<h2>成绩查询系统</h2>
</div>
<div class="card-body p-4">
<form id="queryForm">
<div class="mb-3">
<label for="zkzh" class="form-label">准考证号</label>
<input type="text" class="form-control" id="zkzh" required>
</div>
<div class="mb-3">
<label for="sj" class="form-label">密码</label>
<input type="password" class="form-control" id="sj">
</div>
<div class="d-grid">
<button type="submit" class="btn btn-primary btn-lg">查询成绩</button>
</div>
</form>
<!-- 等待信息 -->
<div id="waitingInfo" class="waiting-info alert alert-info">
<div class="text-center mb-3">
<div class="spinner-border text-primary" role="status">
<span class="visually-hidden">加载中...</span>
</div>
</div>
<h5 class="text-center">您的查询正在排队处理中</h5>
<p>当前队列位置: <span id="queuePosition">--</span></p>
<p>预计等待时间: <span id="estimatedTime">--</span></p>
<div class="progress">
<div id="waitingProgress" class="progress-bar progress-bar-striped progress-bar-animated" role="progressbar" style="width: 0%"></div>
</div>
</div>
<!-- 加载指示器 -->
<div id="loadingIndicator" class="loading-indicator">
<div class="spinner-border text-primary" role="status">
<span class="visually-hidden">加载中...</span>
</div>
<p class="mt-2">正在查询,请稍候...</p>
</div>
<!-- 错误提示 -->
<div id="errorAlert" class="alert alert-danger" style="display: none;"></div>
<!-- 查询结果 -->
<div id="resultContainer" style="display: none;">
<div class="alert alert-success mt-4">
查询成功!以下是您的成绩信息:
</div>
<table class="table table-bordered result-table">
<tbody>
<tr>
<th width="30%">姓名</th>
<td id="result-xm"></td>
</tr>
<tr>
<th>准考证号</th>
<td id="result-zkzh"></td>
</tr>
<tr>
<th>手机号</th>
<td id="result-sjh"></td>
</tr>
<tr>
<th>单位名称</th>
<td id="result-dwmc"></td>
</tr>
<tr>
<th>职位名称</th>
<td id="result-zwmc"></td>
</tr>
<tr>
<th>考点名称</th>
<td id="result-kdmc"></td>
</tr>
<tr>
<th>考场号</th>
<td id="result-kch"></td>
</tr>
<tr>
<th>座位号</th>
<td id="result-zwh"></td>
</tr>
<tr>
<th>笔试成绩</th>
<td id="result-bscj" class="fw-bold"></td>
</tr>
<tr>
<th>排名</th>
<td id="result-pm"></td>
</tr>
<tr>
<th>备注</th>
<td id="result-bz"></td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<div class="text-center text-muted small">
<p>© 2025 成绩查询系统 | 如有问题请联系管理员</p>
</div>
</div>
<!-- 不需要引用外部JS库 -->
<script>
document.addEventListener('DOMContentLoaded', function() {
// 表单提交
const queryForm = document.getElementById('queryForm');
const loadingIndicator = document.getElementById('loadingIndicator');
const errorAlert = document.getElementById('errorAlert');
const resultContainer = document.getElementById('resultContainer');
const waitingInfo = document.getElementById('waitingInfo');
const waitingProgress = document.getElementById('waitingProgress');
queryForm.addEventListener('submit', async function(e) {
e.preventDefault();
// 获取表单数据
const zkzh = document.getElementById('zkzh').value.trim();
const pwd = document.getElementById('sj').value.trim(); // 使用 sj 字段
// 验证表单
if (!zkzh) {
showError('请输入准考证号');
return;
}
if (!pwd) {
showError('请输入密码');
return;
}
// 隐藏之前的错误和结果
errorAlert.style.display = 'none';
resultContainer.style.display = 'none';
waitingInfo.style.display = 'none';
// 显示加载指示器
loadingIndicator.style.display = 'block';
try {
// 准备请求数据 - 确保准考证号为字符串
const queryData = {
zkzh: zkzh.toString(),
sj: pwd.toString() // 添加手机号作为密码
};
// 调试输出请求数据
console.log("发送请求数据:", queryData);
// 发送请求
const response = await fetch('/api/query_score', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(queryData)
});
const data = await response.json();
// 调试输出
console.log("API响应数据:", data);
// 隐藏加载指示器
loadingIndicator.style.display = 'none';
// 处理响应
if (data.success) {
// 如果有队列位置信息,显示等待界面
if (data.queue_position && data.estimated_wait_time) {
showWaitingInfo(data.queue_position, data.estimated_wait_time);
pollQueryStatus(data.queue_position, data.estimated_wait_time);
return;
}
// 显示结果
if (data.data) {
console.log("查询结果数据:", data.data);
displayResult(data.data);
}
} else {
showError(data.message || '查询失败,请稍后再试');
}
} catch (error) {
console.error('查询失败:', error);
loadingIndicator.style.display = 'none';
showError('系统错误,请稍后再试');
}
});
// 显示错误信息
function showError(message) {
errorAlert.textContent = message;
errorAlert.style.display = 'block';
loadingIndicator.style.display = 'none';
waitingInfo.style.display = 'none';
}
// 显示查询结果
function displayResult(data) {
// 显示考生基本信息
document.getElementById('result-xm').textContent = data.xm || '--';
document.getElementById('result-zkzh').textContent = data.zkzh || '--';
document.getElementById('result-sjh').textContent = data.sjh || '--';
document.getElementById('result-dwmc').textContent = data.dwmc || '--';
document.getElementById('result-zwmc').textContent = data.zwmc || '--';
document.getElementById('result-kdmc').textContent = data.kdmc || '--';
document.getElementById('result-kch').textContent = data.kch || '--';
document.getElementById('result-zwh').textContent = data.zwh || '--';
// 设置成绩并添加颜色
const bscjElement = document.getElementById('result-bscj');
bscjElement.textContent = data.bscj || '--';
// 根据成绩添加颜色
const score = parseFloat(data.bscj);
if (score >= 90) {
bscjElement.classList.add('text-success');
} else if (score >= 80) {
bscjElement.classList.add('text-primary');
} else if (score >= 60) {
bscjElement.classList.add('text-warning');
} else {
bscjElement.classList.add('text-danger');
}
document.getElementById('result-pm').textContent = data.pm || '--';
document.getElementById('result-bz').textContent = data.bz || '--';
resultContainer.style.display = 'block';
}
// 显示等待信息
function showWaitingInfo(position, estimatedTime) {
document.getElementById('queuePosition').textContent = position;
document.getElementById('estimatedTime').textContent = estimatedTime;
const progressPercentage = Math.min(100, Math.max(5, 100 - (position / 10)));
waitingProgress.style.width = progressPercentage + '%';
waitingInfo.style.display = 'block';
}
// 轮询查询状态
function pollQueryStatus(position, estimatedTime) {
// 创建一个模拟任务ID实际应用中应该从服务器获取
const taskId = 'task_' + Math.random().toString(36).substr(2, 9);
let remainingTime = estimatedTime;
const totalTime = estimatedTime;
// 更新进度条
const updateProgress = () => {
remainingTime -= 0.1;
const progress = Math.min(100, Math.max(0, ((totalTime - remainingTime) / totalTime) * 100));
waitingProgress.style.width = progress + '%';
if (remainingTime <= 0) {
clearInterval(progressInterval);
}
};
const progressInterval = setInterval(updateProgress, 100);
// 模拟轮询
setTimeout(async () => {
try {
// 在实际应用中,应该调用 /api/query_status/{taskId} 获取结果
// 这里直接再次调用查询接口
const zkzhVal = document.getElementById('zkzh').value.trim();
const pwdVal = document.getElementById('sj').value.trim(); // 使用 sj 字段
const queryData = {
zkzh: zkzhVal.toString(),
sj: pwdVal // 添加手机号作为密码
};
const response = await fetch('/api/query_score', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(queryData)
});
const data = await response.json();
// 调试输出
console.log("轮询响应:", data);
clearInterval(progressInterval);
waitingInfo.style.display = 'none';
if (data.success && data.data) {
console.log("轮询查询返回的数据:", data.data);
displayResult(data.data);
} else if (data.queue_position && data.estimated_wait_time) {
// 如果仍在等待,继续轮询
showWaitingInfo(data.queue_position, data.estimated_wait_time);
pollQueryStatus(data.queue_position, data.estimated_wait_time);
} else {
showError(data.message || '查询失败,请稍后再试');
}
} catch (error) {
clearInterval(progressInterval);
waitingInfo.style.display = 'none';
showError('系统错误,请稍后再试');
}
}, estimatedTime * 1000);
}
});
</script>
</body>
</html>

733
main.py Normal file
View File

@ -0,0 +1,733 @@
from fastapi import FastAPI, HTTPException, Depends, Query, BackgroundTasks
from fastapi.middleware.cors import CORSMiddleware
from fastapi.staticfiles import StaticFiles
from fastapi.responses import RedirectResponse
from pydantic import BaseModel
from typing import Optional, List, Dict, Any
import uvicorn
import aiomysql
import asyncio
import time
import os
import json
import aioredis
from contextlib import asynccontextmanager
import random
from config import * # 导入配置
import monitoring # 导入监控模块
from prometheus_client import make_asgi_app
from fastapi.staticfiles import StaticFiles
from fastapi.responses import HTMLResponse
from fastapi.responses import JSONResponse
# 数据库配置
# DB_CONFIG = {
# "host": "127.0.0.1",
# "port": 3306,
# "user": "root", # 请根据实际情况修改
# "password": "123456", # 请根据实际情况修改
# "db": "harsjselect",
# "charset": "utf8mb4"
# }
# Redis配置
# REDIS_CONFIG = {
# "host": "127.0.0.1",
# "port": 6379,
# "db": 0,
# "password": 123456, # 如果有密码,请设置
# "encoding": "utf-8"
# }
# 缓存过期时间(秒)
# CACHE_EXPIRE = 3600 # 1小时
# 连接池
pool = None
redis = None
# 测试数据
TEST_DATA = [
{
"zkzh": 2023001,
"xm": "张三",
"sj": "13800000001",
"dw": "北京市第一中学",
"subjects": [
{"km": "语文", "cj": "98", "pm": "1", "bz": "优秀"},
{"km": "数学", "cj": "92", "pm": "3", "bz": "良好"},
{"km": "英语", "cj": "95", "pm": "2", "bz": "优秀"},
{"km": "物理", "cj": "88", "pm": "5", "bz": "良好"},
{"km": "化学", "cj": "94", "pm": "2", "bz": "优秀"}
]
},
{
"zkzh": 2023002,
"xm": "李四",
"sj": "13800000002",
"dw": "北京市第二中学",
"subjects": [
{"km": "语文", "cj": "85", "pm": "12", "bz": "良好"},
{"km": "数学", "cj": "98", "pm": "1", "bz": "优秀"},
{"km": "英语", "cj": "88", "pm": "8", "bz": "良好"},
{"km": "物理", "cj": "92", "pm": "3", "bz": "优秀"},
{"km": "化学", "cj": "86", "pm": "10", "bz": "良好"}
]
}
]
# 等待室配置
# WAITING_ROOM_CAPACITY = 15000 # 等待室容量增加到15000
# CONCURRENT_QUERIES = 1000 # 并发查询数量增加到1000
waiting_room = asyncio.Queue()
current_processing = 0
processing_lock = asyncio.Lock()
# 初始化测试数据
async def init_test_data():
print("开始初始化测试数据...")
if not pool:
print("数据库连接池未初始化,无法添加测试数据")
return
try:
async with pool.acquire() as conn:
async with conn.cursor() as cursor:
# 检查表是否存在
try:
await cursor.execute("SELECT 1 FROM rsjcjselect LIMIT 1")
table_exists = True
except Exception:
table_exists = False
# 如果表不存在,创建表
if not table_exists:
print("表不存在,创建表...")
await cursor.execute("""
CREATE TABLE IF NOT EXISTS `rsjcjselect` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '序号',
`pm` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '排名',
`cj` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '成绩',
`zkzh` int(11) NOT NULL COMMENT '准考证号',
`sj` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '手机号',
`dw` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '单位',
`xm` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '姓名',
`km` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '科目',
`bz` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '备注',
PRIMARY KEY (`id`)
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci ROW_FORMAT = Dynamic
""")
# 检查表中是否有数据
await cursor.execute("SELECT COUNT(*) as count FROM rsjcjselect")
result = await cursor.fetchone()
record_count = result[0] if result else 0
# 如果表中已有数据,不再添加测试数据
if record_count > 0:
print(f"表中已有 {record_count} 条数据,跳过初始化")
return
# 清空原有数据
await cursor.execute("DELETE FROM rsjcjselect")
# 准备插入语句
sql = """
INSERT INTO rsjcjselect (pm, cj, zkzh, sj, dw, xm, km, bz)
VALUES (%s, %s, %s, %s, %s, %s, %s, %s)
"""
# 插入测试数据
count = 0
for student in TEST_DATA:
for subject in student["subjects"]:
await cursor.execute(sql, (
subject["pm"],
subject["cj"],
student["zkzh"],
student["sj"],
student["dw"],
student["xm"],
subject["km"],
subject["bz"]
))
count += 1
# 提交事务
await conn.commit()
print(f"成功初始化了 {count} 条测试数据")
except Exception as e:
print(f"初始化测试数据时发生错误: {str(e)}")
# 初始化数据库连接池
async def init_db():
global pool
try:
print(f"正在连接到MySQL数据库: {DB_CONFIG['host']}:{DB_CONFIG['port']}/{DB_CONFIG['db']}")
pool = await aiomysql.create_pool(
host=DB_CONFIG["host"],
port=DB_CONFIG["port"],
user=DB_CONFIG["user"],
password=DB_CONFIG["password"],
db=DB_CONFIG["db"],
charset=DB_CONFIG["charset"],
maxsize=DB_CONFIG["maxsize"],
minsize=DB_CONFIG["minsize"],
pool_recycle=DB_CONFIG["pool_recycle"],
autocommit=True
)
print("MySQL数据库连接池初始化成功")
# 连接池预热
print("正在预热数据库连接池...")
prewarming_conns = []
for _ in range(min(10, DB_CONFIG["maxsize"])):
try:
conn = await asyncio.wait_for(pool.acquire(), timeout=5.0)
prewarming_conns.append(conn)
except Exception as e:
print(f"预热连接失败: {str(e)}")
# 释放预热连接
for conn in prewarming_conns:
pool.release(conn)
print(f"数据库连接池预热完成,预热了 {len(prewarming_conns)} 个连接")
return True
except Exception as e:
print(f"数据库连接失败: {str(e)}")
print("请检查以下可能的问题:")
print("1. MySQL服务是否已启动")
print("2. 数据库用户名和密码是否正确")
print("3. 数据库名是否存在")
print("4. 数据库主机是否可访问")
print(f"当前配置: 主机={DB_CONFIG['host']}, 端口={DB_CONFIG['port']}, 用户={DB_CONFIG['user']}, 数据库={DB_CONFIG['db']}")
return False
# 初始化Redis连接池
async def init_redis():
global redis
try:
print(f"正在连接到Redis: {REDIS_CONFIG['host']}:{REDIS_CONFIG['port']}/{REDIS_CONFIG['db']}")
redis = aioredis.from_url(
f"redis://{REDIS_CONFIG['host']}:{REDIS_CONFIG['port']}/{REDIS_CONFIG['db']}",
password=REDIS_CONFIG.get('password'),
encoding=REDIS_CONFIG.get('encoding', 'utf-8'),
decode_responses=True,
socket_timeout=5.0, # 设置超时
socket_connect_timeout=5.0,
max_connections=100 # 最大连接数
)
# 测试Redis连接
await redis.ping()
print("Redis连接池初始化成功")
return True
except Exception as e:
print(f"Redis连接失败: {str(e)}")
print("请检查以下可能的问题:")
print("1. Redis服务是否已启动")
print("2. Redis密码是否正确")
print("3. Redis主机是否可访问")
print(f"当前配置: 主机={REDIS_CONFIG['host']}, 端口={REDIS_CONFIG['port']}, 数据库={REDIS_CONFIG['db']}")
redis = None
return False
# 应用启动和关闭事件
@asynccontextmanager
async def lifespan(app: FastAPI):
# 启动时执行
global pool, redis
# 初始化数据库
db_success = await init_db()
if not db_success:
print("警告: 数据库连接失败,应用程序将以有限功能运行")
# 初始化Redis
redis_success = await init_redis()
if not redis_success:
print("警告: Redis连接失败将禁用缓存功能")
# 启动等待室处理协程
asyncio.create_task(process_waiting_room())
print("成绩查询系统启动完成")
print(f"访问地址: http://{SERVER_HOST}:{SERVER_PORT}")
yield
# 关闭时执行
if pool:
pool.close()
await pool.wait_closed()
print("数据库连接池已关闭")
if redis:
await redis.close()
print("Redis连接池已关闭")
# 创建FastAPI应用
app = FastAPI(lifespan=lifespan)
# 添加Prometheus指标端点
metrics_app = make_asgi_app()
app.mount("/metrics", metrics_app)
# 允许跨域请求
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# 定义查询请求模型
class ScoreQuery(BaseModel):
zkzh: str # 准考证号
sj: Optional[str] = None # 手机号作为密码验证
# 定义查询响应模型
class ScoreResponse(BaseModel):
success: bool
message: str
data: Optional[dict] = None
queue_position: Optional[int] = None
estimated_wait_time: Optional[int] = None
# API路由应该定义在挂载静态文件之前
# 定义API路由处理函数
@app.post("/api/query_score")
async def query_score(query: ScoreQuery):
try:
# 验证输入
if not query.zkzh:
return JSONResponse(
status_code=400,
content={"success": False, "message": "准考证号不能为空", "data": None}
)
# 打印请求参数
print("接收到查询请求:", query)
# 直接查询,跳过等待室机制
try:
result = await query_score_from_db(query)
print("查询结果:", result)
return result
except Exception as e:
print("查询失败:", str(e))
return JSONResponse(
status_code=500,
content={
"success": False,
"message": f"查询失败: {str(e)}",
"data": None
}
)
except Exception as e:
print("查询处理异常:", str(e))
return JSONResponse(
status_code=500,
content={
"success": False,
"message": f"查询失败: {str(e)}",
"data": None
}
)
@app.get("/api/query_status/{task_id}")
async def query_status(task_id: str):
# 此端点将在前端轮询获取结果时使用
# 在实际实现中,需要一个存储任务状态的机制
# 简化版实现
return {"status": "completed", "result": {}}
# 处理等待室队列
async def process_waiting_room():
global current_processing
print("启动等待室处理协程")
while True:
try:
# 如果当前处理数量小于并发上限,从等待室获取任务处理
async with processing_lock:
can_process = current_processing < CONCURRENT_QUERIES
if can_process:
current_processing += 1
monitoring.update_concurrent_queries(current_processing)
if can_process:
try:
# 尝试获取任务,非阻塞方式
task_info = await asyncio.wait_for(waiting_room.get(), timeout=0.1)
monitoring.update_waiting_room_size(waiting_room.qsize())
# 创建任务处理协程
asyncio.create_task(process_query_task(task_info))
except asyncio.TimeoutError:
# 等待室为空,减少计数
async with processing_lock:
current_processing -= 1
monitoring.update_concurrent_queries(current_processing)
# 更新连接池指标
monitoring.update_pool_metrics(pool, redis)
# 短暂等待避免CPU过度使用
await asyncio.sleep(0.1)
except Exception as e:
print(f"等待室处理出错: {str(e)}")
monitoring.record_query_error("waiting_room_error")
await asyncio.sleep(1) # 出错后稍长延迟
# 处理单个查询任务
async def process_query_task(task_info):
global current_processing
query_data, future = task_info
try:
# 如果future已经被取消直接释放资源不执行查询
if future.cancelled():
print("任务已被取消,跳过处理")
return
# 执行实际查询
result = await query_score_from_db(query_data)
# 输出调试信息
print("查询任务结果:", result)
if hasattr(result, "data") and result.data is not None:
print("查询结果data字段:", result.data)
if "subjects" in result.data:
print("subjects字段存在", len(result.data["subjects"]), "个科目")
else:
print("警告: subjects字段不存在!")
else:
print("警告: data字段为空!")
# 设置future的结果
if not future.done():
future.set_result(result)
except Exception as e:
print("处理查询任务异常:", str(e))
if not future.done():
future.set_exception(e)
finally:
# 减少当前处理计数
async with processing_lock:
current_processing -= 1
# 显式清理结果对象,帮助垃圾回收
result = None
# 从数据库查询成绩
async def query_score_from_db(query: ScoreQuery):
if not pool:
raise HTTPException(status_code=503, detail="数据库服务不可用,请稍后再试")
# 记录查询尝试
monitoring.record_query_attempt()
# 设置查询超时
QUERY_TIMEOUT = 5.0 # 5秒超时
conn = None
cursor = None
try:
# 构建查询条件
zkzh_val = query.zkzh.strip()
# 构建缓存键
cache_key = f"score:{zkzh_val}"
if query.sj: # 使用sj字段进行缓存
cache_key += f":sj:{query.sj}"
# 尝试从缓存获取数据
if redis:
try:
cached_data = await redis.get(cache_key)
if cached_data:
print(f"从Redis缓存获取数据: {cache_key}")
# 记录缓存命中
monitoring.record_cache_hit()
student_info = json.loads(cached_data)
return ScoreResponse(
success=True,
message="成绩查询成功(来自缓存)",
data=student_info
)
else:
# 记录缓存未命中
monitoring.record_cache_miss()
except Exception as e:
print(f"从Redis获取缓存失败: {str(e)}")
# 记录缓存错误
monitoring.record_query_error("redis_error")
# 使用计时器测量数据库查询性能
with monitoring.query_timer():
# 如果没有缓存,从数据库查询,设置超时
try:
# 获取数据库连接
with monitoring.connection_timer():
conn = await asyncio.wait_for(pool.acquire(), timeout=QUERY_TIMEOUT)
cursor = await conn.cursor(aiomysql.DictCursor)
# 构建查询条件
conditions = ["zkzh = %s"]
params = [zkzh_val]
# 打印查询参数
print("查询条件:", conditions, "参数:", params)
# 使用sjh字段进行验证 (密码应该匹配手机号字段)
if query.sj:
conditions.append("sjh = %s")
params.append(query.sj)
else:
raise HTTPException(status_code=400, detail="必须提供手机号作为验证条件")
# 构建SQL查询 - 获取考生信息
sql = f"""
SELECT id, dwdm, dwmc, zwdm, zwmc, zprs, kdmc, kch, zwh, zkzh, xm, sjh, bscj, pm, bz
FROM rsjcjselect
WHERE {' AND '.join(conditions)}
LIMIT 1
"""
# 执行查询
await asyncio.wait_for(cursor.execute(sql, params), timeout=QUERY_TIMEOUT)
result = await asyncio.wait_for(cursor.fetchone(), timeout=QUERY_TIMEOUT)
if not result:
monitoring.record_query_error("no_results")
return ScoreResponse(
success=False,
message="未找到匹配的成绩记录",
data=None
)
# 处理查询结果
student_info = {
"zkzh": result["zkzh"],
"xm": result["xm"],
"sjh": result["sjh"],
"dwmc": result["dwmc"],
"zwmc": result["zwmc"],
"kdmc": result["kdmc"],
"kch": result["kch"],
"zwh": result["zwh"],
"bscj": result["bscj"],
"pm": result["pm"],
"bz": result["bz"]
}
# 打印调试信息
print("查询结果:", student_info)
# 将结果存入Redis缓存
if redis:
try:
await redis.set(cache_key, json.dumps(student_info), ex=CACHE_EXPIRE)
print(f"成绩数据已缓存: {cache_key}, 过期时间: {CACHE_EXPIRE}")
except Exception as e:
print(f"缓存数据到Redis失败: {str(e)}")
monitoring.record_query_error("redis_cache_error")
# 查询成功
return ScoreResponse(
success=True,
message="成绩查询成功",
data=student_info
)
except asyncio.TimeoutError:
monitoring.record_query_error("db_timeout")
raise HTTPException(status_code=504, detail="数据库查询超时,请稍后再试")
except Exception as e:
print("数据库查询错误:", str(e))
monitoring.record_query_error("db_error")
raise HTTPException(status_code=500, detail=f"数据库查询错误: {str(e)}")
except HTTPException:
raise
except Exception as e:
print("查询过程中发生错误:", str(e))
monitoring.record_query_error("general_error")
raise HTTPException(status_code=500, detail=f"查询过程中发生错误: {str(e)}")
finally:
# 确保资源释放
try:
if cursor:
await cursor.close()
if conn:
pool.release(conn)
except Exception as e:
print(f"释放资源错误: {str(e)}")
monitoring.record_query_error("resource_cleanup_error")
# 提交查询到等待室
async def submit_to_waiting_room(query: ScoreQuery):
# 如果等待室已满,拒绝请求
if waiting_room.qsize() >= WAITING_ROOM_CAPACITY:
monitoring.record_query_error("waiting_room_full")
return ScoreResponse(
success=False,
message=f"系统繁忙,等待人数已达上限({WAITING_ROOM_CAPACITY}),请稍后再试",
data=None
), None
# 放入等待室并获取位置
queue_position = waiting_room.qsize() + 1
future = asyncio.Future()
await waiting_room.put((query, future))
# 更新监控指标
monitoring.update_waiting_room_size(waiting_room.qsize())
# 计算预计等待时间每个请求平均处理时间假设为1秒
estimated_wait_time = queue_position * 1 # 秒
return ScoreResponse(
success=True,
message=f"您的请求已提交,当前排队位置: {queue_position},预计等待时间: {estimated_wait_time}",
data=None,
queue_position=queue_position,
estimated_wait_time=estimated_wait_time
), future
# 启动事件
@app.on_event("startup")
async def startup():
# 初始化数据库连接池
db_success = await init_db()
if not db_success:
print("警告: 数据库连接失败,应用程序将以有限功能运行")
print("您可以尝试以下操作:")
print("1. 检查MySQL服务是否已启动")
print("2. 确认config.py中的数据库配置是否正确")
print("3. 运行run_with_test_data.bat或run_with_test_data.sh脚本导入测试数据")
# 初始化Redis连接池
redis_success = await init_redis()
if not redis_success:
print("警告: Redis连接失败将禁用缓存功能")
print("您可以尝试以下操作:")
print("1. 检查Redis服务是否已启动")
print("2. 确认config.py中的Redis配置是否正确")
print("3. 如果不需要Redis可以忽略此警告")
print("成绩查询系统启动完成")
print(f"访问地址: http://{SERVER_HOST}:{SERVER_PORT}")
print("按Ctrl+C停止服务")
# 关闭事件
@app.on_event("shutdown")
async def shutdown():
# 关闭数据库连接池
if pool:
pool.close()
await pool.wait_closed()
print("数据库连接池已关闭")
# 关闭Redis连接池
if redis:
await redis.close()
print("Redis连接池已关闭")
# 根路由 - 返回主页
@app.get("/", response_class=HTMLResponse)
async def read_root():
with open("html/index.html", "r", encoding="utf-8") as f:
html_content = f.read()
return HTMLResponse(content=html_content)
@app.get("/api/test_data")
async def test_data():
"""测试端点,用于检查数据库中的数据"""
if not pool:
return {"error": "数据库连接未初始化"}
async with pool.acquire() as conn:
async with conn.cursor(aiomysql.DictCursor) as cursor:
# 查询所有数据
await cursor.execute("SELECT * FROM rsjcjselect")
results = await cursor.fetchall()
# 统计每个准考证号的科目数量
stats = {}
for row in results:
zkzh = row["zkzh"]
if zkzh not in stats:
stats[zkzh] = {
"xm": row["xm"],
"subjects": []
}
stats[zkzh]["subjects"].append(row["km"])
return {
"total_records": len(results),
"students": stats
}
@app.post("/api/init_test_data")
async def api_init_test_data():
"""手动初始化测试数据的API端点"""
try:
await init_test_data()
return {"success": True, "message": "测试数据初始化成功"}
except Exception as e:
return {"success": False, "message": f"测试数据初始化失败: {str(e)}"}
# 添加缓存清除端点
@app.post("/api/clear_cache")
async def clear_cache(zkzh: Optional[str] = None):
"""清除缓存的API端点
如果提供了准考证号则只清除该学生的缓存
否则清除所有成绩缓存
"""
if not redis:
return {"success": False, "message": "Redis未连接无法清除缓存"}
try:
if zkzh:
# 清除特定学生的缓存
pattern = f"score:{zkzh}:*"
keys = await redis.keys(pattern)
if keys:
count = 0
for key in keys:
await redis.delete(key)
count += 1
return {"success": True, "message": f"已清除准考证号 {zkzh}{count} 条缓存记录"}
else:
return {"success": False, "message": f"未找到准考证号 {zkzh} 的缓存记录"}
else:
# 清除所有成绩缓存
pattern = "score:*"
keys = await redis.keys(pattern)
if keys:
count = 0
for key in keys:
await redis.delete(key)
count += 1
return {"success": True, "message": f"已清除所有 {count} 条成绩缓存记录"}
else:
return {"success": False, "message": "缓存中没有成绩记录"}
except Exception as e:
return {"success": False, "message": f"清除缓存失败: {str(e)}"}
# 挂载静态文件 - 必须在定义所有路由后挂载
app.mount("/static", StaticFiles(directory="static"), name="static")
if __name__ == "__main__":
uvicorn.run("main:app",
host=SERVER_HOST,
port=SERVER_PORT,
reload=False, # 生产环境关闭重载
workers=WORKERS, # 根据配置使用多个工作进程
timeout_keep_alive=TIMEOUT) # 长连接超时时间

74
monitoring.py Normal file
View File

@ -0,0 +1,74 @@
"""
系统监控模块 - 提供性能指标和健康检查
"""
from prometheus_client import Counter, Gauge, Histogram, Summary
import time
# 定义指标
QUERY_COUNTER = Counter('student_grade_queries_total', '成绩查询总次数')
QUERY_ERRORS = Counter('student_grade_query_errors_total', '成绩查询错误次数', ['error_type'])
WAITING_ROOM_SIZE = Gauge('waiting_room_current_size', '等待室当前大小')
CONCURRENT_QUERIES_GAUGE = Gauge('concurrent_queries_current', '当前并发查询数')
CACHE_HIT_COUNTER = Counter('cache_hits_total', '缓存命中次数')
CACHE_MISS_COUNTER = Counter('cache_misses_total', '缓存未命中次数')
QUERY_LATENCY = Histogram('query_latency_seconds', '查询延迟',
buckets=[0.05, 0.1, 0.5, 1.0, 2.5, 5.0, 10.0])
DB_CONNECTION_LATENCY = Summary('db_connection_latency_seconds', '数据库连接延迟')
# 连接池指标
DB_POOL_SIZE = Gauge('db_pool_size', '数据库连接池大小')
DB_POOL_FREE = Gauge('db_pool_free', '数据库连接池空闲连接数')
REDIS_POOL_SIZE = Gauge('redis_pool_size', 'Redis连接池大小')
class Timer:
"""计时器上下文管理器"""
def __init__(self, metric):
self.metric = metric
def __enter__(self):
self.start = time.time()
return self
def __exit__(self, exc_type, exc_val, exc_tb):
self.metric.observe(time.time() - self.start)
def record_query_attempt():
"""记录查询尝试"""
QUERY_COUNTER.inc()
def record_query_error(error_type):
"""记录查询错误"""
QUERY_ERRORS.labels(error_type=error_type).inc()
def update_waiting_room_size(size):
"""更新等待室大小"""
WAITING_ROOM_SIZE.set(size)
def update_concurrent_queries(count):
"""更新当前并发查询数"""
CONCURRENT_QUERIES_GAUGE.set(count)
def record_cache_hit():
"""记录缓存命中"""
CACHE_HIT_COUNTER.inc()
def record_cache_miss():
"""记录缓存未命中"""
CACHE_MISS_COUNTER.inc()
def query_timer():
"""查询计时器"""
return Timer(QUERY_LATENCY)
def connection_timer():
"""连接计时器"""
return Timer(DB_CONNECTION_LATENCY)
def update_pool_metrics(db_pool, redis_client):
"""更新连接池指标"""
if db_pool:
DB_POOL_SIZE.set(db_pool.maxsize)
DB_POOL_FREE.set(db_pool.freesize)
if hasattr(redis_client, 'connection_pool'):
REDIS_POOL_SIZE.set(redis_client.connection_pool.max_connections)

12
requirements.txt Normal file
View File

@ -0,0 +1,12 @@
fastapi>=0.95.0,<0.100.0
uvicorn>=0.21.0
aiomysql>=0.1.1
pydantic>=1.10.7,<2.0.0
python-multipart>=0.0.6
redis>=4.5.1
aioredis>=2.0.0
gunicorn>=20.1.0
prometheus-client>=0.16.0 # 用于监控
ujson>=5.7.0 # 更快的JSON处理
httptools>=0.5.0 # 更快的HTTP
uvloop>=0.17.0 # 更快的事件循环

37
start.sh Normal file
View File

@ -0,0 +1,37 @@
#!/bin/bash
# 加载环境变量
export DB_HOST=${DB_HOST:-"192.140.160.11"}
export DB_PORT=${DB_PORT:-"3306"}
export DB_USER=${DB_USER:-"root"}
export DB_PASSWORD=${DB_PASSWORD:-"Boyue123"}
export DB_NAME=${DB_NAME:-"harsjselect"}
export DB_POOL_SIZE=${DB_POOL_SIZE:-"100"}
export DB_MAX_CONN=${DB_MAX_CONN:-"200"}
export REDIS_HOST=${REDIS_HOST:-"192.140.160.11"}
export REDIS_PORT=${REDIS_PORT:-"6379"}
export REDIS_DB=${REDIS_DB:-"0"}
export REDIS_PASSWORD=${REDIS_PASSWORD:-"boyue123"}
export REDIS_POOL_SIZE=${REDIS_POOL_SIZE:-"100"}
export CACHE_EXPIRE=${CACHE_EXPIRE:-"3600"}
export WAITING_ROOM_CAPACITY=${WAITING_ROOM_CAPACITY:-"15000"}
export CONCURRENT_QUERIES=${CONCURRENT_QUERIES:-"1000"}
export SERVER_HOST=${SERVER_HOST:-"0.0.0.0"}
export SERVER_PORT=${SERVER_PORT:-"80"}
export WORKERS=${WORKERS:-"4"}
export TIMEOUT=${TIMEOUT:-"300"}
# 检查并创建日志目录
LOG_DIR="./logs"
mkdir -p $LOG_DIR
# 启动应用
echo "启动成绩查询系统,使用 $WORKERS 个工作进程"
echo "数据库:$DB_HOST:$DB_PORT/$DB_NAME"
echo "Redis$REDIS_HOST:$REDIS_PORT/$REDIS_DB"
echo "支持最大并发查询:$CONCURRENT_QUERIES,等待室容量:$WAITING_ROOM_CAPACITY"
python3 main.py >> $LOG_DIR/app.log 2>&1

130
static/style.css Normal file
View File

@ -0,0 +1,130 @@
/* 成绩查询系统样式 */
body {
background-color: #f8f9fa;
font-family: "Microsoft YaHei", sans-serif;
}
.container {
max-width: 800px;
margin: 0 auto;
padding: 15px;
}
.card {
border-radius: 15px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
background-color: #fff;
margin-bottom: 20px;
}
.card-header {
background-color: #0066cc;
color: white;
border-radius: 15px 15px 0 0 !important;
padding: 15px;
text-align: center;
}
.card-body {
padding: 20px;
}
.btn-primary {
background-color: #0066cc;
border-color: #0055aa;
color: white;
padding: 10px 20px;
border-radius: 5px;
border: none;
cursor: pointer;
font-size: 16px;
width: 100%;
}
.btn-primary:hover {
background-color: #0055aa;
border-color: #004499;
}
.result-table {
margin-top: 20px;
width: 100%;
border-collapse: collapse;
}
.result-table th, .result-table td {
border: 1px solid #dee2e6;
padding: 8px;
text-align: left;
}
.result-table th {
background-color: #f8f9fa;
}
.loading-indicator {
display: none;
text-align: center;
margin: 20px 0;
}
.alert {
margin-top: 20px;
padding: 15px;
border-radius: 5px;
}
.alert-danger {
background-color: #f8d7da;
color: #721c24;
border: 1px solid #f5c6cb;
}
.alert-success {
background-color: #d4edda;
color: #155724;
border: 1px solid #c3e6cb;
}
.alert-info {
background-color: #d1ecf1;
color: #0c5460;
border: 1px solid #bee5eb;
}
.waiting-info {
display: none;
margin-top: 20px;
}
.form-label {
font-weight: bold;
margin-bottom: 5px;
display: block;
}
.form-control {
width: 100%;
padding: 10px;
border: 1px solid #ced4da;
border-radius: 5px;
margin-bottom: 15px;
}
.progress {
height: 20px;
background-color: #e9ecef;
border-radius: 5px;
margin-top: 10px;
}
.progress-bar {
background-color: #0066cc;
height: 100%;
border-radius: 5px;
}
.text-center {
text-align: center;
}
.text-muted {
color: #6c757d;
}
.small {
font-size: 85%;
}
.fw-bold {
font-weight: bold;
}
.text-success {
color: #28a745;
}
.text-primary {
color: #007bff;
}
.text-warning {
color: #ffc107;
}
.text-danger {
color: #dc3545;
}