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 @@
@@ -0,0 +1,68 @@
from typing import Annotated
from fastapi import APIRouter, Body, Depends, Query, Security
from fastapi.responses import JSONResponse
from redis.asyncio.client import Redis
from sqlalchemy.ext.asyncio import AsyncSession
from app.common.request import PaginationService
from app.common.response import ResponseSchema, SuccessResponse
from app.core.base_schema import AuthSchema, PaginationQueryParam
from app.core.dependencies import AuthPermission, db_getter, get_current_user, redis_getter
from app.core.router_class import OperationLogRoute
from .schema import DashboardStatsSchema, OnlineOutSchema, OnlineQueryParam
from .service import OnlineService
OnlineRouter = APIRouter(route_class=OperationLogRoute, prefix="/online", tags=["在线用户"])
@OnlineRouter.get("/list", summary="获取在线用户列表", response_model=ResponseSchema[list[OnlineOutSchema]], dependencies=[Security(AuthPermission(["module_monitor:online:query"]))])
async def get_online_list_controller(
redis: Annotated[Redis, Depends(redis_getter)],
page: Annotated[PaginationQueryParam, Depends()],
search: Annotated[OnlineQueryParam, Query()],
) -> JSONResponse:
result_dict_list = await OnlineService.get_online_list(redis=redis, search=search)
result_dict = await PaginationService.paginate(
data_list=result_dict_list,
page_no=page.page_no,
page_size=page.page_size,
)
return SuccessResponse(data=result_dict, msg="获取成功")
@OnlineRouter.get("/current", summary="获取当前用户的在线会话", response_model=ResponseSchema[list[OnlineOutSchema]], dependencies=[Depends(get_current_user)])
async def get_current_online_controller(
redis: Annotated[Redis, Depends(redis_getter)],
auth: Annotated[AuthSchema, Depends(get_current_user)],
) -> JSONResponse:
sessions = await OnlineService.get_current_user_sessions(redis=redis, user_id=auth.user.id)
return SuccessResponse(data=sessions, msg="获取当前用户在线会话成功")
@OnlineRouter.delete("/delete", summary="强制下线", response_model=ResponseSchema[None], dependencies=[Security(AuthPermission(["module_monitor:online:delete"]))])
async def delete_online_controller(
session_id: Annotated[str, Body(description="会话编号")],
redis: Annotated[Redis, Depends(redis_getter)],
) -> JSONResponse:
await OnlineService.delete_online(redis=redis, session_id=session_id)
return SuccessResponse(msg="强制下线成功")
@OnlineRouter.delete("/clear", summary="清除所有在线用户", response_model=ResponseSchema[None], dependencies=[Security(AuthPermission(["module_monitor:online:delete"]))])
async def clear_online_controller(
redis: Annotated[Redis, Depends(redis_getter)],
) -> JSONResponse:
await OnlineService.clear_online(redis=redis)
return SuccessResponse(msg="清除所有在线用户成功")
@OnlineRouter.get("/stats", summary="获取仪表盘统计数据", response_model=ResponseSchema[DashboardStatsSchema])
async def get_dashboard_stats_controller(
db: Annotated[AsyncSession, Depends(db_getter)],
redis: Annotated[Redis, Depends(redis_getter)],
_auth: Annotated[AuthSchema, Security(AuthPermission(["module_monitor:dashboard:query"]))],
) -> JSONResponse:
data = await OnlineService.get_dashboard_stats(db=db, redis=redis)
return SuccessResponse(data=data, msg="获取仪表盘统计成功")
@@ -0,0 +1,36 @@
from datetime import datetime
from pydantic import BaseModel, Field
from app.core.base_schema import SessionInfoSchema
class OnlineOutSchema(SessionInfoSchema):
"""在线用户响应模型 — ``SessionInfoSchema`` 的公开子集。"""
class OnlineQueryParam(BaseModel):
"""在线用户查询参数"""
name: str | None = Field(None, description="登录名称")
ipaddr: str | None = Field(None, description="登陆IP地址")
login_location: str | None = Field(None, description="登录所属地")
class RecentLoginItem(BaseModel):
"""最近登录记录"""
username: str
status: int
login_time: datetime
login_ip: str | None = None
login_location: str | None = None
class DashboardStatsSchema(BaseModel):
"""仪表盘统计数据"""
online_users: int = 0
total_users: int = 0
today_login_count: int = 0
today_unique_users: int = 0
week_user_created: int = 0
recent_logins: list[RecentLoginItem] = []
@@ -0,0 +1,140 @@
import json
from datetime import date, datetime, timedelta
from redis.asyncio.client import Redis
from sqlalchemy import func, select
from sqlalchemy.ext.asyncio import AsyncSession
from app.api.v1.module_system.log.model import LoginLogModel
from app.api.v1.module_system.user.model import UserModel
from app.common.enums import RedisInitKeyConfig
from app.core.logger import logger
from app.core.redis_crud import RedisCURD
from app.core.security import decode_access_token
from .schema import DashboardStatsSchema, OnlineQueryParam, RecentLoginItem
class OnlineService:
"""在线用户管理模块服务层"""
@staticmethod
async def get_online_list(redis: Redis, search: OnlineQueryParam | None = None) -> list[dict]:
keys = await RedisCURD(redis).scan_keys(f"{RedisInitKeyConfig.ACCESS_TOKEN.key}:*")
tokens = await RedisCURD(redis).mget(keys)
online_users = []
for key, token in zip(keys, tokens, strict=True):
if not token:
continue
try:
payload = decode_access_token(token=token)
session_id = payload.sub
# 从 Redis 读取完整会话信息
raw = await RedisCURD(redis).get(f"{RedisInitKeyConfig.USER_SESSION.key}:{session_id}")
if not raw:
continue
session_info = json.loads(raw)
# 内联搜索匹配逻辑
if search:
if search.name and search.name[1]:
kw = search.name[1].strip("%")
if kw.lower() not in session_info.get("name", "").lower():
continue
if search.ipaddr and search.ipaddr[1]:
kw = search.ipaddr[1].strip("%")
if kw not in session_info.get("ipaddr", ""):
continue
if search.login_location and search.login_location[1]:
kw = search.login_location[1].strip("%")
if kw.lower() not in session_info.get("login_location", "").lower():
continue
online_users.append(session_info)
except Exception:
# token 已过期或无效,清理 Redis 中的脏数据
key_str = key.decode() if isinstance(key, bytes) else key
session_id = key_str.split(":")[-1]
await RedisCURD(redis).delete(key_str)
await RedisCURD(redis).delete(f"{RedisInitKeyConfig.REFRESH_TOKEN.key}:{session_id}")
await RedisCURD(redis).delete(f"{RedisInitKeyConfig.USER_SESSION.key}:{session_id}")
continue
online_users.sort(key=lambda x: x.get("login_time", ""), reverse=True)
return online_users
@staticmethod
async def get_current_user_sessions(redis: Redis, user_id: int) -> list[dict]:
"""获取当前用户的在线会话列表"""
all_online = await OnlineService.get_online_list(redis)
return [s for s in all_online if s.get("user_id") == user_id]
@staticmethod
async def delete_online(redis: Redis, session_id: str) -> None:
await RedisCURD(redis).delete(f"{RedisInitKeyConfig.ACCESS_TOKEN.key}:{session_id}")
await RedisCURD(redis).delete(f"{RedisInitKeyConfig.REFRESH_TOKEN.key}:{session_id}")
await RedisCURD(redis).delete(f"{RedisInitKeyConfig.USER_SESSION.key}:{session_id}")
logger.info(f"强制下线用户会话: {session_id}")
@staticmethod
async def clear_online(redis: Redis) -> None:
await RedisCURD(redis).clear(f"{RedisInitKeyConfig.ACCESS_TOKEN.key}:*")
await RedisCURD(redis).clear(f"{RedisInitKeyConfig.REFRESH_TOKEN.key}:*")
await RedisCURD(redis).clear(f"{RedisInitKeyConfig.USER_SESSION.key}:*")
logger.info("清除所有在线用户会话成功")
@staticmethod
async def get_dashboard_stats(db: AsyncSession, redis: Redis) -> DashboardStatsSchema:
"""获取仪表盘统计数据"""
today_start = datetime.combine(date.today(), datetime.min.time())
week_start = today_start - timedelta(days=7)
online_count = len(await OnlineService.get_online_list(redis))
users_sql = select(func.count()).select_from(UserModel).where(UserModel.is_deleted.is_(False))
user_count = (await db.execute(users_sql)).scalar() or 0
users_week_sql = (
select(func.count()).select_from(UserModel)
.where(UserModel.is_deleted.is_(False), UserModel.created_time >= week_start)
)
user_week_count = (await db.execute(users_week_sql)).scalar() or 0
today_login_sql = (
select(func.count()).select_from(LoginLogModel)
.where(LoginLogModel.created_time >= today_start)
)
today_login_count = (await db.execute(today_login_sql)).scalar() or 0
today_unique_sql = (
select(func.count(func.distinct(LoginLogModel.username)))
.select_from(LoginLogModel)
.where(LoginLogModel.created_time >= today_start)
)
today_unique_count = (await db.execute(today_unique_sql)).scalar() or 0
recent_stmt = (
select(LoginLogModel.username, LoginLogModel.status, LoginLogModel.created_time,
LoginLogModel.login_ip, LoginLogModel.login_location)
.where(LoginLogModel.is_deleted.is_(False))
.order_by(LoginLogModel.created_time.desc())
.limit(10)
)
recent_rows = (await db.execute(recent_stmt)).all()
recent_logins = [
RecentLoginItem(username=r.username, status=r.status, login_time=r.created_time,
login_ip=r.login_ip, login_location=r.login_location)
for r in recent_rows
]
result = DashboardStatsSchema(
online_users=online_count,
total_users=user_count,
today_login_count=today_login_count,
today_unique_users=today_unique_count,
week_user_created=user_week_count,
recent_logins=recent_logins,
)
return result