init: 初始化 dpb 桃育种系统代码库

前后端 + 后端 FastAPI 全量源码、部署脚本与文档。
This commit is contained in:
34047007@qq.com
2026-08-06 00:17:49 +08:00
commit b95053c52c
1469 changed files with 322298 additions and 0 deletions
@@ -0,0 +1,9 @@
from fastapi import APIRouter
from .file.controller import FileRouter
from .health import HealthRouter
common_router = APIRouter(prefix="/common")
common_router.include_router(FileRouter)
common_router.include_router(HealthRouter)
@@ -0,0 +1 @@
@@ -0,0 +1,48 @@
from pathlib import Path
from typing import Annotated, Literal
from fastapi import APIRouter, BackgroundTasks, Body, Depends, File, Form, Query, Request, Security, UploadFile
from fastapi.responses import FileResponse, JSONResponse
from app.common.response import ResponseSchema, SuccessResponse, UploadFileResponse
from app.core.base_schema import AuthSchema, UploadResponseSchema
from app.core.dependencies import AuthPermission, get_current_user
from app.core.router_class import OperationLogRoute
from app.utils.upload_util import UploadUtil
from .service import FileService
FileRouter = APIRouter(route_class=OperationLogRoute, prefix="/file", tags=["文件管理"])
@FileRouter.post("/upload", summary="上传文件", response_model=ResponseSchema[UploadResponseSchema], dependencies=[Security(AuthPermission(["module_common:file:upload"]))])
async def upload_controller(
request: Request,
auth: Annotated[AuthSchema, Depends(get_current_user)],
file: Annotated[UploadFile, File(description="上传文件")],
upload_type: Annotated[
Literal["file", "avatar", "param", "resource"] | None,
Query(description="上传类型: file=通用文件, avatar=头像, param=参数配置, resource=监控资源"),
] = "file",
target_path: Annotated[str | None, Form(description="目标目录路径(仅 resource 类型支持)")] = None,
) -> JSONResponse:
result = await FileService.upload_service(
base_url="/", # 返回 /static 开头的相对路径,去掉域名端口,便于环境迁移(dev 由 vite proxy /static 转发到后端)
file=file,
upload_type=upload_type or "file",
target_path=target_path,
)
return SuccessResponse(data=result, msg="上传文件成功")
@FileRouter.post("/download", summary="下载文件", dependencies=[Security(AuthPermission(["module_common:file:download"]))])
async def download_controller(
auth: Annotated[AuthSchema, Depends(get_current_user)],
background_tasks: BackgroundTasks,
file_path: Annotated[str, Body(description="文件路径")],
delete: Annotated[bool, Body(description="是否删除文件")] = False,
) -> FileResponse:
result = await FileService.download_service(file_path=file_path)
if delete:
background_tasks.add_task(UploadUtil.delete_file, Path(result.file_path))
return UploadFileResponse(file_path=result.file_path, filename=result.file_name)
@@ -0,0 +1,67 @@
import os
from fastapi import UploadFile
from app.config.setting import settings
from app.core.base_schema import DownloadFileSchema, UploadResponseSchema
from app.core.exceptions import CustomException
from app.core.logger import logger
from app.utils.upload_util import UploadUtil
class FileService:
"""文件管理服务层
"""
@classmethod
async def upload_service(
cls,
base_url: str,
file: UploadFile,
upload_type: str = "file",
target_path: str | None = None,
) -> UploadResponseSchema:
"""上传文件"""
filename, filepath, file_url = await UploadUtil.upload_file(
file=file,
base_url=base_url,
upload_type=upload_type,
target_path=target_path,
)
return UploadResponseSchema(
file_path=f"{filepath}",
file_name=filename,
origin_name=file.filename,
file_url=f"{file_url}",
)
@classmethod
async def download_service(cls, file_path: str) -> DownloadFileSchema:
"""下载文件"""
if not file_path:
raise CustomException(msg="请选择要下载的文件")
dangerous_patterns = ["../", "..\\", "\0"]
for pattern in dangerous_patterns:
if pattern in file_path:
logger.error(f"检测到路径穿越攻击: {file_path}")
raise CustomException(msg="非法的文件路径")
upload_root = settings.UPLOAD_FILE_PATH.resolve()
abs_path = os.path.normpath(os.path.abspath(file_path))
if not abs_path.startswith(str(upload_root)):
logger.error(f"路径不在上传目录内: {file_path}")
raise CustomException(msg="非法的文件路径")
if not UploadUtil.check_file_exists(abs_path):
raise CustomException(msg="文件不存在")
file_name = UploadUtil.download_file(abs_path)
return DownloadFileSchema(
file_path=abs_path,
file_name=str(file_name),
)
@@ -0,0 +1,3 @@
from .controller import HealthRouter
__all__ = ["HealthRouter"]
@@ -0,0 +1,192 @@
import asyncio
import shutil
import time
from collections.abc import AsyncIterable
from datetime import datetime
from fastapi import APIRouter, Request
from fastapi.responses import JSONResponse
from fastapi.sse import EventSourceResponse, ServerSentEvent
from sqlalchemy import text
from app.common.enums import RET
from app.common.response import ErrorResponse, ResponseSchema, SuccessResponse
from app.config.setting import settings
from app.core.database import async_db_session
from app.core.logger import logger
from app.core.router_class import OperationLogRoute
from .schema import DependencyStatus, HealthOut, ReadinessOut
HealthRouter = APIRouter(route_class=OperationLogRoute, prefix="/health", tags=["健康检查"])
# ── 健康检查时间间隔 ──
_HEALTH_STREAM_INTERVAL = 30 # 秒
async def _check_database() -> DependencyStatus:
"""检查数据库连接"""
try:
start = time.perf_counter()
async with async_db_session() as session:
await session.execute(text("SELECT 1"))
latency = (time.perf_counter() - start) * 1000
return DependencyStatus(status=1, latency_ms=round(latency, 2))
except Exception as e:
logger.warning(f"数据库健康检查失败: {e}")
return DependencyStatus(status=0)
async def _check_redis(request: Request) -> DependencyStatus:
"""检查 Redis 连接"""
try:
redis = getattr(request.app.state, "redis", None)
if not redis:
return DependencyStatus(status=0)
start = time.perf_counter()
await redis.ping()
latency = (time.perf_counter() - start) * 1000
return DependencyStatus(status=1, latency_ms=round(latency, 2))
except Exception as e:
logger.warning(f"Redis 健康检查失败: {e}")
return DependencyStatus(status=0)
def _get_disk_usage() -> float:
"""获取磁盘使用率"""
try:
usage = shutil.disk_usage("/")
return round(usage.used / usage.total * 100, 1)
except Exception:
return -1.0
# 应用启动时间戳
_start_time = datetime.now()
@HealthRouter.get("/check", summary="健康检查", response_model=ResponseSchema[HealthOut])
async def health_check() -> JSONResponse:
"""基础健康检查
参数:
- 无
返回:
- SuccessResponse: 包含进程存活状态、启动时间、版本号的 JSON 响应。
"""
uptime = (datetime.now() - _start_time).total_seconds()
return SuccessResponse(
data=HealthOut(
status=1,
timestamp=datetime.now().isoformat(),
version=settings.VERSION,
uptime_seconds=uptime,
),
msg="系统健康",
)
@HealthRouter.get("/live", summary="存活探针", response_model=ResponseSchema[HealthOut])
async def liveness_check() -> JSONResponse:
"""存活探针
参数:
- 无
返回:
- SuccessResponse: 包含进程存活状态、启动时间、版本号的 JSON 响应。
"""
uptime = (datetime.now() - _start_time).total_seconds()
return SuccessResponse(
data=HealthOut(
status=1,
timestamp=datetime.now().isoformat(),
version=settings.VERSION,
uptime_seconds=uptime,
),
msg="进程存活",
)
@HealthRouter.get("/ready", summary="就绪探针", response_model=ResponseSchema[ReadinessOut])
async def readiness_check(request: Request) -> JSONResponse:
"""就绪探针
参数:
- request (Request): FastAPI 请求对象,用于获取 Redis 客户端。
返回:
- SuccessResponse | ErrorResponse: 依赖就绪时返回 200,未就绪返回 503。
"""
uptime = (datetime.now() - _start_time).total_seconds()
db_status, redis_status = await asyncio.gather(
_check_database(),
_check_redis(request),
)
dependencies = {
"database": db_status,
"redis": redis_status,
}
# 判断总体状态
def is_ok(d: DependencyStatus) -> bool:
return d.status == 1
all_ok = all(is_ok(d) for d in dependencies.values())
payload = ReadinessOut(
status=1 if all_ok else 0,
timestamp=datetime.now().isoformat(),
version=settings.VERSION,
uptime_seconds=uptime,
dependencies=dependencies,
disk_usage=_get_disk_usage(),
)
if all_ok:
return SuccessResponse(data=payload, msg="依赖就绪")
return ErrorResponse(
data=payload,
msg="依赖未就绪",
code=RET.SERVICE_UNAVAILABLE.code,
status_code=503,
success=False,
)
# ============================================================
# SSE 健康状态实时推送
# ============================================================
async def _build_health_payload(request: Request) -> dict:
"""采集当前健康状态"""
db_status, redis_status = await asyncio.gather(
_check_database(),
_check_redis(request),
)
return {
"status": 1 if db_status.status and redis_status.status else 0,
"dependencies": {
"database": db_status.model_dump(),
"redis": redis_status.model_dump(),
},
"disk_usage": _get_disk_usage(),
"uptime_seconds": (datetime.now() - _start_time).total_seconds(),
"timestamp": datetime.now().isoformat(),
}
@HealthRouter.get("/stream", summary="健康状态实时推送", response_class=EventSourceResponse)
async def health_stream(request: Request) -> AsyncIterable[ServerSentEvent]:
"""SSE 实时推送健康状态,每 30 秒推送一次,客户端无需轮询 /ready。"""
yield ServerSentEvent(data=await _build_health_payload(request), event="health")
while True:
await asyncio.sleep(_HEALTH_STREAM_INTERVAL)
yield ServerSentEvent(data=await _build_health_payload(request), event="health")
@@ -0,0 +1,28 @@
from pydantic import BaseModel, Field
class DependencyStatus(BaseModel):
"""依赖状态"""
status: int = Field(..., description="状态(0:异常 1:正常)")
latency_ms: float | None = Field(default=None, description="延迟(毫秒)")
class HealthOut(BaseModel):
"""基础健康检查响应"""
status: int = Field(..., description="状态(0:异常 1:正常)")
timestamp: str = Field(..., description="时间戳")
version: str = Field(..., description="版本号")
uptime_seconds: float = Field(..., description="运行时间(秒)")
class ReadinessOut(BaseModel):
"""就绪探针响应"""
status: int = Field(..., description="状态(0:异常 1:正常)")
timestamp: str = Field(..., description="时间戳")
version: str = Field(..., description="版本号")
uptime_seconds: float = Field(..., description="运行时间(秒)")
dependencies: dict[str, DependencyStatus] = Field(..., description="依赖状态")
disk_usage: float = Field(..., description="磁盘使用率")
@@ -0,0 +1,8 @@
# 见 docs/PLUGIN_ARCHITECTURE.md
name = "common"
title = "公共"
version = "1.0.0"
description = "公共功能;路由由 module_common/**/controller 动态注册。"
optional = true
tags = ["common"]