add backend source code

This commit is contained in:
2026-02-28 15:06:52 +08:00
parent 1bc330e20c
commit 2c37aa9064
67 changed files with 11654 additions and 0 deletions

View File

View File

@@ -0,0 +1,46 @@
"""
系统配置模块
"""
from pydantic_settings import BaseSettings
from typing import Optional
from functools import lru_cache
class Settings(BaseSettings):
"""系统配置"""
# 应用配置
APP_NAME: str = "医院绩效考核管理系统"
APP_VERSION: str = "1.0.0"
DEBUG: bool = True
API_PREFIX: str = "/api/v1"
# 数据库配置
DATABASE_URL: str = "postgresql+asyncpg://postgres:postgres@localhost:5432/hospital_performance"
DATABASE_POOL_SIZE: int = 20
DATABASE_MAX_OVERFLOW: int = 10
# JWT配置
SECRET_KEY: str = "your-secret-key-change-in-production-min-32-chars"
ALGORITHM: str = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES: int = 60 * 8 # 8小时
# 跨域配置
CORS_ORIGINS: list[str] = ["http://localhost:3000", "http://localhost:5173"]
# 分页配置
DEFAULT_PAGE_SIZE: int = 20
MAX_PAGE_SIZE: int = 100
class Config:
env_file = ".env"
case_sensitive = True
@lru_cache()
def get_settings() -> Settings:
"""获取配置单例"""
return Settings()
settings = get_settings()

View File

@@ -0,0 +1,38 @@
"""
数据库连接模块
"""
from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine, async_sessionmaker
from sqlalchemy.orm import DeclarativeBase
from app.core.config import settings
# 创建异步引擎
engine = create_async_engine(
settings.DATABASE_URL,
echo=settings.DEBUG,
)
# 创建异步会话工厂
async_session_maker = async_sessionmaker(
engine,
class_=AsyncSession,
expire_on_commit=False,
)
class Base(DeclarativeBase):
"""数据库模型基类"""
pass
async def get_db() -> AsyncSession:
"""获取数据库会话依赖"""
async with async_session_maker() as session:
try:
yield session
await session.commit()
except Exception:
await session.rollback()
raise
finally:
await session.close()

114
backend/app/core/init_db.py Normal file
View File

@@ -0,0 +1,114 @@
"""
数据库初始化脚本
创建初始管理员用户和示例数据
"""
import asyncio
from sqlalchemy import select
from app.core.database import async_session_maker
from app.core.security import get_password_hash
from app.models.models import User, Department, Staff, Indicator, DeptType, StaffStatus, IndicatorType
async def init_admin_user():
"""创建初始管理员用户"""
async with async_session_maker() as db:
# 检查是否已存在admin用户
result = await db.execute(select(User).where(User.username == "admin"))
if result.scalar_one_or_none():
print("管理员用户已存在")
return
# 创建admin用户
admin = User(
username="admin",
password_hash=get_password_hash("admin123"),
role="admin",
is_active=True
)
db.add(admin)
await db.flush()
print("创建管理员用户: admin / admin123")
async def init_departments():
"""创建示例科室"""
async with async_session_maker() as db:
result = await db.execute(select(Department))
if result.scalars().first():
print("科室数据已存在")
return
departments = [
Department(name="内科", code="NK001", dept_type=DeptType.CLINICAL, level=1, sort_order=1),
Department(name="外科", code="WK001", dept_type=DeptType.CLINICAL, level=1, sort_order=2),
Department(name="中医科", code="ZYK001", dept_type=DeptType.CLINICAL, level=1, sort_order=3),
Department(name="检验科", code="JYK001", dept_type=DeptType.MEDICAL_TECH, level=1, sort_order=4),
Department(name="放射科", code="FSK001", dept_type=DeptType.MEDICAL_TECH, level=1, sort_order=5),
Department(name="财务科", code="CWK001", dept_type=DeptType.ADMIN, level=1, sort_order=6),
Department(name="办公室", code="BGS001", dept_type=DeptType.ADMIN, level=1, sort_order=7),
]
for dept in departments:
db.add(dept)
await db.flush()
print(f"创建了 {len(departments)} 个科室")
async def init_indicators():
"""创建示例考核指标"""
async with async_session_maker() as db:
result = await db.execute(select(Indicator))
if result.scalars().first():
print("指标数据已存在")
return
indicators = [
Indicator(name="门诊量", code="MZL001", indicator_type=IndicatorType.QUANTITY, weight=1.5, max_score=100, unit="人次"),
Indicator(name="住院量", code="ZYL001", indicator_type=IndicatorType.QUANTITY, weight=1.2, max_score=100, unit="人次"),
Indicator(name="诊断准确率", code="ZDZQL001", indicator_type=IndicatorType.QUALITY, weight=2.0, max_score=100, unit="%"),
Indicator(name="患者满意度", code="HZMYD001", indicator_type=IndicatorType.SERVICE, weight=1.5, max_score=100, unit="%"),
Indicator(name="医疗成本控制", code="YLCBKZ001", indicator_type=IndicatorType.COST, weight=1.0, max_score=100, unit="%"),
Indicator(name="工作效率", code="GZXL001", indicator_type=IndicatorType.EFFICIENCY, weight=1.0, max_score=100, unit="%"),
]
for ind in indicators:
db.add(ind)
await db.flush()
print(f"创建了 {len(indicators)} 个考核指标")
async def init_sample_staff():
"""创建示例员工"""
async with async_session_maker() as db:
result = await db.execute(select(Staff))
if result.scalars().first():
print("员工数据已存在")
return
# 获取科室ID
dept_result = await db.execute(select(Department))
departments = {d.code: d.id for d in dept_result.scalars().all()}
staff_list = [
Staff(employee_id="EMP001", name="张三", department_id=departments.get("NK001"), position="主治医师", title="副主任医师", base_salary=8000, performance_ratio=1.2, status=StaffStatus.ACTIVE),
Staff(employee_id="EMP002", name="李四", department_id=departments.get("WK001"), position="住院医师", title="主治医师", base_salary=7000, performance_ratio=1.0, status=StaffStatus.ACTIVE),
Staff(employee_id="EMP003", name="王五", department_id=departments.get("ZYK001"), position="主任医师", title="主任医师", base_salary=10000, performance_ratio=1.5, status=StaffStatus.ACTIVE),
Staff(employee_id="EMP004", name="赵六", department_id=departments.get("JYK001"), position="检验师", title="主管检验师", base_salary=6000, performance_ratio=1.0, status=StaffStatus.ACTIVE),
Staff(employee_id="EMP005", name="钱七", department_id=departments.get("CWK001"), position="会计", title="会计师", base_salary=5000, performance_ratio=0.8, status=StaffStatus.ACTIVE),
]
for staff in staff_list:
db.add(staff)
await db.flush()
print(f"创建了 {len(staff_list)} 个员工")
async def main():
"""初始化所有数据"""
print("开始初始化数据库...")
await init_departments()
await init_indicators()
await init_sample_staff()
await init_admin_user()
print("数据库初始化完成!")
if __name__ == "__main__":
asyncio.run(main())

View File

@@ -0,0 +1,64 @@
"""
Logging configuration module
"""
import logging
import sys
from pathlib import Path
from logging.handlers import RotatingFileHandler
from datetime import datetime
# Use absolute path - backend directory is parent of app/core
BACKEND_DIR = Path(__file__).resolve().parent.parent.parent
LOG_DIR = BACKEND_DIR / "logs"
# Ensure logs directory exists
LOG_DIR.mkdir(exist_ok=True)
# Log file paths
current_date = datetime.now().strftime('%Y%m%d')
LOG_FILE = LOG_DIR / f"app_{current_date}.log"
ERROR_LOG_FILE = LOG_DIR / f"error_{current_date}.log"
# Log format
LOG_FORMAT = "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
DATE_FORMAT = "%Y-%m-%d %H:%M:%S"
# Create logger
logger = logging.getLogger("hospital_performance")
logger.setLevel(logging.DEBUG)
# Clear existing handlers
logger.handlers.clear()
# Console handler (INFO level)
console_handler = logging.StreamHandler(sys.stdout)
console_handler.setLevel(logging.INFO)
console_handler.setFormatter(logging.Formatter(LOG_FORMAT, DATE_FORMAT))
logger.addHandler(console_handler)
# File handler (DEBUG level, rotating)
file_handler = RotatingFileHandler(
LOG_FILE,
maxBytes=10*1024*1024, # 10MB
backupCount=7, # Keep 7 backups
encoding="utf-8"
)
file_handler.setLevel(logging.DEBUG)
file_handler.setFormatter(logging.Formatter(LOG_FORMAT, DATE_FORMAT))
logger.addHandler(file_handler)
# Error file handler (ERROR level)
error_handler = RotatingFileHandler(
ERROR_LOG_FILE,
maxBytes=10*1024*1024,
backupCount=7,
encoding="utf-8"
)
error_handler.setLevel(logging.ERROR)
error_handler.setFormatter(logging.Formatter(LOG_FORMAT, DATE_FORMAT))
logger.addHandler(error_handler)
def get_logger(name: str) -> logging.Logger:
"""Get child logger"""
return logger.getChild(name)

View File

@@ -0,0 +1,109 @@
"""
安全认证模块
"""
from datetime import datetime, timedelta
from typing import Any, Optional, Annotated
from jose import jwt, JWTError
import bcrypt
from fastapi import Depends, HTTPException, status
from fastapi.security import OAuth2PasswordBearer
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select
from app.core.config import settings
from app.core.database import get_db
from app.models.models import User
# 密码加密直接使用 bcrypt
# OAuth2 密码模式
oauth2_scheme = OAuth2PasswordBearer(tokenUrl=f"{settings.API_PREFIX}/auth/login")
def verify_password(plain_password: str, hashed_password: str) -> bool:
"""验证密码"""
return bcrypt.checkpw(plain_password.encode('utf-8'), hashed_password.encode('utf-8'))
def get_password_hash(password: str) -> str:
"""生成密码哈希"""
return bcrypt.hashpw(password.encode('utf-8'), bcrypt.gensalt()).decode('utf-8')
def create_access_token(subject: str | Any, expires_delta: Optional[timedelta] = None) -> str:
"""创建访问令牌"""
if expires_delta:
expire = datetime.utcnow() + expires_delta
else:
expire = datetime.utcnow() + timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES)
to_encode = {"exp": expire, "sub": str(subject)}
encoded_jwt = jwt.encode(to_encode, settings.SECRET_KEY, algorithm=settings.ALGORITHM)
return encoded_jwt
def decode_token(token: str) -> Optional[dict]:
"""解码令牌"""
try:
payload = jwt.decode(token, settings.SECRET_KEY, algorithms=[settings.ALGORITHM])
return payload
except JWTError:
return None
async def get_current_user(
token: Annotated[str, Depends(oauth2_scheme)],
db: AsyncSession = Depends(get_db)
) -> User:
"""从JWT获取当前用户"""
credentials_exception = HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="无法验证凭据",
headers={"WWW-Authenticate": "Bearer"},
)
payload = decode_token(token)
if payload is None:
raise credentials_exception
user_id: str = payload.get("sub")
if user_id is None:
raise credentials_exception
result = await db.execute(
select(User).where(User.id == int(user_id))
)
user = result.scalar_one_or_none()
if user is None:
raise credentials_exception
return user
async def get_current_active_user(
current_user: Annotated[User, Depends(get_current_user)]
) -> User:
"""验证当前用户是否激活"""
if not current_user.is_active:
raise HTTPException(status_code=400, detail="用户已被禁用")
return current_user
async def get_current_admin_user(
current_user: Annotated[User, Depends(get_current_active_user)]
) -> User:
"""验证当前用户是否为管理员"""
if current_user.role != "admin":
raise HTTPException(status_code=403, detail="需要管理员权限")
return current_user
async def get_current_manager_user(
current_user: Annotated[User, Depends(get_current_active_user)]
) -> User:
"""验证当前用户是否为管理员或经理"""
if current_user.role not in ("admin", "manager"):
raise HTTPException(status_code=403, detail="需要管理员或经理权限")
return current_user