mirror of
https://gitee.com/myxzgzs/RSJselet
synced 2025-08-08 00:02:41 +08:00
Initial commit
This commit is contained in:
commit
646a8a3e7a
8
.idea/.gitignore
generated
vendored
Normal file
8
.idea/.gitignore
generated
vendored
Normal 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
8
.idea/RSJselet.iml
generated
Normal 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>
|
6
.idea/inspectionProfiles/Project_Default.xml
generated
Normal file
6
.idea/inspectionProfiles/Project_Default.xml
generated
Normal 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>
|
6
.idea/inspectionProfiles/profiles_settings.xml
generated
Normal file
6
.idea/inspectionProfiles/profiles_settings.xml
generated
Normal 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
6
.idea/misc.xml
generated
Normal 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
8
.idea/modules.xml
generated
Normal 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
103
README.md
Normal 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
38
config.py
Normal 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
197
cstest.py
Normal 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
1083
harsjselect.sql
Normal file
File diff suppressed because it is too large
Load Diff
330
html/index.html
Normal file
330
html/index.html
Normal 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
733
main.py
Normal 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
74
monitoring.py
Normal 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
12
requirements.txt
Normal 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
37
start.sh
Normal 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
130
static/style.css
Normal 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;
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user