log日志+中间件+注释
This commit is contained in:
Generated
+3
-1
@@ -1,7 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<module type="PYTHON_MODULE" version="4">
|
||||
<component name="NewModuleRootManager">
|
||||
<content url="file://$MODULE_DIR$" />
|
||||
<content url="file://$MODULE_DIR$">
|
||||
<excludeFolder url="file://$MODULE_DIR$/.venv" />
|
||||
</content>
|
||||
<orderEntry type="jdk" jdkName="ai0720" jdkType="Python SDK" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
</component>
|
||||
|
||||
+64
@@ -0,0 +1,64 @@
|
||||
# ============================================================
|
||||
# HTTP 请求日志中间件
|
||||
# 功能:拦截每一次 API 请求,记录请求方信息、请求参数、
|
||||
# 响应状态、响应数据以及接口耗时,统一写入 log.txt
|
||||
# 工作流程:
|
||||
# 请求进入 → 记录 method / url → 调用真实接口 → 记录耗时
|
||||
# → 记录响应状态码 & 响应体 → 返回重新组装的 Response
|
||||
# ============================================================
|
||||
|
||||
import time
|
||||
from fastapi import Request
|
||||
from fastapi.responses import Response
|
||||
from Log import Log
|
||||
|
||||
|
||||
async def m1(req: Request, call_next):
|
||||
"""
|
||||
FastAPI HTTP 中间件入口函数。
|
||||
|
||||
参数:
|
||||
req: FastAPI 自动注入的 Request 对象,包含 method / url / body 等
|
||||
call_next: 由中间件机制注入的回调函数,调用它会执行下一个中间件或真正的接口处理器
|
||||
|
||||
注意:
|
||||
中间件里读取响应体后,必须手动重新构造一个 Response 返回;
|
||||
否则 FastAPI 流式响应的 body_iterator 被读完就没了,前端会收到空内容。
|
||||
"""
|
||||
|
||||
# 初始化日志器:日志名 ai0824,默认 INFO 级别
|
||||
log = Log('ai0824', 'INFO')
|
||||
# 将日志输出到 log.txt(会自动追加)
|
||||
log.logFile('log.txt')
|
||||
|
||||
# —— 1. 请求前置记录 ——
|
||||
log.message('Info', '请求方式:' + req.method) # GET / POST / PUT / DELETE ...
|
||||
log.message('Info', '请求地址:' + str(req.url)) # 例如 http://127.0.0.1:12345/basic-information
|
||||
|
||||
# —— 2. 执行真正的接口,记录耗时 ——
|
||||
beginTime = time.time() # 请求开始时间戳(秒)
|
||||
r = await call_next(req) # 调用后续链:下一个中间件或路由处理器
|
||||
endTime = time.time() # 响应返回时间戳
|
||||
|
||||
# —— 3. 读取原始请求体(JSON 参数等)并记录 ——
|
||||
body = await req.body() # body() 是异步方法,必须 await;返回 bytes
|
||||
log.message('Info', '请求参数:' + body.decode('utf-8', 'ignore'))
|
||||
|
||||
# —— 4. 读取流式响应体并记录 ——
|
||||
# call_next 返回的是 StreamingResponse,响应体藏在 body_iterator 里,且只能遍历一次
|
||||
resp_bytes = b''
|
||||
async for chunk in r.body_iterator:
|
||||
resp_bytes += chunk if isinstance(chunk, bytes) else chunk.encode()
|
||||
|
||||
log.message('Info', '响应状态码:' + str(r.status_code))
|
||||
log.message('Info', '响应数据:' + resp_bytes.decode('utf-8', 'ignore'))
|
||||
log.message('Info', '请求耗时:%.2f ms' % ((endTime - beginTime) * 1000))
|
||||
|
||||
# —— 5. 重新构造 Response 返回给前端 ——
|
||||
# 原因:body_iterator 已经被遍历过一次,原始 Response 再返回会导致响应体为空
|
||||
return Response(
|
||||
content=resp_bytes,
|
||||
status_code=r.status_code,
|
||||
headers=dict(r.headers),
|
||||
media_type=r.media_type,
|
||||
)
|
||||
@@ -0,0 +1,90 @@
|
||||
# ============================================================
|
||||
# 日志工具类 — 对标准库 logging 进行简单封装
|
||||
# 提供控制台输出、文件输出、统一 message() 记录三个能力
|
||||
# 避免在各处重复写 logger.handlers / FileHandler / Formatter 等样板代码
|
||||
# ============================================================
|
||||
|
||||
import logging
|
||||
|
||||
|
||||
class Log:
|
||||
"""
|
||||
简易日志封装类。
|
||||
|
||||
用法示例:
|
||||
log = Log('myapp', 'INFO')
|
||||
log.logFile('app.log') # 输出到文件
|
||||
log.logConsol() # 同时输出到控制台
|
||||
log.message('info', '启动成功')
|
||||
|
||||
设计要点:
|
||||
- 同一日志器重复 addHandler 会导致同一条日志输出多次,
|
||||
所以 logFile / logConsol 里做了 handler 去重判断。
|
||||
"""
|
||||
|
||||
def __init__(self, name, lv):
|
||||
"""
|
||||
构造日志器实例。
|
||||
|
||||
参数:
|
||||
name: 日志器名称,同名日志器是全局单例(logging.getLogger 内部维护)
|
||||
lv: 初始日志级别字符串,不区分大小写
|
||||
可选值: 'DEBUG' / 'INFO' / 'WARNING' / 'ERROR' / 'CRITICAL'
|
||||
"""
|
||||
# ① 创建命名日志器
|
||||
self.logName = logging.getLogger(name)
|
||||
|
||||
# ② 设置日志级别(字符串转 logging.XXX 枚举)
|
||||
self.logName.setLevel(eval(f'logging.{lv.upper()}'))
|
||||
|
||||
# ④ 定义统一的日志输出格式:时间 - 名称 - 级别 - 消息
|
||||
self.formatter = logging.Formatter(
|
||||
'%(asctime)s - %(name)s - %(levelname)s - %(message)s',
|
||||
datefmt='%Y-%m-%d %H:%M:%S'
|
||||
)
|
||||
|
||||
# 保存现有 handler 列表,方便后续去重
|
||||
self.handlers = self.logName.handlers
|
||||
|
||||
def logConsol(self):
|
||||
"""将日志输出到控制台(stdout)。"""
|
||||
con_hand = logging.StreamHandler() # ③ 创建控制台处理器
|
||||
con_hand.setFormatter(self.formatter) # 绑定格式
|
||||
# 去重:避免重复添加同一个类型的 handler 导致日志重复打印
|
||||
if 'StreamHandler' not in str(self.handlers):
|
||||
self.logName.addHandler(con_hand) # ⑤ 注册到日志器
|
||||
|
||||
def logFile(self, fileName):
|
||||
"""
|
||||
将日志输出到指定文件(UTF-8 编码)。
|
||||
|
||||
参数:
|
||||
fileName: 日志文件名,例如 'log.txt'
|
||||
"""
|
||||
file_hand = logging.FileHandler(fileName, encoding='utf-8') # ③ 创建文件处理器
|
||||
file_hand.setFormatter(self.formatter)
|
||||
# 去重:同一个文件 handler 只注册一次
|
||||
if 'FileHandler' not in str(self.handlers):
|
||||
self.logName.addHandler(file_hand) # ⑤ 注册到日志器
|
||||
|
||||
def message(self, lv, mes):
|
||||
"""
|
||||
按照指定级别输出一条日志。
|
||||
|
||||
参数:
|
||||
lv: 日志级别字符串(不区分大小写),可选 debug / info / warning / error / critical
|
||||
mes: 日志内容
|
||||
"""
|
||||
lv = lv.lower()
|
||||
if lv == 'debug':
|
||||
self.logName.debug(mes)
|
||||
elif lv == 'info':
|
||||
self.logName.info(mes)
|
||||
elif lv == 'warning':
|
||||
self.logName.warning(mes)
|
||||
elif lv == 'error':
|
||||
self.logName.error(mes)
|
||||
elif lv == 'critical':
|
||||
self.logName.critical(mes)
|
||||
else:
|
||||
print('请输入正确的日志级别!')
|
||||
+124
-56
@@ -1,114 +1,182 @@
|
||||
# ============================================================
|
||||
# API 接口层 — 对外暴露的 HTTP 路由
|
||||
# 职责:
|
||||
# 1. 定义 URL 路径、请求方法(GET/POST)和参数校验规则
|
||||
# 2. 调用 dao 层获取数据
|
||||
# 3. 用 schema 层的 ResponseModel 封装统一响应格式返回给前端
|
||||
# 依赖注入:
|
||||
# db = Depends(get_db) — 由 FastAPI 自动为每个请求创建并回收数据库会话
|
||||
# ============================================================
|
||||
|
||||
from fastapi import APIRouter, Depends, Query
|
||||
from database import get_db
|
||||
from dao.statistics_dao import StatisticsDao
|
||||
from schema.statistics_schema import ResponseModel,ResponseModel1,ResponseModel2,ResponseModel3,ResponseModel4,ResponseModel5,ResponseModel6,ResponseModel7,ResponseModel8
|
||||
from schema.statistics_schema import (
|
||||
ResponseModel, ResponseModel1, ResponseModel2, ResponseModel3,
|
||||
ResponseModel4, ResponseModel5, ResponseModel6, ResponseModel7, ResponseModel8
|
||||
)
|
||||
from math import ceil
|
||||
|
||||
# 统一路由前缀分组,所有接口在 Swagger 里归到"统计分析"标签下
|
||||
BasicInformationAPI = APIRouter(tags=['统计分析'])
|
||||
|
||||
@BasicInformationAPI.get('/basic-information',summary='查询年龄区间内的学员信息')
|
||||
|
||||
@BasicInformationAPI.get('/basic-information', summary='查询年龄区间内的学员信息')
|
||||
def get_students_by_age_range_api(
|
||||
n:int=Query(..., description='页码,从1开始', ge=1),
|
||||
m:int=Query(..., description='每页条数', ge=1),
|
||||
n: int = Query(..., description='页码,从1开始', ge=1),
|
||||
m: int = Query(..., description='每页条数', ge=1),
|
||||
min_age: int = Query(..., description='最小年龄', ge=0),
|
||||
max_age: int = Query(..., description='最大年龄', le=100),
|
||||
db = Depends(get_db)):
|
||||
db=Depends(get_db)
|
||||
):
|
||||
"""
|
||||
按年龄区间分页查询学员基本信息。
|
||||
前端传入 min_age / max_age,后端换算为出生日期区间后在 student_info 表中过滤。
|
||||
"""
|
||||
# 参数合法性校验:最小年龄不能大于最大年龄
|
||||
if min_age > max_age:
|
||||
return {f'code=400,message=最小年龄不能大于最大年龄'}
|
||||
return {'code': 400, 'message': '最小年龄不能大于最大年龄', 'total': 0, 'total_pages': 0, 'data': []}
|
||||
|
||||
req,total = StatisticsDao.get_students_by_age_range_dao(n,m,min_age, max_age,db)
|
||||
# 调用 DAO 层查询
|
||||
req, total = StatisticsDao.get_students_by_age_range_dao(n, m, min_age, max_age, db)
|
||||
|
||||
# 计算总页数:ceil 向上取整;total 为 0 时避免除以 0
|
||||
total_pages = ceil(total / m) if total > 0 else 0
|
||||
|
||||
return ResponseModel(code=200,
|
||||
message='查询成功',
|
||||
total=total,
|
||||
total_pages=ceil(total/m) if total > 0 else 0,
|
||||
total_pages=total_pages,
|
||||
data=req)
|
||||
|
||||
@BasicInformationAPI.get('/basic-information/{class_id}',summary='统计指定班级的⼈数以及男⽣/⼥⽣的⼈数')
|
||||
def get_students_by_class_id_api(class_id:str,
|
||||
db = Depends(get_db)):
|
||||
data = StatisticsDao.get_students_by_class_id_dao(class_id,db)
|
||||
|
||||
@BasicInformationAPI.get('/basic-information/{class_id}', summary='统计指定班级的人数以及男生/女生人数')
|
||||
def get_students_by_class_id_api(class_id: str, db=Depends(get_db)):
|
||||
"""
|
||||
根据班级编号查询该班级的总人数、男生人数、女生人数。
|
||||
URL 路径参数 class_id 由 FastAPI 自动解析。
|
||||
"""
|
||||
data = StatisticsDao.get_students_by_class_id_dao(class_id, db)
|
||||
return ResponseModel7(code=200,
|
||||
message='查询成功',
|
||||
data=data)
|
||||
|
||||
@BasicInformationAPI.get('/class-stats',summary='统计所有班级的人数以及男⽣/⼥⽣的⼈数')
|
||||
def get_class_stats_api(db = Depends(get_db)):
|
||||
|
||||
@BasicInformationAPI.get('/class-stats', summary='统计所有班级的人数以及男生/女生人数')
|
||||
def get_class_stats_api(db=Depends(get_db)):
|
||||
"""
|
||||
一次性返回所有班级的统计结果(不需要分页,数据量通常可控)。
|
||||
"""
|
||||
data = StatisticsDao.get_class_stats_dao(db)
|
||||
return ResponseModel8(code=200,
|
||||
message='查询成功',
|
||||
data=data)
|
||||
|
||||
|
||||
@BasicInformationAPI.get('/scores',summary='在某个分数段的学生')
|
||||
@BasicInformationAPI.get('/scores', summary='查询所有考试成绩都大于某个分数阈值的学生')
|
||||
def get_students_by_score_api(
|
||||
n:int=Query(..., description='页码,从1开始', ge=1),
|
||||
m:int=Query(..., description='每页条数', ge=1),
|
||||
score:float=Query(ge=0,le=100),
|
||||
db = Depends(get_db)):
|
||||
n: int = Query(..., description='页码,从1开始', ge=1),
|
||||
m: int = Query(..., description='每页条数', ge=1),
|
||||
score: float = Query(ge=0, le=100, description='分数阈值,0~100'),
|
||||
db=Depends(get_db)
|
||||
):
|
||||
"""
|
||||
查询最低单科成绩 > score 的学生(即所有考试成绩都超过阈值)。
|
||||
例如 score=60,表示没有任何一门不及格的学生。
|
||||
"""
|
||||
if score < 0 or score > 100:
|
||||
return {f'code=400,message=分数需要在0~100!'}
|
||||
req,total = StatisticsDao.get_students_by_score_dao(n,m,score,db)
|
||||
return {'code': 400, 'message': '分数需要在0~100!', 'total': 0, 'total_pages': 0, 'data': []}
|
||||
|
||||
req, total = StatisticsDao.get_students_by_score_dao(n, m, score, db)
|
||||
|
||||
return ResponseModel1(code=200,
|
||||
message='查询成功',
|
||||
total=total,
|
||||
total_pages=ceil(total/m) if total > 0 else 0,
|
||||
total_pages=ceil(total / m) if total > 0 else 0,
|
||||
data=req)
|
||||
|
||||
@BasicInformationAPI.get('/scores_no_pass',summary='查询有n门成绩不合格的学生')
|
||||
def get_students_by_no_pass_api(
|
||||
n:int=Query(..., description='页码,从1开始', ge=1),
|
||||
m:int=Query(..., description='每页条数', ge=1),
|
||||
fail_count:int=Query(..., description='不及格门数', ge=1),
|
||||
db = Depends(get_db)):
|
||||
req,total = StatisticsDao.get_student_by_no_pass_dao(n,m,fail_count,db)
|
||||
return ResponseModel2(code=200,
|
||||
message='查询成功',
|
||||
total=total,
|
||||
total_pages=ceil(total/m) if total > 0 else 0,
|
||||
data=req)
|
||||
|
||||
@BasicInformationAPI.get('/scores_avg', summary='统计每次考试每个班级的平均分(从高到低排序)')
|
||||
@BasicInformationAPI.get('/scores_no_pass', summary='查询不及格门数 >= fail_count 的学生')
|
||||
def get_students_by_no_pass_api(
|
||||
n: int = Query(..., description='页码,从1开始', ge=1),
|
||||
m: int = Query(..., description='每页条数', ge=1),
|
||||
fail_count: int = Query(..., description='不及格门数阈值', ge=1),
|
||||
db=Depends(get_db)
|
||||
):
|
||||
"""
|
||||
统计每个学生不及格门数,返回不及格门数 >= fail_count 的学生名单。
|
||||
"""
|
||||
req, total = StatisticsDao.get_student_by_no_pass_dao(n, m, fail_count, db)
|
||||
return ResponseModel2(code=200,
|
||||
message='查询成功',
|
||||
total=total,
|
||||
total_pages=ceil(total / m) if total > 0 else 0,
|
||||
data=req)
|
||||
|
||||
|
||||
@BasicInformationAPI.get('/scores_avg', summary='统计每个班级每次考试的平均分(从高到低排序)')
|
||||
def get_class_exam_avg_api(
|
||||
n:int=Query(..., description='页码,从1开始', ge=1),
|
||||
m:int=Query(..., description='每页条数', ge=1),
|
||||
db = Depends(get_db)):
|
||||
req,total = StatisticsDao.get_class_exam_avg_score_dao(n,m,db)
|
||||
n: int = Query(..., description='页码,从1开始', ge=1),
|
||||
m: int = Query(..., description='每页条数', ge=1),
|
||||
db=Depends(get_db)
|
||||
):
|
||||
"""
|
||||
三表联查 student_score → student_info → class_info,
|
||||
按 course_id + class_id 分组,计算每个班级每门课的平均分,按平均分降序排列。
|
||||
"""
|
||||
req, total = StatisticsDao.get_class_exam_avg_score_dao(n, m, db)
|
||||
return ResponseModel3(code=200,
|
||||
message='查询成功',
|
||||
total=total,
|
||||
total_pages=ceil(total/m) if total > 0 else 0,
|
||||
total_pages=ceil(total / m) if total > 0 else 0,
|
||||
data=req)
|
||||
|
||||
@BasicInformationAPI.get('/salary_top', summary='查询就业薪资最高的前五名学生')
|
||||
|
||||
@BasicInformationAPI.get('/salary_top', summary='查询就业薪资前 m 名的学生')
|
||||
def get_salary_top_api(
|
||||
m:int=Query(5, description='查询前m名', ge=1),
|
||||
db = Depends(get_db)):
|
||||
req = StatisticsDao.get_salary_top_dao(m,db)
|
||||
m: int = Query(5, description='返回前 m 名,默认5', ge=1),
|
||||
db=Depends(get_db)
|
||||
):
|
||||
"""
|
||||
查询就业薪资最高的前 m 名学生的姓名、班级、就业公司、就业时间和薪资。
|
||||
每人只取自己历史最高薪资那条记录。
|
||||
"""
|
||||
req = StatisticsDao.get_salary_top_dao(m, db)
|
||||
return ResponseModel4(code=200,
|
||||
message='查询成功',
|
||||
data=req)
|
||||
|
||||
@BasicInformationAPI.get('/time_size', summary='统计每个学生的就业时长(offer下发时间-就业开放时间)')
|
||||
|
||||
@BasicInformationAPI.get('/time_size', summary='统计每个学生的就业时长(offer下发时间 - 开放简历时间)')
|
||||
def get_time_size_api(
|
||||
n:int=Query(..., description='页码,从1开始', ge=1),
|
||||
m:int=Query(..., description='每页条数', ge=1),
|
||||
db = Depends(get_db)):
|
||||
req,total = StatisticsDao.get_time_size_dao(n,m,db)
|
||||
n: int = Query(..., description='页码,从1开始', ge=1),
|
||||
m: int = Query(..., description='每页条数', ge=1),
|
||||
db=Depends(get_db)
|
||||
):
|
||||
"""
|
||||
计算每个学生从开放简历到拿到 offer 的天数差 time_size。
|
||||
只统计开放简历时间不为空的记录。
|
||||
"""
|
||||
req, total = StatisticsDao.get_time_size_dao(n, m, db)
|
||||
return ResponseModel5(code=200,
|
||||
message='查询成功',
|
||||
total=total,
|
||||
total_pages=ceil(total/m) if total > 0 else 0,
|
||||
total_pages=ceil(total / m) if total > 0 else 0,
|
||||
data=req)
|
||||
|
||||
@BasicInformationAPI.get('/class_avg_time_size', summary='统计每个班级的平均就业时长(只统计进入就业阶段的学生)')
|
||||
|
||||
@BasicInformationAPI.get('/class_avg_time_size', summary='统计每个班级的平均就业时长')
|
||||
def get_class_avg_time_size_api(
|
||||
n:int=Query(..., description='页码,从1开始', ge=1),
|
||||
m:int=Query(..., description='每页条数', ge=1),
|
||||
db = Depends(get_db)):
|
||||
req,total = StatisticsDao.get_class_avg_time_size_dao(n,m,db)
|
||||
n: int = Query(..., description='页码,从1开始', ge=1),
|
||||
m: int = Query(..., description='每页条数', ge=1),
|
||||
db=Depends(get_db)
|
||||
):
|
||||
"""
|
||||
按班级分组,计算进入就业阶段(resume_open_date 不为空)学生的平均就业时长。
|
||||
"""
|
||||
req, total = StatisticsDao.get_class_avg_time_size_dao(n, m, db)
|
||||
return ResponseModel6(code=200,
|
||||
message='查询成功',
|
||||
total=total,
|
||||
total_pages=ceil(total/m) if total > 0 else 0,
|
||||
total_pages=ceil(total / m) if total > 0 else 0,
|
||||
data=req)
|
||||
@@ -1,3 +1,10 @@
|
||||
# ============================================================
|
||||
# 数据访问层 DAO — 直接与数据库交互
|
||||
# 职责:接收 API 层传来的参数,拼装 SQLAlchemy 查询,返回查询结果
|
||||
# 所有查询都带有 is_deleted = '0' 过滤,实现逻辑删除
|
||||
# 异常统一捕获并 rollback 后重新抛出,由 API 层决定如何响应
|
||||
# ============================================================
|
||||
|
||||
from model.statistics_model import StudentInfo, StudentScore, ClassInfo, EmploymentInfo
|
||||
from datetime import date
|
||||
from dateutil.relativedelta import relativedelta
|
||||
|
||||
+39
-8
@@ -1,18 +1,49 @@
|
||||
# ============================================================
|
||||
# 数据库连接与会话管理
|
||||
# 负责:
|
||||
# 1. 构建 SQLAlchemy 引擎(engine),建立与 MySQL 的连接
|
||||
# 2. 声明式基类 Base — 所有 ORM 模型都继承它
|
||||
# 3. 提供 FastAPI 依赖注入用的 get_db() 会话生成器
|
||||
# ============================================================
|
||||
|
||||
from sqlalchemy import *
|
||||
from sqlalchemy.orm import declarative_base,sessionmaker
|
||||
from sqlalchemy.orm import declarative_base, sessionmaker
|
||||
|
||||
db_url = "mysql+pymysql://root:123456@127.0.0.1:3306/student?charset=utf8mb4" #创建连接对象
|
||||
engine = create_engine(db_url) #与数据库进行连接
|
||||
# MySQL 连接串格式: mysql+pymysql://用户名:密码@主机:端口/数据库?字符集
|
||||
# 当前项目使用 pymysql 作为底层驱动,数据库为本地 student
|
||||
db_url = "mysql+pymysql://root:123456@127.0.0.1:3306/student?charset=utf8mb4"
|
||||
|
||||
Base = declarative_base() # 执行函数,返回一个基类
|
||||
# 创建 SQLAlchemy 引擎:负责连接池管理、SQL 执行、事务提交等底层工作
|
||||
# 所有 ORM 操作最终都经由 engine 下发到 MySQL
|
||||
engine = create_engine(db_url)
|
||||
|
||||
# 声明式基类:ORM 模型必须继承它才能被 SQLAlchemy 识别为表
|
||||
# main.py 中的 Base.metadata.create_all(engine) 会据此自动建表
|
||||
Base = declarative_base()
|
||||
|
||||
# 会话工厂:每次调用 Session() 就会拿到一个全新的数据库会话
|
||||
# autoflush=False —— 不自动 flush,由我们在事务边界手动控制写入
|
||||
# autocommit=False —— 关闭自动提交,事务由调用方显式 commit / rollback
|
||||
Session = sessionmaker(
|
||||
bind=engine,
|
||||
autoflush=False,
|
||||
autocommit=False,
|
||||
)
|
||||
|
||||
Session = sessionmaker(bind=engine
|
||||
,autoflush=False
|
||||
,autocommit = False
|
||||
)
|
||||
|
||||
def get_db():
|
||||
"""
|
||||
FastAPI 依赖注入函数 —— 为每个请求生成一个独立的数据库会话。
|
||||
|
||||
使用 yield 构成生成器,前半段(yield 之前)在接口执行前拿到 Session,
|
||||
后半段(yield 之后,finally 块)在接口执行完毕后自动 close,
|
||||
确保无论接口正常返回还是抛异常,会话都不会泄漏。
|
||||
|
||||
典型用法(在路由函数里声明 db = Depends(get_db)):
|
||||
def some_api(db = Depends(get_db)):
|
||||
... 使用 db.query(...) 查数据 ...
|
||||
db.commit() # 有写入时手动提交
|
||||
"""
|
||||
db = Session()
|
||||
try:
|
||||
yield db
|
||||
|
||||
@@ -1,12 +1,27 @@
|
||||
# ============================================================
|
||||
# 项目入口主文件 — FastAPI 应用启动与路由装配
|
||||
# 职责:
|
||||
# 1. 初始化 ORM 引擎,自动建表
|
||||
# 2. 创建 FastAPI 实例并配置跨域中间件
|
||||
# 3. 挂载 API 路由和 HTTP 请求日志中间件
|
||||
# 4. 通过 uvicorn 启动服务
|
||||
# ============================================================
|
||||
|
||||
from fastapi import FastAPI
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from database import engine, Base
|
||||
from api.statistics_api import BasicInformationAPI
|
||||
from model import statistics_model #不可以删
|
||||
Base.metadata.create_all(engine) #创建所有的表
|
||||
from HttpLog import m1
|
||||
from model import statistics_model # 必须导入,触发 SQLAlchemy 把模型注册到 Base.metadata
|
||||
|
||||
# 根据 model 中定义的 ORM 模型,在数据库中创建所有表(若已存在则跳过)
|
||||
Base.metadata.create_all(engine)
|
||||
|
||||
# 创建 FastAPI 应用实例
|
||||
app = FastAPI()
|
||||
|
||||
# 配置跨域中间件:允许任意来源、任意方法、任意请求头
|
||||
# 解决前端开发时浏览器的同源策略拦截问题
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=["*"],
|
||||
@@ -14,10 +29,14 @@ app.add_middleware(
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
# 将统计分析模块的路由挂载到主应用,所有路由前缀在路由文件内定义
|
||||
app.include_router(BasicInformationAPI)
|
||||
|
||||
# 注册 HTTP 请求日志中间件:对每一次请求记录方法、地址、参数、响应和耗时
|
||||
app.middleware('http')(m1)
|
||||
|
||||
# 直接运行本文件时,用 uvicorn 启动开发服务器
|
||||
# 地址:http://127.0.0.1:12345
|
||||
if __name__ == '__main__':
|
||||
import uvicorn
|
||||
|
||||
uvicorn.run("main:app", host='0.0.0.0', port=12345)
|
||||
uvicorn.run("main:app", host='127.0.0.1', port=12345)
|
||||
@@ -1,3 +1,9 @@
|
||||
# ============================================================
|
||||
# ORM 模型定义层 — 与 MySQL 数据库表一一对应
|
||||
# 每个类继承 database.py 里的 Base,类属性即表字段
|
||||
# 字段上的 comment 会同步到 MySQL 的字段注释,方便数据库维护
|
||||
# ============================================================
|
||||
|
||||
from sqlalchemy import *
|
||||
from database import DATETIME,Base,Column,Integer,String
|
||||
|
||||
@@ -115,4 +121,4 @@ class CourseInfo(Base):
|
||||
course_type = Column(Enum('1', '2', '3'), nullable=False, comment='课程类型:1 必修课 2 选修课 3 实训课')
|
||||
is_deleted = Column(Enum('0', '1'), nullable=False, comment='逻辑删除:0=未删除, 1=已删除')
|
||||
create_time = Column(DATETIME, nullable=False, comment='创建时间')
|
||||
update_time = Column(DATETIME, nullable=False, comment='更新时间')
|
||||
update_time = Column(DATETIME, nullable=False, comment='更新时间')
|
||||
@@ -1,3 +1,11 @@
|
||||
# ============================================================
|
||||
# 数据校验 / 序列化模型层(Pydantic Schema)
|
||||
# 职责:
|
||||
# 1. 定义接口请求参数和响应数据的结构,自动做类型校验
|
||||
# 2. 作为 FastAPI 自动生成 Swagger 文档的依据
|
||||
# 3. ResponseModel* 系列封装统一响应格式 { code, message, total, total_pages, data }
|
||||
# ============================================================
|
||||
|
||||
from pydantic import BaseModel,ConfigDict # FastAPI 推荐用 pydantic 做数据校验和序列化
|
||||
from datetime import date # 日期类型,用于 birthday、enrollment_date 等字段
|
||||
from typing import List # 类型提示,声明列表
|
||||
|
||||
Reference in New Issue
Block a user