add backend source code
This commit is contained in:
0
backend/app/core/__init__.py
Normal file
0
backend/app/core/__init__.py
Normal file
46
backend/app/core/config.py
Normal file
46
backend/app/core/config.py
Normal 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()
|
||||
38
backend/app/core/database.py
Normal file
38
backend/app/core/database.py
Normal 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
114
backend/app/core/init_db.py
Normal 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())
|
||||
64
backend/app/core/logging_config.py
Normal file
64
backend/app/core/logging_config.py
Normal 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)
|
||||
109
backend/app/core/security.py
Normal file
109
backend/app/core/security.py
Normal 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
|
||||
Reference in New Issue
Block a user