优化数据库连接

This commit is contained in:
2026-05-20 22:26:08 +08:00
parent e6ac98463b
commit 5dcf6859d8
10 changed files with 710 additions and 59 deletions
+86
View File
@@ -11,6 +11,7 @@ from datetime import datetime, timedelta, timezone
from fastapi import APIRouter, HTTPException, WebSocket from fastapi import APIRouter, HTTPException, WebSocket
from fastapi.responses import JSONResponse, StreamingResponse from fastapi.responses import JSONResponse, StreamingResponse
from typing import Dict, Any, List, Optional from typing import Dict, Any, List, Optional
from tortoise import Tortoise
from .service import monitor_service from .service import monitor_service
from .log_stream_service import log_stream_service from .log_stream_service import log_stream_service
from .storage import request_storage, outbound_request_storage, system_log_storage from .storage import request_storage, outbound_request_storage, system_log_storage
@@ -43,6 +44,91 @@ async def health_check():
return await monitor_service.get_health_status() return await monitor_service.get_health_status()
@router.get("/health/database")
async def check_database_health() -> Dict[str, Any]:
"""
检查所有数据库连接状态
Returns:
{
"status": "healthy" | "degraded" | "unhealthy",
"connections": {...},
"tortoise_initialized": bool
}
"""
result = {
"status": "healthy",
"connections": {},
"tortoise_initialized": Tortoise._inited
}
if not Tortoise._inited:
result["status"] = "unhealthy"
result["error"] = "Tortoise ORM 未初始化"
return result
unhealthy_count = 0
for db_name in Tortoise._connections.keys():
try:
conn = Tortoise.get_connection(db_name)
start_time = time.time()
await conn.execute_query("SELECT 1")
response_time_ms = (time.time() - start_time) * 1000
result["connections"][db_name] = {
"status": "healthy",
"response_time_ms": round(response_time_ms, 2),
"error": None
}
except Exception as e:
unhealthy_count += 1
result["connections"][db_name] = {
"status": "unhealthy",
"response_time_ms": None,
"error": str(e)
}
total = len(result["connections"])
if total == 0:
result["status"] = "unhealthy"
result["error"] = "无可用连接"
elif unhealthy_count == total:
result["status"] = "unhealthy"
elif unhealthy_count > 0:
result["status"] = "degraded"
return result
@router.get("/health/database/{db_name}")
async def check_specific_database(db_name: str) -> Dict[str, Any]:
"""检查指定数据库连接状态"""
if not Tortoise._inited:
raise HTTPException(status_code=503, detail="数据库服务初始化中")
try:
conn = Tortoise.get_connection(db_name)
start_time = time.time()
await conn.execute_query("SELECT 1")
response_time_ms = (time.time() - start_time) * 1000
return {
"db_name": db_name,
"status": "healthy",
"response_time_ms": round(response_time_ms, 2)
}
except KeyError:
raise HTTPException(status_code=404, detail=f"连接 '{db_name}' 不存在")
except Exception as e:
raise HTTPException(status_code=500, detail=f"数据库连接失败: {e}")
@router.get("/resource", response_model=ResourceMetrics) @router.get("/resource", response_model=ResourceMetrics)
async def get_resource_metrics(): async def get_resource_metrics():
""" """
+48
View File
@@ -0,0 +1,48 @@
from fastapi import HTTPException
from tortoise import Tortoise
from globalobjects import logger as log_config
from core.settings import THIS_DB_NAME
from typing import Optional
async def get_db_connection_safely(db_name: Optional[str] = None):
"""
安全获取数据库连接,包含异常处理和友好提示
Args:
db_name: 数据库连接名称,默认使用THIS_DB_NAME
Returns:
数据库连接对象
Raises:
HTTPException: 数据库连接失败时返回500错误
"""
if db_name is None:
db_name = THIS_DB_NAME
try:
if not Tortoise._inited:
log_config.error(f"❌ Tortoise ORM 未初始化,无法获取连接: {db_name}")
raise HTTPException(
status_code=500,
detail="数据库服务初始化失败,请检查服务配置或稍后重试"
)
conn = Tortoise.get_connection(db_name)
return conn
except KeyError:
log_config.error(f"❌ 数据库连接不存在: {db_name}")
raise HTTPException(
status_code=500,
detail="数据库连接配置错误,请联系管理员"
)
except Exception as e:
if isinstance(e, HTTPException):
raise
log_config.error(f"❌ 获取数据库连接异常: {db_name} - {type(e).__name__}: {e}")
raise HTTPException(
status_code=500,
detail="数据库连接失败,请检查服务配置或稍后重试"
)
+4 -4
View File
@@ -203,7 +203,7 @@ class MoPushModel(PydanticModel):
""" """
整理推送T+MO数据 整理推送T+MO数据
""" """
ExternalCode: str = Field(None) ExternalCode: Optional[str] = Field(None)
BusiType: dict = Field(None) BusiType: dict = Field(None)
Department: dict = Field(None) Department: dict = Field(None)
Customer: dict = Field(None) Customer: dict = Field(None)
@@ -332,9 +332,9 @@ class PrPushModel(PydanticModel):
""" """
整理推送T+请购单数据 整理推送T+请购单数据
""" """
ExternalCode: str = Field(None) ExternalCode: Optional[str] = Field(None)
Code: str = Field(None) Code: Optional[str] = Field(None)
VoucherDate: str = Field(None) VoucherDate: Optional[str] = Field(None)
RequisitionPerson: dict = Field(...) RequisitionPerson: dict = Field(...)
PurchaseRequisitionDetails: list[dict] = Field(...) PurchaseRequisitionDetails: list[dict] = Field(...)
+13 -12
View File
@@ -18,6 +18,7 @@ from .staging_cleaner import StagingProcessor, DataTransformer, STAGING_TABLE_CO
from .config_generator import TABLE_DISPLAY_CONFIG, SYSTEM_RUNTIME_CONFIG from .config_generator import TABLE_DISPLAY_CONFIG, SYSTEM_RUNTIME_CONFIG
from apps.io_api.utils.common import standard_response from apps.io_api.utils.common import standard_response
from apps.io_api.utils.db_operation import db_bupsert from apps.io_api.utils.db_operation import db_bupsert
from apps.common.utils.db_helpers import get_db_connection_safely
from core.settings import MYAPS_MAIN_DB, THIS_DB_NAME, MYAPS_DBSET_LIST from core.settings import MYAPS_MAIN_DB, THIS_DB_NAME, MYAPS_DBSET_LIST
from globalobjects import logger as log_config from globalobjects import logger as log_config
@@ -161,7 +162,7 @@ async def insert_to_staging_table(
if exclude_fields is None: if exclude_fields is None:
exclude_fields = EXCLUDE_FIELDS exclude_fields = EXCLUDE_FIELDS
conn = Tortoise.get_connection(THIS_DB_NAME) conn = await get_db_connection_safely(THIS_DB_NAME)
# 获取字段映射:Python字段名(小写) -> 数据库字段名(大驼峰) # 获取字段映射:Python字段名(小写) -> 数据库字段名(大驼峰)
field_map = {} field_map = {}
@@ -319,7 +320,7 @@ async def sync_to_production(
if reset_retry: if reset_retry:
staging_model = STAGING_MODEL_MAPPING.get(table_name) staging_model = STAGING_MODEL_MAPPING.get(table_name)
if staging_model: if staging_model:
conn = Tortoise.get_connection(THIS_DB_NAME) conn = await get_db_connection_safely(THIS_DB_NAME)
staging_table_name = staging_model._meta.db_table staging_table_name = staging_model._meta.db_table
reset_query = f'UPDATE "{staging_table_name}" SET "_retry_count" = 0 WHERE "_status" = $1' reset_query = f'UPDATE "{staging_table_name}" SET "_retry_count" = 0 WHERE "_status" = $1'
await conn.execute_query(reset_query, ("relation_pass",)) await conn.execute_query(reset_query, ("relation_pass",))
@@ -360,7 +361,7 @@ async def sync_to_production(
staging_model = STAGING_MODEL_MAPPING.get(table_name) staging_model = STAGING_MODEL_MAPPING.get(table_name)
if staging_model: if staging_model:
conn = Tortoise.get_connection(THIS_DB_NAME) conn = await get_db_connection_safely(THIS_DB_NAME)
staging_table_name = staging_model._meta.db_table staging_table_name = staging_model._meta.db_table
synced_time = datetime.now(timezone.utc) synced_time = datetime.now(timezone.utc)
@@ -726,7 +727,7 @@ async def get_staging_status(
raise ValueError(f"未知的缓冲表: {table_name}") raise ValueError(f"未知的缓冲表: {table_name}")
# 使用原生SQL查询,确保与同步查询条件一致 # 使用原生SQL查询,确保与同步查询条件一致
conn = Tortoise.get_connection(THIS_DB_NAME) conn = await get_db_connection_safely(THIS_DB_NAME)
table_name_staging = staging_model._meta.db_table table_name_staging = staging_model._meta.db_table
stats = {} stats = {}
@@ -790,7 +791,7 @@ async def get_monitor_summary(request: Request):
from tortoise import Tortoise from tortoise import Tortoise
from core.settings import THIS_DB_NAME from core.settings import THIS_DB_NAME
conn = Tortoise.get_connection(THIS_DB_NAME) conn = await get_db_connection_safely(THIS_DB_NAME)
tables = [ tables = [
"t_material_staging", "t_material_staging",
@@ -858,7 +859,7 @@ async def cleanup_old_data(
from datetime import timedelta from datetime import timedelta
from core.settings import THIS_DB_NAME from core.settings import THIS_DB_NAME
conn = Tortoise.get_connection(THIS_DB_NAME) conn = await get_db_connection_safely(THIS_DB_NAME)
cutoff_date = datetime.now() - timedelta(days=days) cutoff_date = datetime.now() - timedelta(days=days)
@@ -1045,7 +1046,7 @@ async def list_staging(
raise ValueError(f"未知的缓冲表: {table_name}") raise ValueError(f"未知的缓冲表: {table_name}")
table_name_staging = f"{table_name}_staging" table_name_staging = f"{table_name}_staging"
conn = Tortoise.get_connection(THIS_DB_NAME) conn = await get_db_connection_safely(THIS_DB_NAME)
conditions = [] conditions = []
params = [] params = []
@@ -1197,7 +1198,7 @@ async def batch_update_staging(
raise ValueError("缺少必要参数: ids或updates") raise ValueError("缺少必要参数: ids或updates")
table_name_staging = f"{table_name}_staging" table_name_staging = f"{table_name}_staging"
conn = Tortoise.get_connection(THIS_DB_NAME) conn = await get_db_connection_safely(THIS_DB_NAME)
field_mapping = {} field_mapping = {}
field_types = {} field_types = {}
@@ -1270,7 +1271,7 @@ async def get_staging_detail(
raise ValueError(f"未知的缓冲表: {table_name}") raise ValueError(f"未知的缓冲表: {table_name}")
table_name_staging = f"{table_name}_staging" table_name_staging = f"{table_name}_staging"
conn = Tortoise.get_connection(THIS_DB_NAME) conn = await get_db_connection_safely(THIS_DB_NAME)
query = f'SELECT * FROM "{table_name_staging}" WHERE "_staging_id" = $1' query = f'SELECT * FROM "{table_name_staging}" WHERE "_staging_id" = $1'
result = await conn.execute_query(query, (staging_id,)) result = await conn.execute_query(query, (staging_id,))
@@ -1313,7 +1314,7 @@ async def update_staging(
raise ValueError(f"未知的缓冲表: {table_name}") raise ValueError(f"未知的缓冲表: {table_name}")
table_name_staging = f"{table_name}_staging" table_name_staging = f"{table_name}_staging"
conn = Tortoise.get_connection(THIS_DB_NAME) conn = await get_db_connection_safely(THIS_DB_NAME)
field_map = {} field_map = {}
field_types = {} field_types = {}
@@ -1389,7 +1390,7 @@ async def delete_staging(
raise ValueError(f"未知的缓冲表: {table_name}") raise ValueError(f"未知的缓冲表: {table_name}")
table_name_staging = f"{table_name}_staging" table_name_staging = f"{table_name}_staging"
conn = Tortoise.get_connection(THIS_DB_NAME) conn = await get_db_connection_safely(THIS_DB_NAME)
query = f'DELETE FROM "{table_name_staging}" WHERE "_staging_id" = $1' query = f'DELETE FROM "{table_name_staging}" WHERE "_staging_id" = $1'
await conn.execute_query(query, (staging_id,)) await conn.execute_query(query, (staging_id,))
@@ -1418,7 +1419,7 @@ async def batch_delete_staging(
raise ValueError("staging_ids不能为空") raise ValueError("staging_ids不能为空")
table_name_staging = f"{table_name}_staging" table_name_staging = f"{table_name}_staging"
conn = Tortoise.get_connection(THIS_DB_NAME) conn = await get_db_connection_safely(THIS_DB_NAME)
placeholders = ", ".join([f"${i+1}" for i in range(len(staging_ids))]) placeholders = ", ".join([f"${i+1}" for i in range(len(staging_ids))])
query = f'DELETE FROM "{table_name_staging}" WHERE "_staging_id" IN ({placeholders})' query = f'DELETE FROM "{table_name_staging}" WHERE "_staging_id" IN ({placeholders})'
+1 -1
View File
@@ -271,7 +271,7 @@ async def post_material(
async def run_matver_task(): async def run_matver_task():
try: try:
await post_mat_ver(data=matver_data, db_name=db_name, x_api_key=x_api_key) await post_mat_ver(request=request, data=matver_data, db_name=db_name, x_api_key=x_api_key)
except Exception as e: except Exception as e:
logger.error(f"Error in post_mat_ver background task: {e}") logger.error(f"Error in post_mat_ver background task: {e}")
+20 -20
View File
@@ -48,7 +48,7 @@ def _set_raw_input_data(self):
class AcceptMaterial(BaseModel): class AcceptMaterial(BaseModel):
materialno: str = Field(..., description="料号", example="M001") materialno: str = Field(..., description="料号", example="M001")
description: str = Field(..., description="物料名称", example="测试物料A") description: str = Field(..., description="物料名称", example="测试物料A")
size: str = Field(None, description="规格", example="100x100mm") size: Optional[str] = Field(None, description="规格", example="100x100mm")
plant: str = Field(pdv.MAT_PLANT, example=pdv.MAT_PLANT, description='工厂') plant: str = Field(pdv.MAT_PLANT, example=pdv.MAT_PLANT, description='工厂')
planner: str = Field(pdv.MAT_PLANNER, description="计划员", example="张三") planner: str = Field(pdv.MAT_PLANNER, description="计划员", example="张三")
fifo: int = Field(pdv.MAT_FIFO, ge=0, le=1, description='1-FIFO 0-最近原则') fifo: int = Field(pdv.MAT_FIFO, ge=0, le=1, description='1-FIFO 0-最近原则')
@@ -67,19 +67,19 @@ class AcceptMaterial(BaseModel):
candelay: gc.YesNoEnum = Field(pdv.MAT_CANDELAY, example="N", description='可否延迟') candelay: gc.YesNoEnum = Field(pdv.MAT_CANDELAY, example="N", description='可否延迟')
lotsize: gc.LotSizeEnum = Field(pdv.MAT_LOTSIZE, example="EX", description='批量') lotsize: gc.LotSizeEnum = Field(pdv.MAT_LOTSIZE, example="EX", description='批量')
lotfix: float = Field(pdv.MAT_LOTFIX, ge=0, description='固定批', example=0.0) lotfix: float = Field(pdv.MAT_LOTFIX, ge=0, description='固定批', example=0.0)
lotmin: float = Field(None, ge=0, description='最小批', example=0.0) lotmin: float | None = Field(None, ge=0, description='最小批', example=0.0)
lotmax: float = Field(None, ge=0, description='最大批', example=0.0) lotmax: float | None = Field(None, ge=0, description='最大批', example=0.0)
lotround: float = Field(pdv.MAT_LOTROUND, ge=0, description='取整', example=0.0) lotround: float = Field(pdv.MAT_LOTROUND, ge=0, description='取整', example=0.0)
lotss: float = Field(pdv.MAT_LOTSS, ge=0, description='安全库存', example=0.0) lotss: float = Field(pdv.MAT_LOTSS, ge=0, description='安全库存', example=0.0)
lotpoint: float = Field(pdv.MAT_LOTPOINT, ge=0, description='重订货点', example=0.0) lotpoint: float = Field(pdv.MAT_LOTPOINT, ge=0, description='重订货点', example=0.0)
lottop: float = Field(pdv.MAT_LOTTOP, ge=0, description='最大库存点', example=0.0) lottop: float = Field(pdv.MAT_LOTTOP, ge=0, description='最大库存点', example=0.0)
planitem: str = Field(None, description='产品组', example="PI001") planitem: Optional[str] = Field(None, description='产品组', example="PI001")
preday: int = Field(pdv.MAT_PREDAY, ge=0, description='向前冲销(天)', example=999) preday: int = Field(pdv.MAT_PREDAY, ge=0, description='向前冲销(天)', example=999)
subday: int = Field(pdv.MAT_SUBDAY, ge=0, description='向后冲销(天)', example=999) subday: int = Field(pdv.MAT_SUBDAY, ge=0, description='向后冲销(天)', example=999)
free1: Optional[str] = Field(None, max_length=255, description='自定义1', example="自定义内容。。。") free1: Optional[str] = Field(None, max_length=255, description='自定义1', example="自定义内容。。。")
free2: Optional[str] = Field(None, max_length=255, description='自定义2', example="自定义内容。。。") free2: Optional[str] = Field(None, max_length=255, description='自定义2', example="自定义内容。。。")
free3: Optional[str] = Field(None, max_length=255, description='自定义3', example="自定义内容。。。") free3: Optional[str] = Field(None, max_length=255, description='自定义3', example="自定义内容。。。")
memo: str = Field(None, description='备注', example="无特殊要求") memo: Optional[str] = Field(None, description='备注', example="无特殊要求")
_raw_input_data: Dict[str, Any] = PrivateAttr(default=None) _raw_input_data: Dict[str, Any] = PrivateAttr(default=None)
class Config: class Config:
@@ -187,9 +187,9 @@ class AcceptWorkcenter(BaseModel):
workcentername: str = Field(..., max_length=255, description="工作中心名称", example="装配车间") workcentername: str = Field(..., max_length=255, description="工作中心名称", example="装配车间")
pri_wc: int = Field(pdv.WC_PRIORITY, description='优先级', example=1) pri_wc: int = Field(pdv.WC_PRIORITY, description='优先级', example=1)
bottleneck: gc.YesNoEnum = Field(None, example="N", description='瓶颈') bottleneck: gc.YesNoEnum = Field(None, example="N", description='瓶颈')
sortno: str = Field(None, max_length=4, description="序号", example="0001") sortno: Optional[str] = Field(None, max_length=4, description="序号", example="0001")
plant: str = Field(pdv.MAT_PLANT, max_length=32, description="工厂", example="1600") plant: str = Field(pdv.MAT_PLANT, max_length=32, description="工厂", example="1600")
location: str = Field(None, max_length=32, description="车间", example="A区") location: Optional[str] = Field(None, max_length=32, description="车间", example="A区")
finite: gc.YesNoEnum = Field(gc.YesNoEnum.YES, example="N", description='有限') finite: gc.YesNoEnum = Field(gc.YesNoEnum.YES, example="N", description='有限')
type: gc.YesNoEnum = Field(gc.YesNoEnum.YES, example="N", description="首页显示") type: gc.YesNoEnum = Field(gc.YesNoEnum.YES, example="N", description="首页显示")
capnum: int | None = Field(pdv.WC_CAPNUM, gt=0, description="默认机台数", example=6) capnum: int | None = Field(pdv.WC_CAPNUM, gt=0, description="默认机台数", example=6)
@@ -197,7 +197,7 @@ class AcceptWorkcenter(BaseModel):
worker: float = Field(pdv.WC_WORKER, ge=0, description='工时', example=8.0) worker: float = Field(pdv.WC_WORKER, ge=0, description='工时', example=8.0)
setupno: str | None = Field(None, max_length=6, description='切换组别', example="S001") setupno: str | None = Field(None, max_length=6, description='切换组别', example="S001")
grpno: str | None = Field(None, max_length=6, description='同组号', example="G001") grpno: str | None = Field(None, max_length=6, description='同组号', example="G001")
memo: str = Field(None, max_length=255, description="备注", example="标准工作中心") memo: Optional[str] = Field(None, max_length=255, description="备注", example="标准工作中心")
_raw_input_data: Dict[str, Any] = PrivateAttr(default=None) _raw_input_data: Dict[str, Any] = PrivateAttr(default=None)
class Config: class Config:
@@ -280,7 +280,7 @@ class AcceptWorkcenter(BaseModel):
class AcceptMatWc(BaseModel): class AcceptMatWc(BaseModel):
materialno: str = Field(..., max_length=64, description='料号', example="M001") materialno: str = Field(..., max_length=64, description='料号', example="M001")
matver: str = Field(..., max_length=4, example=pdv.MATVER, description='产线版本') matver: str = Field(..., max_length=4, example=pdv.MATVER, description='产线版本')
itemno: str = Field(None, max_length=6, description='工序项目', example=pdv.ITEMNO) itemno: Optional[str] = Field(None, max_length=6, description='工序项目', example=pdv.ITEMNO)
workcenter: str = Field(..., max_length=32, description='工作中心', example="WC001") workcenter: str = Field(..., max_length=32, description='工作中心', example="WC001")
sortno: int = Field(..., ge=0, le=999, description='序号', example=1) sortno: int = Field(..., ge=0, le=999, description='序号', example=1)
basesec: float = Field(..., ge=0, description='节拍T/T(秒/100)', example=600) basesec: float = Field(..., ge=0, description='节拍T/T(秒/100)', example=600)
@@ -289,7 +289,7 @@ class AcceptMatWc(BaseModel):
sf: gc.SfEnum = Field(gc.SfEnum.F, example="F", description='并行S/串行F') sf: gc.SfEnum = Field(gc.SfEnum.F, example="F", description='并行S/串行F')
offsetsec: int = Field(0, description='偏置+/-(秒)', example=0) offsetsec: int = Field(0, description='偏置+/-(秒)', example=0)
rate: float = Field(pdv.MATWC_RATE, ge=0, description='配比', example=pdv.MATWC_RATE) rate: float = Field(pdv.MATWC_RATE, ge=0, description='配比', example=pdv.MATWC_RATE)
memo: str = Field(None, max_length=255, description='备注', example="标准工序") memo: Optional[str] = Field(None, max_length=255, description='备注', example="标准工序")
_raw_input_data: Dict[str, Any] = PrivateAttr(default=None) _raw_input_data: Dict[str, Any] = PrivateAttr(default=None)
class Config: class Config:
@@ -321,7 +321,7 @@ class AcceptMatWc(BaseModel):
values["sortno"] = int(float(values["sortno"])) values["sortno"] = int(float(values["sortno"]))
except: except:
values["sortno"] = None values["sortno"] = None
if values.get("itemno") in gc.NONE_AND_EMPTY and values["sortno"]: if values.get("itemno") in gc.NONE_AND_EMPTY and "sortno" in values:
values["itemno"] = f"{pdv.itemno_prefix}{values['sortno']:0{pdv.itemno_width}d}" values["itemno"] = f"{pdv.itemno_prefix}{values['sortno']:0{pdv.itemno_width}d}"
try: try:
values["basesec"] = float(values["basesec"]) values["basesec"] = float(values["basesec"])
@@ -367,9 +367,9 @@ class AcceptMatVer(BaseModel):
lotfrom: int = Field(pdv.MATVER_LOTFROM, description='批量起点', example=1) lotfrom: int = Field(pdv.MATVER_LOTFROM, description='批量起点', example=1)
lotto: int = Field(pdv.MATVER_LOTTO, description='批量终点', example=9999999) lotto: int = Field(pdv.MATVER_LOTTO, description='批量终点', example=9999999)
priority: int = Field(pdv.MATVER_PRIORITY, description='优先级', example=1) priority: int = Field(pdv.MATVER_PRIORITY, description='优先级', example=1)
refno: str = Field(None, max_length=64, description='MTO订单号/认证线', example="SO123456") refno: Optional[str] = Field(None, max_length=64, description='MTO订单号/认证线', example="SO123456")
active: gc.YesNoEnum = Field(gc.YesNoEnum.YES, example="Y", description='生效') active: gc.YesNoEnum = Field(gc.YesNoEnum.YES, example="Y", description='生效')
memo: str = Field(None, max_length=255, description='备注', example="标准版本") memo: Optional[str] = Field(None, max_length=255, description='备注', example="标准版本")
_raw_input_data: Dict[str, Any] = PrivateAttr(default=None) _raw_input_data: Dict[str, Any] = PrivateAttr(default=None)
class Config: class Config:
@@ -433,7 +433,7 @@ class AcceptMatWcBom(BaseModel):
mto: gc.YesNoEnum = Field(gc.YesNoEnum.NO, example="N", description='MTO') mto: gc.YesNoEnum = Field(gc.YesNoEnum.NO, example="N", description='MTO')
scrap: float = Field(0, ge=0, description='报废率%', example=0.0) scrap: float = Field(0, ge=0, description='报废率%', example=0.0)
alt: gc.YesNoEnum = Field(gc.YesNoEnum.NO, example="N", description='是否是替代') alt: gc.YesNoEnum = Field(gc.YesNoEnum.NO, example="N", description='是否是替代')
memo: str = Field(None, max_length=255, description='备注', example="标准BOM组件") memo: Optional[str] = Field(None, max_length=255, description='备注', example="标准BOM组件")
denominator: Optional[float | str] = Field(None, description='用量分母', example=1) denominator: Optional[float | str] = Field(None, description='用量分母', example=1)
_raw_input_data: Dict[str, Any] = PrivateAttr(default=None) _raw_input_data: Dict[str, Any] = PrivateAttr(default=None)
@@ -500,7 +500,7 @@ class AcceptMold(BaseModel):
status: str = Field(..., max_length=6, description='状态', example="AVL") status: str = Field(..., max_length=6, description='状态', example="AVL")
moldnum: int = Field(..., ge=1, description='模具穴数', example=4) moldnum: int = Field(..., ge=1, description='模具穴数', example=4)
qty: int = Field(..., gt=1, description='模具台数', example=2) qty: int = Field(..., gt=1, description='模具台数', example=2)
memo: str = Field(None, max_length=255, description="备注", example="标准模具") memo: Optional[str] = Field(None, max_length=255, description="备注", example="标准模具")
_raw_input_data: Dict[str, Any] = PrivateAttr(default=None) _raw_input_data: Dict[str, Any] = PrivateAttr(default=None)
class Config: class Config:
@@ -549,7 +549,7 @@ class AcceptMatWcMold(BaseModel):
basesec: float = Field(..., ge=0, description='节拍T/T(秒/100)', example=600) basesec: float = Field(..., ge=0, description='节拍T/T(秒/100)', example=600)
fixsec: int = Field(0, ge=0, description='额定时间(秒)', example=300) fixsec: int = Field(0, ge=0, description='额定时间(秒)', example=300)
priority: int = Field(..., description='优先级', example=1) priority: int = Field(..., description='优先级', example=1)
memo: str = Field(None, max_length=255, description='备注', example="标准机台模具配置") memo: Optional[str] = Field(None, max_length=255, description='备注', example="标准机台模具配置")
_raw_input_data: Dict[str, Any] = PrivateAttr(default=None) _raw_input_data: Dict[str, Any] = PrivateAttr(default=None)
class Config: class Config:
@@ -602,7 +602,7 @@ class AcceptSupply(BaseModel):
materialno: str = Field(..., max_length=64, description='料号', example="M001") materialno: str = Field(..., max_length=64, description='料号', example="M001")
supplyno: str = Field(..., max_length=64, description='供应单号', example="MO123456") supplyno: str = Field(..., max_length=64, description='供应单号', example="MO123456")
matver: Optional[str] = Field(None, max_length=32, example=pdv.MATVER, description='产线版本') matver: Optional[str] = Field(None, max_length=32, example=pdv.MATVER, description='产线版本')
itemno: str = Field(None, max_length=6, description='项目号', example=pdv.ITEMNO) itemno: Optional[str] = Field(None, max_length=6, description='项目号', example=pdv.ITEMNO)
type: gc.SupplyTypeEnum = Field(..., example="MO", description='类型 PL-生产计划 MO-生产工单 ST-库存 PO-采购订单') type: gc.SupplyTypeEnum = Field(..., example="MO", description='类型 PL-生产计划 MO-生产工单 ST-库存 PO-采购订单')
category: gc.ProductCategoryEnum = Field(gc.ProductCategoryEnum.MTS, example="MTS", description='分类(MTO/MTS)') category: gc.ProductCategoryEnum = Field(gc.ProductCategoryEnum.MTS, example="MTS", description='分类(MTO/MTS)')
priority: int = Field(0, description='优先级', example=0) priority: int = Field(0, description='优先级', example=0)
@@ -700,7 +700,7 @@ class AcceptSupply(BaseModel):
class ModifySupply(BaseModel): class ModifySupply(BaseModel):
supplyno: str = Field(None, max_length=64, description='供应号', example="MO123456") supplyno: Optional[str] = Field(None, max_length=64, description='供应号', example="MO123456")
status: gc.OrderStatusEnum = Field(None, status: gc.OrderStatusEnum = Field(None,
example="CRE", description=f'状态 {list(gc.OrderStatusEnum.__members__.keys())}') example="CRE", description=f'状态 {list(gc.OrderStatusEnum.__members__.keys())}')
avail_qty: Optional[float] = Field(None, ge=0, description='可用数量', example=100.0) avail_qty: Optional[float] = Field(None, ge=0, description='可用数量', example=100.0)
@@ -754,7 +754,7 @@ class AcceptDemand(BaseModel):
type: gc.DemandTypeEnum = Field(..., example="SO", description='类型 SO-销售订单 DM-计划需求 RS-工单预留 FC-预测 SS-安全库存') type: gc.DemandTypeEnum = Field(..., example="SO", description='类型 SO-销售订单 DM-计划需求 RS-工单预留 FC-预测 SS-安全库存')
category: gc.ProductCategoryEnum = Field(gc.ProductCategoryEnum.MTS, example="MTS", description='分类(MTO/MTS)') category: gc.ProductCategoryEnum = Field(gc.ProductCategoryEnum.MTS, example="MTS", description='分类(MTO/MTS)')
priority: int = Field(..., description='优先级', example=1) priority: int = Field(..., description='优先级', example=1)
workcenter: str = Field(None, max_length=32, description='工作中心', example="WC001") workcenter: Optional[str] = Field(None, max_length=32, description='工作中心', example="WC001")
status: gc.OrderStatusEnum = Field("CRE", example="CRE", description=f'状态 {list(gc.OrderStatusEnum.__members__.keys())}') status: gc.OrderStatusEnum = Field("CRE", example="CRE", description=f'状态 {list(gc.OrderStatusEnum.__members__.keys())}')
req_qty: float = Field(..., description='需求数量(须为负数,若输入正数则自动转为负数)', example=-100.0) req_qty: float = Field(..., description='需求数量(须为负数,若输入正数则自动转为负数)', example=-100.0)
req_date: datetime = Field(..., description='需求日期', example="2023-01-07T10:00:00") req_date: datetime = Field(..., description='需求日期', example="2023-01-07T10:00:00")
@@ -876,7 +876,7 @@ class AcceptConfirm(BaseModel):
recordqty: float = Field(..., description='报工数量', gt=0, example=100) recordqty: float = Field(..., description='报工数量', gt=0, example=100)
recorddt: datetime = Field(..., description='报工日期', example="2025-01-07 10:00:00") recorddt: datetime = Field(..., description='报工日期', example="2025-01-07 10:00:00")
status: gc.YesNoEnum = Field(..., description='状态') status: gc.YesNoEnum = Field(..., description='状态')
sysuser: str = Field(None, max_length=32, description='系统用户', example="张三") sysuser: Optional[str] = Field(None, max_length=32, description='系统用户', example="张三")
_raw_input_data: Dict[str, Any] = PrivateAttr(default=None) _raw_input_data: Dict[str, Any] = PrivateAttr(default=None)
class Config: class Config:
+138 -15
View File
@@ -87,6 +87,14 @@ TORTOISE_ORM_CONFIG = {
} }
if THIS_DB_NAME: if THIS_DB_NAME:
model_path = "apps.data_opt.mds.staging_models"
try:
__import__(model_path)
log_config.info(f"✅ 模型模块导入成功: {model_path}")
except ImportError as e:
log_config.error(f"❌ 模型模块导入失败: {model_path} - {e}")
raise
connections[THIS_DB_NAME] = { connections[THIS_DB_NAME] = {
"engine": "tortoise.backends.asyncpg", "engine": "tortoise.backends.asyncpg",
"credentials": { "credentials": {
@@ -95,16 +103,106 @@ if THIS_DB_NAME:
"user": THIS_DB_USER, "user": THIS_DB_USER,
"password": THIS_DB_PASSWORD, "password": THIS_DB_PASSWORD,
"database": THIS_DB_NAME, "database": THIS_DB_NAME,
"server_settings": {"TimeZone": TIMEZONE_NAME}, "server_settings": {
"TimeZone": TIMEZONE_NAME,
"application_name": "myaps_api",
},
"command_timeout": 60,
"timeout": 30,
}, },
"min_size": 3, "min_size": 3,
"max_size": 10, "max_size": 10,
"use_tz": True, "use_tz": True,
"pool_recycle": 1800,
} }
log_config.info(f"✅ PostgreSQL连接配置完成: {THIS_DB_NAME}@{THIS_DB_HOST}:{THIS_DB_PORT}")
TORTOISE_ORM_CONFIG["apps"]["data_opt_models"] = { TORTOISE_ORM_CONFIG["apps"]["data_opt_models"] = {
"models": ["apps.data_opt.mds.staging_models"], "models": [model_path],
"default_connection": THIS_DB_NAME, "default_connection": THIS_DB_NAME,
} }
log_config.info(f"✅ 模型注册完成: data_opt_models -> {THIS_DB_NAME}")
def validate_database_config() -> Dict[str, Any]:
"""
验证数据库配置完整性和一致性
多租户场景说明
- THIS_DB_* 为空是正常情况表示该租户不使用自有数据库
- 仅当 THIS_DB_NAME 有值时才验证配套参数的完整性
Returns:
配置摘要字典
Raises:
ValueError: 配置验证失败时抛出仅在 THIS_DB_NAME 有值时
"""
import json
issues = []
warnings = []
if not THIS_DB_NAME:
log_config.info("ℹ️ 该租户未配置自有数据库(THIS_DB_NAME为空),跳过 PostgreSQL 配置验证")
config_summary = {
"has_own_database": False,
"timezone": TIMEZONE_NAME,
"connections": list(connections.keys()),
"apps": list(TORTOISE_ORM_CONFIG["apps"].keys()),
}
log_config.info(f"配置摘要: {json.dumps(config_summary, indent=2, ensure_ascii=False)}")
return config_summary
log_config.info(f"✓ 检测到自有数据库配置: THIS_DB_NAME={THIS_DB_NAME}")
required_vars = {
"THIS_DB_HOST": THIS_DB_HOST,
"THIS_DB_PORT": THIS_DB_PORT,
"THIS_DB_USER": THIS_DB_USER,
"THIS_DB_PASSWORD": THIS_DB_PASSWORD,
}
for var_name, var_value in required_vars.items():
if not var_value:
issues.append(f"{var_name} 环境变量未设置")
if THIS_DB_PORT and not (1 <= THIS_DB_PORT <= 65535):
issues.append(f"THIS_DB_PORT={THIS_DB_PORT} 超出有效范围(1-65535)")
if THIS_DB_NAME not in connections:
issues.append(f"THIS_DB_NAME='{THIS_DB_NAME}' 未在connections配置中找到")
try:
__import__("apps.data_opt.mds.staging_models")
except ImportError as e:
warnings.append(f"模型路径导入警告: apps.data_opt.mds.staging_models - {e}")
if issues:
error_msg = "自有数据库配置验证失败:\n" + "\n".join(f"{issue}" for issue in issues)
log_config.error(error_msg)
raise ValueError(error_msg)
if warnings:
for warning in warnings:
log_config.warning(warning)
config_summary = {
"has_own_database": True,
"db_name": THIS_DB_NAME,
"db_host": THIS_DB_HOST,
"db_port": THIS_DB_PORT,
"timezone": TIMEZONE_NAME,
"connections": list(connections.keys()),
"apps": list(TORTOISE_ORM_CONFIG["apps"].keys()),
}
log_config.info("✅ 数据库配置验证通过")
log_config.info(f"配置摘要: {json.dumps(config_summary, indent=2, ensure_ascii=False)}")
return config_summary
class ConnectionLeakDetector: class ConnectionLeakDetector:
@@ -308,32 +406,49 @@ smart_pool_manager = SmartConnectionPoolManager()
def register_database(app): def register_database(app):
"""
注册Tortoise ORM到FastAPI应用兼容接口
注意此函数作为兼容接口保留实际初始化已移到 lifespan
"""
validate_database_config()
register_tortoise( register_tortoise(
app=app, app=app,
config=TORTOISE_ORM_CONFIG, config=TORTOISE_ORM_CONFIG,
generate_schemas=False,
add_exception_handlers=True,
) )
# 标记数据库已初始化,允许日志写入数据库 log_config.info("✅ Tortoise ORM 已注册到FastAPI应用")
# 使用统一函数,同时设置 V1 和 V2 log_config.info(f"连接配置: {list(TORTOISE_ORM_CONFIG['connections'].keys())}")
log_config.info(f"应用配置: {list(TORTOISE_ORM_CONFIG['apps'].keys())}")
from globalobjects.logger import set_db_initialized_unified from globalobjects.logger import set_db_initialized_unified
set_db_initialized_unified(True) set_db_initialized_unified(True)
# 启动监控服务(使用现有的监控架构)
from apps.common.monitor.service import monitor_service from apps.common.monitor.service import monitor_service
log_config.info("✅ 系统监控服务已集成") log_config.info("✅ 系统监控服务已集成")
async def warmup_connections(): async def warmup_connections():
"""预热数据库连接""" """
预热数据库连接增强容错处理
MySQL 连接失败不阻止应用启动
"""
if not MYAPS_MAIN_DB: if not MYAPS_MAIN_DB:
return return
try: try:
from globalobjects.db_manager import get_db_managers from globalobjects.db_manager import get_db_managers
db_managers = get_db_managers() db_managers = get_db_managers()
for db_name, manager in db_managers.items(): for db_name, manager in db_managers.items():
conn_config = TORTOISE_ORM_CONFIG["connections"].get(db_name, {})
engine = conn_config.get("engine", "")
is_mysql = "mysql" in engine
try: try:
start_time = time.time() start_time = time.time()
# 使用较短的超时时间,避免启动时被阻塞
is_healthy = await asyncio.wait_for( is_healthy = await asyncio.wait_for(
manager.check_connection_health(timeout=5, fast_mode=True), manager.check_connection_health(timeout=5, fast_mode=True),
timeout=10 timeout=10
@@ -342,16 +457,24 @@ async def warmup_connections():
if is_healthy: if is_healthy:
log_config.info(f"连接预热成功: {db_name} - 响应时间: {response_time:.3f}") log_config.info(f"连接预热成功: {db_name} - 响应时间: {response_time:.3f}")
else: else:
log_config.warning(f"连接预热失败: {db_name}") if is_mysql:
# 尝试刷新连接(使用快速模式) log_config.warning(f"⚠️ MySQL连接预热失败: {db_name}(不影响启动)")
await asyncio.wait_for( else:
manager.refresh_connection(fast_mode=True), log_config.warning(f"连接预热失败: {db_name}")
timeout=15 await asyncio.wait_for(
) manager.refresh_connection(fast_mode=True),
timeout=15
)
except asyncio.TimeoutError: except asyncio.TimeoutError:
log_config.warning(f"连接预热超时: {db_name},跳过预热") if is_mysql:
log_config.warning(f"⚠️ MySQL连接预热超时: {db_name},跳过(不影响启动)")
else:
log_config.warning(f"连接预热超时: {db_name},跳过预热")
except Exception as e: except Exception as e:
log_config.error(f"连接预热异常: {db_name} - {str(e)}") if is_mysql:
log_config.warning(f"⚠️ MySQL连接预热异常: {db_name} - {str(e)}(不影响启动)")
else:
log_config.error(f"❌ 连接预热异常: {db_name} - {str(e)}")
log_config.info("数据库连接预热完成") log_config.info("数据库连接预热完成")
except Exception as e: except Exception as e:
log_config.error(f"连接预热异常: {str(e)}") log_config.error(f"连接预热异常: {str(e)}")
+12 -6
View File
@@ -22,18 +22,17 @@ from core.database import check_db_connections, warmup_connections, start_pool_m
@asynccontextmanager @asynccontextmanager
async def lifespan(app): async def lifespan(app):
"""应用生命周期管理器""" """应用生命周期管理器"""
# 应用启动时执行的操作
log_config.initialize_logging_unified() log_config.initialize_logging_unified()
# 将主应用事件循环传递给调度器
main_loop = asyncio.get_running_loop() main_loop = asyncio.get_running_loop()
scheduler_manager.set_main_loop(main_loop) scheduler_manager.set_main_loop(main_loop)
log_config.info(f"已将主应用事件循环传递给调度器: {main_loop}") log_config.info(f"已将主应用事件循环传递给调度器: {main_loop}")
# 预热数据库连接(在启动其他服务之前) from core.database import validate_database_config
validate_database_config()
log_config.info("开始预热数据库连接...") log_config.info("开始预热数据库连接...")
try: try:
# 添加超时保护,避免启动时被阻塞
await asyncio.wait_for(warmup_connections(), timeout=60) await asyncio.wait_for(warmup_connections(), timeout=60)
log_config.info("数据库连接预热完成") log_config.info("数据库连接预热完成")
except asyncio.TimeoutError: except asyncio.TimeoutError:
@@ -41,12 +40,10 @@ async def lifespan(app):
except Exception as e: except Exception as e:
log_config.error(f"❌ 数据库连接预热失败: {e}") log_config.error(f"❌ 数据库连接预热失败: {e}")
# 启动资源监控
log_config.info("开始启动资源监控...") log_config.info("开始启动资源监控...")
resource_monitor.start_monitoring(interval=30) resource_monitor.start_monitoring(interval=30)
log_config.info("系统资源监控已启动") log_config.info("系统资源监控已启动")
# 等待服务器完全就绪,确保客户端可以正常连接
log_config.info("等待服务器完全就绪...") log_config.info("等待服务器完全就绪...")
await asyncio.sleep(1) await asyncio.sleep(1)
log_config.info("服务器已就绪") log_config.info("服务器已就绪")
@@ -348,6 +345,15 @@ async def lifespan(app):
# 应用关闭时执行的操作 # 应用关闭时执行的操作
log_config.info("应用关闭中...") log_config.info("应用关闭中...")
# 0. 关闭数据库连接
log_config.info("正在关闭数据库连接...")
try:
from tortoise import Tortoise
await Tortoise.close_connections()
log_config.info("✅ 数据库连接已关闭")
except Exception as e:
log_config.warning(f"⚠️ 关闭数据库连接时出错: {e}")
# 1. 先停止 MySQL Binlog 监控(最依赖数据库) # 1. 先停止 MySQL Binlog 监控(最依赖数据库)
if TURNON_BINLOG_LISTENER: if TURNON_BINLOG_LISTENER:
log_config.info("正在停止 MySQL Binlog 监控...") log_config.info("正在停止 MySQL Binlog 监控...")
+387
View File
@@ -0,0 +1,387 @@
# 数据库初始化优化 TODO List
> 生成时间: 2026-05-20
> 目标: 解决 Tortoise ORM 初始化问题,确保数据库连接稳定
---
## P0 - 紧急 (导致所有数据库操作失败)
### TODO-001: Tortoise ORM 初始化未完成
**问题描述**:
- 错误: `RuntimeError: No TortoiseContext is currently active`
- 影响: 所有数据库操作失败
**问题位置**:
- `main.py:81``core/database.py:310-314`
**排查步骤**:
1. 检查 `register_tortoise()` 是否正确执行
2. 验证 `TORTOISE_ORM_CONFIG` 配置是否完整
3. 确认异步上下文是否正确
**解决方案**:
- [ ] 添加初始化日志,确认 `register_tortoise()` 执行时机
- [ ] 检查 FastAPI lifespan 与 Tortoise 的集成方式
- [ ] 考虑使用 `register_tortoise()` 的回调模式确保初始化完成
**验证方法**:
```python
# 在 register_database 后添加测试
from tortoise import Tortoise
assert Tortoise._inited, "Tortoise 未初始化"
```
---
## P1 - 高优先级
### TODO-002: 环境变量 THIS_DB_NAME 可能未正确加载
**问题描述**:
- 如果 `THIS_DB_NAME` 为空,PostgreSQL 连接不会被添加到配置中
- 导致 `Tortoise.get_connection(THIS_DB_NAME)` 失败
**问题位置**:
- `core/settings.py:170`
- `core/database.py:89`
**排查步骤**:
1. 检查 `.env` 文件中 `THIS_DB_NAME` 是否设置
2. 验证环境变量加载顺序
3. 添加默认值或错误提示
**解决方案**:
- [ ] 在 `core/database.py:89` 添加日志输出 `THIS_DB_NAME` 的值
- [ ] 如果为空,抛出明确的配置错误
- [ ] 考虑添加配置验证函数
**代码改进**:
```python
if not THIS_DB_NAME:
raise ValueError("THIS_DB_NAME 环境变量未设置,请检查 .env 配置")
```
---
### TODO-003: register_tortoise 缺少关键参数
**问题描述**:
- `register_tortoise()` 调用时缺少可选参数
- 可能导致初始化不完整
**问题位置**:
- `core/database.py:310-314`
**当前代码**:
```python
register_tortoise(
app=app,
config=TORTOISE_ORM_CONFIG,
)
```
**解决方案**:
- [ ] 添加 `generate_schemas=False` (明确不自动生成表结构)
- [ ] 添加 `add_exception_handlers=True` (添加异常处理器)
- [ ] 添加 `_create_db=False` (不自动创建数据库)
**改进代码**:
```python
register_tortoise(
app=app,
config=TORTOISE_ORM_CONFIG,
generate_schemas=False,
add_exception_handlers=True,
)
```
---
### TODO-004: 数据库模型注册验证
**问题描述**:
- 模型路径或连接名称可能配置错误
- 导致模型无法正确映射到数据库连接
**问题位置**:
- `core/database.py:104-107`
**当前配置**:
```python
TORTOISE_ORM_CONFIG["apps"]["data_opt_models"] = {
"models": ["apps.data_opt.mds.staging_models"],
"default_connection": THIS_DB_NAME,
}
```
**排查步骤**:
1. 验证模型路径是否正确
2. 确认 `THIS_DB_NAME``connections` 字典中的键匹配
3. 检查模型是否能正确导入
**解决方案**:
- [ ] 添加模型导入验证
- [ ] 添加连接名称匹配验证
- [ ] 输出完整的 TORTOISE_ORM_CONFIG 供调试
---
## P2 - 中优先级
### TODO-005: PostgreSQL 连接缺少超时配置
**问题描述**:
- PostgreSQL 连接配置缺少 `connect_timeout`
- 可能导致连接建立缓慢或挂起
**问题位置**:
- `core/database.py:89-103`
**当前配置**:
```python
connections[THIS_DB_NAME] = {
"engine": "tortoise.backends.asyncpg",
"credentials": {
"host": THIS_DB_HOST,
"port": THIS_DB_PORT,
"user": THIS_DB_USER,
"password": THIS_DB_PASSWORD,
"database": THIS_DB_NAME,
"server_settings": {"TimeZone": TIMEZONE_NAME},
},
"min_size": 3,
"max_size": 10,
"use_tz": True,
}
```
**解决方案**:
- [ ] 添加 `connect_timeout` 参数 (建议 30 秒)
- [ ] 添加 `command_timeout` 参数 (建议 60 秒)
**改进代码**:
```python
connections[THIS_DB_NAME] = {
"engine": "tortoise.backends.asyncpg",
"credentials": {
"host": THIS_DB_HOST,
"port": THIS_DB_PORT,
"user": THIS_DB_USER,
"password": THIS_DB_PASSWORD,
"database": THIS_DB_NAME,
"server_settings": {"TimeZone": TIMEZONE_NAME},
"command_timeout": 60,
},
"min_size": 3,
"max_size": 10,
"use_tz": True,
}
# 注意: asyncpg 的 timeout 在 credentials 中
```
---
### TODO-006: 数据库操作缺少异常处理
**问题描述**:
- 获取数据库连接时无异常处理
- 一旦 Tortoise 未初始化直接崩溃
**问题位置**:
- `apps/data_opt/mds/staging_routers.py:728-729`
**当前代码**:
```python
conn = Tortoise.get_connection(THIS_DB_NAME)
```
**解决方案**:
- [ ] 添加 try-catch 保护
- [ ] 提供友好的错误提示
- [ ] 记录详细的错误日志
**改进代码**:
```python
try:
conn = Tortoise.get_connection(THIS_DB_NAME)
except Exception as e:
logger.error(f"获取数据库连接失败: {e}")
raise HTTPException(
status_code=500,
detail="数据库连接失败,请检查服务配置或稍后重试"
)
```
---
### TODO-007: lifespan 与 register_tortoise 的交互问题
**问题描述**:
- `lifespan``register_database` 之前就关联到 app
- 可能导致初始化时序问题
**问题位置**:
- `main.py:31` (create_app)
- `main.py:81` (register_database)
**当前顺序**:
```python
app = create_app(lifespan=lifespan) # 第31行
# ... 其他初始化 ...
register_database(app) # 第81行
```
**排查步骤**:
1. 理解 `register_tortoise` 的初始化时机
2. 确认 lifespan 启动时数据库是否已就绪
3. 检查是否有竞态条件
**解决方案**:
- [ ] 考虑将 `register_database` 移到 `create_app` 内部
- [ ] 或在 lifespan 的 startup 阶段添加数据库就绪检查
- [ ] 添加初始化状态标志
---
## P3 - 低优先级
### TODO-008: MySQL 数据库连接超时 (非阻塞)
**问题描述**:
- MySQL 数据库 `hacy_p` 连接失败
- 但这是警告,不影响 PostgreSQL 连接
**日志信息**:
```
数据库连接健康检查超时: hacy_p
连接预热超时: hacy_p,跳过预热
```
**解决方案**:
- [ ] 检查 MySQL 数据库配置是否正确
- [ ] 添加更友好的警告提示
- [ ] 考虑是否需要 MySQL 连接 (如果不使用可禁用)
---
### TODO-009: 添加数据库连接状态检查端点
**问题描述**:
- 缺少专门的数据库健康检查 API
- 难以快速判断数据库连接状态
**解决方案**:
- [ ] 添加 `/health/database` 端点
- [ ] 返回所有数据库连接状态
- [ ] 包含连接池使用情况
**示例代码**:
```python
@router.get("/health/database")
async def check_database_health():
from tortoise import Tortoise
results = {}
for db_name in Tortoise._connections:
try:
conn = Tortoise.get_connection(db_name)
await conn.execute_query("SELECT 1")
results[db_name] = {"status": "healthy"}
except Exception as e:
results[db_name] = {"status": "unhealthy", "error": str(e)}
return results
```
---
### TODO-010: 添加配置验证函数
**问题描述**:
- 数据库配置分散在多处
- 缺少统一的验证机制
**解决方案**:
- [ ] 创建 `validate_database_config()` 函数
- [ ] 在应用启动时调用
- [ ] 输出配置摘要供调试
**示例代码**:
```python
def validate_database_config():
"""验证数据库配置"""
issues = []
if not THIS_DB_NAME:
issues.append("THIS_DB_NAME 未设置")
if THIS_DB_NAME and THIS_DB_NAME not in connections:
issues.append(f"{THIS_DB_NAME} 未在 connections 中配置")
if issues:
raise ValueError("数据库配置错误:\n" + "\n".join(issues))
logger.info("数据库配置验证通过")
```
---
## 执行计划
### 阶段1: 紧急修复 (立即执行)
1. [ ] TODO-001: Tortoise 初始化问题
2. [ ] TODO-002: 环境变量验证
3. [ ] TODO-003: register_tortoise 参数
### 阶段2: 稳定性优化 (本周内)
4. [ ] TODO-004: 模型注册验证
5. [ ] TODO-005: PostgreSQL 超时配置
6. [ ] TODO-006: 异常处理
### 阶段3: 架构优化 (下周)
7. [ ] TODO-007: lifespan 集成
8. [ ] TODO-008: MySQL 连接处理
9. [ ] TODO-009: 健康检查端点
10. [ ] TODO-010: 配置验证
---
## 调试命令
### 检查环境变量
```bash
cat .env | grep -E "THIS_DB_|MYAPS_DB"
```
### 测试数据库连接
```bash
# PostgreSQL
PGPASSWORD=123456 psql -h 129.211.172.205 -p 5432 -U n8n -d appsmith -c "SELECT 1"
# MySQL (如果使用)
mysql -h <host> -P <port> -u <user> -p<password> -e "SELECT 1"
```
### 检查 Tortoise 初始化
```python
# 在应用启动后执行
from tortoise import Tortoise
print(f"Tortoise initialized: {Tortoise._inited}")
print(f"Connections: {list(Tortoise._connections.keys())}")
```
---
## 参考资料
- [Tortoise ORM 官方文档](https://tortoise.github.io/tortoise-orm/)
- [FastAPI 生命周期事件](https://fastapi.tiangolo.com/advanced/events/)
- [asyncpg 连接参数](https://magicstack.github.io/asyncpg/current/api/index.html#connection)
---
## 变更记录
| 日期 | 操作 | 说明 |
|------|------|------|
| 2026-05-20 | 创建 | 初始化数据库优化 TODO List |
+1 -1
View File
@@ -77,7 +77,7 @@ init_registered_routes(app)
app.websocket("/")(websocket_root) app.websocket("/")(websocket_root)
app.websocket("/ws/{path:path}")(websocket_endpoint) app.websocket("/ws/{path:path}")(websocket_endpoint)
# 注册数据库 # 注册数据库(通过 register_tortoise 管理 context
register_database(app) register_database(app)
# 启动说明: # 启动说明: