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

0
backend/app/__init__.py Normal file
View File

View File

View File

@@ -0,0 +1,17 @@
from fastapi import APIRouter
from app.api.v1 import departments, staff, indicators, assessments, salary, stats, auth, finance, performance_plans, menus, templates, surveys
api_router = APIRouter()
api_router.include_router(auth.router)
api_router.include_router(departments.router)
api_router.include_router(staff.router)
api_router.include_router(indicators.router)
api_router.include_router(assessments.router)
api_router.include_router(salary.router)
api_router.include_router(stats.router)
api_router.include_router(finance.router)
api_router.include_router(performance_plans.router)
api_router.include_router(menus.router)
api_router.include_router(templates.router)
api_router.include_router(surveys.router)

View File

@@ -0,0 +1,165 @@
"""
API路由 - 绩效考核管理
"""
from typing import Annotated, Optional, List
from fastapi import APIRouter, Depends, HTTPException, Query
from sqlalchemy.ext.asyncio import AsyncSession
from app.core.database import get_db
from app.core.security import get_current_active_user, get_current_manager_user
from app.schemas.schemas import (
AssessmentCreate, AssessmentUpdate, AssessmentResponse,
AssessmentListResponse, ResponseBase
)
from app.services.assessment_service import AssessmentService
from app.models.models import User
router = APIRouter(prefix="/assessments", tags=["绩效考核"])
@router.get("", summary="获取考核列表")
async def get_assessments(
staff_id: Optional[int] = Query(None, description="员工ID"),
department_id: Optional[int] = Query(None, description="科室ID"),
period_year: Optional[int] = Query(None, description="年度"),
period_month: Optional[int] = Query(None, description="月份"),
status: Optional[str] = Query(None, description="状态"),
page: int = Query(1, ge=1, description="页码"),
page_size: int = Query(20, ge=1, le=100, description="每页数量"),
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_active_user)
):
"""获取考核列表"""
assessments, total = await AssessmentService.get_list(
db, staff_id, department_id, period_year, period_month, status, page, page_size
)
# 转换响应
result = []
for assessment in assessments:
item = AssessmentListResponse.model_validate(assessment).model_dump()
item["staff_name"] = assessment.staff.name if assessment.staff else None
item["department_name"] = assessment.staff.department.name if assessment.staff and assessment.staff.department else None
result.append(item)
return {
"code": 200,
"message": "success",
"data": result,
"total": total,
"page": page,
"page_size": page_size
}
@router.get("/{assessment_id}", summary="获取考核详情")
async def get_assessment(
assessment_id: int,
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_active_user)
):
"""获取考核详情"""
assessment = await AssessmentService.get_by_id(db, assessment_id)
if not assessment:
raise HTTPException(status_code=404, detail="考核记录不存在")
result = AssessmentResponse.model_validate(assessment).model_dump()
result["staff_name"] = assessment.staff.name if assessment.staff else None
result["department_name"] = assessment.staff.department.name if assessment.staff and assessment.staff.department else None
# 添加明细指标名称
for detail in result.get("details", []):
for d in assessment.details:
if d.id == detail["id"] and d.indicator:
detail["indicator_name"] = d.indicator.name
break
return {"code": 200, "message": "success", "data": result}
@router.post("", summary="创建考核记录")
async def create_assessment(
assessment_data: AssessmentCreate,
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_active_user)
):
"""创建考核记录"""
assessment = await AssessmentService.create(db, assessment_data)
return {"code": 200, "message": "创建成功", "data": {"id": assessment.id}}
@router.put("/{assessment_id}", summary="更新考核记录")
async def update_assessment(
assessment_id: int,
assessment_data: AssessmentUpdate,
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_active_user)
):
"""更新考核记录"""
assessment = await AssessmentService.update(db, assessment_id, assessment_data)
if not assessment:
raise HTTPException(status_code=400, detail="无法更新,考核记录不存在或状态不允许")
return {"code": 200, "message": "更新成功", "data": {"id": assessment.id}}
@router.post("/{assessment_id}/submit", summary="提交考核")
async def submit_assessment(
assessment_id: int,
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_active_user)
):
"""提交考核"""
assessment = await AssessmentService.submit(db, assessment_id)
if not assessment:
raise HTTPException(status_code=400, detail="无法提交,考核记录不存在或状态不允许")
return {"code": 200, "message": "提交成功"}
@router.post("/{assessment_id}/review", summary="审核考核")
async def review_assessment(
assessment_id: int,
approved: bool = Query(..., description="是否通过"),
remark: Optional[str] = Query(None, description="审核意见"),
db: AsyncSession = Depends(get_db),
current_user: Annotated[User, Depends(get_current_manager_user)] = None
):
"""审核考核(需要管理员或经理权限)"""
# 从当前用户获取审核人ID
reviewer_id = current_user.id
assessment = await AssessmentService.review(db, assessment_id, reviewer_id, approved, remark)
if not assessment:
raise HTTPException(status_code=400, detail="无法审核,考核记录不存在或状态不允许")
return {"code": 200, "message": "审核通过" if approved else "已驳回"}
@router.post("/{assessment_id}/finalize", summary="确认考核")
async def finalize_assessment(
assessment_id: int,
db: AsyncSession = Depends(get_db),
current_user: Annotated[User, Depends(get_current_manager_user)] = None
):
"""确认考核(需要管理员或经理权限)"""
assessment = await AssessmentService.finalize(db, assessment_id)
if not assessment:
raise HTTPException(status_code=400, detail="无法确认,考核记录不存在或状态不允许")
return {"code": 200, "message": "确认成功"}
@router.post("/batch-create", summary="批量创建考核")
async def batch_create_assessments(
department_id: int = Query(..., description="科室ID"),
period_year: int = Query(..., description="年度"),
period_month: int = Query(..., description="月份"),
indicators: List[int] = Query(..., description="指标ID列表"),
db: AsyncSession = Depends(get_db),
current_user: Annotated[User, Depends(get_current_manager_user)] = None
):
"""为科室批量创建考核(需要管理员或经理权限)"""
assessments = await AssessmentService.batch_create_for_department(
db, department_id, period_year, period_month, indicators
)
return {
"code": 200,
"message": f"成功创建 {len(assessments)} 条考核记录",
"data": {"count": len(assessments)}
}

135
backend/app/api/v1/auth.py Normal file
View File

@@ -0,0 +1,135 @@
"""
API路由 - 用户认证
"""
from typing import Annotated, List
from fastapi import APIRouter, Depends, HTTPException, Query
from fastapi.security import OAuth2PasswordRequestForm
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select
from app.core.database import get_db
from app.core.security import verify_password, create_access_token, get_current_active_user
from app.schemas.schemas import UserLogin, UserCreate, UserResponse, Token
from app.models.models import User
router = APIRouter(prefix="/auth", tags=["用户认证"])
@router.post("/login", response_model=Token, summary="用户登录")
async def login(
form_data: OAuth2PasswordRequestForm = Depends(),
db: AsyncSession = Depends(get_db)
):
"""用户登录"""
from sqlalchemy import select
result = await db.execute(
select(User).where(User.username == form_data.username)
)
user = result.scalar_one_or_none()
if not user or not verify_password(form_data.password, user.password_hash):
raise HTTPException(status_code=401, detail="用户名或密码错误")
if not user.is_active:
raise HTTPException(status_code=403, detail="账户已禁用")
access_token = create_access_token(user.id)
return {"access_token": access_token, "token_type": "bearer"}
@router.post("/login-json", summary="用户登录(JSON格式)")
async def login_json(
login_data: UserLogin,
db: AsyncSession = Depends(get_db)
):
"""用户登录(JSON格式)"""
result = await db.execute(
select(User).where(User.username == login_data.username)
)
user = result.scalar_one_or_none()
if not user or not verify_password(login_data.password, user.password_hash):
raise HTTPException(status_code=401, detail="用户名或密码错误")
if not user.is_active:
raise HTTPException(status_code=403, detail="账户已禁用")
access_token = create_access_token(user.id)
return {"code": 200, "message": "登录成功", "data": {"access_token": access_token, "token_type": "bearer"}}
@router.post("/register", summary="用户注册")
async def register(
user_data: UserCreate,
db: AsyncSession = Depends(get_db)
):
"""用户注册"""
from sqlalchemy import select
from app.core.security import get_password_hash
# 检查用户名是否已存在
result = await db.execute(
select(User).where(User.username == user_data.username)
)
if result.scalar_one_or_none():
raise HTTPException(status_code=400, detail="用户名已存在")
user = User(
username=user_data.username,
password_hash=get_password_hash(user_data.password),
staff_id=user_data.staff_id,
role=user_data.role
)
db.add(user)
await db.flush()
return {"code": 200, "message": "注册成功", "data": {"id": user.id}}
@router.get("/me", response_model=UserResponse, summary="获取当前用户")
async def get_current_user_info(
current_user: Annotated[User, Depends(get_current_active_user)]
):
"""获取当前用户信息"""
return current_user
@router.get("/users", summary="获取用户列表")
async def get_users(
page: int = Query(1, ge=1),
page_size: int = Query(20, ge=1, le=100),
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_active_user)
):
"""获取用户列表(需要登录)"""
# 查询用户总数
count_result = await db.execute(select(User))
all_users = count_result.scalars().all()
total = len(all_users)
# 分页查询
offset = (page - 1) * page_size
result = await db.execute(
select(User).order_by(User.id.desc()).offset(offset).limit(page_size)
)
users = result.scalars().all()
return {
"code": 200,
"message": "success",
"data": [
{
"id": u.id,
"username": u.username,
"role": u.role,
"is_active": u.is_active,
"last_login": u.last_login,
"created_at": u.created_at
}
for u in users
],
"total": total,
"page": page,
"page_size": page_size
}

View File

@@ -0,0 +1,107 @@
"""
API路由 - 科室管理
"""
from typing import Annotated, Optional
from fastapi import APIRouter, Depends, HTTPException, Query
from sqlalchemy.ext.asyncio import AsyncSession
from app.core.database import get_db
from app.core.security import get_current_active_user, get_current_manager_user
from app.schemas.schemas import (
DepartmentCreate, DepartmentUpdate, DepartmentResponse,
ResponseBase, PaginatedResponse
)
from app.services.department_service import DepartmentService
from app.models.models import User
router = APIRouter(prefix="/departments", tags=["科室管理"])
@router.get("", summary="获取科室列表")
async def get_departments(
dept_type: Optional[str] = Query(None, description="科室类型"),
is_active: Optional[bool] = Query(None, description="是否启用"),
page: int = Query(1, ge=1, description="页码"),
page_size: int = Query(20, ge=1, le=100, description="每页数量"),
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_active_user)
):
"""获取科室列表"""
departments, total = await DepartmentService.get_list(
db, dept_type, is_active, page, page_size
)
return {
"code": 200,
"message": "success",
"data": [DepartmentResponse.model_validate(d) for d in departments],
"total": total,
"page": page,
"page_size": page_size
}
@router.get("/tree", summary="获取科室树形结构")
async def get_department_tree(
dept_type: Optional[str] = Query(None, description="科室类型"),
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_active_user)
):
"""获取科室树形结构"""
tree = await DepartmentService.get_tree(db, dept_type)
return {"code": 200, "message": "success", "data": tree}
@router.get("/{dept_id}", summary="获取科室详情")
async def get_department(
dept_id: int,
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_active_user)
):
"""获取科室详情"""
department = await DepartmentService.get_by_id(db, dept_id)
if not department:
raise HTTPException(status_code=404, detail="科室不存在")
return {"code": 200, "message": "success", "data": DepartmentResponse.model_validate(department)}
@router.post("", summary="创建科室")
async def create_department(
dept_data: DepartmentCreate,
db: AsyncSession = Depends(get_db),
current_user: Annotated[User, Depends(get_current_manager_user)] = None
):
"""创建科室(需要管理员或经理权限)"""
# 检查编码是否已存在
existing = await DepartmentService.get_by_code(db, dept_data.code)
if existing:
raise HTTPException(status_code=400, detail="科室编码已存在")
department = await DepartmentService.create(db, dept_data)
return {"code": 200, "message": "创建成功", "data": DepartmentResponse.model_validate(department)}
@router.put("/{dept_id}", summary="更新科室")
async def update_department(
dept_id: int,
dept_data: DepartmentUpdate,
db: AsyncSession = Depends(get_db),
current_user: Annotated[User, Depends(get_current_manager_user)] = None
):
"""更新科室(需要管理员或经理权限)"""
department = await DepartmentService.update(db, dept_id, dept_data)
if not department:
raise HTTPException(status_code=404, detail="科室不存在")
return {"code": 200, "message": "更新成功", "data": DepartmentResponse.model_validate(department)}
@router.delete("/{dept_id}", summary="删除科室")
async def delete_department(
dept_id: int,
db: AsyncSession = Depends(get_db),
current_user: Annotated[User, Depends(get_current_manager_user)] = None
):
"""删除科室(需要管理员或经理权限)"""
success = await DepartmentService.delete(db, dept_id)
if not success:
raise HTTPException(status_code=400, detail="无法删除,科室下存在子科室")
return {"code": 200, "message": "删除成功"}

View File

@@ -0,0 +1,216 @@
"""
API路由 - 财务核算
"""
from typing import Annotated, Optional
from fastapi import APIRouter, Depends, HTTPException, Query
from sqlalchemy.ext.asyncio import AsyncSession
from app.core.database import get_db
from app.core.security import get_current_active_user, get_current_manager_user
from app.schemas.schemas import (
FinanceRecordCreate, FinanceRecordUpdate, FinanceRecordResponse,
DepartmentBalance, CategorySummary, ResponseBase
)
from app.services.finance_service import FinanceService
from app.models.finance import RevenueCategory, ExpenseCategory, FinanceType
from app.models.models import User
router = APIRouter(prefix="/finance", tags=["财务核算"])
@router.get("/revenue", summary="获取科室收入")
async def get_revenue(
department_id: Optional[int] = Query(None, description="科室ID"),
period_year: Optional[int] = Query(None, description="年度"),
period_month: Optional[int] = Query(None, description="月份"),
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_active_user)
):
"""获取科室收入列表"""
data = await FinanceService.get_department_revenue(
db, department_id, period_year, period_month
)
return {
"code": 200,
"message": "success",
"data": data
}
@router.get("/expense", summary="获取科室支出")
async def get_expense(
department_id: Optional[int] = Query(None, description="科室ID"),
period_year: Optional[int] = Query(None, description="年度"),
period_month: Optional[int] = Query(None, description="月份"),
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_active_user)
):
"""获取科室支出列表"""
data = await FinanceService.get_department_expense(
db, department_id, period_year, period_month
)
return {
"code": 200,
"message": "success",
"data": data
}
@router.get("/balance", summary="获取收支结余")
async def get_balance(
department_id: Optional[int] = Query(None, description="科室ID"),
period_year: Optional[int] = Query(None, description="年度"),
period_month: Optional[int] = Query(None, description="月份"),
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_active_user)
):
"""获取科室收支结余"""
data = await FinanceService.get_department_balance(
db, department_id, period_year, period_month
)
return {
"code": 200,
"message": "success",
"data": data
}
@router.get("/revenue/by-category", summary="按类别统计收入")
async def get_revenue_by_category(
department_id: Optional[int] = Query(None, description="科室ID"),
period_year: Optional[int] = Query(None, description="年度"),
period_month: Optional[int] = Query(None, description="月份"),
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_active_user)
):
"""按类别统计收入"""
data = await FinanceService.get_revenue_by_category(
db, department_id, period_year, period_month
)
return {
"code": 200,
"message": "success",
"data": data
}
@router.get("/expense/by-category", summary="按类别统计支出")
async def get_expense_by_category(
department_id: Optional[int] = Query(None, description="科室ID"),
period_year: Optional[int] = Query(None, description="年度"),
period_month: Optional[int] = Query(None, description="月份"),
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_active_user)
):
"""按类别统计支出"""
data = await FinanceService.get_expense_by_category(
db, department_id, period_year, period_month
)
return {
"code": 200,
"message": "success",
"data": data
}
@router.get("/summary", summary="获取科室财务汇总")
async def get_department_summary(
period_year: int = Query(..., description="年度"),
period_month: int = Query(..., ge=1, le=12, description="月份"),
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_active_user)
):
"""获取所有科室的财务汇总"""
data = await FinanceService.get_department_summary(
db, period_year, period_month
)
return {
"code": 200,
"message": "success",
"data": data
}
@router.get("/categories", summary="获取财务类别")
async def get_categories(
current_user: User = Depends(get_current_active_user)
):
"""获取收入和支出类别"""
revenue_categories = [
{"value": cat.value, "label": label}
for cat, label in FinanceService.REVENUE_LABELS.items()
]
expense_categories = [
{"value": cat.value, "label": label}
for cat, label in FinanceService.EXPENSE_LABELS.items()
]
return {
"code": 200,
"message": "success",
"data": {
"revenue": revenue_categories,
"expense": expense_categories
}
}
@router.post("", summary="创建财务记录")
async def create_finance_record(
record_data: FinanceRecordCreate,
db: AsyncSession = Depends(get_db),
current_user: Annotated[User, Depends(get_current_manager_user)] = None
):
"""创建财务记录(需要管理员或经理权限)"""
# 验证类别
if record_data.finance_type == FinanceType.REVENUE:
valid_categories = [cat.value for cat in RevenueCategory]
else:
valid_categories = [cat.value for cat in ExpenseCategory]
if record_data.category not in valid_categories:
raise HTTPException(
status_code=400,
detail=f"无效的类别: {record_data.category}"
)
record = await FinanceService.create_finance_record(
db,
department_id=record_data.department_id,
finance_type=record_data.finance_type,
category=record_data.category,
amount=record_data.amount,
period_year=record_data.period_year,
period_month=record_data.period_month,
source=record_data.source,
remark=record_data.remark
)
return {"code": 200, "message": "创建成功", "data": {"id": record.id}}
@router.put("/{record_id}", summary="更新财务记录")
async def update_finance_record(
record_id: int,
record_data: FinanceRecordUpdate,
db: AsyncSession = Depends(get_db),
current_user: Annotated[User, Depends(get_current_manager_user)] = None
):
"""更新财务记录(需要管理员或经理权限)"""
record = await FinanceService.update_finance_record(
db, record_id, **record_data.model_dump(exclude_unset=True)
)
if not record:
raise HTTPException(status_code=404, detail="财务记录不存在")
return {"code": 200, "message": "更新成功"}
@router.delete("/{record_id}", summary="删除财务记录")
async def delete_finance_record(
record_id: int,
db: AsyncSession = Depends(get_db),
current_user: Annotated[User, Depends(get_current_manager_user)] = None
):
"""删除财务记录(需要管理员或经理权限)"""
success = await FinanceService.delete_finance_record(db, record_id)
if not success:
raise HTTPException(status_code=404, detail="财务记录不存在")
return {"code": 200, "message": "删除成功"}

View File

@@ -0,0 +1,141 @@
"""
API路由 - 考核指标管理
"""
from typing import Annotated, Optional
from fastapi import APIRouter, Depends, HTTPException, Query
from sqlalchemy.ext.asyncio import AsyncSession
from app.core.database import get_db
from app.core.security import get_current_active_user, get_current_manager_user
from app.schemas.schemas import (
IndicatorCreate, IndicatorUpdate, IndicatorResponse,
ResponseBase
)
from app.services.indicator_service import IndicatorService
from app.models.models import User
router = APIRouter(prefix="/indicators", tags=["考核指标"])
@router.get("", summary="获取指标列表")
async def get_indicators(
indicator_type: Optional[str] = Query(None, description="指标类型"),
bs_dimension: Optional[str] = Query(None, description="BSC 维度"),
is_active: Optional[bool] = Query(None, description="是否启用"),
page: int = Query(1, ge=1, description="页码"),
page_size: int = Query(20, ge=1, le=100, description="每页数量"),
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_active_user)
):
"""获取指标列表"""
indicators, total = await IndicatorService.get_list(
db, indicator_type, bs_dimension, is_active, page, page_size
)
return {
"code": 200,
"message": "success",
"data": [IndicatorResponse.model_validate(i, from_attributes=True) for i in indicators],
"total": total,
"page": page,
"page_size": page_size
}
@router.get("/active", summary="获取所有启用的指标")
async def get_active_indicators(
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_active_user)
):
"""获取所有启用的指标"""
indicators = await IndicatorService.get_active_indicators(db)
return {
"code": 200,
"message": "success",
"data": [IndicatorResponse.model_validate(i, from_attributes=True) for i in indicators]
}
@router.get("/{indicator_id}", summary="获取指标详情")
async def get_indicator(
indicator_id: int,
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_active_user)
):
"""获取指标详情"""
indicator = await IndicatorService.get_by_id(db, indicator_id)
if not indicator:
raise HTTPException(status_code=404, detail="指标不存在")
return {"code": 200, "message": "success", "data": IndicatorResponse.model_validate(indicator, from_attributes=True)}
@router.post("", summary="创建指标")
async def create_indicator(
indicator_data: IndicatorCreate,
db: AsyncSession = Depends(get_db),
current_user: Annotated[User, Depends(get_current_manager_user)] = None
):
"""创建指标(需要管理员或经理权限)"""
# 检查编码是否已存在
existing = await IndicatorService.get_by_code(db, indicator_data.code)
if existing:
raise HTTPException(status_code=400, detail="指标编码已存在")
indicator = await IndicatorService.create(db, indicator_data)
return {"code": 200, "message": "创建成功", "data": IndicatorResponse.model_validate(indicator, from_attributes=True)}
@router.put("/{indicator_id}", summary="更新指标")
async def update_indicator(
indicator_id: int,
indicator_data: IndicatorUpdate,
db: AsyncSession = Depends(get_db),
current_user: Annotated[User, Depends(get_current_manager_user)] = None
):
"""更新指标(需要管理员或经理权限)"""
indicator = await IndicatorService.update(db, indicator_id, indicator_data)
if not indicator:
raise HTTPException(status_code=404, detail="指标不存在")
return {"code": 200, "message": "更新成功", "data": IndicatorResponse.model_validate(indicator, from_attributes=True)}
@router.delete("/{indicator_id}", summary="删除指标")
async def delete_indicator(
indicator_id: int,
db: AsyncSession = Depends(get_db),
current_user: Annotated[User, Depends(get_current_manager_user)] = None
):
"""删除指标(需要管理员或经理权限)"""
success = await IndicatorService.delete(db, indicator_id)
if not success:
raise HTTPException(status_code=404, detail="指标不存在")
return {"code": 200, "message": "删除成功"}
@router.get("/templates/list", summary="获取指标模板列表")
async def get_indicator_templates(
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_active_user)
):
"""获取指标模板列表"""
templates = await IndicatorService.get_templates()
return {
"code": 200,
"message": "success",
"data": templates
}
@router.post("/templates/import", summary="导入指标模板")
async def import_indicator_template(
template_data: dict,
overwrite: bool = Query(False, description="是否覆盖已存在的指标"),
db: AsyncSession = Depends(get_db),
current_user: Annotated[User, Depends(get_current_manager_user)] = None
):
"""导入指标模板"""
count = await IndicatorService.import_template(db, template_data, overwrite)
return {
"code": 200,
"message": f"成功导入 {count} 个指标",
"data": {"created_count": count}
}

163
backend/app/api/v1/menus.py Normal file
View File

@@ -0,0 +1,163 @@
"""
菜单管理 API
"""
from typing import Annotated, Optional
from fastapi import APIRouter, Depends, HTTPException, Query
from sqlalchemy.ext.asyncio import AsyncSession
from app.core.database import get_db
from app.core.security import get_current_active_user, get_current_manager_user
from app.schemas.schemas import MenuCreate, MenuUpdate, MenuResponse, ResponseBase
from app.services.menu_service import MenuService
from app.models.models import User, Menu
router = APIRouter(prefix="/menus", tags=["菜单管理"])
@router.get("/tree", summary="获取菜单树")
async def get_menu_tree(
visible_only: bool = Query(True, description="是否只返回可见菜单"),
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_active_user)
):
"""获取菜单树形结构"""
tree = await MenuService.get_tree(db, visible_only)
return {
"code": 200,
"message": "success",
"data": tree
}
@router.get("", summary="获取菜单列表")
async def get_menus(
menu_type: Optional[str] = Query(None, description="菜单类型"),
is_visible: Optional[bool] = Query(None, description="是否可见"),
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_active_user)
):
"""获取菜单列表"""
menus = await MenuService.get_list(db, menu_type, is_visible)
menu_list = []
for menu in menus:
menu_dict = {
"id": menu.id,
"parent_id": menu.parent_id,
"menu_type": menu.menu_type,
"menu_name": menu.menu_name,
"menu_icon": menu.menu_icon,
"path": menu.path,
"component": menu.component,
"permission": menu.permission,
"sort_order": menu.sort_order,
"is_visible": menu.is_visible,
"is_active": menu.is_active,
"created_at": menu.created_at,
"updated_at": menu.updated_at
}
menu_list.append(menu_dict)
return {
"code": 200,
"message": "success",
"data": menu_list
}
@router.get("/{menu_id}", summary="获取菜单详情")
async def get_menu(
menu_id: int,
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_active_user)
):
"""获取菜单详情"""
menu = await MenuService.get_by_id(db, menu_id)
if not menu:
raise HTTPException(status_code=404, detail="菜单不存在")
return {
"code": 200,
"message": "success",
"data": {
"id": menu.id,
"parent_id": menu.parent_id,
"menu_type": menu.menu_type,
"menu_name": menu.menu_name,
"menu_icon": menu.menu_icon,
"path": menu.path,
"component": menu.component,
"permission": menu.permission,
"sort_order": menu.sort_order,
"is_visible": menu.is_visible,
"is_active": menu.is_active
}
}
@router.post("", summary="创建菜单")
async def create_menu(
menu_data: MenuCreate,
db: AsyncSession = Depends(get_db),
current_user: Annotated[User, Depends(get_current_manager_user)] = None
):
"""创建菜单(需要管理员或经理权限)"""
# 如果指定了父菜单,检查父菜单是否存在
if menu_data.parent_id:
parent = await MenuService.get_by_id(db, menu_data.parent_id)
if not parent:
raise HTTPException(status_code=400, detail="父菜单不存在")
menu = await MenuService.create(db, menu_data.model_dump())
return {
"code": 200,
"message": "创建成功",
"data": {"id": menu.id}
}
@router.put("/{menu_id}", summary="更新菜单")
async def update_menu(
menu_id: int,
menu_data: MenuUpdate,
db: AsyncSession = Depends(get_db),
current_user: Annotated[User, Depends(get_current_manager_user)] = None
):
"""更新菜单(需要管理员或经理权限)"""
menu = await MenuService.update(db, menu_id, menu_data.model_dump(exclude_unset=True))
if not menu:
raise HTTPException(status_code=404, detail="菜单不存在")
return {
"code": 200,
"message": "更新成功",
"data": {"id": menu.id}
}
@router.delete("/{menu_id}", summary="删除菜单")
async def delete_menu(
menu_id: int,
db: AsyncSession = Depends(get_db),
current_user: Annotated[User, Depends(get_current_manager_user)] = None
):
"""删除菜单(需要管理员或经理权限)"""
success = await MenuService.delete(db, menu_id)
if not success:
raise HTTPException(status_code=400, detail="无法删除,菜单不存在或存在子菜单")
return {
"code": 200,
"message": "删除成功"
}
@router.post("/init", summary="初始化默认菜单")
async def init_default_menus(
db: AsyncSession = Depends(get_db),
current_user: Annotated[User, Depends(get_current_manager_user)] = None
):
"""初始化默认菜单(需要管理员权限)"""
await MenuService.init_default_menus(db)
return {
"code": 200,
"message": "初始化成功"
}

View File

@@ -0,0 +1,309 @@
"""
绩效计划管理 API
"""
from typing import Annotated, Optional
from fastapi import APIRouter, Depends, HTTPException, Query
from sqlalchemy.ext.asyncio import AsyncSession
from app.core.database import get_db
from app.core.security import get_current_active_user, get_current_manager_user
from app.schemas.schemas import (
PerformancePlanCreate, PerformancePlanUpdate, PerformancePlanResponse,
PerformancePlanStats, PlanKpiRelationCreate, PlanKpiRelationUpdate,
ResponseBase
)
from app.services.performance_plan_service import PerformancePlanService
from app.models.models import User, PlanStatus
router = APIRouter(prefix="/plans", tags=["绩效计划管理"])
@router.get("", summary="获取绩效计划列表")
async def get_performance_plans(
plan_level: Optional[str] = Query(None, description="计划层级"),
plan_year: Optional[int] = Query(None, description="计划年度"),
department_id: Optional[int] = Query(None, description="科室 ID"),
status: Optional[str] = Query(None, description="状态"),
page: int = Query(1, ge=1, description="页码"),
page_size: int = Query(20, ge=1, le=100, description="每页数量"),
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_active_user)
):
"""获取绩效计划列表"""
plans, total = await PerformancePlanService.get_list(
db, plan_level, plan_year, department_id, status, page, page_size
)
# 构建响应数据
plan_list = []
for plan in plans:
plan_dict = {
"id": plan.id,
"plan_name": plan.plan_name,
"plan_code": plan.plan_code,
"plan_level": plan.plan_level,
"plan_year": plan.plan_year,
"plan_month": plan.plan_month,
"status": plan.status,
"department_id": plan.department_id,
"department_name": plan.department.name if plan.department else None,
"staff_id": plan.staff_id,
"staff_name": plan.staff.name if plan.staff else None,
"description": plan.description,
"created_at": plan.created_at,
"updated_at": plan.updated_at
}
plan_list.append(plan_dict)
return {
"code": 200,
"message": "success",
"data": plan_list,
"total": total,
"page": page,
"page_size": page_size
}
@router.get("/tree", summary="获取绩效计划树")
async def get_performance_plan_tree(
plan_year: Optional[int] = Query(None, description="计划年度"),
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_active_user)
):
"""获取绩效计划树形结构"""
tree = await PerformancePlanService.get_tree(db, plan_year)
return {
"code": 200,
"message": "success",
"data": tree
}
@router.get("/stats", summary="获取绩效计划统计")
async def get_performance_plan_stats(
plan_year: Optional[int] = Query(None, description="计划年度"),
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_active_user)
):
"""获取绩效计划统计信息"""
stats = await PerformancePlanService.get_stats(db, plan_year)
return {
"code": 200,
"message": "success",
"data": stats
}
@router.get("/{plan_id}", summary="获取绩效计划详情")
async def get_performance_plan(
plan_id: int,
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_active_user)
):
"""获取绩效计划详情"""
plan = await PerformancePlanService.get_by_id(db, plan_id)
if not plan:
raise HTTPException(status_code=404, detail="绩效计划不存在")
# 构建响应数据
kpi_relations = []
for relation in plan.kpi_relations:
kpi_relations.append({
"id": relation.id,
"indicator_id": relation.indicator_id,
"indicator_name": relation.indicator.name if relation.indicator else None,
"indicator_code": relation.indicator.code if relation.indicator else None,
"target_value": relation.target_value,
"target_unit": relation.target_unit,
"weight": relation.weight,
"scoring_method": relation.scoring_method,
"scoring_params": relation.scoring_params,
"remark": relation.remark
})
plan_data = {
"id": plan.id,
"plan_name": plan.plan_name,
"plan_code": plan.plan_code,
"plan_level": plan.plan_level,
"plan_year": plan.plan_year,
"plan_month": plan.plan_month,
"plan_type": plan.plan_type,
"department_id": plan.department_id,
"department_name": plan.department.name if plan.department else None,
"staff_id": plan.staff_id,
"staff_name": plan.staff.name if plan.staff else None,
"parent_plan_id": plan.parent_plan_id,
"description": plan.description,
"strategic_goals": plan.strategic_goals,
"key_initiatives": plan.key_initiatives,
"status": plan.status,
"submitter_id": plan.submitter_id,
"submit_time": plan.submit_time,
"approver_id": plan.approver_id,
"approve_time": plan.approve_time,
"approve_remark": plan.approve_remark,
"version": plan.version,
"is_active": plan.is_active,
"created_at": plan.created_at,
"updated_at": plan.updated_at,
"kpi_relations": kpi_relations
}
return {
"code": 200,
"message": "success",
"data": plan_data
}
@router.post("", summary="创建绩效计划")
async def create_performance_plan(
plan_data: PerformancePlanCreate,
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_active_user)
):
"""创建绩效计划"""
plan = await PerformancePlanService.create(db, plan_data, current_user.id)
return {
"code": 200,
"message": "创建成功",
"data": {"id": plan.id}
}
@router.put("/{plan_id}", summary="更新绩效计划")
async def update_performance_plan(
plan_id: int,
plan_data: PerformancePlanUpdate,
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_active_user)
):
"""更新绩效计划"""
plan = await PerformancePlanService.update(db, plan_id, plan_data)
if not plan:
raise HTTPException(status_code=404, detail="绩效计划不存在")
return {
"code": 200,
"message": "更新成功",
"data": {"id": plan.id}
}
@router.post("/{plan_id}/submit", summary="提交绩效计划")
async def submit_performance_plan(
plan_id: int,
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_active_user)
):
"""提交绩效计划"""
plan = await PerformancePlanService.submit(db, plan_id)
if not plan:
raise HTTPException(status_code=400, detail="无法提交,计划不存在或状态不允许")
return {
"code": 200,
"message": "提交成功"
}
@router.post("/{plan_id}/approve", summary="审批绩效计划")
async def approve_performance_plan(
plan_id: int,
approved: bool = Query(..., description="是否通过"),
remark: Optional[str] = Query(None, description="审批意见"),
db: AsyncSession = Depends(get_db),
current_user: Annotated[User, Depends(get_current_manager_user)] = None
):
"""审批绩效计划(需要管理员或经理权限)"""
plan = await PerformancePlanService.approve(db, plan_id, current_user.id, approved, remark)
if not plan:
raise HTTPException(status_code=400, detail="无法审批,计划不存在或状态不允许")
return {
"code": 200,
"message": "审核通过" if approved else "已驳回"
}
@router.post("/{plan_id}/activate", summary="激活绩效计划")
async def activate_performance_plan(
plan_id: int,
db: AsyncSession = Depends(get_db),
current_user: Annotated[User, Depends(get_current_manager_user)] = None
):
"""激活绩效计划(需要管理员或经理权限)"""
plan = await PerformancePlanService.activate(db, plan_id)
if not plan:
raise HTTPException(status_code=400, detail="无法激活,计划不存在或状态不允许")
return {
"code": 200,
"message": "激活成功"
}
@router.delete("/{plan_id}", summary="删除绩效计划")
async def delete_performance_plan(
plan_id: int,
db: AsyncSession = Depends(get_db),
current_user: Annotated[User, Depends(get_current_manager_user)] = None
):
"""删除绩效计划(需要管理员或经理权限)"""
success = await PerformancePlanService.delete(db, plan_id)
if not success:
raise HTTPException(status_code=404, detail="绩效计划不存在")
return {
"code": 200,
"message": "删除成功"
}
@router.post("/{plan_id}/kpi-relations", summary="添加计划指标关联")
async def add_kpi_relation(
plan_id: int,
kpi_data: PlanKpiRelationCreate,
db: AsyncSession = Depends(get_db),
current_user: Annotated[User, Depends(get_current_manager_user)] = None
):
"""添加计划指标关联(需要管理员或经理权限)"""
relation = await PerformancePlanService.add_kpi_relation(db, plan_id, kpi_data)
if not relation:
raise HTTPException(status_code=404, detail="绩效计划不存在")
return {
"code": 200,
"message": "添加成功",
"data": {"id": relation.id}
}
@router.put("/kpi-relations/{relation_id}", summary="更新计划指标关联")
async def update_kpi_relation(
relation_id: int,
kpi_data: PlanKpiRelationUpdate,
db: AsyncSession = Depends(get_db),
current_user: Annotated[User, Depends(get_current_manager_user)] = None
):
"""更新计划指标关联(需要管理员或经理权限)"""
relation = await PerformancePlanService.update_kpi_relation(db, relation_id, kpi_data.model_dump())
if not relation:
raise HTTPException(status_code=404, detail="指标关联不存在")
return {
"code": 200,
"message": "更新成功",
"data": {"id": relation.id}
}
@router.delete("/kpi-relations/{relation_id}", summary="删除计划指标关联")
async def delete_kpi_relation(
relation_id: int,
db: AsyncSession = Depends(get_db),
current_user: Annotated[User, Depends(get_current_manager_user)] = None
):
"""删除计划指标关联(需要管理员或经理权限)"""
success = await PerformancePlanService.delete_kpi_relation(db, relation_id)
if not success:
raise HTTPException(status_code=404, detail="指标关联不存在")
return {
"code": 200,
"message": "删除成功"
}

View File

@@ -0,0 +1,155 @@
"""
API路由 - 工资核算管理
"""
from typing import Annotated, Optional
from fastapi import APIRouter, Depends, HTTPException, Query
from sqlalchemy.ext.asyncio import AsyncSession
from app.core.database import get_db
from app.core.security import get_current_active_user, get_current_manager_user
from app.schemas.schemas import (
SalaryRecordCreate, SalaryRecordUpdate, SalaryRecordResponse,
ResponseBase
)
from app.services.salary_service import SalaryService
from app.models.models import User
router = APIRouter(prefix="/salary", tags=["工资核算"])
@router.get("", summary="获取工资记录列表")
async def get_salary_records(
staff_id: Optional[int] = Query(None, description="员工ID"),
department_id: Optional[int] = Query(None, description="科室ID"),
period_year: Optional[int] = Query(None, description="年度"),
period_month: Optional[int] = Query(None, description="月份"),
status: Optional[str] = Query(None, description="状态"),
page: int = Query(1, ge=1, description="页码"),
page_size: int = Query(20, ge=1, le=100, description="每页数量"),
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_active_user)
):
"""获取工资记录列表"""
records, total = await SalaryService.get_list(
db, staff_id, department_id, period_year, period_month, status, page, page_size
)
result = []
for record in records:
item = SalaryRecordResponse.model_validate(record).model_dump()
item["staff_name"] = record.staff.name if record.staff else None
item["department_name"] = record.staff.department.name if record.staff and record.staff.department else None
result.append(item)
return {
"code": 200,
"message": "success",
"data": result,
"total": total,
"page": page,
"page_size": page_size
}
@router.get("/{record_id}", summary="获取工资记录详情")
async def get_salary_record(
record_id: int,
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_active_user)
):
"""获取工资记录详情"""
record = await SalaryService.get_by_id(db, record_id)
if not record:
raise HTTPException(status_code=404, detail="工资记录不存在")
result = SalaryRecordResponse.model_validate(record).model_dump()
result["staff_name"] = record.staff.name if record.staff else None
result["department_name"] = record.staff.department.name if record.staff and record.staff.department else None
return {"code": 200, "message": "success", "data": result}
@router.post("", summary="创建工资记录")
async def create_salary_record(
record_data: SalaryRecordCreate,
db: AsyncSession = Depends(get_db),
current_user: Annotated[User, Depends(get_current_manager_user)] = None
):
"""创建工资记录(需要管理员或经理权限)"""
record = await SalaryService.create(db, record_data)
return {"code": 200, "message": "创建成功", "data": {"id": record.id}}
@router.put("/{record_id}", summary="更新工资记录")
async def update_salary_record(
record_id: int,
record_data: SalaryRecordUpdate,
db: AsyncSession = Depends(get_db),
current_user: Annotated[User, Depends(get_current_manager_user)] = None
):
"""更新工资记录(需要管理员或经理权限)"""
record = await SalaryService.update(db, record_id, record_data)
if not record:
raise HTTPException(status_code=400, detail="无法更新,记录不存在或状态不允许")
return {"code": 200, "message": "更新成功", "data": {"id": record.id}}
@router.post("/generate", summary="根据考核生成工资")
async def generate_salary(
staff_id: int = Query(..., description="员工ID"),
period_year: int = Query(..., description="年度"),
period_month: int = Query(..., description="月份"),
db: AsyncSession = Depends(get_db),
current_user: Annotated[User, Depends(get_current_manager_user)] = None
):
"""根据考核记录生成工资记录(需要管理员或经理权限)"""
record = await SalaryService.generate_from_assessment(
db, staff_id, period_year, period_month
)
if not record:
raise HTTPException(status_code=400, detail="无法生成,未找到已确认的考核记录或已存在工资记录")
return {"code": 200, "message": "生成成功", "data": {"id": record.id}}
@router.post("/batch-generate", summary="批量生成工资")
async def batch_generate_salary(
department_id: int = Query(..., description="科室ID"),
period_year: int = Query(..., description="年度"),
period_month: int = Query(..., description="月份"),
db: AsyncSession = Depends(get_db),
current_user: Annotated[User, Depends(get_current_manager_user)] = None
):
"""为科室批量生成工资记录(需要管理员或经理权限)"""
records = await SalaryService.batch_generate_for_department(
db, department_id, period_year, period_month
)
return {
"code": 200,
"message": f"成功生成 {len(records)} 条工资记录",
"data": {"count": len(records)}
}
@router.post("/{record_id}/confirm", summary="确认工资")
async def confirm_salary(
record_id: int,
db: AsyncSession = Depends(get_db),
current_user: Annotated[User, Depends(get_current_manager_user)] = None
):
"""确认工资(需要管理员或经理权限)"""
record = await SalaryService.confirm(db, record_id)
if not record:
raise HTTPException(status_code=400, detail="无法确认,记录不存在或状态不允许")
return {"code": 200, "message": "确认成功"}
@router.post("/batch-confirm", summary="批量确认工资")
async def batch_confirm_salary(
period_year: int = Query(..., description="年度"),
period_month: int = Query(..., description="月份"),
department_id: Optional[int] = Query(None, description="科室ID"),
db: AsyncSession = Depends(get_db),
current_user: Annotated[User, Depends(get_current_manager_user)] = None
):
"""批量确认工资(需要管理员或经理权限)"""
count = await SalaryService.batch_confirm(db, period_year, period_month, department_id)
return {"code": 200, "message": f"成功确认 {count} 条工资记录", "data": {"count": count}}

123
backend/app/api/v1/staff.py Normal file
View File

@@ -0,0 +1,123 @@
"""
API路由 - 员工管理
"""
from typing import Annotated, Optional
from fastapi import APIRouter, Depends, HTTPException, Query
from sqlalchemy.ext.asyncio import AsyncSession
from app.core.database import get_db
from app.core.security import get_current_active_user, get_current_manager_user
from app.schemas.schemas import (
StaffCreate, StaffUpdate, StaffResponse,
ResponseBase
)
from app.services.staff_service import StaffService
from app.models.models import User
router = APIRouter(prefix="/staff", tags=["员工管理"])
@router.get("", summary="获取员工列表")
async def get_staff_list(
department_id: Optional[int] = Query(None, description="科室ID"),
status: Optional[str] = Query(None, description="状态"),
keyword: Optional[str] = Query(None, description="搜索关键词"),
page: int = Query(1, ge=1, description="页码"),
page_size: int = Query(20, ge=1, le=100, description="每页数量"),
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_active_user)
):
"""获取员工列表"""
staff_list, total = await StaffService.get_list(
db, department_id, status, keyword, page, page_size
)
# 添加科室名称
result = []
for staff in staff_list:
staff_dict = StaffResponse.model_validate(staff).model_dump()
staff_dict["department_name"] = staff.department.name if staff.department else None
result.append(staff_dict)
return {
"code": 200,
"message": "success",
"data": result,
"total": total,
"page": page,
"page_size": page_size
}
@router.get("/{staff_id}", summary="获取员工详情")
async def get_staff(
staff_id: int,
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_active_user)
):
"""获取员工详情"""
staff = await StaffService.get_by_id(db, staff_id)
if not staff:
raise HTTPException(status_code=404, detail="员工不存在")
staff_dict = StaffResponse.model_validate(staff).model_dump()
staff_dict["department_name"] = staff.department.name if staff.department else None
return {"code": 200, "message": "success", "data": staff_dict}
@router.post("", summary="创建员工")
async def create_staff(
staff_data: StaffCreate,
db: AsyncSession = Depends(get_db),
current_user: Annotated[User, Depends(get_current_manager_user)] = None
):
"""创建员工(需要管理员或经理权限)"""
# 检查工号是否已存在
existing = await StaffService.get_by_employee_id(db, staff_data.employee_id)
if existing:
raise HTTPException(status_code=400, detail="工号已存在")
staff = await StaffService.create(db, staff_data)
return {"code": 200, "message": "创建成功", "data": StaffResponse.model_validate(staff)}
@router.put("/{staff_id}", summary="更新员工")
async def update_staff(
staff_id: int,
staff_data: StaffUpdate,
db: AsyncSession = Depends(get_db),
current_user: Annotated[User, Depends(get_current_manager_user)] = None
):
"""更新员工(需要管理员或经理权限)"""
staff = await StaffService.update(db, staff_id, staff_data)
if not staff:
raise HTTPException(status_code=404, detail="员工不存在")
return {"code": 200, "message": "更新成功", "data": StaffResponse.model_validate(staff)}
@router.delete("/{staff_id}", summary="删除员工")
async def delete_staff(
staff_id: int,
db: AsyncSession = Depends(get_db),
current_user: Annotated[User, Depends(get_current_manager_user)] = None
):
"""删除员工(需要管理员或经理权限)"""
success = await StaffService.delete(db, staff_id)
if not success:
raise HTTPException(status_code=404, detail="员工不存在")
return {"code": 200, "message": "删除成功"}
@router.get("/department/{department_id}", summary="获取科室员工")
async def get_department_staff(
department_id: int,
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_active_user)
):
"""获取科室下所有员工"""
staff_list = await StaffService.get_by_department(db, department_id)
return {
"code": 200,
"message": "success",
"data": [StaffResponse.model_validate(s) for s in staff_list]
}

241
backend/app/api/v1/stats.py Normal file
View File

@@ -0,0 +1,241 @@
"""
统计报表 API
"""
from typing import Optional
from datetime import datetime
from fastapi import APIRouter, Depends, Query
from sqlalchemy.ext.asyncio import AsyncSession
from app.core.database import get_db
from app.core.security import get_current_active_user
from app.models.models import User
from app.services.stats_service import StatsService
router = APIRouter(prefix="/stats", tags=["统计报表"])
@router.get("/bsc-dimension", summary="BSC 维度分析")
async def get_bsc_dimension_stats(
department_id: Optional[int] = Query(None, description="科室 ID"),
period_year: int = Query(..., description="年度"),
period_month: int = Query(..., description="月份"),
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_active_user)
):
"""获取 BSC 四个维度的统计分析"""
result = await StatsService.get_bsc_dimension_stats(
db, department_id, period_year, period_month
)
return {
"code": 200,
"message": "success",
"data": result
}
@router.get("/department", summary="科室绩效统计")
async def get_department_stats(
period_year: int = Query(..., description="年度"),
period_month: int = Query(..., description="月份"),
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_active_user)
):
"""获取各科室绩效统计"""
result = await StatsService.get_department_stats(db, period_year, period_month)
return {
"code": 200,
"message": "success",
"data": result
}
@router.get("/trend", summary="趋势分析")
async def get_trend_stats(
department_id: Optional[int] = Query(None, description="科室 ID"),
period_year: Optional[int] = Query(None, description="年度"),
months: Optional[int] = Query(6, ge=1, le=24, description="最近几个月"),
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_active_user)
):
"""获取绩效趋势分析(月度)"""
# 如果没有指定年份,使用当前年份
if not period_year:
period_year = datetime.now().year
result = await StatsService.get_trend_stats(db, department_id, period_year, months)
return {
"code": 200,
"message": "success",
"data": result
}
@router.get("/alerts", summary="预警数据")
async def get_alerts(
limit: Optional[int] = Query(10, ge=1, le=100, description="返回数量"),
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_active_user)
):
"""获取预警数据(考核到期、工资未发等)"""
# TODO: 从数据库实际查询预警数据
# 目前返回模拟数据用于演示
return {
"code": 200,
"message": "success",
"data": {
"lowScoreStaff": [], # 低分员工
"incompleteDepartments": [], # 未完成考核科室
"anomalyData": [] # 异常数据
}
}
@router.get("/period", summary="周期统计")
async def get_period_stats(
period_year: Optional[int] = Query(None, description="年度"),
period_month: Optional[int] = Query(None, description="月份"),
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_active_user)
):
"""获取周期统计数据"""
# 如果没有指定年月,使用当前年月
if not period_year:
period_year = datetime.now().year
if not period_month:
period_month = datetime.now().month
# 获取该周期的考核统计
result = await StatsService.get_department_stats(db, period_year, period_month)
# 计算汇总数据
total_departments = len(result)
total_staff = sum(dept.get('staff_count', 0) for dept in result)
avg_score = sum(dept.get('avg_score', 0) for dept in result) / total_departments if total_departments > 0 else 0
return {
"code": 200,
"message": "success",
"data": {
"period": f"{period_year}{period_month}",
"total_departments": total_departments,
"total_staff": total_staff,
"avg_score": round(avg_score, 2),
"departments": result
}
}
@router.get("/kpi-gauges", summary="关键指标仪表盘")
async def get_kpi_gauges(
period_year: Optional[int] = Query(None, description="年度"),
period_month: Optional[int] = Query(None, description="月份"),
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_active_user)
):
"""获取关键指标仪表盘数据"""
# 如果没有指定年月,使用当前年月
if not period_year:
period_year = datetime.now().year
if not period_month:
period_month = datetime.now().month
# TODO: 从数据库实际计算这些指标
# 目前返回模拟数据用于演示
return {
"code": 200,
"message": "success",
"data": {
"bed_usage_rate": 85.5, # 床位使用率 (%)
"drug_ratio": 32.8, # 药占比 (%)
"material_ratio": 18.5, # 材料占比 (%)
"satisfaction_rate": 92.3 # 患者满意度 (%)
}
}
@router.get("/finance-trend", summary="收支趋势")
async def get_finance_trend(
months: Optional[int] = Query(6, ge=1, le=24, description="最近几个月"),
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_active_user)
):
"""获取收支趋势数据"""
# TODO: 从数据库实际查询收支数据
# 目前返回模拟数据用于演示
from datetime import datetime
current_month = datetime.now().month
data = []
for i in range(months, 0, -1):
month = current_month - i + 1
if month < 1:
month += 12
data.append({
"period": f"{month}",
"income": 1000000 + (months - i) * 50000,
"expense": 800000 + (months - i) * 30000,
"profit": 200000 + (months - i) * 20000
})
return {
"code": 200,
"message": "success",
"data": data
}
@router.get("/department-ranking", summary="科室绩效排名")
async def get_department_ranking(
period_year: Optional[int] = Query(None, description="年度"),
period_month: Optional[int] = Query(None, description="月份"),
limit: Optional[int] = Query(10, ge=1, le=100, description="返回数量"),
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_active_user)
):
"""获取科室绩效排名"""
# 如果没有指定年月,使用当前年月
if not period_year:
period_year = datetime.now().year
if not period_month:
period_month = datetime.now().month
result = await StatsService.get_department_stats(db, period_year, period_month)
# 返回前 limit 个
return {
"code": 200,
"message": "success",
"data": result[:limit] if limit else result
}
@router.get("/ranking", summary="绩效排名")
async def get_ranking_stats(
period_year: int = Query(..., description="年度"),
period_month: int = Query(..., description="月份"),
limit: int = Query(10, ge=1, le=100, description="返回数量"),
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_active_user)
):
"""获取绩效排名前 N 名"""
result = await StatsService.get_ranking_stats(db, period_year, period_month, limit)
return {
"code": 200,
"message": "success",
"data": result
}
@router.get("/completion", summary="指标完成度")
async def get_completion_stats(
indicator_id: Optional[int] = Query(None, description="指标 ID"),
period_year: int = Query(..., description="年度"),
period_month: int = Query(..., description="月份"),
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_active_user)
):
"""获取指标完成度统计"""
result = await StatsService.get_completion_stats(db, indicator_id, period_year, period_month)
return {
"code": 200,
"message": "success",
"data": result
}

View File

@@ -0,0 +1,401 @@
"""
满意度调查 API 接口
"""
from typing import Optional, List
from fastapi import APIRouter, Depends, HTTPException, Query
from sqlalchemy.ext.asyncio import AsyncSession
from pydantic import BaseModel, Field, ConfigDict
from datetime import datetime
import json
from app.core.database import get_db
from app.models.models import SurveyStatus, SurveyType, QuestionType
from app.services.survey_service import SurveyService
router = APIRouter(prefix="/surveys", tags=["满意度调查"])
# ==================== Pydantic Schemas ====================
class QuestionCreate(BaseModel):
"""题目创建"""
question_text: str = Field(..., description="题目内容")
question_type: str = Field(..., description="题目类型")
options: Optional[List[dict]] = Field(None, description="选项列表")
score_max: int = Field(5, description="最高分值")
is_required: bool = Field(True, description="是否必答")
class QuestionResponse(BaseModel):
"""题目响应"""
model_config = ConfigDict(from_attributes=True)
id: int
question_text: str
question_type: str
options: Optional[str] = None
score_max: int
is_required: bool
sort_order: int
class SurveyCreate(BaseModel):
"""问卷创建"""
survey_name: str = Field(..., description="问卷名称")
survey_code: str = Field(..., description="问卷编码")
survey_type: str = Field(..., description="问卷类型")
description: Optional[str] = Field(None, description="描述")
target_departments: Optional[List[int]] = Field(None, description="目标科室")
is_anonymous: bool = Field(True, description="是否匿名")
questions: Optional[List[QuestionCreate]] = Field(None, description="题目列表")
class SurveyUpdate(BaseModel):
"""问卷更新"""
survey_name: Optional[str] = None
description: Optional[str] = None
target_departments: Optional[List[int]] = None
is_anonymous: Optional[bool] = None
class SurveyResponse(BaseModel):
"""问卷响应"""
model_config = ConfigDict(from_attributes=True)
id: int
survey_name: str
survey_code: str
survey_type: str
description: Optional[str] = None
target_departments: Optional[str] = None
total_questions: int
status: str
start_date: Optional[datetime] = None
end_date: Optional[datetime] = None
is_anonymous: bool
created_at: datetime
class SurveyDetailResponse(SurveyResponse):
"""问卷详情响应"""
questions: Optional[List[QuestionResponse]] = None
class AnswerSubmit(BaseModel):
"""回答提交"""
question_id: int
answer_value: str
class ResponseSubmit(BaseModel):
"""问卷回答提交"""
department_id: Optional[int] = None
respondent_type: str = "patient"
respondent_id: Optional[int] = None
respondent_phone: Optional[str] = None
answers: List[AnswerSubmit]
class ResponseResult(BaseModel):
"""回答结果"""
model_config = ConfigDict(from_attributes=True)
id: int
survey_id: int
department_id: Optional[int] = None
total_score: float
max_score: float
satisfaction_rate: float
submitted_at: datetime
# ==================== API Endpoints ====================
@router.get("", summary="获取问卷列表")
async def get_survey_list(
survey_type: Optional[str] = Query(None, description="问卷类型"),
status: Optional[str] = Query(None, description="状态"),
page: int = Query(1, ge=1),
page_size: int = Query(20, ge=1, le=100),
db: AsyncSession = Depends(get_db)
):
"""获取问卷列表"""
type_filter = SurveyType(survey_type) if survey_type else None
status_filter = SurveyStatus(status) if status else None
surveys, total = await SurveyService.get_survey_list(
db, type_filter, status_filter, page, page_size
)
return {
"code": 200,
"message": "success",
"data": [
SurveyResponse.model_validate(s) for s in surveys
],
"total": total,
"page": page,
"page_size": page_size
}
@router.get("/{survey_id}", summary="获取问卷详情")
async def get_survey_detail(
survey_id: int,
db: AsyncSession = Depends(get_db)
):
"""获取问卷详情"""
survey = await SurveyService.get_survey_by_id(db, survey_id)
if not survey:
raise HTTPException(status_code=404, detail="问卷不存在")
detail = SurveyDetailResponse.model_validate(survey)
if survey.questions:
detail.questions = [QuestionResponse.model_validate(q) for q in survey.questions]
return {
"code": 200,
"message": "success",
"data": detail
}
@router.post("", summary="创建问卷")
async def create_survey(
survey_data: SurveyCreate,
db: AsyncSession = Depends(get_db)
):
"""创建问卷"""
try:
survey_type = SurveyType(survey_data.survey_type)
except ValueError:
raise HTTPException(status_code=400, detail="无效的问卷类型")
survey = await SurveyService.create_survey(
db,
survey_name=survey_data.survey_name,
survey_code=survey_data.survey_code,
survey_type=survey_type,
description=survey_data.description,
target_departments=survey_data.target_departments,
is_anonymous=survey_data.is_anonymous
)
# 添加题目
if survey_data.questions:
for question in survey_data.questions:
try:
question_type = QuestionType(question.question_type)
except ValueError:
continue
await SurveyService.add_question(
db,
survey_id=survey.id,
question_text=question.question_text,
question_type=question_type,
options=question.options,
score_max=question.score_max,
is_required=question.is_required
)
await db.commit()
return {
"code": 200,
"message": "创建成功",
"data": SurveyResponse.model_validate(survey)
}
@router.put("/{survey_id}", summary="更新问卷")
async def update_survey(
survey_id: int,
survey_data: SurveyUpdate,
db: AsyncSession = Depends(get_db)
):
"""更新问卷"""
try:
update_dict = survey_data.model_dump(exclude_unset=True)
survey = await SurveyService.update_survey(db, survey_id, **update_dict)
if not survey:
raise HTTPException(status_code=404, detail="问卷不存在")
await db.commit()
return {
"code": 200,
"message": "更新成功",
"data": SurveyResponse.model_validate(survey)
}
except ValueError as e:
raise HTTPException(status_code=400, detail=str(e))
@router.post("/{survey_id}/publish", summary="发布问卷")
async def publish_survey(
survey_id: int,
db: AsyncSession = Depends(get_db)
):
"""发布问卷"""
try:
survey = await SurveyService.publish_survey(db, survey_id)
if not survey:
raise HTTPException(status_code=404, detail="问卷不存在")
await db.commit()
return {
"code": 200,
"message": "发布成功",
"data": SurveyResponse.model_validate(survey)
}
except ValueError as e:
raise HTTPException(status_code=400, detail=str(e))
@router.post("/{survey_id}/close", summary="结束问卷")
async def close_survey(
survey_id: int,
db: AsyncSession = Depends(get_db)
):
"""结束问卷"""
try:
survey = await SurveyService.close_survey(db, survey_id)
if not survey:
raise HTTPException(status_code=404, detail="问卷不存在")
await db.commit()
return {
"code": 200,
"message": "问卷已结束",
"data": SurveyResponse.model_validate(survey)
}
except ValueError as e:
raise HTTPException(status_code=400, detail=str(e))
@router.delete("/{survey_id}", summary="删除问卷")
async def delete_survey(
survey_id: int,
db: AsyncSession = Depends(get_db)
):
"""删除问卷"""
try:
success = await SurveyService.delete_survey(db, survey_id)
if not success:
raise HTTPException(status_code=404, detail="问卷不存在")
await db.commit()
return {
"code": 200,
"message": "删除成功"
}
except ValueError as e:
raise HTTPException(status_code=400, detail=str(e))
@router.post("/{survey_id}/submit", summary="提交问卷回答")
async def submit_response(
survey_id: int,
response_data: ResponseSubmit,
db: AsyncSession = Depends(get_db)
):
"""提交问卷回答"""
try:
answers = [a.model_dump() for a in response_data.answers]
response = await SurveyService.submit_response(
db,
survey_id=survey_id,
department_id=response_data.department_id,
answers=answers,
respondent_type=response_data.respondent_type,
respondent_id=response_data.respondent_id,
respondent_phone=response_data.respondent_phone
)
await db.commit()
return {
"code": 200,
"message": "提交成功",
"data": ResponseResult.model_validate(response)
}
except ValueError as e:
raise HTTPException(status_code=400, detail=str(e))
@router.get("/{survey_id}/stats", summary="获取问卷统计")
async def get_survey_stats(
survey_id: int,
db: AsyncSession = Depends(get_db)
):
"""获取问卷各题目统计"""
survey = await SurveyService.get_survey_by_id(db, survey_id)
if not survey:
raise HTTPException(status_code=404, detail="问卷不存在")
stats = await SurveyService.get_question_stats(db, survey_id)
return {
"code": 200,
"message": "success",
"data": {
"survey_id": survey_id,
"survey_name": survey.survey_name,
"total_responses": await _get_response_count(db, survey_id),
"question_stats": stats
}
}
@router.get("/stats/department", summary="获取科室满意度统计")
async def get_department_satisfaction(
survey_id: Optional[int] = Query(None, description="问卷ID"),
department_id: Optional[int] = Query(None, description="科室ID"),
period_year: Optional[int] = Query(None, description="年度"),
period_month: Optional[int] = Query(None, description="月份"),
db: AsyncSession = Depends(get_db)
):
"""获取科室满意度统计"""
stats = await SurveyService.get_department_satisfaction(
db, survey_id, department_id, period_year, period_month
)
return {
"code": 200,
"message": "success",
"data": stats
}
@router.get("/stats/trend/{department_id}", summary="获取满意度趋势")
async def get_satisfaction_trend(
department_id: int,
months: int = Query(6, ge=1, le=12, description="查询月数"),
db: AsyncSession = Depends(get_db)
):
"""获取满意度趋势"""
trend = await SurveyService.get_satisfaction_trend(db, department_id, months)
return {
"code": 200,
"message": "success",
"data": trend
}
async def _get_response_count(db: AsyncSession, survey_id: int) -> int:
"""获取问卷回答数"""
from sqlalchemy import select, func
from app.models.models import SurveyResponse
result = await db.execute(
select(func.count(SurveyResponse.id))
.where(SurveyResponse.survey_id == survey_id)
)
return result.scalar() or 0

View File

@@ -0,0 +1,271 @@
"""
API路由 - 指标模板管理
"""
from typing import Annotated, Optional, List
from fastapi import APIRouter, Depends, HTTPException, Query
from sqlalchemy.ext.asyncio import AsyncSession
from app.core.database import get_db
from app.core.security import get_current_active_user, get_current_manager_user
from app.schemas.schemas import (
IndicatorTemplateCreate, IndicatorTemplateUpdate,
IndicatorTemplateResponse, IndicatorTemplateListResponse,
TemplateIndicatorCreate, TemplateIndicatorUpdate, TemplateIndicatorResponse,
ResponseBase
)
from app.services.template_service import TemplateService
from app.models.models import User
router = APIRouter(prefix="/templates", tags=["指标模板"])
@router.get("", summary="获取模板列表")
async def get_templates(
template_type: Optional[str] = Query(None, description="模板类型"),
is_active: Optional[bool] = Query(None, description="是否启用"),
page: int = Query(1, ge=1, description="页码"),
page_size: int = Query(20, ge=1, le=100, description="每页数量"),
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_active_user)
):
"""获取模板列表"""
templates, total = await TemplateService.get_list(
db, template_type, is_active, page, page_size
)
return {
"code": 200,
"message": "success",
"data": templates,
"total": total,
"page": page,
"page_size": page_size
}
@router.get("/types", summary="获取模板类型列表")
async def get_template_types(
current_user: User = Depends(get_current_active_user)
):
"""获取模板类型列表"""
types = [
{"value": "general", "label": "通用模板"},
{"value": "surgical", "label": "手术临床科室"},
{"value": "nonsurgical_ward", "label": "非手术有病房科室"},
{"value": "nonsurgical_noward", "label": "非手术无病房科室"},
{"value": "medical_tech", "label": "医技科室"},
{"value": "nursing", "label": "护理单元"},
{"value": "admin", "label": "行政科室"},
{"value": "logistics", "label": "后勤科室"}
]
return {"code": 200, "message": "success", "data": types}
@router.get("/dimensions", summary="获取BSC维度列表")
async def get_dimensions(
current_user: User = Depends(get_current_active_user)
):
"""获取BSC维度列表"""
dimensions = [
{"value": "financial", "label": "财务管理", "weight_range": "30%-40%"},
{"value": "customer", "label": "顾客服务", "weight_range": "25%-35%"},
{"value": "internal_process", "label": "内部流程", "weight_range": "20%-30%"},
{"value": "learning_growth", "label": "学习与成长", "weight_range": "5%-15%"}
]
return {"code": 200, "message": "success", "data": dimensions}
@router.get("/{template_id}", summary="获取模板详情")
async def get_template(
template_id: int,
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_active_user)
):
"""获取模板详情"""
template = await TemplateService.get_by_id(db, template_id)
if not template:
raise HTTPException(status_code=404, detail="模板不存在")
# 构建响应数据
indicators = []
for ti in template.indicators:
ind_dict = {
"id": ti.id,
"template_id": ti.template_id,
"indicator_id": ti.indicator_id,
"indicator_name": ti.indicator.name if ti.indicator else None,
"indicator_code": ti.indicator.code if ti.indicator else None,
"indicator_type": ti.indicator.indicator_type.value if ti.indicator else None,
"bs_dimension": ti.indicator.bs_dimension.value if ti.indicator else None,
"category": ti.category,
"target_value": float(ti.target_value) if ti.target_value else None,
"target_unit": ti.target_unit,
"weight": float(ti.weight),
"scoring_method": ti.scoring_method,
"scoring_params": ti.scoring_params,
"sort_order": ti.sort_order,
"remark": ti.remark,
"created_at": ti.created_at,
"updated_at": ti.updated_at
}
indicators.append(ind_dict)
response_data = {
"id": template.id,
"template_name": template.template_name,
"template_code": template.template_code,
"template_type": template.template_type.value,
"description": template.description,
"dimension_weights": template.dimension_weights,
"assessment_cycle": template.assessment_cycle,
"is_active": template.is_active,
"created_at": template.created_at,
"updated_at": template.updated_at,
"indicators": indicators
}
return {"code": 200, "message": "success", "data": response_data}
@router.post("", summary="创建模板")
async def create_template(
template_data: IndicatorTemplateCreate,
db: AsyncSession = Depends(get_db),
current_user: Annotated[User, Depends(get_current_manager_user)] = None
):
"""创建模板(需要管理员或经理权限)"""
# 检查编码是否已存在
existing = await TemplateService.get_by_code(db, template_data.template_code)
if existing:
raise HTTPException(status_code=400, detail="模板编码已存在")
template = await TemplateService.create(db, template_data)
return {"code": 200, "message": "创建成功", "data": {"id": template.id}}
@router.put("/{template_id}", summary="更新模板")
async def update_template(
template_id: int,
template_data: IndicatorTemplateUpdate,
db: AsyncSession = Depends(get_db),
current_user: Annotated[User, Depends(get_current_manager_user)] = None
):
"""更新模板(需要管理员或经理权限)"""
template = await TemplateService.update(db, template_id, template_data)
if not template:
raise HTTPException(status_code=404, detail="模板不存在")
return {"code": 200, "message": "更新成功"}
@router.delete("/{template_id}", summary="删除模板")
async def delete_template(
template_id: int,
db: AsyncSession = Depends(get_db),
current_user: Annotated[User, Depends(get_current_manager_user)] = None
):
"""删除模板(需要管理员或经理权限)"""
success = await TemplateService.delete(db, template_id)
if not success:
raise HTTPException(status_code=404, detail="模板不存在")
return {"code": 200, "message": "删除成功"}
# ==================== 模板指标管理 ====================
@router.get("/{template_id}/indicators", summary="获取模板指标列表")
async def get_template_indicators(
template_id: int,
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_active_user)
):
"""获取模板指标列表"""
indicators = await TemplateService.get_template_indicators(db, template_id)
result = []
for ti in indicators:
ind_dict = {
"id": ti.id,
"template_id": ti.template_id,
"indicator_id": ti.indicator_id,
"indicator_name": ti.indicator.name if ti.indicator else None,
"indicator_code": ti.indicator.code if ti.indicator else None,
"indicator_type": ti.indicator.indicator_type.value if ti.indicator else None,
"bs_dimension": ti.indicator.bs_dimension.value if ti.indicator else None,
"category": ti.category,
"target_value": float(ti.target_value) if ti.target_value else None,
"target_unit": ti.target_unit,
"weight": float(ti.weight),
"scoring_method": ti.scoring_method,
"scoring_params": ti.scoring_params,
"sort_order": ti.sort_order,
"remark": ti.remark,
"created_at": ti.created_at,
"updated_at": ti.updated_at
}
result.append(ind_dict)
return {"code": 200, "message": "success", "data": result}
@router.post("/{template_id}/indicators", summary="添加模板指标")
async def add_template_indicator(
template_id: int,
indicator_data: TemplateIndicatorCreate,
db: AsyncSession = Depends(get_db),
current_user: Annotated[User, Depends(get_current_manager_user)] = None
):
"""添加模板指标"""
ti = await TemplateService.add_indicator(db, template_id, indicator_data)
if not ti:
raise HTTPException(status_code=400, detail="添加失败,模板不存在或指标已存在")
return {"code": 200, "message": "添加成功", "data": {"id": ti.id}}
@router.put("/{template_id}/indicators/{indicator_id}", summary="更新模板指标")
async def update_template_indicator(
template_id: int,
indicator_id: int,
indicator_data: TemplateIndicatorUpdate,
db: AsyncSession = Depends(get_db),
current_user: Annotated[User, Depends(get_current_manager_user)] = None
):
"""更新模板指标"""
ti = await TemplateService.update_indicator(db, template_id, indicator_id, indicator_data)
if not ti:
raise HTTPException(status_code=404, detail="模板指标不存在")
return {"code": 200, "message": "更新成功"}
@router.delete("/{template_id}/indicators/{indicator_id}", summary="移除模板指标")
async def remove_template_indicator(
template_id: int,
indicator_id: int,
db: AsyncSession = Depends(get_db),
current_user: Annotated[User, Depends(get_current_manager_user)] = None
):
"""移除模板指标"""
success = await TemplateService.remove_indicator(db, template_id, indicator_id)
if not success:
raise HTTPException(status_code=404, detail="模板指标不存在")
return {"code": 200, "message": "移除成功"}
@router.post("/{template_id}/indicators/batch", summary="批量添加模板指标")
async def batch_add_template_indicators(
template_id: int,
indicators_data: List[TemplateIndicatorCreate],
db: AsyncSession = Depends(get_db),
current_user: Annotated[User, Depends(get_current_manager_user)] = None
):
"""批量添加模板指标"""
added_count = 0
for idx, ind_data in enumerate(indicators_data):
ind_data.sort_order = ind_data.sort_order or idx
ti = await TemplateService.add_indicator(db, template_id, ind_data)
if ti:
added_count += 1
return {
"code": 200,
"message": f"成功添加 {added_count} 个指标",
"data": {"added_count": added_count}
}

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

91
backend/app/main.py Normal file
View File

@@ -0,0 +1,91 @@
"""
FastAPI Main Application
"""
import logging
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from fastapi.exceptions import RequestValidationError
from starlette.exceptions import HTTPException as StarletteHTTPException
from app.core.config import settings
from app.core.logging_config import logger
from app.api.v1 import api_router
def create_app() -> FastAPI:
"""Create application instance"""
logger.info("Creating FastAPI application instance...")
app = FastAPI(
title=settings.APP_NAME,
version=settings.APP_VERSION,
description="""
## Hospital Performance Management System API
### Function Modules
- **Basic Data Management**: Department, Staff, Indicators
- **Performance Assessment**: Assessment records, review workflow
- **Data Analysis Reports**: Department statistics, trends, rankings
- **Salary Calculation**: Performance-based payroll
### Tech Stack
- FastAPI + SQLAlchemy 2.0
- PostgreSQL
- Async IO support
""",
openapi_url=f"{settings.API_PREFIX}/openapi.json",
docs_url=f"{settings.API_PREFIX}/docs",
redoc_url=f"{settings.API_PREFIX}/redoc",
)
# CORS middleware
app.add_middleware(
CORSMiddleware,
allow_origins=settings.CORS_ORIGINS,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# Register router
app.include_router(api_router, prefix=settings.API_PREFIX)
# Health check
@app.get("/health", tags=["System"])
async def health_check():
return {"status": "healthy", "version": settings.APP_VERSION}
# HTTP exception handler
@app.exception_handler(StarletteHTTPException)
async def http_exception_handler(request, exc):
logger.warning(f"HTTP Exception: {request.method} {request.url} - {exc.status_code} - {exc.detail}")
raise exc
# Request validation exception handler
@app.exception_handler(RequestValidationError)
async def validation_exception_handler(request, exc):
logger.error(f"Validation Error: {request.method} {request.url} - {exc}")
raise exc
# Global exception handler
@app.exception_handler(Exception)
async def global_exception_handler(request, exc):
logger.error(f"Global Exception: {request.method} {request.url} - {exc}", exc_info=True)
raise exc
logger.info("FastAPI application instance created successfully")
return app
app = create_app()
if __name__ == "__main__":
import uvicorn
uvicorn.run(
"app.main:app",
host="0.0.0.0",
port=8000,
reload=True,
log_level="info"
)

View File

@@ -0,0 +1,2 @@
from app.models.models import * # noqa
from app.models.finance import * # noqa

View File

@@ -0,0 +1,78 @@
"""
财务核算模型模块
"""
from datetime import datetime
from typing import Optional, List
from sqlalchemy import (
String, Text, Integer, Numeric, Boolean, DateTime, ForeignKey, Enum as SQLEnum,
Index, CheckConstraint
)
from sqlalchemy.orm import Mapped, mapped_column, relationship
from enum import Enum
from app.core.database import Base
class RevenueCategory(str, Enum):
"""收入类别"""
EXAMINATION = "examination" # 检查费
LAB_TEST = "lab_test" # 检验费
RADIOLOGY = "radiology" # 放射费
BED = "bed" # 床位费
NURSING = "nursing" # 护理费
TREATMENT = "treatment" # 治疗费
SURGERY = "surgery" # 手术费
INJECTION = "injection" # 注射费
OXYGEN = "oxygen" # 吸氧费
OTHER = "other" # 其他
class ExpenseCategory(str, Enum):
"""支出类别"""
MATERIAL = "material" # 材料费
PERSONNEL = "personnel" # 人员支出
MAINTENANCE = "maintenance" # 维修费
UTILITY = "utility" # 水电费
OTHER = "other" # 其他
class FinanceType(str, Enum):
"""财务类型"""
REVENUE = "revenue" # 收入
EXPENSE = "expense" # 支出
class DepartmentFinance(Base):
"""科室财务记录"""
__tablename__ = "department_finances"
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
department_id: Mapped[int] = mapped_column(ForeignKey("departments.id"), nullable=False, comment="科室ID")
period_year: Mapped[int] = mapped_column(Integer, nullable=False, comment="年度")
period_month: Mapped[int] = mapped_column(Integer, nullable=False, comment="月份")
finance_type: Mapped[FinanceType] = mapped_column(
SQLEnum(FinanceType, values_callable=lambda x: [e.value for e in x]),
nullable=False,
comment="财务类型"
)
category: Mapped[str] = mapped_column(String(50), nullable=False, comment="类别")
amount: Mapped[float] = mapped_column(Numeric(12, 2), default=0, comment="金额")
source: Mapped[Optional[str]] = mapped_column(String(100), nullable=True, comment="数据来源")
remark: Mapped[Optional[str]] = mapped_column(Text, nullable=True, comment="备注")
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow, comment="创建时间")
updated_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, comment="更新时间")
# 关系
department: Mapped["Department"] = relationship("Department", backref="finances")
__table_args__ = (
Index("idx_finance_dept", "department_id"),
Index("idx_finance_period", "period_year", "period_month"),
Index("idx_finance_type", "finance_type"),
Index("idx_finance_category", "category"),
CheckConstraint("amount >= 0", name="ck_finance_amount"),
)
# 为了避免循环导入在models.py中导入时使用
from app.models.models import Department # noqa

View File

@@ -0,0 +1,588 @@
"""
数据模型模块
"""
from datetime import datetime
from typing import Optional, List
from sqlalchemy import (
String, Text, Integer, Numeric, Boolean, DateTime, ForeignKey, Enum as SQLEnum,
Index, CheckConstraint
)
from sqlalchemy.orm import Mapped, mapped_column, relationship
from enum import Enum
from app.core.database import Base
class DeptType(Enum):
"""科室类型"""
CLINICAL_SURGICAL = "clinical_surgical" # 手术临床科室
CLINICAL_NONSURGICAL_WARD = "clinical_nonsurgical_ward" # 非手术有病房科室
CLINICAL_NONSURGICAL_NOWARD = "clinical_nonsurgical_noward" # 非手术无病房科室
MEDICAL_TECH = "medical_tech" # 医技科室
MEDICAL_AUXILIARY = "medical_auxiliary" # 医辅科室
NURSING = "nursing" # 护理单元
ADMIN = "admin" # 行政科室
FINANCE = "finance" # 财务科室
LOGISTICS = "logistics" # 后勤保障科室
class BSCDimension(Enum):
"""平衡计分卡维度"""
FINANCIAL = "financial" # 财务维度
CUSTOMER = "customer" # 客户维度
INTERNAL_PROCESS = "internal_process" # 内部流程维度
LEARNING_GROWTH = "learning_growth" # 学习与成长维度
class StaffStatus(Enum):
"""员工状态"""
ACTIVE = "active" # 在职
LEAVE = "leave" # 休假
RESIGNED = "resigned" # 离职
RETIRED = "retired" # 退休
class AssessmentStatus(Enum):
"""考核状态"""
DRAFT = "draft" # 草稿
SUBMITTED = "submitted" # 已提交
REVIEWED = "reviewed" # 已审核
FINALIZED = "finalized" # 已确认
REJECTED = "rejected" # 已驳回
class IndicatorType(Enum):
"""指标类型"""
QUALITY = "quality" # 质量指标
QUANTITY = "quantity" # 数量指标
EFFICIENCY = "efficiency" # 效率指标
SERVICE = "service" # 服务指标
COST = "cost" # 成本指标
class Department(Base):
"""科室信息表"""
__tablename__ = "departments"
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
name: Mapped[str] = mapped_column(String(100), nullable=False, comment="科室名称")
code: Mapped[str] = mapped_column(String(20), unique=True, nullable=False, comment="科室编码")
dept_type: Mapped[DeptType] = mapped_column(SQLEnum(DeptType, values_callable=lambda x: [e.value for e in x]), nullable=False, comment="科室类型")
parent_id: Mapped[Optional[int]] = mapped_column(ForeignKey("departments.id"), nullable=True, comment="上级科室")
level: Mapped[int] = mapped_column(Integer, default=1, comment="层级")
sort_order: Mapped[int] = mapped_column(Integer, default=0, comment="排序")
is_active: Mapped[bool] = mapped_column(Boolean, default=True, comment="是否启用")
description: Mapped[Optional[str]] = mapped_column(Text, nullable=True, comment="描述")
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow, comment="创建时间")
updated_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, comment="更新时间")
# 关系
parent: Mapped[Optional["Department"]] = relationship("Department", remote_side=[id], backref="children")
staff: Mapped[List["Staff"]] = relationship("Staff", back_populates="department")
__table_args__ = (
Index("idx_dept_type", "dept_type"),
Index("idx_dept_parent", "parent_id"),
)
class Staff(Base):
"""员工信息表"""
__tablename__ = "staff"
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
employee_id: Mapped[str] = mapped_column(String(20), unique=True, nullable=False, comment="工号")
name: Mapped[str] = mapped_column(String(50), nullable=False, comment="姓名")
department_id: Mapped[int] = mapped_column(ForeignKey("departments.id"), nullable=False, comment="所属科室")
position: Mapped[str] = mapped_column(String(50), nullable=False, comment="职位")
title: Mapped[Optional[str]] = mapped_column(String(50), nullable=True, comment="职称")
phone: Mapped[Optional[str]] = mapped_column(String(20), nullable=True, comment="联系电话")
email: Mapped[Optional[str]] = mapped_column(String(100), nullable=True, comment="邮箱")
base_salary: Mapped[float] = mapped_column(Numeric(10, 2), default=0, comment="基本工资")
performance_ratio: Mapped[float] = mapped_column(Numeric(5, 2), default=1.0, comment="绩效系数")
status: Mapped[StaffStatus] = mapped_column(SQLEnum(StaffStatus, values_callable=lambda x: [e.value for e in x]), default=StaffStatus.ACTIVE, comment="状态")
hire_date: Mapped[Optional[datetime]] = mapped_column(DateTime, nullable=True, comment="入职日期")
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow, comment="创建时间")
updated_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, comment="更新时间")
# 关系
department: Mapped["Department"] = relationship("Department", back_populates="staff")
assessments: Mapped[List["Assessment"]] = relationship("Assessment", foreign_keys="Assessment.staff_id", back_populates="staff")
salary_records: Mapped[List["SalaryRecord"]] = relationship("SalaryRecord", back_populates="staff")
__table_args__ = (
Index("idx_staff_dept", "department_id"),
Index("idx_staff_status", "status"),
)
class Indicator(Base):
"""考核指标表"""
__tablename__ = "indicators"
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
name: Mapped[str] = mapped_column(String(100), nullable=False, comment="指标名称")
code: Mapped[str] = mapped_column(String(20), unique=True, nullable=False, comment="指标编码")
indicator_type: Mapped[IndicatorType] = mapped_column(SQLEnum(IndicatorType, values_callable=lambda x: [e.value for e in x]), nullable=False, comment="指标类型")
bs_dimension: Mapped[BSCDimension] = mapped_column(SQLEnum(BSCDimension, values_callable=lambda x: [e.value for e in x]), nullable=False, comment="平衡计分卡维度")
weight: Mapped[float] = mapped_column(Numeric(5, 2), default=1.0, comment="权重")
max_score: Mapped[float] = mapped_column(Numeric(5, 2), default=100.0, comment="最高分值")
target_value: Mapped[Optional[float]] = mapped_column(Numeric(10, 2), nullable=True, comment="目标值")
target_unit: Mapped[Optional[str]] = mapped_column(String(50), nullable=True, comment="目标值单位")
calculation_method: Mapped[Optional[str]] = mapped_column(Text, nullable=True, comment="计算方法/公式")
assessment_method: Mapped[Optional[str]] = mapped_column(Text, nullable=True, comment="考核方法")
deduction_standard: Mapped[Optional[str]] = mapped_column(Text, nullable=True, comment="扣分标准")
data_source: Mapped[Optional[str]] = mapped_column(String(100), nullable=True, comment="数据来源")
applicable_dept_types: Mapped[Optional[str]] = mapped_column(Text, nullable=True, comment="适用科室类型 (JSON 数组)")
is_veto: Mapped[bool] = mapped_column(Boolean, default=False, comment="是否一票否决指标")
is_active: Mapped[bool] = mapped_column(Boolean, default=True, comment="是否启用")
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow, comment="创建时间")
updated_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, comment="更新时间")
# 关系
assessment_details: Mapped[List["AssessmentDetail"]] = relationship("AssessmentDetail", back_populates="indicator")
__table_args__ = (
Index("idx_indicator_type", "indicator_type"),
CheckConstraint("weight > 0", name="ck_indicator_weight"),
)
class Assessment(Base):
"""考核记录表"""
__tablename__ = "assessments"
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
staff_id: Mapped[int] = mapped_column(ForeignKey("staff.id"), nullable=False, comment="员工ID")
period_year: Mapped[int] = mapped_column(Integer, nullable=False, comment="考核年度")
period_month: Mapped[int] = mapped_column(Integer, nullable=False, comment="考核月份")
period_type: Mapped[str] = mapped_column(String(20), default="monthly", comment="考核周期类型")
total_score: Mapped[float] = mapped_column(Numeric(5, 2), default=0, comment="总分")
weighted_score: Mapped[float] = mapped_column(Numeric(5, 2), default=0, comment="加权得分")
status: Mapped[AssessmentStatus] = mapped_column(SQLEnum(AssessmentStatus, values_callable=lambda x: [e.value for e in x]), default=AssessmentStatus.DRAFT, comment="状态")
assessor_id: Mapped[Optional[int]] = mapped_column(ForeignKey("staff.id"), nullable=True, comment="考核人")
reviewer_id: Mapped[Optional[int]] = mapped_column(ForeignKey("staff.id"), nullable=True, comment="审核人")
submit_time: Mapped[Optional[datetime]] = mapped_column(DateTime, nullable=True, comment="提交时间")
review_time: Mapped[Optional[datetime]] = mapped_column(DateTime, nullable=True, comment="审核时间")
remark: Mapped[Optional[str]] = mapped_column(Text, nullable=True, comment="备注")
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow, comment="创建时间")
updated_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, comment="更新时间")
# 关系
staff: Mapped["Staff"] = relationship("Staff", foreign_keys=[staff_id], back_populates="assessments")
assessor: Mapped[Optional["Staff"]] = relationship("Staff", foreign_keys=[assessor_id])
reviewer: Mapped[Optional["Staff"]] = relationship("Staff", foreign_keys=[reviewer_id])
details: Mapped[List["AssessmentDetail"]] = relationship("AssessmentDetail", back_populates="assessment", cascade="all, delete-orphan")
__table_args__ = (
Index("idx_assessment_staff", "staff_id"),
Index("idx_assessment_period", "period_year", "period_month"),
Index("idx_assessment_status", "status"),
)
class AssessmentDetail(Base):
"""考核明细表"""
__tablename__ = "assessment_details"
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
assessment_id: Mapped[int] = mapped_column(ForeignKey("assessments.id"), nullable=False, comment="考核记录ID")
indicator_id: Mapped[int] = mapped_column(ForeignKey("indicators.id"), nullable=False, comment="指标ID")
actual_value: Mapped[Optional[float]] = mapped_column(Numeric(10, 2), nullable=True, comment="实际值")
score: Mapped[float] = mapped_column(Numeric(5, 2), default=0, comment="得分")
evidence: Mapped[Optional[str]] = mapped_column(Text, nullable=True, comment="佐证材料")
remark: Mapped[Optional[str]] = mapped_column(Text, nullable=True, comment="备注")
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow, comment="创建时间")
updated_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, comment="更新时间")
# 关系
assessment: Mapped["Assessment"] = relationship("Assessment", back_populates="details")
indicator: Mapped["Indicator"] = relationship("Indicator", back_populates="assessment_details")
__table_args__ = (
Index("idx_detail_assessment", "assessment_id"),
Index("idx_detail_indicator", "indicator_id"),
)
class SalaryRecord(Base):
"""工资核算记录表"""
__tablename__ = "salary_records"
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
staff_id: Mapped[int] = mapped_column(ForeignKey("staff.id"), nullable=False, comment="员工ID")
period_year: Mapped[int] = mapped_column(Integer, nullable=False, comment="年度")
period_month: Mapped[int] = mapped_column(Integer, nullable=False, comment="月份")
base_salary: Mapped[float] = mapped_column(Numeric(10, 2), default=0, comment="基本工资")
performance_score: Mapped[float] = mapped_column(Numeric(5, 2), default=0, comment="绩效得分")
performance_bonus: Mapped[float] = mapped_column(Numeric(10, 2), default=0, comment="绩效奖金")
deduction: Mapped[float] = mapped_column(Numeric(10, 2), default=0, comment="扣款")
allowance: Mapped[float] = mapped_column(Numeric(10, 2), default=0, comment="补贴")
total_salary: Mapped[float] = mapped_column(Numeric(10, 2), default=0, comment="应发工资")
status: Mapped[str] = mapped_column(String(20), default="pending", comment="状态")
remark: Mapped[Optional[str]] = mapped_column(Text, nullable=True, comment="备注")
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow, comment="创建时间")
updated_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, comment="更新时间")
# 关系
staff: Mapped["Staff"] = relationship("Staff", back_populates="salary_records")
__table_args__ = (
Index("idx_salary_staff", "staff_id"),
Index("idx_salary_period", "period_year", "period_month"),
)
class PlanStatus(Enum):
"""计划状态"""
DRAFT = "draft" # 草稿
PENDING = "pending" # 待审批
APPROVED = "approved" # 已批准
REJECTED = "rejected" # 已驳回
ACTIVE = "active" # 执行中
COMPLETED = "completed" # 已完成
CANCELLED = "cancelled" # 已取消
class User(Base):
"""系统用户表"""
__tablename__ = "users"
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
username: Mapped[str] = mapped_column(String(50), unique=True, nullable=False, comment="用户名")
password_hash: Mapped[str] = mapped_column(String(255), nullable=False, comment="密码哈希")
staff_id: Mapped[Optional[int]] = mapped_column(ForeignKey("staff.id"), nullable=True, comment="关联员工")
role: Mapped[str] = mapped_column(String(20), default="staff", comment="角色")
is_active: Mapped[bool] = mapped_column(Boolean, default=True, comment="是否启用")
last_login: Mapped[Optional[datetime]] = mapped_column(DateTime, nullable=True, comment="最后登录")
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow, comment="创建时间")
updated_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, comment="更新时间")
__table_args__ = (
Index("idx_user_username", "username"),
)
class PlanLevel(Enum):
"""计划层级"""
HOSPITAL = "hospital" # 医院级
DEPARTMENT = "department" # 科室级
INDIVIDUAL = "individual" # 个人级
class PerformancePlan(Base):
"""绩效计划表"""
__tablename__ = "performance_plans"
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
plan_name: Mapped[str] = mapped_column(String(200), nullable=False, comment="计划名称")
plan_code: Mapped[str] = mapped_column(String(50), unique=True, nullable=False, comment="计划编码")
plan_level: Mapped[PlanLevel] = mapped_column(SQLEnum(PlanLevel, values_callable=lambda x: [e.value for e in x]), nullable=False, comment="计划层级")
plan_year: Mapped[int] = mapped_column(Integer, nullable=False, comment="计划年度")
plan_month: Mapped[Optional[int]] = mapped_column(Integer, nullable=True, comment="计划月份 (月度计划)")
plan_type: Mapped[str] = mapped_column(String(20), default="annual", comment="计划类型 (annual/monthly)")
department_id: Mapped[Optional[int]] = mapped_column(ForeignKey("departments.id"), nullable=True, comment="所属科室 ID")
staff_id: Mapped[Optional[int]] = mapped_column(ForeignKey("staff.id"), nullable=True, comment="责任人 ID")
parent_plan_id: Mapped[Optional[int]] = mapped_column(ForeignKey("performance_plans.id"), nullable=True, comment="上级计划 ID")
description: Mapped[Optional[str]] = mapped_column(Text, nullable=True, comment="计划描述")
strategic_goals: Mapped[Optional[str]] = mapped_column(Text, nullable=True, comment="战略目标")
key_initiatives: Mapped[Optional[str]] = mapped_column(Text, nullable=True, comment="关键举措")
status: Mapped[PlanStatus] = mapped_column(SQLEnum(PlanStatus, values_callable=lambda x: [e.value for e in x]), default=PlanStatus.DRAFT, comment="状态")
submitter_id: Mapped[Optional[int]] = mapped_column(ForeignKey("users.id"), nullable=True, comment="提交人 ID")
submit_time: Mapped[Optional[datetime]] = mapped_column(DateTime, nullable=True, comment="提交时间")
approver_id: Mapped[Optional[int]] = mapped_column(ForeignKey("users.id"), nullable=True, comment="审批人 ID")
approve_time: Mapped[Optional[datetime]] = mapped_column(DateTime, nullable=True, comment="审批时间")
approve_remark: Mapped[Optional[str]] = mapped_column(Text, nullable=True, comment="审批意见")
version: Mapped[int] = mapped_column(Integer, default=1, comment="版本号")
is_active: Mapped[bool] = mapped_column(Boolean, default=True, comment="是否启用")
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow, comment="创建时间")
updated_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, comment="更新时间")
# 关系
department: Mapped[Optional["Department"]] = relationship("Department", backref="performance_plans")
staff: Mapped[Optional["Staff"]] = relationship("Staff", backref="performance_plans")
parent_plan: Mapped[Optional["PerformancePlan"]] = relationship("PerformancePlan", remote_side=[id], backref="child_plans")
submitter: Mapped[Optional["User"]] = relationship("User", foreign_keys=[submitter_id])
approver: Mapped[Optional["User"]] = relationship("User", foreign_keys=[approver_id])
kpi_relations: Mapped[List["PlanKpiRelation"]] = relationship("PlanKpiRelation", back_populates="plan", cascade="all, delete-orphan")
__table_args__ = (
Index("idx_plan_level", "plan_level"),
Index("idx_plan_year", "plan_year"),
Index("idx_plan_department", "department_id"),
Index("idx_plan_status", "status"),
)
class PlanKpiRelation(Base):
"""计划指标关联表"""
__tablename__ = "plan_kpi_relations"
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
plan_id: Mapped[int] = mapped_column(ForeignKey("performance_plans.id"), nullable=False, comment="计划 ID")
indicator_id: Mapped[int] = mapped_column(ForeignKey("indicators.id"), nullable=False, comment="指标 ID")
target_value: Mapped[Optional[float]] = mapped_column(Numeric(10, 2), nullable=True, comment="目标值")
target_unit: Mapped[Optional[str]] = mapped_column(String(50), nullable=True, comment="目标值单位")
weight: Mapped[float] = mapped_column(Numeric(5, 2), default=1.0, comment="权重")
scoring_method: Mapped[Optional[str]] = mapped_column(String(50), nullable=True, comment="评分方法")
scoring_params: Mapped[Optional[str]] = mapped_column(Text, nullable=True, comment="评分参数 (JSON)")
remark: Mapped[Optional[str]] = mapped_column(Text, nullable=True, comment="备注")
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow, comment="创建时间")
updated_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, comment="更新时间")
# 关系
plan: Mapped["PerformancePlan"] = relationship("PerformancePlan", back_populates="kpi_relations")
indicator: Mapped["Indicator"] = relationship("Indicator", backref="plan_relations")
__table_args__ = (
Index("idx_relation_plan", "plan_id"),
Index("idx_relation_indicator", "indicator_id"),
Index("idx_relation_unique", "plan_id", "indicator_id", unique=True),
)
class MenuType(Enum):
"""菜单类型"""
MENU = "menu" # 菜单
BUTTON = "button" # 按钮
class Menu(Base):
"""系统菜单表"""
__tablename__ = "menus"
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
parent_id: Mapped[Optional[int]] = mapped_column(ForeignKey("menus.id"), nullable=True, comment="父菜单 ID")
menu_type: Mapped[MenuType] = mapped_column(SQLEnum(MenuType, values_callable=lambda x: [e.value for e in x]), default=MenuType.MENU, comment="菜单类型")
menu_name: Mapped[str] = mapped_column(String(100), nullable=False, comment="菜单名称")
menu_icon: Mapped[Optional[str]] = mapped_column(String(50), nullable=True, comment="菜单图标")
path: Mapped[str] = mapped_column(String(200), nullable=False, comment="路由路径")
component: Mapped[Optional[str]] = mapped_column(String(200), nullable=True, comment="组件路径")
permission: Mapped[Optional[str]] = mapped_column(String(100), nullable=True, comment="权限标识")
sort_order: Mapped[int] = mapped_column(Integer, default=0, comment="排序")
is_visible: Mapped[bool] = mapped_column(Boolean, default=True, comment="是否可见")
is_active: Mapped[bool] = mapped_column(Boolean, default=True, comment="是否启用")
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow, comment="创建时间")
updated_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, comment="更新时间")
# 关系
parent: Mapped[Optional["Menu"]] = relationship("Menu", remote_side=[id], backref="children")
__table_args__ = (
Index("idx_menu_parent", "parent_id"),
Index("idx_menu_type", "menu_type"),
Index("idx_menu_visible", "is_visible"),
)
class TemplateType(Enum):
"""模板类型"""
GENERAL = "general" # 通用模板
SURGICAL = "surgical" # 手术临床科室
NON_SURGICAL_WARD = "nonsurgical_ward" # 非手术有病房科室
NON_SURGICAL_NOWARD = "nonsurgical_noward" # 非手术无病房科室
MEDICAL_TECH = "medical_tech" # 医技科室
NURSING = "nursing" # 护理单元
ADMIN = "admin" # 行政科室
LOGISTICS = "logistics" # 后勤科室
class IndicatorTemplate(Base):
"""考核指标模板表"""
__tablename__ = "indicator_templates"
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
template_name: Mapped[str] = mapped_column(String(200), nullable=False, comment="模板名称")
template_code: Mapped[str] = mapped_column(String(50), unique=True, nullable=False, comment="模板编码")
template_type: Mapped[TemplateType] = mapped_column(SQLEnum(TemplateType, values_callable=lambda x: [e.value for e in x]), nullable=False, comment="模板类型")
description: Mapped[Optional[str]] = mapped_column(Text, nullable=True, comment="模板描述")
dimension_weights: Mapped[Optional[str]] = mapped_column(Text, nullable=True, comment="维度权重 (JSON)")
assessment_cycle: Mapped[str] = mapped_column(String(20), default="monthly", comment="考核周期")
is_active: Mapped[bool] = mapped_column(Boolean, default=True, comment="是否启用")
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow, comment="创建时间")
updated_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, comment="更新时间")
# 关系
indicators: Mapped[List["TemplateIndicator"]] = relationship("TemplateIndicator", back_populates="template", cascade="all, delete-orphan")
__table_args__ = (
Index("idx_template_type", "template_type"),
Index("idx_template_active", "is_active"),
)
class TemplateIndicator(Base):
"""模板指标关联表"""
__tablename__ = "template_indicators"
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
template_id: Mapped[int] = mapped_column(ForeignKey("indicator_templates.id"), nullable=False, comment="模板 ID")
indicator_id: Mapped[int] = mapped_column(ForeignKey("indicators.id"), nullable=False, comment="指标 ID")
category: Mapped[Optional[str]] = mapped_column(String(100), nullable=True, comment="指标分类 (二级指标)")
target_value: Mapped[Optional[float]] = mapped_column(Numeric(10, 2), nullable=True, comment="目标值")
target_unit: Mapped[Optional[str]] = mapped_column(String(50), nullable=True, comment="目标值单位")
weight: Mapped[float] = mapped_column(Numeric(5, 2), default=1.0, comment="权重")
scoring_method: Mapped[Optional[str]] = mapped_column(String(50), nullable=True, comment="评分方法")
scoring_params: Mapped[Optional[str]] = mapped_column(Text, nullable=True, comment="评分参数 (JSON)")
sort_order: Mapped[int] = mapped_column(Integer, default=0, comment="排序")
remark: Mapped[Optional[str]] = mapped_column(Text, nullable=True, comment="备注")
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow, comment="创建时间")
updated_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, comment="更新时间")
# 关系
template: Mapped["IndicatorTemplate"] = relationship("IndicatorTemplate", back_populates="indicators")
indicator: Mapped["Indicator"] = relationship("Indicator", backref="template_relations")
__table_args__ = (
Index("idx_ti_template", "template_id"),
Index("idx_ti_indicator", "indicator_id"),
Index("idx_ti_unique", "template_id", "indicator_id", unique=True),
)
class DeptTypeDimensionWeight(Base):
"""科室类型BSC维度权重配置表"""
__tablename__ = "dept_type_dimension_weights"
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
dept_type: Mapped[DeptType] = mapped_column(SQLEnum(DeptType, values_callable=lambda x: [e.value for e in x]), nullable=False, comment="科室类型")
financial_weight: Mapped[float] = mapped_column(Numeric(5, 2), default=0.60, comment="财务维度权重")
customer_weight: Mapped[float] = mapped_column(Numeric(5, 2), default=0.15, comment="客户维度权重")
internal_process_weight: Mapped[float] = mapped_column(Numeric(5, 2), default=0.20, comment="内部流程维度权重")
learning_growth_weight: Mapped[float] = mapped_column(Numeric(5, 2), default=0.05, comment="学习成长维度权重")
description: Mapped[Optional[str]] = mapped_column(Text, nullable=True, comment="描述说明")
is_active: Mapped[bool] = mapped_column(Boolean, default=True, comment="是否启用")
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow, comment="创建时间")
updated_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, comment="更新时间")
__table_args__ = (
Index("idx_weight_dept_type", "dept_type"),
Index("idx_weight_active", "is_active"),
)
# ==================== 满意度调查模块 ====================
class SurveyStatus(Enum):
"""调查状态"""
DRAFT = "draft" # 草稿
PUBLISHED = "published" # 已发布
CLOSED = "closed" # 已结束
ARCHIVED = "archived" # 已归档
class SurveyType(Enum):
"""调查类型"""
INPATIENT = "inpatient" # 住院患者满意度
OUTPATIENT = "outpatient" # 门诊患者满意度
INTERNAL = "internal" # 内部员工满意度
DEPARTMENT = "department" # 科室间满意度
class Survey(Base):
"""满意度调查问卷表"""
__tablename__ = "surveys"
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
survey_name: Mapped[str] = mapped_column(String(200), nullable=False, comment="调查名称")
survey_code: Mapped[str] = mapped_column(String(50), unique=True, nullable=False, comment="调查编码")
survey_type: Mapped[SurveyType] = mapped_column(SQLEnum(SurveyType, values_callable=lambda x: [e.value for e in x]), nullable=False, comment="调查类型")
description: Mapped[Optional[str]] = mapped_column(Text, nullable=True, comment="调查描述")
target_departments: Mapped[Optional[str]] = mapped_column(Text, nullable=True, comment="目标科室(JSON数组)")
total_questions: Mapped[int] = mapped_column(Integer, default=0, comment="题目总数")
status: Mapped[SurveyStatus] = mapped_column(SQLEnum(SurveyStatus, values_callable=lambda x: [e.value for e in x]), default=SurveyStatus.DRAFT, comment="状态")
start_date: Mapped[Optional[datetime]] = mapped_column(DateTime, nullable=True, comment="开始日期")
end_date: Mapped[Optional[datetime]] = mapped_column(DateTime, nullable=True, comment="结束日期")
is_anonymous: Mapped[bool] = mapped_column(Boolean, default=True, comment="是否匿名")
is_active: Mapped[bool] = mapped_column(Boolean, default=True, comment="是否启用")
created_by: Mapped[Optional[int]] = mapped_column(ForeignKey("users.id"), nullable=True, comment="创建人")
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow, comment="创建时间")
updated_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, comment="更新时间")
# 关系
questions: Mapped[List["SurveyQuestion"]] = relationship("SurveyQuestion", back_populates="survey", cascade="all, delete-orphan")
responses: Mapped[List["SurveyResponse"]] = relationship("SurveyResponse", back_populates="survey", cascade="all, delete-orphan")
__table_args__ = (
Index("idx_survey_type", "survey_type"),
Index("idx_survey_status", "status"),
Index("idx_survey_dates", "start_date", "end_date"),
)
class QuestionType(Enum):
"""题目类型"""
SINGLE_CHOICE = "single_choice" # 单选题
MULTIPLE_CHOICE = "multiple_choice" # 多选题
SCORE = "score" # 评分题(1-5分)
TEXT = "text" # 文本题
class SurveyQuestion(Base):
"""调查问卷题目表"""
__tablename__ = "survey_questions"
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
survey_id: Mapped[int] = mapped_column(ForeignKey("surveys.id"), nullable=False, comment="调查ID")
question_text: Mapped[str] = mapped_column(Text, nullable=False, comment="题目内容")
question_type: Mapped[QuestionType] = mapped_column(SQLEnum(QuestionType, values_callable=lambda x: [e.value for e in x]), nullable=False, comment="题目类型")
options: Mapped[Optional[str]] = mapped_column(Text, nullable=True, comment="选项(JSON数组)")
score_max: Mapped[int] = mapped_column(Integer, default=5, comment="最高分值(评分题)")
is_required: Mapped[bool] = mapped_column(Boolean, default=True, comment="是否必答")
sort_order: Mapped[int] = mapped_column(Integer, default=0, comment="排序")
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow, comment="创建时间")
# 关系
survey: Mapped["Survey"] = relationship("Survey", back_populates="questions")
answers: Mapped[List["SurveyAnswer"]] = relationship("SurveyAnswer", back_populates="question", cascade="all, delete-orphan")
__table_args__ = (
Index("idx_question_survey", "survey_id"),
)
class SurveyResponse(Base):
"""调查问卷回答记录表"""
__tablename__ = "survey_responses"
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
survey_id: Mapped[int] = mapped_column(ForeignKey("surveys.id"), nullable=False, comment="调查ID")
department_id: Mapped[Optional[int]] = mapped_column(ForeignKey("departments.id"), nullable=True, comment="评价科室")
respondent_type: Mapped[str] = mapped_column(String(20), default="patient", comment="回答者类型")
respondent_id: Mapped[Optional[int]] = mapped_column(Integer, nullable=True, comment="回答者ID(员工时)")
respondent_phone: Mapped[Optional[str]] = mapped_column(String(20), nullable=True, comment="回答者手机")
total_score: Mapped[float] = mapped_column(Numeric(5, 2), default=0, comment="总得分")
max_score: Mapped[float] = mapped_column(Numeric(5, 2), default=0, comment="最高可能得分")
satisfaction_rate: Mapped[float] = mapped_column(Numeric(5, 2), default=0, comment="满意度比例")
ip_address: Mapped[Optional[str]] = mapped_column(String(50), nullable=True, comment="IP地址")
user_agent: Mapped[Optional[str]] = mapped_column(String(500), nullable=True, comment="用户代理")
submitted_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow, comment="提交时间")
# 关系
survey: Mapped["Survey"] = relationship("Survey", back_populates="responses")
department: Mapped[Optional["Department"]] = relationship("Department", backref="survey_responses")
answers: Mapped[List["SurveyAnswer"]] = relationship("SurveyAnswer", back_populates="response", cascade="all, delete-orphan")
__table_args__ = (
Index("idx_response_survey", "survey_id"),
Index("idx_response_dept", "department_id"),
Index("idx_response_time", "submitted_at"),
)
class SurveyAnswer(Base):
"""调查问卷回答明细表"""
__tablename__ = "survey_answers"
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
response_id: Mapped[int] = mapped_column(ForeignKey("survey_responses.id"), nullable=False, comment="回答记录ID")
question_id: Mapped[int] = mapped_column(ForeignKey("survey_questions.id"), nullable=False, comment="题目ID")
answer_value: Mapped[Optional[str]] = mapped_column(Text, nullable=True, comment="回答值")
score: Mapped[float] = mapped_column(Numeric(5, 2), default=0, comment="得分")
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow, comment="创建时间")
# 关系
response: Mapped["SurveyResponse"] = relationship("SurveyResponse", back_populates="answers")
question: Mapped["SurveyQuestion"] = relationship("SurveyQuestion", back_populates="answers")
__table_args__ = (
Index("idx_answer_response", "response_id"),
Index("idx_answer_question", "question_id"),
)

View File

@@ -0,0 +1 @@
from app.schemas.schemas import * # noqa

View File

@@ -0,0 +1,742 @@
"""
Pydantic数据模式
"""
from datetime import datetime
from typing import Optional, List
from pydantic import BaseModel, Field, ConfigDict
from enum import Enum
# ==================== 枚举类型 ====================
class DeptType(str, Enum):
CLINICAL_SURGICAL = "clinical_surgical"
CLINICAL_NONSURGICAL_WARD = "clinical_nonsurgical_ward"
CLINICAL_NONSURGICAL_NOWARD = "clinical_nonsurgical_noward"
MEDICAL_TECH = "medical_tech"
MEDICAL_AUXILIARY = "medical_auxiliary"
NURSING = "nursing"
ADMIN = "admin"
FINANCE = "finance"
LOGISTICS = "logistics"
class StaffStatus(str, Enum):
ACTIVE = "active"
LEAVE = "leave"
RESIGNED = "resigned"
RETIRED = "retired"
class AssessmentStatus(str, Enum):
DRAFT = "draft"
SUBMITTED = "submitted"
REVIEWED = "reviewed"
FINALIZED = "finalized"
REJECTED = "rejected"
class IndicatorType(str, Enum):
QUALITY = "quality"
QUANTITY = "quantity"
EFFICIENCY = "efficiency"
SERVICE = "service"
COST = "cost"
# ==================== 通用响应 ====================
class ResponseBase(BaseModel):
"""通用响应基类"""
code: int = Field(default=200, description="状态码")
message: str = Field(default="success", description="消息")
class PaginatedResponse(ResponseBase):
"""分页响应"""
total: int = Field(description="总数")
page: int = Field(description="当前页")
page_size: int = Field(description="每页数量")
# ==================== 科室相关 ====================
class DepartmentBase(BaseModel):
"""科室基础模式"""
name: str = Field(..., max_length=100, description="科室名称")
code: str = Field(..., max_length=20, description="科室编码")
dept_type: DeptType = Field(..., description="科室类型")
parent_id: Optional[int] = Field(None, description="上级科室ID")
level: int = Field(default=1, ge=1, le=5, description="层级")
sort_order: int = Field(default=0, description="排序")
description: Optional[str] = Field(None, description="描述")
class DepartmentCreate(DepartmentBase):
"""创建科室"""
pass
class DepartmentUpdate(BaseModel):
"""更新科室"""
name: Optional[str] = Field(None, max_length=100)
dept_type: Optional[DeptType] = None
parent_id: Optional[int] = None
sort_order: Optional[int] = None
is_active: Optional[bool] = None
description: Optional[str] = None
class DepartmentResponse(DepartmentBase):
"""科室响应"""
model_config = ConfigDict(from_attributes=True)
id: int
is_active: bool
created_at: datetime
updated_at: datetime
class DepartmentTree(DepartmentResponse):
"""科室树形结构"""
children: List["DepartmentTree"] = []
# ==================== 员工相关 ====================
class StaffBase(BaseModel):
"""员工基础模式"""
employee_id: str = Field(..., max_length=20, description="工号")
name: str = Field(..., max_length=50, description="姓名")
department_id: int = Field(..., description="所属科室ID")
position: str = Field(..., max_length=50, description="职位")
title: Optional[str] = Field(None, max_length=50, description="职称")
phone: Optional[str] = Field(None, max_length=20, description="电话")
email: Optional[str] = Field(None, max_length=100, description="邮箱")
base_salary: float = Field(default=0, ge=0, description="基本工资")
performance_ratio: float = Field(default=1.0, ge=0, le=5, description="绩效系数")
class StaffCreate(StaffBase):
"""创建员工"""
status: StaffStatus = Field(default=StaffStatus.ACTIVE)
hire_date: Optional[datetime] = None
class StaffUpdate(BaseModel):
"""更新员工"""
name: Optional[str] = Field(None, max_length=50)
department_id: Optional[int] = None
position: Optional[str] = Field(None, max_length=50)
title: Optional[str] = Field(None, max_length=50)
phone: Optional[str] = Field(None, max_length=20)
email: Optional[str] = Field(None, max_length=100)
base_salary: Optional[float] = Field(None, ge=0)
performance_ratio: Optional[float] = Field(None, ge=0, le=5)
status: Optional[StaffStatus] = None
class StaffResponse(StaffBase):
"""员工响应"""
model_config = ConfigDict(from_attributes=True)
id: int
status: StaffStatus
hire_date: Optional[datetime]
created_at: datetime
updated_at: datetime
department_name: Optional[str] = None
# ==================== 指标相关 ====================
class IndicatorBase(BaseModel):
"""指标基础模式"""
name: str = Field(..., max_length=100, description="指标名称")
code: str = Field(..., max_length=20, description="指标编码")
indicator_type: IndicatorType = Field(..., description="指标类型")
weight: float = Field(default=1.0, gt=0, le=20, description="权重")
max_score: float = Field(default=100.0, ge=0, le=1000, description="最高分值")
target_value: Optional[float] = Field(None, description="目标值")
unit: Optional[str] = Field(None, max_length=20, description="计量单位")
calculation_method: Optional[str] = Field(None, description="计算方法")
description: Optional[str] = Field(None, description="描述")
class IndicatorCreate(IndicatorBase):
"""创建指标"""
pass
class IndicatorUpdate(BaseModel):
"""更新指标"""
name: Optional[str] = Field(None, max_length=100)
indicator_type: Optional[IndicatorType] = None
weight: Optional[float] = Field(None, gt=0, le=20)
max_score: Optional[float] = Field(None, ge=0, le=1000)
target_value: Optional[float] = None
unit: Optional[str] = None
calculation_method: Optional[str] = None
description: Optional[str] = None
is_active: Optional[bool] = None
class IndicatorResponse(IndicatorBase):
"""指标响应"""
model_config = ConfigDict(from_attributes=True)
id: int
is_active: bool
created_at: datetime
updated_at: datetime
# ==================== 考核相关 ====================
class AssessmentDetailBase(BaseModel):
"""考核明细基础模式"""
indicator_id: int = Field(..., description="指标ID")
actual_value: Optional[float] = Field(None, description="实际值")
score: float = Field(default=0, ge=0, description="得分")
evidence: Optional[str] = Field(None, description="佐证材料")
remark: Optional[str] = Field(None, description="备注")
class AssessmentDetailCreate(AssessmentDetailBase):
"""创建考核明细"""
pass
class AssessmentDetailResponse(AssessmentDetailBase):
"""考核明细响应"""
model_config = ConfigDict(from_attributes=True)
id: int
assessment_id: int
indicator_name: Optional[str] = None
created_at: datetime
class AssessmentBase(BaseModel):
"""考核基础模式"""
staff_id: int = Field(..., description="员工ID")
period_year: int = Field(..., ge=2020, le=2100, description="年度")
period_month: int = Field(..., ge=1, le=12, description="月份")
period_type: str = Field(default="monthly", description="周期类型")
class AssessmentCreate(AssessmentBase):
"""创建考核"""
details: List[AssessmentDetailCreate] = Field(default_factory=list, description="考核明细")
class AssessmentUpdate(BaseModel):
"""更新考核"""
details: Optional[List[AssessmentDetailCreate]] = None
remark: Optional[str] = None
class AssessmentResponse(AssessmentBase):
"""考核响应"""
model_config = ConfigDict(from_attributes=True)
id: int
total_score: float
weighted_score: float
status: AssessmentStatus
assessor_id: Optional[int]
reviewer_id: Optional[int]
submit_time: Optional[datetime]
review_time: Optional[datetime]
remark: Optional[str]
created_at: datetime
updated_at: datetime
staff_name: Optional[str] = None
department_name: Optional[str] = None
details: List[AssessmentDetailResponse] = []
class AssessmentListResponse(AssessmentBase):
"""考核列表响应"""
model_config = ConfigDict(from_attributes=True)
id: int
total_score: float
weighted_score: float
status: AssessmentStatus
staff_name: Optional[str] = None
department_name: Optional[str] = None
created_at: datetime
# ==================== 工资相关 ====================
class SalaryRecordBase(BaseModel):
"""工资记录基础模式"""
staff_id: int = Field(..., description="员工ID")
period_year: int = Field(..., ge=2020, le=2100, description="年度")
period_month: int = Field(..., ge=1, le=12, description="月份")
base_salary: float = Field(default=0, ge=0, description="基本工资")
performance_score: float = Field(default=0, ge=0, description="绩效得分")
performance_bonus: float = Field(default=0, ge=0, description="绩效奖金")
deduction: float = Field(default=0, ge=0, description="扣款")
allowance: float = Field(default=0, ge=0, description="补贴")
class SalaryRecordCreate(SalaryRecordBase):
"""创建工资记录"""
remark: Optional[str] = None
class SalaryRecordUpdate(BaseModel):
"""更新工资记录"""
performance_bonus: Optional[float] = Field(None, ge=0)
deduction: Optional[float] = Field(None, ge=0)
allowance: Optional[float] = Field(None, ge=0)
remark: Optional[str] = None
class SalaryRecordResponse(SalaryRecordBase):
"""工资记录响应"""
model_config = ConfigDict(from_attributes=True)
id: int
total_salary: float
status: str
remark: Optional[str]
created_at: datetime
updated_at: datetime
staff_name: Optional[str] = None
department_name: Optional[str] = None
# ==================== 用户认证相关 ====================
class UserLogin(BaseModel):
"""用户登录"""
username: str = Field(..., min_length=3, max_length=50)
password: str = Field(..., min_length=6, max_length=100)
class UserCreate(BaseModel):
"""创建用户"""
username: str = Field(..., min_length=3, max_length=50)
password: str = Field(..., min_length=6, max_length=100)
staff_id: Optional[int] = None
role: str = Field(default="staff", pattern="^(admin|manager|staff)$")
class Token(BaseModel):
"""令牌响应"""
access_token: str
token_type: str = "bearer"
class UserResponse(BaseModel):
"""用户响应"""
model_config = ConfigDict(from_attributes=True)
id: int
username: str
role: str
is_active: bool
last_login: Optional[datetime]
created_at: datetime
# ==================== 统计报表相关 ====================
class DepartmentStats(BaseModel):
"""科室统计"""
department_id: int
department_name: str
staff_count: int
avg_score: float
total_bonus: float
class PeriodStats(BaseModel):
"""周期统计"""
period_year: int
period_month: int
total_staff: int
assessed_count: int
avg_score: float
total_bonus: float
score_distribution: dict
class TrendData(BaseModel):
"""趋势数据"""
period: str
avg_score: float
total_bonus: float
# ==================== 财务核算相关 ====================
class RevenueCategory(str, Enum):
"""收入类别"""
EXAMINATION = "examination" # 检查费
LAB_TEST = "lab_test" # 检验费
RADIOLOGY = "radiology" # 放射费
BED = "bed" # 床位费
NURSING = "nursing" # 护理费
TREATMENT = "treatment" # 治疗费
SURGERY = "surgery" # 手术费
INJECTION = "injection" # 注射费
OXYGEN = "oxygen" # 吸氧费
OTHER = "other" # 其他
class ExpenseCategory(str, Enum):
"""支出类别"""
MATERIAL = "material" # 材料费
PERSONNEL = "personnel" # 人员支出
MAINTENANCE = "maintenance" # 维修费
UTILITY = "utility" # 水电费
OTHER = "other" # 其他
class FinanceType(str, Enum):
"""财务类型"""
REVENUE = "revenue" # 收入
EXPENSE = "expense" # 支出
class FinanceRecordBase(BaseModel):
"""财务记录基础模式"""
department_id: int = Field(..., description="科室ID")
finance_type: FinanceType = Field(..., description="财务类型")
category: str = Field(..., max_length=50, description="类别")
amount: float = Field(..., ge=0, description="金额")
period_year: int = Field(..., ge=2020, le=2100, description="年度")
period_month: int = Field(..., ge=1, le=12, description="月份")
source: Optional[str] = Field(None, max_length=100, description="数据来源")
remark: Optional[str] = Field(None, description="备注")
class FinanceRecordCreate(FinanceRecordBase):
"""创建财务记录"""
pass
class FinanceRecordUpdate(BaseModel):
"""更新财务记录"""
category: Optional[str] = Field(None, max_length=50)
amount: Optional[float] = Field(None, ge=0)
source: Optional[str] = Field(None, max_length=100)
remark: Optional[str] = None
class FinanceRecordResponse(FinanceRecordBase):
"""财务记录响应"""
model_config = ConfigDict(from_attributes=True)
id: int
department_name: Optional[str] = None
category_label: Optional[str] = None
created_at: datetime
updated_at: datetime
class DepartmentBalance(BaseModel):
"""科室收支结余"""
department_id: Optional[int] = None
department_name: Optional[str] = None
period_year: Optional[int] = None
period_month: Optional[int] = None
total_revenue: float = Field(default=0, description="总收入")
total_expense: float = Field(default=0, description="总支出")
balance: float = Field(default=0, description="结余")
class CategorySummary(BaseModel):
"""类别汇总"""
category: str
category_label: str
amount: float
# ==================== 绩效计划相关 ====================
class PlanLevel(str, Enum):
"""计划层级"""
HOSPITAL = "hospital"
DEPARTMENT = "department"
INDIVIDUAL = "individual"
class PlanStatus(str, Enum):
"""计划状态"""
DRAFT = "draft"
PENDING = "pending"
APPROVED = "approved"
REJECTED = "rejected"
ACTIVE = "active"
COMPLETED = "completed"
CANCELLED = "cancelled"
class PlanKpiRelationBase(BaseModel):
"""计划指标关联基础模式"""
indicator_id: int = Field(..., description="指标 ID")
target_value: Optional[float] = Field(None, description="目标值")
target_unit: Optional[str] = Field(None, max_length=50, description="目标值单位")
weight: float = Field(default=1.0, ge=0, le=100, description="权重")
scoring_method: Optional[str] = Field(None, max_length=50, description="评分方法")
scoring_params: Optional[str] = Field(None, description="评分参数 (JSON)")
remark: Optional[str] = Field(None, description="备注")
class PlanKpiRelationCreate(PlanKpiRelationBase):
"""创建计划指标关联"""
pass
class PlanKpiRelationUpdate(BaseModel):
"""更新计划指标关联"""
target_value: Optional[float] = Field(None, description="目标值")
target_unit: Optional[str] = Field(None, max_length=50, description="目标值单位")
weight: Optional[float] = Field(None, ge=0, le=100, description="权重")
scoring_method: Optional[str] = Field(None, max_length=50, description="评分方法")
scoring_params: Optional[str] = Field(None, description="评分参数 (JSON)")
remark: Optional[str] = Field(None, description="备注")
class PlanKpiRelationResponse(PlanKpiRelationBase):
"""计划指标关联响应"""
model_config = ConfigDict(from_attributes=True)
id: int
plan_id: int
indicator_name: Optional[str] = None
indicator_code: Optional[str] = None
created_at: datetime
updated_at: datetime
class PerformancePlanBase(BaseModel):
"""绩效计划基础模式"""
plan_name: str = Field(..., max_length=200, description="计划名称")
plan_code: str = Field(..., max_length=50, description="计划编码")
plan_level: PlanLevel = Field(..., description="计划层级")
plan_year: int = Field(..., ge=2000, le=2100, description="计划年度")
plan_month: Optional[int] = Field(None, ge=1, le=12, description="计划月份")
plan_type: str = Field(default="annual", max_length=20, description="计划类型")
department_id: Optional[int] = Field(None, description="所属科室 ID")
staff_id: Optional[int] = Field(None, description="责任人 ID")
parent_plan_id: Optional[int] = Field(None, description="上级计划 ID")
description: Optional[str] = Field(None, description="计划描述")
strategic_goals: Optional[str] = Field(None, description="战略目标")
key_initiatives: Optional[str] = Field(None, description="关键举措")
class PerformancePlanCreate(PerformancePlanBase):
"""创建绩效计划"""
kpi_relations: Optional[List[PlanKpiRelationCreate]] = Field(None, description="指标关联列表")
class PerformancePlanUpdate(BaseModel):
"""更新绩效计划"""
plan_name: Optional[str] = Field(None, max_length=200, description="计划名称")
plan_level: Optional[PlanLevel] = Field(None, description="计划层级")
department_id: Optional[int] = Field(None, description="所属科室 ID")
staff_id: Optional[int] = Field(None, description="责任人 ID")
description: Optional[str] = Field(None, description="计划描述")
strategic_goals: Optional[str] = Field(None, description="战略目标")
key_initiatives: Optional[str] = Field(None, description="关键举措")
status: Optional[PlanStatus] = Field(None, description="状态")
approve_remark: Optional[str] = Field(None, description="审批意见")
class PerformancePlanResponse(PerformancePlanBase):
"""绩效计划响应"""
model_config = ConfigDict(from_attributes=True)
id: int
status: PlanStatus
submitter_id: Optional[int] = None
submit_time: Optional[datetime] = None
approver_id: Optional[int] = None
approve_time: Optional[datetime] = None
version: int
is_active: bool
created_at: datetime
updated_at: datetime
department_name: Optional[str] = None
staff_name: Optional[str] = None
kpi_relations: Optional[List[PlanKpiRelationResponse]] = None
class PerformancePlanStats(BaseModel):
"""绩效计划统计"""
total_plans: int = Field(0, description="总计划数")
draft_count: int = Field(0, description="草稿数")
pending_count: int = Field(0, description="待审批数")
approved_count: int = Field(0, description="已批准数")
active_count: int = Field(0, description="执行中数")
completed_count: int = Field(0, description="已完成数")
# ==================== 菜单管理相关 ====================
class MenuType(str, Enum):
"""菜单类型"""
MENU = "menu"
BUTTON = "button"
class MenuBase(BaseModel):
"""菜单基础模式"""
parent_id: Optional[int] = Field(None, description="父菜单 ID")
menu_type: MenuType = Field(default=MenuType.MENU, description="菜单类型")
menu_name: str = Field(..., max_length=100, description="菜单名称")
menu_icon: Optional[str] = Field(None, max_length=50, description="菜单图标")
path: str = Field(..., max_length=200, description="路由路径")
component: Optional[str] = Field(None, max_length=200, description="组件路径")
permission: Optional[str] = Field(None, max_length=100, description="权限标识")
sort_order: int = Field(default=0, ge=0, description="排序")
is_visible: bool = Field(default=True, description="是否可见")
is_active: bool = Field(default=True, description="是否启用")
class MenuCreate(MenuBase):
"""创建菜单"""
pass
class MenuUpdate(BaseModel):
"""更新菜单"""
menu_name: Optional[str] = Field(None, max_length=100, description="菜单名称")
menu_icon: Optional[str] = Field(None, max_length=50, description="菜单图标")
path: Optional[str] = Field(None, max_length=200, description="路由路径")
component: Optional[str] = Field(None, max_length=200, description="组件路径")
permission: Optional[str] = Field(None, max_length=100, description="权限标识")
sort_order: Optional[int] = Field(None, ge=0, description="排序")
is_visible: Optional[bool] = Field(None, description="是否可见")
is_active: Optional[bool] = Field(None, description="是否启用")
class MenuResponse(MenuBase):
"""菜单响应"""
model_config = ConfigDict(from_attributes=True)
id: int
children: Optional[List["MenuResponse"]] = None
created_at: datetime
updated_at: datetime
class MenuTree(BaseModel):
"""菜单树节点"""
id: int
menu_name: str
menu_icon: Optional[str] = None
path: str
children: Optional[List["MenuTree"]] = None
# ==================== 指标模板相关 ====================
class TemplateType(str, Enum):
"""模板类型"""
GENERAL = "general" # 通用模板
SURGICAL = "surgical" # 手术临床科室
NON_SURGICAL_WARD = "nonsurgical_ward" # 非手术有病房科室
NON_SURGICAL_NOWARD = "nonsurgical_noward" # 非手术无病房科室
MEDICAL_TECH = "medical_tech" # 医技科室
NURSING = "nursing" # 护理单元
ADMIN = "admin" # 行政科室
LOGISTICS = "logistics" # 后勤科室
class TemplateIndicatorBase(BaseModel):
"""模板指标关联基础模式"""
indicator_id: int = Field(..., description="指标 ID")
category: Optional[str] = Field(None, max_length=100, description="指标分类")
target_value: Optional[float] = Field(None, description="目标值")
target_unit: Optional[str] = Field(None, max_length=50, description="目标值单位")
weight: float = Field(default=1.0, ge=0, le=100, description="权重")
scoring_method: Optional[str] = Field(None, max_length=50, description="评分方法")
scoring_params: Optional[str] = Field(None, description="评分参数 (JSON)")
sort_order: int = Field(default=0, ge=0, description="排序")
remark: Optional[str] = Field(None, description="备注")
class TemplateIndicatorCreate(TemplateIndicatorBase):
"""创建模板指标关联"""
pass
class TemplateIndicatorUpdate(BaseModel):
"""更新模板指标关联"""
category: Optional[str] = Field(None, max_length=100, description="指标分类")
target_value: Optional[float] = Field(None, description="目标值")
target_unit: Optional[str] = Field(None, max_length=50, description="目标值单位")
weight: Optional[float] = Field(None, ge=0, le=100, description="权重")
scoring_method: Optional[str] = Field(None, max_length=50, description="评分方法")
scoring_params: Optional[str] = Field(None, description="评分参数 (JSON)")
sort_order: Optional[int] = Field(None, ge=0, description="排序")
remark: Optional[str] = Field(None, description="备注")
class TemplateIndicatorResponse(TemplateIndicatorBase):
"""模板指标关联响应"""
model_config = ConfigDict(from_attributes=True)
id: int
template_id: int
indicator_name: Optional[str] = None
indicator_code: Optional[str] = None
indicator_type: Optional[str] = None
bs_dimension: Optional[str] = None
created_at: datetime
updated_at: datetime
class IndicatorTemplateBase(BaseModel):
"""指标模板基础模式"""
template_name: str = Field(..., max_length=200, description="模板名称")
template_code: str = Field(..., max_length=50, description="模板编码")
template_type: TemplateType = Field(..., description="模板类型")
description: Optional[str] = Field(None, description="模板描述")
dimension_weights: Optional[str] = Field(None, description="维度权重 (JSON)")
assessment_cycle: str = Field(default="monthly", max_length=20, description="考核周期")
class IndicatorTemplateCreate(IndicatorTemplateBase):
"""创建指标模板"""
indicators: Optional[List[TemplateIndicatorCreate]] = Field(None, description="指标列表")
class IndicatorTemplateUpdate(BaseModel):
"""更新指标模板"""
template_name: Optional[str] = Field(None, max_length=200, description="模板名称")
template_type: Optional[TemplateType] = None
description: Optional[str] = Field(None, description="模板描述")
dimension_weights: Optional[str] = Field(None, description="维度权重 (JSON)")
assessment_cycle: Optional[str] = Field(None, max_length=20, description="考核周期")
is_active: Optional[bool] = Field(None, description="是否启用")
class IndicatorTemplateResponse(IndicatorTemplateBase):
"""指标模板响应"""
model_config = ConfigDict(from_attributes=True)
id: int
is_active: bool
created_at: datetime
updated_at: datetime
indicators: Optional[List[TemplateIndicatorResponse]] = None
class IndicatorTemplateListResponse(IndicatorTemplateBase):
"""指标模板列表响应"""
model_config = ConfigDict(from_attributes=True)
id: int
is_active: bool
indicator_count: int = Field(default=0, description="指标数量")
created_at: datetime
updated_at: datetime

View File

View File

@@ -0,0 +1,275 @@
"""
初始化指标模板数据
根据考核指标模板文档初始化模板和指标数据
"""
import asyncio
import json
import sys
import io
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
from sqlalchemy.orm import sessionmaker
from sqlalchemy import select
# 设置标准输出编码
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
from app.core.config import settings
from app.models.models import (
Indicator, IndicatorTemplate, TemplateIndicator,
IndicatorType, BSCDimension, TemplateType
)
# 指标数据 - 基于文档中的模板
INDICATORS_DATA = [
# 财务管理维度
{"code": "FIN001", "name": "业务收支结余率", "indicator_type": IndicatorType.COST, "bs_dimension": BSCDimension.FINANCIAL, "weight": 1.0, "max_score": 100, "target_value": None, "target_unit": "%", "calculation_method": "(收入-支出)/收入×100%", "assessment_method": "区间法达标满分每低1%扣2分", "data_source": "财务科"},
{"code": "FIN002", "name": "百元医疗收入卫生材料消耗", "indicator_type": IndicatorType.COST, "bs_dimension": BSCDimension.FINANCIAL, "weight": 1.0, "max_score": 100, "target_value": 20, "target_unit": "", "calculation_method": "卫生材料消耗/医疗收入×100", "assessment_method": "目标参照法(≤目标值满分,超扣分)", "data_source": "物资科"},
{"code": "FIN003", "name": "可控成本占比", "indicator_type": IndicatorType.COST, "bs_dimension": BSCDimension.FINANCIAL, "weight": 1.0, "max_score": 100, "target_value": 30, "target_unit": "%", "calculation_method": "可控成本/总成本×100%", "assessment_method": "区间法", "data_source": "财务科"},
{"code": "FIN004", "name": "百元固定资产收入", "indicator_type": IndicatorType.EFFICIENCY, "bs_dimension": BSCDimension.FINANCIAL, "weight": 1.0, "max_score": 100, "target_value": 150, "target_unit": "", "calculation_method": "业务收入/固定资产原值×100", "assessment_method": "比较法(与去年同期/标杆比)", "data_source": "财务科、设备科"},
# 顾客服务维度
{"code": "CUS001", "name": "住院患者满意度得分", "indicator_type": IndicatorType.SERVICE, "bs_dimension": BSCDimension.CUSTOMER, "weight": 1.0, "max_score": 100, "target_value": 90, "target_unit": "", "calculation_method": "满意度调查问卷统计", "assessment_method": "区间法90-100满分每低1分扣2分", "data_source": "满意度调查"},
{"code": "CUS002", "name": "门诊患者满意度得分", "indicator_type": IndicatorType.SERVICE, "bs_dimension": BSCDimension.CUSTOMER, "weight": 1.0, "max_score": 100, "target_value": 85, "target_unit": "", "calculation_method": "满意度调查问卷统计", "assessment_method": "区间法", "data_source": "满意度调查"},
{"code": "CUS003", "name": "预约就诊率", "indicator_type": IndicatorType.SERVICE, "bs_dimension": BSCDimension.CUSTOMER, "weight": 1.0, "max_score": 100, "target_value": 60, "target_unit": "%", "calculation_method": "预约挂号人次/总挂号人次×100%", "assessment_method": "目标参照法", "data_source": "HIS系统"},
{"code": "CUS004", "name": "有效投诉次数", "indicator_type": IndicatorType.SERVICE, "bs_dimension": BSCDimension.CUSTOMER, "weight": 1.0, "max_score": 100, "target_value": 0, "target_unit": "", "calculation_method": "有效投诉统计", "assessment_method": "扣分法每发生1次扣5分", "data_source": "投诉办、党办"},
# 内部流程维度
{"code": "IPR001", "name": "出院患者平均住院日", "indicator_type": IndicatorType.EFFICIENCY, "bs_dimension": BSCDimension.INTERNAL_PROCESS, "weight": 1.0, "max_score": 100, "target_value": 9, "target_unit": "", "calculation_method": "出院患者占用总床日数/出院人次", "assessment_method": "区间法≤目标值满分每超1天扣2分", "data_source": "病案室"},
{"code": "IPR002", "name": "病历甲级率", "indicator_type": IndicatorType.QUALITY, "bs_dimension": BSCDimension.INTERNAL_PROCESS, "weight": 1.0, "max_score": 100, "target_value": 90, "target_unit": "%", "calculation_method": "甲级病历数/抽查病历数×100%", "assessment_method": "区间法", "data_source": "质控科"},
{"code": "IPR003", "name": "医疗事故/严重差错发生数", "indicator_type": IndicatorType.QUALITY, "bs_dimension": BSCDimension.INTERNAL_PROCESS, "weight": 1.0, "max_score": 100, "target_value": 0, "target_unit": "", "calculation_method": "医疗事故统计", "assessment_method": "一票否决/扣分法", "data_source": "医务科", "is_veto": True},
{"code": "IPR004", "name": "医院感染发生率", "indicator_type": IndicatorType.QUALITY, "bs_dimension": BSCDimension.INTERNAL_PROCESS, "weight": 1.0, "max_score": 100, "target_value": 5, "target_unit": "%", "calculation_method": "医院感染例数/出院人次×100%", "assessment_method": "目标参照法", "data_source": "院感科"},
{"code": "IPR005", "name": "抗菌药物使用强度", "indicator_type": IndicatorType.QUALITY, "bs_dimension": BSCDimension.INTERNAL_PROCESS, "weight": 1.0, "max_score": 100, "target_value": 40, "target_unit": "DDDs", "calculation_method": "抗菌药物累计DDD数/同期收治患者人天数×100", "assessment_method": "区间法", "data_source": "药剂科"},
# 学习与成长维度
{"code": "LRN001", "name": "发表论文数", "indicator_type": IndicatorType.QUANTITY, "bs_dimension": BSCDimension.LEARNING_GROWTH, "weight": 1.0, "max_score": 100, "target_value": 2, "target_unit": "篇/年", "calculation_method": "核心/统计源期刊发表论文数", "assessment_method": "加分法每篇加5分封顶20分", "data_source": "科教科"},
{"code": "LRN002", "name": "带教实习生/进修生人数", "indicator_type": IndicatorType.QUANTITY, "bs_dimension": BSCDimension.LEARNING_GROWTH, "weight": 1.0, "max_score": 100, "target_value": 5, "target_unit": "人/年", "calculation_method": "带教人数统计", "assessment_method": "目标参照法", "data_source": "科教科"},
{"code": "LRN003", "name": "参加院内外培训人次", "indicator_type": IndicatorType.QUANTITY, "bs_dimension": BSCDimension.LEARNING_GROWTH, "weight": 1.0, "max_score": 100, "target_value": 20, "target_unit": "人次/季", "calculation_method": "培训人次统计", "assessment_method": "区间法", "data_source": "人事科"},
{"code": "LRN004", "name": "科室内部业务学习次数", "indicator_type": IndicatorType.QUANTITY, "bs_dimension": BSCDimension.LEARNING_GROWTH, "weight": 1.0, "max_score": 100, "target_value": 4, "target_unit": "次/月", "calculation_method": "学习记录统计", "assessment_method": "核查法少1次扣2分", "data_source": "科室自查"},
# 手术科室专项指标
{"code": "SRG001", "name": "DRG组数", "indicator_type": IndicatorType.QUANTITY, "bs_dimension": BSCDimension.FINANCIAL, "weight": 1.0, "max_score": 100, "target_value": None, "target_unit": "", "calculation_method": "出院病例进入的DRG组数量", "assessment_method": "比较法", "data_source": "病案室、DRG分组器"},
{"code": "SRG002", "name": "CMI值", "indicator_type": IndicatorType.QUALITY, "bs_dimension": BSCDimension.FINANCIAL, "weight": 1.0, "max_score": 100, "target_value": 1.0, "target_unit": None, "calculation_method": "科室出院病例平均权重", "assessment_method": "比较法", "data_source": "病案室、DRG分组器"},
{"code": "SRG003", "name": "费用消耗指数", "indicator_type": IndicatorType.COST, "bs_dimension": BSCDimension.FINANCIAL, "weight": 1.0, "max_score": 100, "target_value": 1.0, "target_unit": None, "calculation_method": "本科室DRG平均费用/区域同级医院同DRG平均费用", "assessment_method": "目标参照法", "data_source": "医保办、财务科"},
{"code": "SRG004", "name": "时间消耗指数", "indicator_type": IndicatorType.EFFICIENCY, "bs_dimension": BSCDimension.FINANCIAL, "weight": 1.0, "max_score": 100, "target_value": 1.0, "target_unit": None, "calculation_method": "本科室DRG平均住院日/区域同级医院同DRG平均住院日", "assessment_method": "目标参照法", "data_source": "病案室"},
{"code": "SRG005", "name": "手术并发症发生率", "indicator_type": IndicatorType.QUALITY, "bs_dimension": BSCDimension.INTERNAL_PROCESS, "weight": 1.0, "max_score": 100, "target_value": 1, "target_unit": "%", "calculation_method": "手术并发症例数/手术人次×100%", "assessment_method": "区间法", "data_source": "医务科"},
{"code": "SRG006", "name": "非计划重返手术室率", "indicator_type": IndicatorType.QUALITY, "bs_dimension": BSCDimension.INTERNAL_PROCESS, "weight": 1.0, "max_score": 100, "target_value": 0.5, "target_unit": "%", "calculation_method": "非计划重返手术室人次/手术人次×100%", "assessment_method": "区间法", "data_source": "医务科"},
{"code": "SRG007", "name": "围手术期死亡率", "indicator_type": IndicatorType.QUALITY, "bs_dimension": BSCDimension.INTERNAL_PROCESS, "weight": 1.0, "max_score": 100, "target_value": 0, "target_unit": "%", "calculation_method": "围手术期死亡人次/手术人次×100%", "assessment_method": "一票否决", "data_source": "医务科", "is_veto": True},
# 医技科室专项指标
{"code": "MTT001", "name": "检验/检查报告准确率", "indicator_type": IndicatorType.QUALITY, "bs_dimension": BSCDimension.INTERNAL_PROCESS, "weight": 1.0, "max_score": 100, "target_value": 99.5, "target_unit": "%", "calculation_method": "准确报告数/总报告数×100%", "assessment_method": "每低0.1%扣2分", "data_source": "质控科、临床反馈"},
{"code": "MTT002", "name": "室内质控达标率", "indicator_type": IndicatorType.QUALITY, "bs_dimension": BSCDimension.INTERNAL_PROCESS, "weight": 1.0, "max_score": 100, "target_value": 100, "target_unit": "%", "calculation_method": "质控达标项次/总质控项次×100%", "assessment_method": "未达标项次扣分", "data_source": "科室自查记录"},
{"code": "MTT003", "name": "危急值及时报告率", "indicator_type": IndicatorType.QUALITY, "bs_dimension": BSCDimension.INTERNAL_PROCESS, "weight": 1.0, "max_score": 100, "target_value": 100, "target_unit": "%", "calculation_method": "及时报告危急值数/危急值总数×100%", "assessment_method": "每漏报/迟报1例扣5分", "data_source": "HIS系统追踪"},
{"code": "MTT004", "name": "门诊常规报告出具时间", "indicator_type": IndicatorType.EFFICIENCY, "bs_dimension": BSCDimension.INTERNAL_PROCESS, "weight": 1.0, "max_score": 100, "target_value": 2, "target_unit": "小时", "calculation_method": "报告出具时间-标本接收时间", "assessment_method": "超时率每超5%扣2分", "data_source": "LIS/RIS系统"},
{"code": "MTT005", "name": "急诊报告出具时间", "indicator_type": IndicatorType.EFFICIENCY, "bs_dimension": BSCDimension.INTERNAL_PROCESS, "weight": 1.0, "max_score": 100, "target_value": 30, "target_unit": "分钟", "calculation_method": "报告出具时间-标本接收时间", "assessment_method": "超时率每超5%扣2分", "data_source": "LIS/RIS系统"},
{"code": "MTT006", "name": "临床科室对医技服务满意度", "indicator_type": IndicatorType.SERVICE, "bs_dimension": BSCDimension.CUSTOMER, "weight": 1.0, "max_score": 100, "target_value": 90, "target_unit": "", "calculation_method": "内部满意度调查", "assessment_method": "区间法", "data_source": "内部满意度调查"},
# 行政科室专项指标
{"code": "ADM001", "name": "服务态度与响应及时性", "indicator_type": IndicatorType.SERVICE, "bs_dimension": BSCDimension.CUSTOMER, "weight": 1.0, "max_score": 100, "target_value": 90, "target_unit": "", "calculation_method": "临床科室满意度测评", "assessment_method": "问卷/投票评分", "data_source": "满意度调查"},
{"code": "ADM002", "name": "遵纪守法与廉洁自律", "indicator_type": IndicatorType.QUALITY, "bs_dimension": BSCDimension.INTERNAL_PROCESS, "weight": 1.0, "max_score": 100, "target_value": 100, "target_unit": "%", "calculation_method": "违规情况统计", "assessment_method": "扣分法(违规即扣)", "data_source": "党办/纪检监察室"},
{"code": "ADM003", "name": "工作计划与总结", "indicator_type": IndicatorType.QUALITY, "bs_dimension": BSCDimension.INTERNAL_PROCESS, "weight": 1.0, "max_score": 100, "target_value": 100, "target_unit": "%", "calculation_method": "按时提交率", "assessment_method": "目标参照法+检查扣分", "data_source": "院办"},
{"code": "ADM004", "name": "任务按时完成率", "indicator_type": IndicatorType.EFFICIENCY, "bs_dimension": BSCDimension.INTERNAL_PROCESS, "weight": 1.0, "max_score": 100, "target_value": 95, "target_unit": "%", "calculation_method": "按时完成任务数/总任务数×100%", "assessment_method": "核查法(任务逾期扣分)", "data_source": "院办"},
# 后勤科室专项指标
{"code": "LOG001", "name": "后勤保障及时率", "indicator_type": IndicatorType.EFFICIENCY, "bs_dimension": BSCDimension.INTERNAL_PROCESS, "weight": 1.0, "max_score": 100, "target_value": 95, "target_unit": "%", "calculation_method": "及时响应次数/总报修次数×100%", "assessment_method": "区间法", "data_source": "总务科"},
{"code": "LOG002", "name": "安全生产检查达标率", "indicator_type": IndicatorType.QUALITY, "bs_dimension": BSCDimension.INTERNAL_PROCESS, "weight": 1.0, "max_score": 100, "target_value": 100, "target_unit": "%", "calculation_method": "达标项次/检查项次×100%", "assessment_method": "扣分法", "data_source": "安全检查记录"},
{"code": "LOG003", "name": "设备完好率", "indicator_type": IndicatorType.QUALITY, "bs_dimension": BSCDimension.INTERNAL_PROCESS, "weight": 1.0, "max_score": 100, "target_value": 95, "target_unit": "%", "calculation_method": "完好设备数/设备总数×100%", "assessment_method": "区间法", "data_source": "设备科"},
]
# 模板数据
TEMPLATES_DATA = [
{
"template_code": "TPL_GENERAL",
"template_name": "平衡计分卡四维度通用考核方案",
"template_type": TemplateType.GENERAL,
"description": "基于平衡计分卡理论,整合财务、顾客、内部流程、学习成长四个维度,适用于全院各科室",
"dimension_weights": {"financial": 35, "customer": 30, "internal_process": 25, "learning_growth": 10},
"assessment_cycle": "monthly",
"indicators": [
{"code": "FIN001", "category": "收支管理", "weight": 10},
{"code": "FIN002", "category": "成本控制", "weight": 8},
{"code": "FIN003", "category": "成本控制", "weight": 7},
{"code": "FIN004", "category": "资产效率", "weight": 5},
{"code": "CUS001", "category": "患者满意度", "weight": 10},
{"code": "CUS002", "category": "患者满意度", "weight": 5},
{"code": "CUS003", "category": "服务可及性", "weight": 5},
{"code": "CUS004", "category": "投诉管理", "weight": 5},
{"code": "IPR001", "category": "医疗质量", "weight": 6},
{"code": "IPR002", "category": "医疗质量", "weight": 6},
{"code": "IPR003", "category": "医疗安全", "weight": 8},
{"code": "IPR004", "category": "院感控制", "weight": 5},
{"code": "IPR005", "category": "合理用药", "weight": 5},
{"code": "LRN001", "category": "科研教学", "weight": 4},
{"code": "LRN002", "category": "科研教学", "weight": 3},
{"code": "LRN003", "category": "人才培养", "weight": 3},
{"code": "LRN004", "category": "人才培养", "weight": 2},
]
},
{
"template_code": "TPL_SURGICAL",
"template_name": "临床手术科室RBRVS/DRG导向绩效方案",
"template_type": TemplateType.SURGICAL,
"description": "结合RBRVS和DRG理念体现技术难度、风险和工作量适用于外科、妇科、眼科等手术科室",
"dimension_weights": {"financial": 35, "customer": 20, "internal_process": 30, "learning_growth": 15},
"assessment_cycle": "monthly",
"indicators": [
{"code": "SRG001", "category": "财务与效率", "weight": 8},
{"code": "SRG002", "category": "财务与效率", "weight": 10},
{"code": "SRG003", "category": "财务与效率", "weight": 8},
{"code": "SRG004", "category": "财务与效率", "weight": 9},
{"code": "FIN001", "category": "财务与效率", "weight": 5},
{"code": "SRG005", "category": "质量与安全", "weight": 10},
{"code": "SRG006", "category": "质量与安全", "weight": 8},
{"code": "SRG007", "category": "质量与安全", "weight": 10},
{"code": "IPR002", "category": "质量与安全", "weight": 8},
{"code": "CUS001", "category": "患者与服务", "weight": 10},
{"code": "CUS004", "category": "患者与服务", "weight": 5},
{"code": "LRN001", "category": "学习与创新", "weight": 5},
{"code": "LRN002", "category": "学习与创新", "weight": 4},
]
},
{
"template_code": "TPL_MEDICAL_TECH",
"template_name": "医技科室质量效率双核心考核方案",
"template_type": TemplateType.MEDICAL_TECH,
"description": "以工作质量、报告准确性和内部服务效率为核心,兼顾成本控制,适用于检验科、放射科、超声科、药剂科等",
"dimension_weights": {"financial": 20, "customer": 30, "internal_process": 40, "learning_growth": 10},
"assessment_cycle": "monthly",
"indicators": [
{"code": "MTT001", "category": "工作质量与安全", "weight": 12},
{"code": "MTT002", "category": "工作质量与安全", "weight": 10},
{"code": "MTT003", "category": "工作质量与安全", "weight": 10},
{"code": "IPR003", "category": "工作质量与安全", "weight": 8},
{"code": "MTT004", "category": "内部服务效率", "weight": 8},
{"code": "MTT005", "category": "内部服务效率", "weight": 10},
{"code": "MTT006", "category": "内部服务效率", "weight": 8},
{"code": "FIN002", "category": "成本与资源管理", "weight": 10},
{"code": "LOG003", "category": "成本与资源管理", "weight": 8},
{"code": "LRN001", "category": "学科发展与服务", "weight": 5},
{"code": "LRN002", "category": "学科发展与服务", "weight": 5},
]
},
{
"template_code": "TPL_ADMIN",
"template_name": "行政后勤科室服务支持导向考核方案",
"template_type": TemplateType.ADMIN,
"description": "以保障临床、服务一线、管理效能为核心,侧重过程管理与内部客户满意度,适用于院办、党办、医务科、护理部、财务科等",
"dimension_weights": {"financial": 10, "customer": 40, "internal_process": 40, "learning_growth": 10},
"assessment_cycle": "quarterly",
"indicators": [
{"code": "ADM001", "category": "基本素质与服务质量", "weight": 20},
{"code": "ADM002", "category": "遵纪守法与廉洁自律", "weight": 15},
{"code": "ADM003", "category": "科室内部管理", "weight": 10},
{"code": "ADM004", "category": "制度建设与执行力", "weight": 15},
{"code": "CUS001", "category": "内部服务满意度", "weight": 20},
{"code": "LRN003", "category": "学习与成长", "weight": 10},
]
},
{
"template_code": "TPL_LOGISTICS",
"template_name": "后勤保障科室考核方案",
"template_type": TemplateType.LOGISTICS,
"description": "以后勤保障及时性、安全生产和设备完好率为核心,适用于总务科、设备科、基建科等",
"dimension_weights": {"financial": 20, "customer": 30, "internal_process": 40, "learning_growth": 10},
"assessment_cycle": "monthly",
"indicators": [
{"code": "LOG001", "category": "后勤保障", "weight": 20},
{"code": "LOG002", "category": "安全生产", "weight": 15},
{"code": "LOG003", "category": "设备管理", "weight": 15},
{"code": "ADM001", "category": "服务质量", "weight": 20},
{"code": "ADM004", "category": "任务执行", "weight": 15},
{"code": "LRN003", "category": "学习与成长", "weight": 10},
]
},
]
async def init_data():
"""初始化数据"""
engine = create_async_engine(settings.DATABASE_URL, echo=True)
async_session = sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)
async with async_session() as session:
# 创建指标
print("正在创建指标...")
indicator_map = {} # code -> indicator object
for ind_data in INDICATORS_DATA:
result = await session.execute(
select(Indicator).where(Indicator.code == ind_data["code"])
)
existing = result.scalar_one_or_none()
if existing:
indicator_map[ind_data["code"]] = existing
continue
indicator = Indicator(
code=ind_data["code"],
name=ind_data["name"],
indicator_type=ind_data["indicator_type"],
bs_dimension=ind_data["bs_dimension"],
weight=ind_data["weight"],
max_score=ind_data["max_score"],
target_value=ind_data.get("target_value"),
target_unit=ind_data.get("target_unit"),
calculation_method=ind_data.get("calculation_method"),
assessment_method=ind_data.get("assessment_method"),
deduction_standard=ind_data.get("deduction_standard"),
data_source=ind_data.get("data_source"),
is_veto=ind_data.get("is_veto", False),
is_active=True
)
session.add(indicator)
indicator_map[ind_data["code"]] = indicator
await session.commit()
print(f"已创建 {len(indicator_map)} 个指标")
# 创建模板
print("正在创建模板...")
for tpl_data in TEMPLATES_DATA:
result = await session.execute(
select(IndicatorTemplate).where(IndicatorTemplate.template_code == tpl_data["template_code"])
)
existing = result.scalar_one_or_none()
if existing:
print(f"模板 {tpl_data['template_code']} 已存在,跳过")
continue
template = IndicatorTemplate(
template_code=tpl_data["template_code"],
template_name=tpl_data["template_name"],
template_type=tpl_data["template_type"],
description=tpl_data["description"],
dimension_weights=json.dumps(tpl_data["dimension_weights"]),
assessment_cycle=tpl_data["assessment_cycle"],
is_active=True
)
session.add(template)
await session.flush()
# 添加指标关联
for idx, ind_ref in enumerate(tpl_data["indicators"]):
indicator = indicator_map.get(ind_ref["code"])
if not indicator:
print(f"警告:指标 {ind_ref['code']} 不存在")
continue
ti = TemplateIndicator(
template_id=template.id,
indicator_id=indicator.id,
category=ind_ref.get("category"),
weight=ind_ref.get("weight", 1.0),
sort_order=idx
)
session.add(ti)
print(f"已创建模板: {tpl_data['template_name']}")
await session.commit()
print("初始化完成!")
if __name__ == "__main__":
asyncio.run(init_data())

View File

@@ -0,0 +1,15 @@
from app.services.department_service import DepartmentService
from app.services.staff_service import StaffService
from app.services.indicator_service import IndicatorService
from app.services.assessment_service import AssessmentService
from app.services.salary_service import SalaryService
from app.services.stats_service import StatsService
__all__ = [
"DepartmentService",
"StaffService",
"IndicatorService",
"AssessmentService",
"SalaryService",
"StatsService",
]

View File

@@ -0,0 +1,262 @@
"""
绩效考核服务层
"""
from typing import Optional, List
from datetime import datetime
from sqlalchemy import select, func, and_
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import selectinload
from app.models.models import Assessment, AssessmentDetail, Staff, Indicator, AssessmentStatus
from app.schemas.schemas import AssessmentCreate, AssessmentUpdate
class AssessmentService:
"""绩效考核服务"""
@staticmethod
async def get_list(
db: AsyncSession,
staff_id: Optional[int] = None,
department_id: Optional[int] = None,
period_year: Optional[int] = None,
period_month: Optional[int] = None,
status: Optional[str] = None,
page: int = 1,
page_size: int = 20
) -> tuple[List[Assessment], int]:
"""获取考核列表"""
query = select(Assessment).options(
selectinload(Assessment.staff).selectinload(Staff.department)
)
if staff_id:
query = query.where(Assessment.staff_id == staff_id)
if department_id:
query = query.join(Staff).where(Staff.department_id == department_id)
if period_year:
query = query.where(Assessment.period_year == period_year)
if period_month:
query = query.where(Assessment.period_month == period_month)
if status:
query = query.where(Assessment.status == status)
# 统计总数
count_query = select(func.count()).select_from(query.subquery())
total = await db.scalar(count_query)
# 分页
query = query.order_by(Assessment.period_year.desc(), Assessment.period_month.desc(), Assessment.id.desc())
query = query.offset((page - 1) * page_size).limit(page_size)
result = await db.execute(query)
assessments = result.scalars().all()
return assessments, total or 0
@staticmethod
async def get_by_id(db: AsyncSession, assessment_id: int) -> Optional[Assessment]:
"""根据ID获取考核详情"""
result = await db.execute(
select(Assessment)
.options(
selectinload(Assessment.staff).selectinload(Staff.department),
selectinload(Assessment.details).selectinload(AssessmentDetail.indicator)
)
.where(Assessment.id == assessment_id)
)
return result.scalar_one_or_none()
@staticmethod
async def create(db: AsyncSession, assessment_data: AssessmentCreate, assessor_id: Optional[int] = None) -> Assessment:
"""创建考核记录"""
# 计算总分
total_score = sum(d.score for d in assessment_data.details)
# 获取指标权重计算加权得分
weighted_score = 0.0
for detail in assessment_data.details:
indicator = await db.execute(
select(Indicator).where(Indicator.id == detail.indicator_id)
)
ind = indicator.scalar_one_or_none()
if ind:
weighted_score += detail.score * float(ind.weight)
assessment = Assessment(
staff_id=assessment_data.staff_id,
period_year=assessment_data.period_year,
period_month=assessment_data.period_month,
period_type=assessment_data.period_type,
total_score=total_score,
weighted_score=weighted_score,
assessor_id=assessor_id,
)
db.add(assessment)
await db.flush()
# 创建明细
for detail_data in assessment_data.details:
detail = AssessmentDetail(
assessment_id=assessment.id,
**detail_data.model_dump()
)
db.add(detail)
await db.flush()
await db.refresh(assessment)
return assessment
@staticmethod
async def update(db: AsyncSession, assessment_id: int, assessment_data: AssessmentUpdate) -> Optional[Assessment]:
"""更新考核记录"""
assessment = await AssessmentService.get_by_id(db, assessment_id)
if not assessment:
return None
if assessment.status not in ["draft", "rejected"]:
return None
if assessment_data.details is not None:
# 删除旧明细
await db.execute(
select(AssessmentDetail).where(AssessmentDetail.assessment_id == assessment_id)
)
for detail in assessment.details:
await db.delete(detail)
# 创建新明细
total_score = 0.0
weighted_score = 0.0
for detail_data in assessment_data.details:
detail = AssessmentDetail(
assessment_id=assessment_id,
**detail_data.model_dump()
)
db.add(detail)
total_score += detail_data.score
# 获取权重
indicator = await db.execute(
select(Indicator).where(Indicator.id == detail_data.indicator_id)
)
ind = indicator.scalar_one_or_none()
if ind:
weighted_score += detail_data.score * float(ind.weight)
assessment.total_score = total_score
assessment.weighted_score = weighted_score
if assessment_data.remark is not None:
assessment.remark = assessment_data.remark
await db.flush()
await db.refresh(assessment)
return assessment
@staticmethod
async def submit(db: AsyncSession, assessment_id: int) -> Optional[Assessment]:
"""提交考核"""
assessment = await AssessmentService.get_by_id(db, assessment_id)
if not assessment or assessment.status != AssessmentStatus.DRAFT:
return None
assessment.status = AssessmentStatus.SUBMITTED
assessment.submit_time = datetime.utcnow()
await db.flush()
await db.refresh(assessment)
return assessment
@staticmethod
async def review(db: AsyncSession, assessment_id: int, reviewer_id: int, approved: bool, remark: Optional[str] = None) -> Optional[Assessment]:
"""审核考核"""
assessment = await AssessmentService.get_by_id(db, assessment_id)
if not assessment or assessment.status != AssessmentStatus.SUBMITTED:
return None
assessment.reviewer_id = reviewer_id
assessment.review_time = datetime.utcnow()
if approved:
assessment.status = AssessmentStatus.REVIEWED
else:
assessment.status = AssessmentStatus.REJECTED
if remark:
assessment.remark = remark
await db.flush()
await db.refresh(assessment)
return assessment
@staticmethod
async def finalize(db: AsyncSession, assessment_id: int) -> Optional[Assessment]:
"""确认考核"""
assessment = await AssessmentService.get_by_id(db, assessment_id)
if not assessment or assessment.status != AssessmentStatus.REVIEWED:
return None
assessment.status = AssessmentStatus.FINALIZED
await db.flush()
await db.refresh(assessment)
return assessment
@staticmethod
async def batch_create_for_department(
db: AsyncSession,
department_id: int,
period_year: int,
period_month: int,
indicators: List[int]
) -> List[Assessment]:
"""为科室批量创建考核"""
# 获取科室所有在职员工
staff_result = await db.execute(
select(Staff).where(
Staff.department_id == department_id,
Staff.status == "active"
)
)
staff_list = staff_result.scalars().all()
assessments = []
for staff in staff_list:
# 检查是否已存在
existing = await db.execute(
select(Assessment).where(
Assessment.staff_id == staff.id,
Assessment.period_year == period_year,
Assessment.period_month == period_month
)
)
if existing.scalar_one_or_none():
continue
# 创建考核记录
assessment = Assessment(
staff_id=staff.id,
period_year=period_year,
period_month=period_month,
period_type="monthly",
total_score=0,
weighted_score=0,
)
db.add(assessment)
await db.flush()
# 创建明细
for indicator_id in indicators:
detail = AssessmentDetail(
assessment_id=assessment.id,
indicator_id=indicator_id,
score=0
)
db.add(detail)
assessments.append(assessment)
await db.flush()
return assessments

View File

@@ -0,0 +1,149 @@
"""
科室服务层
"""
from typing import Optional, List
from sqlalchemy import select, func
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import selectinload
from app.models.models import Department
from app.schemas.schemas import DepartmentCreate, DepartmentUpdate, DepartmentTree
class DepartmentService:
"""科室服务"""
@staticmethod
async def get_list(
db: AsyncSession,
dept_type: Optional[str] = None,
is_active: Optional[bool] = None,
page: int = 1,
page_size: int = 20
) -> tuple[List[Department], int]:
"""获取科室列表"""
query = select(Department)
if dept_type:
query = query.where(Department.dept_type == dept_type)
if is_active is not None:
query = query.where(Department.is_active == is_active)
# 统计总数
count_query = select(func.count()).select_from(query.subquery())
total = await db.scalar(count_query)
# 分页
query = query.order_by(Department.sort_order, Department.id)
query = query.offset((page - 1) * page_size).limit(page_size)
result = await db.execute(query)
departments = result.scalars().all()
return departments, total or 0
@staticmethod
async def get_by_id(db: AsyncSession, dept_id: int) -> Optional[Department]:
"""根据ID获取科室"""
result = await db.execute(
select(Department).where(Department.id == dept_id)
)
return result.scalar_one_or_none()
@staticmethod
async def get_by_code(db: AsyncSession, code: str) -> Optional[Department]:
"""根据编码获取科室"""
result = await db.execute(
select(Department).where(Department.code == code)
)
return result.scalar_one_or_none()
@staticmethod
async def create(db: AsyncSession, dept_data: DepartmentCreate) -> Department:
"""创建科室"""
# 计算层级
level = 1
if dept_data.parent_id:
parent = await DepartmentService.get_by_id(db, dept_data.parent_id)
if parent:
level = parent.level + 1
department = Department(
**dept_data.model_dump(exclude={'level'}),
level=level
)
db.add(department)
await db.flush()
await db.refresh(department)
return department
@staticmethod
async def update(db: AsyncSession, dept_id: int, dept_data: DepartmentUpdate) -> Optional[Department]:
"""更新科室"""
department = await DepartmentService.get_by_id(db, dept_id)
if not department:
return None
update_data = dept_data.model_dump(exclude_unset=True)
for key, value in update_data.items():
setattr(department, key, value)
await db.flush()
await db.refresh(department)
return department
@staticmethod
async def delete(db: AsyncSession, dept_id: int) -> bool:
"""删除科室"""
department = await DepartmentService.get_by_id(db, dept_id)
if not department:
return False
# 检查是否有子科室
result = await db.execute(
select(func.count()).where(Department.parent_id == dept_id)
)
if result.scalar() > 0:
return False
await db.delete(department)
return True
@staticmethod
async def get_tree(db: AsyncSession, dept_type: Optional[str] = None) -> List[DepartmentTree]:
"""获取科室树形结构"""
query = select(Department).order_by(Department.sort_order, Department.id)
if dept_type:
query = query.where(Department.dept_type == dept_type)
result = await db.execute(query)
departments = result.scalars().all()
# 构建树形结构 - 手动构建避免懒加载问题
dept_map = {}
for d in departments:
dept_map[d.id] = DepartmentTree(
id=d.id,
name=d.name,
code=d.code,
dept_type=d.dept_type,
parent_id=d.parent_id,
level=d.level,
sort_order=d.sort_order,
is_active=d.is_active,
description=d.description,
created_at=d.created_at,
updated_at=d.updated_at,
children=[]
)
roots = []
for dept in departments:
tree_node = dept_map[dept.id]
if dept.parent_id and dept.parent_id in dept_map:
dept_map[dept.parent_id].children.append(tree_node)
else:
roots.append(tree_node)
return roots

View File

@@ -0,0 +1,265 @@
"""
科室类型BSC维度权重配置服务
根据详细设计文档中的考核维度权重总览:
| 科室类型 | 财务维度 | 顾客维度 | 内部流程 | 学习成长 | 合计 |
|----------|----------|----------|----------|----------|------|
| 手术临床科室 | 60% | 15% | 20% | 5% | 100% |
| 非手术有病房科室 | 60% | 15% | 20% | 5% | 100% |
| 非手术无病房科室 | 60% | 15% | 20% | 5% | 100% |
| 医技科室 | 40% | 25% | 30% | 5% | 100% |
| 医疗辅助/行政科室 | 40% | 25% | 30% | 5% | 100% |
| 护理单元 | 20% | 15% | 50% | 15% | 100% |
| 药学部门 | 30% | 15% | 55% | - | 100% |
"""
from typing import Optional, List, Dict, Any
from sqlalchemy import select, func
from sqlalchemy.ext.asyncio import AsyncSession
from app.models.models import DeptType, DeptTypeDimensionWeight
# 默认权重配置(根据详细设计文档)
DEFAULT_WEIGHTS = {
DeptType.CLINICAL_SURGICAL: {
"financial": 0.60,
"customer": 0.15,
"internal_process": 0.20,
"learning_growth": 0.05,
"description": "手术临床科室:外科、骨科、泌尿外科、心胸外科、神经外科等"
},
DeptType.CLINICAL_NONSURGICAL_WARD: {
"financial": 0.60,
"customer": 0.15,
"internal_process": 0.20,
"learning_growth": 0.05,
"description": "非手术有病房科室:内科、神经内科、呼吸内科、消化内科等"
},
DeptType.CLINICAL_NONSURGICAL_NOWARD: {
"financial": 0.60,
"customer": 0.15,
"internal_process": 0.20,
"learning_growth": 0.05,
"description": "非手术无病房科室:门诊科室、急诊科等"
},
DeptType.MEDICAL_TECH: {
"financial": 0.40,
"customer": 0.25,
"internal_process": 0.30,
"learning_growth": 0.05,
"description": "医技科室:放射科、检验科、超声科、病理科、功能检查科等"
},
DeptType.MEDICAL_AUXILIARY: {
"financial": 0.40,
"customer": 0.25,
"internal_process": 0.30,
"learning_growth": 0.05,
"description": "医疗辅助/行政科室:设备科、信息科、总务科、财务科、人事科、医务科等"
},
DeptType.NURSING: {
"financial": 0.20,
"customer": 0.15,
"internal_process": 0.50,
"learning_growth": 0.15,
"description": "护理单元:各病区护理单元"
},
DeptType.ADMIN: {
"financial": 0.40,
"customer": 0.25,
"internal_process": 0.30,
"learning_growth": 0.05,
"description": "行政科室"
},
DeptType.FINANCE: {
"financial": 0.40,
"customer": 0.25,
"internal_process": 0.30,
"learning_growth": 0.05,
"description": "财务科室"
},
DeptType.LOGISTICS: {
"financial": 0.40,
"customer": 0.25,
"internal_process": 0.30,
"learning_growth": 0.05,
"description": "后勤保障科室"
},
}
class DimensionWeightService:
"""科室类型BSC维度权重配置服务"""
@staticmethod
async def get_by_dept_type(db: AsyncSession, dept_type: DeptType) -> Optional[DeptTypeDimensionWeight]:
"""根据科室类型获取权重配置"""
result = await db.execute(
select(DeptTypeDimensionWeight)
.where(DeptTypeDimensionWeight.dept_type == dept_type)
.where(DeptTypeDimensionWeight.is_active == True)
)
return result.scalar_one_or_none()
@staticmethod
async def get_all(db: AsyncSession, active_only: bool = True) -> List[DeptTypeDimensionWeight]:
"""获取所有权重配置"""
query = select(DeptTypeDimensionWeight)
if active_only:
query = query.where(DeptTypeDimensionWeight.is_active == True)
query = query.order_by(DeptTypeDimensionWeight.dept_type)
result = await db.execute(query)
return result.scalars().all()
@staticmethod
async def create_or_update(
db: AsyncSession,
dept_type: DeptType,
financial_weight: float,
customer_weight: float,
internal_process_weight: float,
learning_growth_weight: float,
description: Optional[str] = None
) -> DeptTypeDimensionWeight:
"""创建或更新权重配置"""
# 验证权重总和为1
total = financial_weight + customer_weight + internal_process_weight + learning_growth_weight
if abs(total - 1.0) > 0.01:
raise ValueError(f"权重总和必须为100%,当前总和为{total * 100}%")
# 查找现有配置
existing = await DimensionWeightService.get_by_dept_type(db, dept_type)
if existing:
# 更新现有配置
existing.financial_weight = financial_weight
existing.customer_weight = customer_weight
existing.internal_process_weight = internal_process_weight
existing.learning_growth_weight = learning_growth_weight
if description:
existing.description = description
await db.flush()
await db.refresh(existing)
return existing
else:
# 创建新配置
config = DeptTypeDimensionWeight(
dept_type=dept_type,
financial_weight=financial_weight,
customer_weight=customer_weight,
internal_process_weight=internal_process_weight,
learning_growth_weight=learning_growth_weight,
description=description
)
db.add(config)
await db.flush()
await db.refresh(config)
return config
@staticmethod
async def init_default_weights(db: AsyncSession) -> List[DeptTypeDimensionWeight]:
"""初始化默认权重配置(根据详细设计文档)"""
configs = []
for dept_type, weights in DEFAULT_WEIGHTS.items():
config = await DimensionWeightService.create_or_update(
db,
dept_type=dept_type,
financial_weight=weights["financial"],
customer_weight=weights["customer"],
internal_process_weight=weights["internal_process"],
learning_growth_weight=weights["learning_growth"],
description=weights.get("description")
)
configs.append(config)
return configs
@staticmethod
def get_dimension_weights(dept_type: DeptType) -> Dict[str, float]:
"""
获取科室类型的维度权重(用于计算)
优先使用数据库配置,如果没有则使用默认配置
"""
default = DEFAULT_WEIGHTS.get(dept_type, {
"financial": 0.40,
"customer": 0.25,
"internal_process": 0.30,
"learning_growth": 0.05,
})
return {
"financial": default["financial"],
"customer": default["customer"],
"internal_process": default["internal_process"],
"learning_growth": default["learning_growth"],
}
@staticmethod
async def calculate_dimension_weighted_score(
db: AsyncSession,
dept_type: DeptType,
financial_score: float,
customer_score: float,
internal_process_score: float,
learning_growth_score: float
) -> Dict[str, Any]:
"""
根据科室类型计算维度加权得分
Args:
db: 数据库会话
dept_type: 科室类型
financial_score: 财务维度得分
customer_score: 客户维度得分
internal_process_score: 内部流程维度得分
learning_growth_score: 学习成长维度得分
Returns:
包含各维度加权得分和总分的字典
"""
# 获取权重配置
config = await DimensionWeightService.get_by_dept_type(db, dept_type)
if config:
weights = {
"financial": float(config.financial_weight),
"customer": float(config.customer_weight),
"internal_process": float(config.internal_process_weight),
"learning_growth": float(config.learning_growth_weight),
}
else:
weights = DimensionWeightService.get_dimension_weights(dept_type)
# 计算各维度加权得分
weighted_scores = {
"financial": financial_score * weights["financial"],
"customer": customer_score * weights["customer"],
"internal_process": internal_process_score * weights["internal_process"],
"learning_growth": learning_growth_score * weights["learning_growth"],
}
# 计算总分
total_score = sum(weighted_scores.values())
return {
"weights": weights,
"weighted_scores": weighted_scores,
"total_score": round(total_score, 2),
"raw_scores": {
"financial": financial_score,
"customer": customer_score,
"internal_process": internal_process_score,
"learning_growth": learning_growth_score,
}
}
@staticmethod
async def delete(db: AsyncSession, config_id: int) -> bool:
"""删除权重配置(软删除)"""
result = await db.execute(
select(DeptTypeDimensionWeight).where(DeptTypeDimensionWeight.id == config_id)
)
config = result.scalar_one_or_none()
if not config:
return False
config.is_active = False
await db.flush()
return True

View File

@@ -0,0 +1,367 @@
"""
财务核算服务层
"""
from typing import Optional, List, Dict, Any
from sqlalchemy import select, func, and_
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import selectinload
from decimal import Decimal
from datetime import datetime
from app.models.finance import (
DepartmentFinance, RevenueCategory, ExpenseCategory, FinanceType
)
from app.models.models import Department
class FinanceService:
"""财务核算服务"""
# 收入类别标签映射
REVENUE_LABELS = {
RevenueCategory.EXAMINATION: "检查费",
RevenueCategory.LAB_TEST: "检验费",
RevenueCategory.RADIOLOGY: "放射费",
RevenueCategory.BED: "床位费",
RevenueCategory.NURSING: "护理费",
RevenueCategory.TREATMENT: "治疗费",
RevenueCategory.SURGERY: "手术费",
RevenueCategory.INJECTION: "注射费",
RevenueCategory.OXYGEN: "吸氧费",
RevenueCategory.OTHER: "其他",
}
# 支出类别标签映射
EXPENSE_LABELS = {
ExpenseCategory.MATERIAL: "材料费",
ExpenseCategory.PERSONNEL: "人员支出",
ExpenseCategory.MAINTENANCE: "维修费",
ExpenseCategory.UTILITY: "水电费",
ExpenseCategory.OTHER: "其他",
}
@staticmethod
async def get_department_revenue(
db: AsyncSession,
department_id: Optional[int] = None,
period_year: Optional[int] = None,
period_month: Optional[int] = None
) -> List[Dict[str, Any]]:
"""获取科室收入"""
query = select(DepartmentFinance).options(
selectinload(DepartmentFinance.department)
).where(DepartmentFinance.finance_type == FinanceType.REVENUE)
if department_id:
query = query.where(DepartmentFinance.department_id == department_id)
if period_year:
query = query.where(DepartmentFinance.period_year == period_year)
if period_month:
query = query.where(DepartmentFinance.period_month == period_month)
query = query.order_by(
DepartmentFinance.period_year.desc(),
DepartmentFinance.period_month.desc(),
DepartmentFinance.id.desc()
)
result = await db.execute(query)
records = result.scalars().all()
# 转换为字典列表并添加额外信息
data = []
for record in records:
record_dict = {
"id": record.id,
"department_id": record.department_id,
"department_name": record.department.name if record.department else None,
"period_year": record.period_year,
"period_month": record.period_month,
"category": record.category,
"category_label": FinanceService.REVENUE_LABELS.get(
RevenueCategory(record.category), record.category
),
"amount": float(record.amount),
"source": record.source,
"remark": record.remark,
"created_at": record.created_at,
}
data.append(record_dict)
return data
@staticmethod
async def get_department_expense(
db: AsyncSession,
department_id: Optional[int] = None,
period_year: Optional[int] = None,
period_month: Optional[int] = None
) -> List[Dict[str, Any]]:
"""获取科室支出"""
query = select(DepartmentFinance).options(
selectinload(DepartmentFinance.department)
).where(DepartmentFinance.finance_type == FinanceType.EXPENSE)
if department_id:
query = query.where(DepartmentFinance.department_id == department_id)
if period_year:
query = query.where(DepartmentFinance.period_year == period_year)
if period_month:
query = query.where(DepartmentFinance.period_month == period_month)
query = query.order_by(
DepartmentFinance.period_year.desc(),
DepartmentFinance.period_month.desc(),
DepartmentFinance.id.desc()
)
result = await db.execute(query)
records = result.scalars().all()
# 转换为字典列表并添加额外信息
data = []
for record in records:
record_dict = {
"id": record.id,
"department_id": record.department_id,
"department_name": record.department.name if record.department else None,
"period_year": record.period_year,
"period_month": record.period_month,
"category": record.category,
"category_label": FinanceService.EXPENSE_LABELS.get(
ExpenseCategory(record.category), record.category
),
"amount": float(record.amount),
"source": record.source,
"remark": record.remark,
"created_at": record.created_at,
}
data.append(record_dict)
return data
@staticmethod
async def get_department_balance(
db: AsyncSession,
department_id: Optional[int] = None,
period_year: Optional[int] = None,
period_month: Optional[int] = None
) -> Dict[str, Any]:
"""获取收支结余"""
# 构建基础查询条件
conditions = []
if department_id:
conditions.append(DepartmentFinance.department_id == department_id)
if period_year:
conditions.append(DepartmentFinance.period_year == period_year)
if period_month:
conditions.append(DepartmentFinance.period_month == period_month)
# 查询总收入
revenue_query = select(func.coalesce(func.sum(DepartmentFinance.amount), 0)).where(
and_(
DepartmentFinance.finance_type == FinanceType.REVENUE,
*conditions
)
)
total_revenue = await db.scalar(revenue_query) or 0
# 查询总支出
expense_query = select(func.coalesce(func.sum(DepartmentFinance.amount), 0)).where(
and_(
DepartmentFinance.finance_type == FinanceType.EXPENSE,
*conditions
)
)
total_expense = await db.scalar(expense_query) or 0
# 计算结余
balance = float(total_revenue) - float(total_expense)
# 获取科室名称
department_name = None
if department_id:
dept_result = await db.execute(
select(Department).where(Department.id == department_id)
)
dept = dept_result.scalar_one_or_none()
department_name = dept.name if dept else None
return {
"department_id": department_id,
"department_name": department_name,
"period_year": period_year,
"period_month": period_month,
"total_revenue": float(total_revenue),
"total_expense": float(total_expense),
"balance": balance,
}
@staticmethod
async def get_revenue_by_category(
db: AsyncSession,
department_id: Optional[int] = None,
period_year: Optional[int] = None,
period_month: Optional[int] = None
) -> List[Dict[str, Any]]:
"""按类别统计收入"""
conditions = [DepartmentFinance.finance_type == FinanceType.REVENUE]
if department_id:
conditions.append(DepartmentFinance.department_id == department_id)
if period_year:
conditions.append(DepartmentFinance.period_year == period_year)
if period_month:
conditions.append(DepartmentFinance.period_month == period_month)
query = select(
DepartmentFinance.category,
func.sum(DepartmentFinance.amount).label("total_amount")
).where(and_(*conditions)).group_by(DepartmentFinance.category)
result = await db.execute(query)
rows = result.all()
data = []
for row in rows:
category = row.category
data.append({
"category": category,
"category_label": FinanceService.REVENUE_LABELS.get(
RevenueCategory(category), category
),
"amount": float(row.total_amount),
})
return data
@staticmethod
async def get_expense_by_category(
db: AsyncSession,
department_id: Optional[int] = None,
period_year: Optional[int] = None,
period_month: Optional[int] = None
) -> List[Dict[str, Any]]:
"""按类别统计支出"""
conditions = [DepartmentFinance.finance_type == FinanceType.EXPENSE]
if department_id:
conditions.append(DepartmentFinance.department_id == department_id)
if period_year:
conditions.append(DepartmentFinance.period_year == period_year)
if period_month:
conditions.append(DepartmentFinance.period_month == period_month)
query = select(
DepartmentFinance.category,
func.sum(DepartmentFinance.amount).label("total_amount")
).where(and_(*conditions)).group_by(DepartmentFinance.category)
result = await db.execute(query)
rows = result.all()
data = []
for row in rows:
category = row.category
data.append({
"category": category,
"category_label": FinanceService.EXPENSE_LABELS.get(
ExpenseCategory(category), category
),
"amount": float(row.total_amount),
})
return data
@staticmethod
async def create_finance_record(
db: AsyncSession,
department_id: int,
finance_type: FinanceType,
category: str,
amount: float,
period_year: int,
period_month: int,
source: Optional[str] = None,
remark: Optional[str] = None
) -> DepartmentFinance:
"""创建财务记录"""
record = DepartmentFinance(
department_id=department_id,
finance_type=finance_type,
category=category,
amount=amount,
period_year=period_year,
period_month=period_month,
source=source,
remark=remark
)
db.add(record)
await db.flush()
await db.refresh(record)
return record
@staticmethod
async def get_by_id(db: AsyncSession, record_id: int) -> Optional[DepartmentFinance]:
"""根据ID获取财务记录"""
result = await db.execute(
select(DepartmentFinance)
.options(selectinload(DepartmentFinance.department))
.where(DepartmentFinance.id == record_id)
)
return result.scalar_one_or_none()
@staticmethod
async def update_finance_record(
db: AsyncSession,
record_id: int,
**kwargs
) -> Optional[DepartmentFinance]:
"""更新财务记录"""
record = await FinanceService.get_by_id(db, record_id)
if not record:
return None
for key, value in kwargs.items():
if value is not None and hasattr(record, key):
setattr(record, key, value)
await db.flush()
await db.refresh(record)
return record
@staticmethod
async def delete_finance_record(db: AsyncSession, record_id: int) -> bool:
"""删除财务记录"""
record = await FinanceService.get_by_id(db, record_id)
if not record:
return False
await db.delete(record)
return True
@staticmethod
async def get_department_summary(
db: AsyncSession,
period_year: int,
period_month: int
) -> List[Dict[str, Any]]:
"""获取所有科室的财务汇总"""
# 获取所有科室
dept_result = await db.execute(
select(Department).where(Department.is_active == True).order_by(Department.name)
)
departments = dept_result.scalars().all()
summaries = []
for dept in departments:
balance_data = await FinanceService.get_department_balance(
db, dept.id, period_year, period_month
)
summaries.append({
"department_id": dept.id,
"department_name": dept.name,
"total_revenue": balance_data["total_revenue"],
"total_expense": balance_data["total_expense"],
"balance": balance_data["balance"],
})
return summaries

View File

@@ -0,0 +1,196 @@
"""
考核指标服务层
"""
import json
from typing import Optional, List, Dict, Any
from sqlalchemy import select, func
from sqlalchemy.ext.asyncio import AsyncSession
from app.models.models import Indicator, IndicatorType, BSCDimension
from app.schemas.schemas import IndicatorCreate, IndicatorUpdate
class IndicatorService:
"""考核指标服务"""
@staticmethod
async def get_list(
db: AsyncSession,
indicator_type: Optional[str] = None,
bs_dimension: Optional[str] = None,
is_active: Optional[bool] = None,
page: int = 1,
page_size: int = 20
) -> tuple[List[Indicator], int]:
"""获取指标列表"""
query = select(Indicator)
if indicator_type:
query = query.where(Indicator.indicator_type == indicator_type)
if bs_dimension:
query = query.where(Indicator.bs_dimension == bs_dimension)
if is_active is not None:
query = query.where(Indicator.is_active == is_active)
# 统计总数
count_query = select(func.count()).select_from(query.subquery())
total = await db.scalar(count_query)
# 分页
query = query.order_by(Indicator.indicator_type, Indicator.id)
query = query.offset((page - 1) * page_size).limit(page_size)
result = await db.execute(query)
indicators = result.scalars().all()
return indicators, total or 0
@staticmethod
async def get_by_id(db: AsyncSession, indicator_id: int) -> Optional[Indicator]:
"""根据 ID 获取指标"""
result = await db.execute(
select(Indicator).where(Indicator.id == indicator_id)
)
return result.scalar_one_or_none()
@staticmethod
async def get_active_indicators(db: AsyncSession) -> List[Indicator]:
"""获取所有启用的指标"""
result = await db.execute(
select(Indicator)
.where(Indicator.is_active == True)
.order_by(Indicator.indicator_type, Indicator.id)
)
return result.scalars().all()
@staticmethod
async def create(db: AsyncSession, indicator_data: IndicatorCreate) -> Indicator:
"""创建指标"""
indicator = Indicator(**indicator_data.model_dump())
db.add(indicator)
await db.commit()
await db.refresh(indicator)
return indicator
@staticmethod
async def update(
db: AsyncSession,
indicator_id: int,
indicator_data: IndicatorUpdate
) -> Optional[Indicator]:
"""更新指标"""
indicator = await IndicatorService.get_by_id(db, indicator_id)
if not indicator:
return None
update_data = indicator_data.model_dump(exclude_unset=True)
for key, value in update_data.items():
setattr(indicator, key, value)
await db.commit()
await db.refresh(indicator)
return indicator
@staticmethod
async def delete(db: AsyncSession, indicator_id: int) -> bool:
"""删除指标"""
indicator = await IndicatorService.get_by_id(db, indicator_id)
if not indicator:
return False
await db.delete(indicator)
await db.commit()
return True
@staticmethod
async def import_template(
db: AsyncSession,
template_data: Dict[str, Any],
overwrite: bool = False
) -> int:
"""导入指标模板"""
dept_type = template_data.get('dept_type')
indicators_data = template_data.get('indicators', [])
created_count = 0
for ind_data in indicators_data:
# 检查是否已存在
existing = await db.execute(
select(Indicator).where(Indicator.code == ind_data['code'])
)
if existing.scalar_one_or_none():
if overwrite:
# 更新现有指标
indicator = existing.scalar_one_or_none()
if indicator:
for key, value in ind_data.items():
if hasattr(indicator, key):
setattr(indicator, key, value)
continue
# 创建新指标
indicator = Indicator(
name=ind_data.get('name'),
code=ind_data.get('code'),
indicator_type=ind_data.get('indicator_type'),
bs_dimension=ind_data.get('bs_dimension'),
weight=ind_data.get('weight', 1.0),
max_score=ind_data.get('max_score', 100.0),
target_value=ind_data.get('target_value'),
target_unit=ind_data.get('target_unit'),
calculation_method=ind_data.get('calculation_method'),
assessment_method=ind_data.get('assessment_method'),
deduction_standard=ind_data.get('deduction_standard'),
data_source=ind_data.get('data_source'),
applicable_dept_types=json.dumps([dept_type]) if dept_type else None,
is_veto=ind_data.get('is_veto', False),
is_active=ind_data.get('is_active', True)
)
db.add(indicator)
created_count += 1
await db.commit()
return created_count
@staticmethod
async def get_templates() -> List[Dict[str, Any]]:
"""获取指标模板列表"""
return [
{
"name": "手术临床科室考核指标",
"dept_type": "clinical_surgical",
"description": "适用于外科系统各手术科室",
"indicator_count": 12
},
{
"name": "非手术有病房科室考核指标",
"dept_type": "clinical_nonsurgical_ward",
"description": "适用于内科系统等有病房科室",
"indicator_count": 10
},
{
"name": "非手术无病房科室考核指标",
"dept_type": "clinical_nonsurgical_noward",
"description": "适用于门诊科室",
"indicator_count": 8
},
{
"name": "医技科室考核指标",
"dept_type": "medical_tech",
"description": "适用于检验科、放射科等医技科室",
"indicator_count": 8
},
{
"name": "行政科室考核指标",
"dept_type": "admin",
"description": "适用于党办、财务科、医保办等行政科室",
"indicator_count": 6
},
{
"name": "后勤保障科室考核指标",
"dept_type": "logistics",
"description": "适用于总务科、采购科、基建科",
"indicator_count": 6
}
]

View File

@@ -0,0 +1,136 @@
"""
菜单服务层
"""
from typing import Optional, List, Dict, Any
from sqlalchemy import select, and_
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import selectinload
from app.models.models import Menu, MenuType
class MenuService:
"""菜单服务"""
@staticmethod
async def get_tree(db: AsyncSession, visible_only: bool = True) -> List[Dict[str, Any]]:
"""获取菜单树形结构"""
query = select(Menu).options(selectinload(Menu.children))
if visible_only:
query = query.where(Menu.is_visible == True, Menu.is_active == True)
query = query.where(Menu.parent_id.is_(None))
query = query.order_by(Menu.sort_order, Menu.id)
result = await db.execute(query)
menus = result.scalars().all()
return [MenuService._menu_to_dict(menu) for menu in menus]
@staticmethod
async def get_list(
db: AsyncSession,
menu_type: Optional[str] = None,
is_visible: Optional[bool] = None
) -> List[Menu]:
"""获取菜单列表"""
query = select(Menu).options(selectinload(Menu.children))
conditions = []
if menu_type:
conditions.append(Menu.menu_type == menu_type)
if is_visible is not None:
conditions.append(Menu.is_visible == is_visible)
if conditions:
query = query.where(and_(*conditions))
query = query.order_by(Menu.sort_order, Menu.id)
result = await db.execute(query)
return result.scalars().all()
@staticmethod
async def get_by_id(db: AsyncSession, menu_id: int) -> Optional[Menu]:
"""根据 ID 获取菜单"""
result = await db.execute(
select(Menu).where(Menu.id == menu_id)
)
return result.scalar_one_or_none()
@staticmethod
async def create(db: AsyncSession, menu_data: dict) -> Menu:
"""创建菜单"""
menu = Menu(**menu_data)
db.add(menu)
await db.commit()
await db.refresh(menu)
return menu
@staticmethod
async def update(db: AsyncSession, menu_id: int, menu_data: dict) -> Optional[Menu]:
"""更新菜单"""
menu = await MenuService.get_by_id(db, menu_id)
if not menu:
return None
for key, value in menu_data.items():
if value is not None and hasattr(menu, key):
setattr(menu, key, value)
await db.commit()
await db.refresh(menu)
return menu
@staticmethod
async def delete(db: AsyncSession, menu_id: int) -> bool:
"""删除菜单"""
menu = await MenuService.get_by_id(db, menu_id)
if not menu:
return False
# 检查是否有子菜单
if menu.children:
return False
await db.delete(menu)
await db.commit()
return True
@staticmethod
def _menu_to_dict(menu: Menu) -> Dict[str, Any]:
"""将菜单对象转换为字典"""
return {
"id": menu.id,
"menu_name": menu.menu_name,
"menu_icon": menu.menu_icon,
"path": menu.path,
"children": [MenuService._menu_to_dict(child) for child in menu.children]
}
@staticmethod
async def init_default_menus(db: AsyncSession) -> None:
"""初始化默认菜单"""
# 检查是否已有菜单
result = await db.execute(select(Menu))
if result.scalar_one_or_none():
return
# 默认菜单数据
default_menus = [
{"menu_name": "工作台", "menu_icon": "HomeFilled", "path": "/dashboard", "component": "Dashboard", "sort_order": 1},
{"menu_name": "科室管理", "menu_icon": "OfficeBuilding", "path": "/departments", "component": "Departments", "sort_order": 2},
{"menu_name": "员工管理", "menu_icon": "User", "path": "/staff", "component": "Staff", "sort_order": 3},
{"menu_name": "考核指标", "menu_icon": "DataAnalysis", "path": "/indicators", "component": "Indicators", "sort_order": 4},
{"menu_name": "考核管理", "menu_icon": "Document", "path": "/assessments", "component": "Assessments", "sort_order": 5},
{"menu_name": "绩效计划", "menu_icon": "Setting", "path": "/plans", "component": "Plans", "sort_order": 6},
{"menu_name": "工资核算", "menu_icon": "Money", "path": "/salary", "component": "Salary", "sort_order": 7},
{"menu_name": "经济核算", "menu_icon": "Coin", "path": "/finance", "component": "Finance", "sort_order": 8},
{"menu_name": "统计报表", "menu_icon": "TrendCharts", "path": "/reports", "component": "Reports", "sort_order": 9},
]
for menu_data in default_menus:
menu = Menu(**menu_data)
db.add(menu)
await db.commit()

View File

@@ -0,0 +1,341 @@
"""
绩效计划服务层
"""
import json
from typing import Optional, List, Dict, Any
from datetime import datetime
from sqlalchemy import select, func, and_
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import selectinload
from app.models.models import PerformancePlan, PlanKpiRelation, PlanStatus, PlanLevel, Indicator
from app.schemas.schemas import PerformancePlanCreate, PerformancePlanUpdate, PlanKpiRelationCreate
class PerformancePlanService:
"""绩效计划服务"""
@staticmethod
async def get_list(
db: AsyncSession,
plan_level: Optional[str] = None,
plan_year: Optional[int] = None,
department_id: Optional[int] = None,
status: Optional[str] = None,
page: int = 1,
page_size: int = 20
) -> tuple[List[PerformancePlan], int]:
"""获取绩效计划列表"""
query = select(PerformancePlan).options(
selectinload(PerformancePlan.department),
selectinload(PerformancePlan.staff)
)
# 构建查询条件
conditions = []
if plan_level:
conditions.append(PerformancePlan.plan_level == plan_level)
if plan_year:
conditions.append(PerformancePlan.plan_year == plan_year)
if department_id:
conditions.append(PerformancePlan.department_id == department_id)
if status:
conditions.append(PerformancePlan.status == status)
if conditions:
query = query.where(and_(*conditions))
# 统计总数
count_query = select(func.count()).select_from(query.subquery())
total = await db.scalar(count_query)
# 分页
query = query.order_by(PerformancePlan.created_at.desc())
query = query.offset((page - 1) * page_size).limit(page_size)
result = await db.execute(query)
plans = result.scalars().all()
return plans, total or 0
@staticmethod
async def get_by_id(db: AsyncSession, plan_id: int) -> Optional[PerformancePlan]:
"""根据 ID 获取绩效计划详情"""
result = await db.execute(
select(PerformancePlan)
.options(
selectinload(PerformancePlan.department),
selectinload(PerformancePlan.staff),
selectinload(PerformancePlan.kpi_relations).selectinload(PlanKpiRelation.indicator)
)
.where(PerformancePlan.id == plan_id)
)
return result.scalar_one_or_none()
@staticmethod
async def create(
db: AsyncSession,
plan_data: PerformancePlanCreate,
submitter_id: Optional[int] = None
) -> PerformancePlan:
"""创建绩效计划"""
# 创建计划
plan_dict = plan_data.model_dump(exclude={'kpi_relations'})
plan = PerformancePlan(**plan_dict)
plan.submitter_id = submitter_id
db.add(plan)
await db.flush()
await db.refresh(plan)
# 创建指标关联
if plan_data.kpi_relations:
for kpi_data in plan_data.kpi_relations:
kpi_relation = PlanKpiRelation(
plan_id=plan.id,
**kpi_data.model_dump()
)
db.add(kpi_relation)
await db.commit()
await db.refresh(plan)
return plan
@staticmethod
async def update(
db: AsyncSession,
plan_id: int,
plan_data: PerformancePlanUpdate
) -> Optional[PerformancePlan]:
"""更新绩效计划"""
plan = await PerformancePlanService.get_by_id(db, plan_id)
if not plan:
return None
update_data = plan_data.model_dump(exclude_unset=True)
for key, value in update_data.items():
if hasattr(plan, key):
setattr(plan, key, value)
# 处理审批相关
if plan_data.status == PlanStatus.APPROVED:
plan.approve_time = datetime.utcnow()
elif plan_data.status == PlanStatus.REJECTED:
plan.approve_time = datetime.utcnow()
await db.commit()
await db.refresh(plan)
return plan
@staticmethod
async def submit(db: AsyncSession, plan_id: int) -> Optional[PerformancePlan]:
"""提交绩效计划"""
plan = await PerformancePlanService.get_by_id(db, plan_id)
if not plan or plan.status != PlanStatus.DRAFT:
return None
plan.status = PlanStatus.PENDING
plan.submit_time = datetime.utcnow()
await db.commit()
await db.refresh(plan)
return plan
@staticmethod
async def approve(
db: AsyncSession,
plan_id: int,
approver_id: int,
approved: bool,
remark: Optional[str] = None
) -> Optional[PerformancePlan]:
"""审批绩效计划"""
plan = await PerformancePlanService.get_by_id(db, plan_id)
if not plan or plan.status != PlanStatus.PENDING:
return None
plan.approver_id = approver_id
plan.approve_time = datetime.utcnow()
plan.approve_remark = remark
if approved:
plan.status = PlanStatus.APPROVED
else:
plan.status = PlanStatus.REJECTED
await db.commit()
await db.refresh(plan)
return plan
@staticmethod
async def activate(db: AsyncSession, plan_id: int) -> Optional[PerformancePlan]:
"""激活绩效计划"""
plan = await PerformancePlanService.get_by_id(db, plan_id)
if not plan or plan.status != PlanStatus.APPROVED:
return None
plan.status = PlanStatus.ACTIVE
plan.is_active = True
await db.commit()
await db.refresh(plan)
return plan
@staticmethod
async def delete(db: AsyncSession, plan_id: int) -> bool:
"""删除绩效计划"""
plan = await PerformancePlanService.get_by_id(db, plan_id)
if not plan:
return False
await db.delete(plan)
await db.commit()
return True
@staticmethod
async def add_kpi_relation(
db: AsyncSession,
plan_id: int,
kpi_data: PlanKpiRelationCreate
) -> Optional[PlanKpiRelation]:
"""添加计划指标关联"""
plan = await PerformancePlanService.get_by_id(db, plan_id)
if not plan:
return None
kpi_relation = PlanKpiRelation(
plan_id=plan_id,
**kpi_data.model_dump()
)
db.add(kpi_relation)
await db.commit()
await db.refresh(kpi_relation)
return kpi_relation
@staticmethod
async def update_kpi_relation(
db: AsyncSession,
relation_id: int,
kpi_data: dict
) -> Optional[PlanKpiRelation]:
"""更新计划指标关联"""
result = await db.execute(
select(PlanKpiRelation).where(PlanKpiRelation.id == relation_id)
)
kpi_relation = result.scalar_one_or_none()
if not kpi_relation:
return None
for key, value in kpi_data.items():
if hasattr(kpi_relation, key) and value is not None:
setattr(kpi_relation, key, value)
await db.commit()
await db.refresh(kpi_relation)
return kpi_relation
@staticmethod
async def delete_kpi_relation(db: AsyncSession, relation_id: int) -> bool:
"""删除计划指标关联"""
result = await db.execute(
select(PlanKpiRelation).where(PlanKpiRelation.id == relation_id)
)
kpi_relation = result.scalar_one_or_none()
if not kpi_relation:
return False
await db.delete(kpi_relation)
await db.commit()
return True
@staticmethod
async def get_stats(
db: AsyncSession,
plan_year: Optional[int] = None
) -> Dict[str, int]:
"""获取绩效计划统计"""
conditions = []
if plan_year:
conditions.append(PerformancePlan.plan_year == plan_year)
query = select(
PerformancePlan.status,
func.count().label('count')
)
if conditions:
query = query.where(and_(*conditions))
query = query.group_by(PerformancePlan.status)
result = await db.execute(query)
rows = result.fetchall()
stats = {
'total_plans': 0,
'draft_count': 0,
'pending_count': 0,
'approved_count': 0,
'active_count': 0,
'completed_count': 0
}
for row in rows:
status = row.status
count = row.count
stats['total_plans'] += count
if status == PlanStatus.DRAFT:
stats['draft_count'] = count
elif status == PlanStatus.PENDING:
stats['pending_count'] = count
elif status == PlanStatus.APPROVED:
stats['approved_count'] = count
elif status == PlanStatus.ACTIVE:
stats['active_count'] = count
elif status == PlanStatus.COMPLETED:
stats['completed_count'] = count
return stats
@staticmethod
async def get_tree(
db: AsyncSession,
plan_year: Optional[int] = None
) -> List[Dict[str, Any]]:
"""获取绩效计划树形结构"""
conditions = []
if plan_year:
conditions.append(PerformancePlan.plan_year == plan_year)
query = select(PerformancePlan).options(
selectinload(PerformancePlan.department),
selectinload(PerformancePlan.staff)
)
if conditions:
query = query.where(and_(*conditions))
query = query.order_by(PerformancePlan.plan_level, PerformancePlan.id)
result = await db.execute(query)
plans = result.scalars().all()
# 构建树形结构
plan_dict = {}
root_plans = []
for plan in plans:
plan_dict[plan.id] = {
'id': plan.id,
'plan_name': plan.plan_name,
'plan_code': plan.plan_code,
'plan_level': plan.plan_level,
'status': plan.status,
'department_name': plan.department.name if plan.department else None,
'staff_name': plan.staff.name if plan.staff else None,
'children': []
}
if plan.parent_plan_id:
if plan.parent_plan_id in plan_dict:
plan_dict[plan.parent_plan_id]['children'].append(plan_dict[plan.id])
else:
root_plans.append(plan_dict[plan.id])
return root_plans

View File

@@ -0,0 +1,259 @@
"""
工资核算服务层
"""
from typing import Optional, List
from sqlalchemy import select, func
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import selectinload
from decimal import Decimal
from app.models.models import SalaryRecord, Staff, Assessment
from app.schemas.schemas import SalaryRecordCreate, SalaryRecordUpdate
class SalaryService:
"""工资核算服务"""
# 绩效奖金基数(可根据医院实际情况调整)
PERFORMANCE_BASE = 3000.0
@staticmethod
async def get_list(
db: AsyncSession,
staff_id: Optional[int] = None,
department_id: Optional[int] = None,
period_year: Optional[int] = None,
period_month: Optional[int] = None,
status: Optional[str] = None,
page: int = 1,
page_size: int = 20
) -> tuple[List[SalaryRecord], int]:
"""获取工资记录列表"""
query = select(SalaryRecord).options(
selectinload(SalaryRecord.staff).selectinload(Staff.department)
)
if staff_id:
query = query.where(SalaryRecord.staff_id == staff_id)
if department_id:
query = query.join(Staff).where(Staff.department_id == department_id)
if period_year:
query = query.where(SalaryRecord.period_year == period_year)
if period_month:
query = query.where(SalaryRecord.period_month == period_month)
if status:
query = query.where(SalaryRecord.status == status)
# 统计总数
count_query = select(func.count()).select_from(query.subquery())
total = await db.scalar(count_query)
# 分页
query = query.order_by(SalaryRecord.period_year.desc(), SalaryRecord.period_month.desc(), SalaryRecord.id.desc())
query = query.offset((page - 1) * page_size).limit(page_size)
result = await db.execute(query)
records = result.scalars().all()
return records, total or 0
@staticmethod
async def get_by_id(db: AsyncSession, record_id: int) -> Optional[SalaryRecord]:
"""根据ID获取工资记录"""
result = await db.execute(
select(SalaryRecord)
.options(selectinload(SalaryRecord.staff).selectinload(Staff.department))
.where(SalaryRecord.id == record_id)
)
return result.scalar_one_or_none()
@staticmethod
async def calculate_performance_bonus(performance_score: float, performance_ratio: float) -> float:
"""计算绩效奖金"""
# 绩效奖金 = 绩效基数 × (绩效得分/100) × 绩效系数
return SalaryService.PERFORMANCE_BASE * (performance_score / 100) * performance_ratio
@staticmethod
async def create(db: AsyncSession, record_data: SalaryRecordCreate) -> SalaryRecord:
"""创建工资记录"""
# 获取员工信息
staff_result = await db.execute(
select(Staff).where(Staff.id == record_data.staff_id)
)
staff = staff_result.scalar_one_or_none()
# 计算总工资
total_salary = (
record_data.base_salary +
record_data.performance_bonus +
record_data.allowance -
record_data.deduction
)
record = SalaryRecord(
**record_data.model_dump(),
total_salary=total_salary,
status="pending"
)
db.add(record)
await db.flush()
await db.refresh(record)
return record
@staticmethod
async def update(db: AsyncSession, record_id: int, record_data: SalaryRecordUpdate) -> Optional[SalaryRecord]:
"""更新工资记录"""
record = await SalaryService.get_by_id(db, record_id)
if not record or record.status != "pending":
return None
update_data = record_data.model_dump(exclude_unset=True)
for key, value in update_data.items():
setattr(record, key, value)
# 重新计算总工资
record.total_salary = (
record.base_salary +
record.performance_bonus +
record.allowance -
record.deduction
)
await db.flush()
await db.refresh(record)
return record
@staticmethod
async def generate_from_assessment(
db: AsyncSession,
staff_id: int,
period_year: int,
period_month: int
) -> Optional[SalaryRecord]:
"""根据考核记录生成工资记录"""
# 获取员工信息
staff_result = await db.execute(
select(Staff).where(Staff.id == staff_id)
)
staff = staff_result.scalar_one_or_none()
if not staff:
return None
# 获取考核记录
assessment_result = await db.execute(
select(Assessment).where(
Assessment.staff_id == staff_id,
Assessment.period_year == period_year,
Assessment.period_month == period_month,
Assessment.status == "finalized"
)
)
assessment = assessment_result.scalar_one_or_none()
if not assessment:
return None
# 检查是否已存在工资记录
existing = await db.execute(
select(SalaryRecord).where(
SalaryRecord.staff_id == staff_id,
SalaryRecord.period_year == period_year,
SalaryRecord.period_month == period_month
)
)
if existing.scalar_one_or_none():
return None
# 计算绩效奖金
performance_bonus = await SalaryService.calculate_performance_bonus(
float(assessment.weighted_score),
float(staff.performance_ratio)
)
# 创建工资记录
total_salary = float(staff.base_salary) + performance_bonus
record = SalaryRecord(
staff_id=staff_id,
period_year=period_year,
period_month=period_month,
base_salary=float(staff.base_salary),
performance_score=float(assessment.weighted_score),
performance_bonus=performance_bonus,
deduction=0,
allowance=0,
total_salary=total_salary,
status="pending"
)
db.add(record)
await db.flush()
await db.refresh(record)
return record
@staticmethod
async def batch_generate_for_department(
db: AsyncSession,
department_id: int,
period_year: int,
period_month: int
) -> List[SalaryRecord]:
"""为科室批量生成工资记录"""
# 获取科室所有已确认考核的员工
result = await db.execute(
select(Assessment).join(Staff).where(
Staff.department_id == department_id,
Assessment.period_year == period_year,
Assessment.period_month == period_month,
Assessment.status == "finalized"
)
)
assessments = result.scalars().all()
records = []
for assessment in assessments:
record = await SalaryService.generate_from_assessment(
db, assessment.staff_id, period_year, period_month
)
if record:
records.append(record)
return records
@staticmethod
async def confirm(db: AsyncSession, record_id: int) -> Optional[SalaryRecord]:
"""确认工资"""
record = await SalaryService.get_by_id(db, record_id)
if not record or record.status != "pending":
return None
record.status = "confirmed"
await db.flush()
await db.refresh(record)
return record
@staticmethod
async def batch_confirm(
db: AsyncSession,
period_year: int,
period_month: int,
department_id: Optional[int] = None
) -> int:
"""批量确认工资"""
query = select(SalaryRecord).where(
SalaryRecord.period_year == period_year,
SalaryRecord.period_month == period_month,
SalaryRecord.status == "pending"
)
if department_id:
query = query.join(Staff).where(Staff.department_id == department_id)
result = await db.execute(query)
records = result.scalars().all()
count = 0
for record in records:
record.status = "confirmed"
count += 1
await db.flush()
return count

View File

@@ -0,0 +1,441 @@
"""
评分方法计算服务 - 实现详细设计文档中的评分方法
支持的评分方法:
1. 目标参照法 - 适用指标: 业务收支结余率等固定目标指标
2. 区间法 - 趋高指标、趋低指标、趋中指标
3. 扣分法 - 适用指标: 投诉、差错、事故、病历质量等
4. 加分法 - 适用指标: 科研、教学、论文、新项目等
"""
from typing import Optional, Dict, Any, List
from enum import Enum
from dataclasses import dataclass
from decimal import Decimal
class ScoringMethod(str, Enum):
"""评分方法类型"""
TARGET_REFERENCE = "target_reference" # 目标参照法
INTERVAL_HIGH = "interval_high" # 区间法-趋高指标
INTERVAL_LOW = "interval_low" # 区间法-趋低指标
INTERVAL_CENTER = "interval_center" # 区间法-趋中指标
DEDUCTION = "deduction" # 扣分法
BONUS = "bonus" # 加分法
@dataclass
class ScoringParams:
"""评分参数"""
weight: float = 1.0 # 权重分
max_score: float = 100.0 # 最高分值
target_value: Optional[float] = None # 目标值
baseline_value: Optional[float] = None # 基准值(区间法用)
best_value: Optional[float] = None # 最佳值(区间法用)
worst_value: Optional[float] = None # 最低值(区间法用)
allowed_deviation: Optional[float] = None # 允许偏差(趋中指标用)
deduction_per_unit: Optional[float] = None # 每单位扣分
bonus_per_unit: Optional[float] = None # 每单位加分
max_bonus_ratio: float = 0.5 # 最大加分比例默认权重分的50%
@dataclass
class ScoringResult:
"""评分结果"""
score: float # 得分
actual_value: float # 实际值
method: str # 使用的评分方法
details: Dict[str, Any] # 详细信息
class ScoringService:
"""评分计算服务"""
@staticmethod
def calculate(
method: ScoringMethod,
actual_value: float,
params: ScoringParams
) -> ScoringResult:
"""
根据评分方法计算得分
Args:
method: 评分方法
actual_value: 实际值
params: 评分参数
Returns:
ScoringResult: 评分结果
"""
method_handlers = {
ScoringMethod.TARGET_REFERENCE: ScoringService._target_reference,
ScoringMethod.INTERVAL_HIGH: ScoringService._interval_high,
ScoringMethod.INTERVAL_LOW: ScoringService._interval_low,
ScoringMethod.INTERVAL_CENTER: ScoringService._interval_center,
ScoringMethod.DEDUCTION: ScoringService._deduction,
ScoringMethod.BONUS: ScoringService._bonus,
}
handler = method_handlers.get(method)
if not handler:
raise ValueError(f"不支持的评分方法: {method}")
return handler(actual_value, params)
@staticmethod
def _target_reference(actual_value: float, params: ScoringParams) -> ScoringResult:
"""
目标参照法
计算公式: 得分 = 权重分 × (实际值 / 目标值)
适用指标: 业务收支结余率等固定目标指标
示例: 业务收支结余率权重 12.6 分,目标 15%,实际 18%
得分 = 12.6 × (18% / 15%) = 15.12 分(可超过满分)
"""
if params.target_value is None or params.target_value == 0:
return ScoringResult(
score=0,
actual_value=actual_value,
method="target_reference",
details={"error": "目标值未设置或为零"}
)
score = params.weight * (actual_value / params.target_value)
return ScoringResult(
score=round(score, 2),
actual_value=actual_value,
method="target_reference",
details={
"formula": "得分 = 权重分 × (实际值 / 目标值)",
"weight": params.weight,
"target_value": params.target_value,
"ratio": actual_value / params.target_value
}
)
@staticmethod
def _interval_high(actual_value: float, params: ScoringParams) -> ScoringResult:
"""
区间法 - 趋高指标(越高越好)
评分标准:
- 实际值 ≥ 最佳值:得满分
- 实际值 ≥ 基准值:得分 = 权重分 × [(实际值 - 最低值)/(最佳值 - 最低值)]
- 实际值 < 基准值:得分 = 权重分 × (实际值/基准值) × 0.8
适用指标: 人均收支结余、满意度、工作量等
"""
weight = params.weight
best = params.best_value
baseline = params.baseline_value
worst = params.worst_value or 0
# 参数校验
if best is None or baseline is None:
return ScoringResult(
score=0,
actual_value=actual_value,
method="interval_high",
details={"error": "最佳值或基准值未设置"}
)
if actual_value >= best:
# 达到最佳值,得满分
score = weight
details = {"status": "达到最佳值,得满分"}
elif actual_value >= baseline:
# 在基准和最佳之间,按比例计算
if best > worst:
ratio = (actual_value - worst) / (best - worst)
score = weight * ratio
else:
score = weight * (actual_value / baseline)
details = {"status": "基准值和最佳值之间"}
else:
# 低于基准值,打折扣
score = weight * (actual_value / baseline) * 0.8
details = {"status": "低于基准值打8折"}
return ScoringResult(
score=round(score, 2),
actual_value=actual_value,
method="interval_high",
details={
**details,
"weight": weight,
"best_value": best,
"baseline_value": baseline,
"worst_value": worst
}
)
@staticmethod
def _interval_low(actual_value: float, params: ScoringParams) -> ScoringResult:
"""
区间法 - 趋低指标(越低越好)
评分标准:
- 实际值 ≤ 目标值:得满分
- 实际值 > 目标值:得分 = 权重分 × (目标值/实际值)
适用指标: 耗材率、药品比例、费用控制率等
"""
weight = params.weight
target = params.target_value
if target is None:
return ScoringResult(
score=0,
actual_value=actual_value,
method="interval_low",
details={"error": "目标值未设置"}
)
if actual_value <= target:
# 达到目标值,得满分
score = weight
details = {"status": "达到目标值,得满分"}
else:
# 超过目标值,按比例扣分
score = weight * (target / actual_value)
details = {"status": "超过目标值,按比例扣分"}
return ScoringResult(
score=round(score, 2),
actual_value=actual_value,
method="interval_low",
details={
**details,
"weight": weight,
"target_value": target
}
)
@staticmethod
def _interval_center(actual_value: float, params: ScoringParams) -> ScoringResult:
"""
区间法 - 趋中指标(接近目标最好)
评分标准:
- |实际值 - 目标值| ≤ 允许偏差:得满分
- |实际值 - 目标值| > 允许偏差:得分 = 权重分 × [1 - (|实际值 - 目标值| - 允许偏差)/允许偏差]
适用指标: 平均住院日等
"""
weight = params.weight
target = params.target_value
deviation = params.allowed_deviation or 0
if target is None:
return ScoringResult(
score=0,
actual_value=actual_value,
method="interval_center",
details={"error": "目标值未设置"}
)
diff = abs(actual_value - target)
if diff <= deviation:
# 在允许偏差范围内,得满分
score = weight
details = {"status": "在允许偏差范围内,得满分"}
else:
# 超出允许偏差,按比例扣分
penalty_ratio = (diff - deviation) / deviation if deviation > 0 else 1
score = max(0, weight * (1 - penalty_ratio))
details = {"status": "超出允许偏差,按比例扣分"}
return ScoringResult(
score=round(score, 2),
actual_value=actual_value,
method="interval_center",
details={
**details,
"weight": weight,
"target_value": target,
"allowed_deviation": deviation,
"actual_deviation": diff
}
)
@staticmethod
def _deduction(actual_value: float, params: ScoringParams) -> ScoringResult:
"""
扣分法
评分标准:
- 得分 = 权重分 - 扣分数
- 扣完为止,不计负分
适用指标: 投诉、差错、事故、病历质量等
扣分细则示例:
- 门诊药品比例: 每超标准 1% 扣 10 分
- 住院药品比例: 每超标准 1% 扣 10 分
- 医保专项: 每超标准 1% 扣 10 分
- 乙级病历: 每份扣 5 分
- 丙级病历: 零发生(发生不得分)
- 投诉: 发生投诉事件不得分
- 差错: 发生差错事件不得分
- 事故与赔偿: 发生事故或赔偿事件不得分
"""
weight = params.weight
deduction_per_unit = params.deduction_per_unit or 0
# 计算扣分
deduction = actual_value * deduction_per_unit
# 扣完为止,不计负分
score = max(0, weight - deduction)
return ScoringResult(
score=round(score, 2),
actual_value=actual_value,
method="deduction",
details={
"weight": weight,
"deduction_per_unit": deduction_per_unit,
"total_deduction": deduction,
"occurrences": actual_value,
"status": "扣完为止" if score == 0 else "正常扣分"
}
)
@staticmethod
def _bonus(actual_value: float, params: ScoringParams) -> ScoringResult:
"""
加分法
评分标准:
- 得分 = 权重分 + 加分
- 加分不超过权重分的 50%(可配置)
适用指标: 科研、教学、论文、新项目等
加分细则示例:
- 开展新技术项目: 每项加 2 分,最高 10 分
- 科研项目立项: 市级加 5 分,省级加 10 分,国家级加 20 分
- 论文发表: 核心期刊加 5 分SCI 加 10 分
- 教学任务完成: 优秀加 5 分,良好加 3 分
"""
weight = params.weight
bonus_per_unit = params.bonus_per_unit or 0
max_bonus = weight * params.max_bonus_ratio
# 计算加分
bonus = actual_value * bonus_per_unit
# 加分不超过上限
bonus = min(bonus, max_bonus)
score = weight + bonus
return ScoringResult(
score=round(score, 2),
actual_value=actual_value,
method="bonus",
details={
"weight": weight,
"bonus_per_unit": bonus_per_unit,
"total_bonus": bonus,
"occurrences": actual_value,
"max_bonus": max_bonus,
"status": "达到加分上限" if bonus >= max_bonus else "正常加分"
}
)
@staticmethod
def calculate_assessment_score(
details: List[Dict[str, Any]]
) -> Dict[str, Any]:
"""
计算考核总分和各维度得分
Args:
details: 考核明细列表,每个明细包含:
- indicator_id: 指标ID
- indicator_name: 指标名称
- bs_dimension: BSC维度
- weight: 权重
- actual_value: 实际值
- scoring_method: 评分方法
- scoring_params: 评分参数
Returns:
包含总分、加权得分、各维度得分的字典
"""
total_score = 0.0
weighted_score = 0.0
dimensions = {
"financial": {"score": 0, "weight": 0, "details": []},
"customer": {"score": 0, "weight": 0, "details": []},
"internal_process": {"score": 0, "weight": 0, "details": []},
"learning_growth": {"score": 0, "weight": 0, "details": []},
}
for detail in details:
# 获取评分方法和参数
method = detail.get("scoring_method")
params_dict = detail.get("scoring_params", {})
actual_value = detail.get("actual_value", 0)
weight = detail.get("weight", 1.0)
dimension = detail.get("bs_dimension", "financial")
# 构建评分参数
params = ScoringParams(
weight=weight,
max_score=detail.get("max_score", 100),
target_value=params_dict.get("target_value"),
baseline_value=params_dict.get("baseline_value"),
best_value=params_dict.get("best_value"),
worst_value=params_dict.get("worst_value"),
allowed_deviation=params_dict.get("allowed_deviation"),
deduction_per_unit=params_dict.get("deduction_per_unit"),
bonus_per_unit=params_dict.get("bonus_per_unit"),
max_bonus_ratio=params_dict.get("max_bonus_ratio", 0.5)
)
# 计算得分
if method:
try:
scoring_method = ScoringMethod(method)
result = ScoringService.calculate(scoring_method, actual_value, params)
score = result.score
except ValueError:
# 未知的评分方法,使用直接得分
score = actual_value * weight
else:
# 没有指定评分方法,使用直接得分
score = actual_value * weight
# 累计总分
total_score += score
weighted_score += score * weight
# 维度得分
dim_key = dimension.value if hasattr(dimension, 'value') else dimension
if dim_key in dimensions:
dimensions[dim_key]["score"] += score
dimensions[dim_key]["weight"] += weight
dimensions[dim_key]["details"].append({
"indicator_id": detail.get("indicator_id"),
"indicator_name": detail.get("indicator_name"),
"score": score,
"weight": weight
})
# 计算维度平均分
for dim in dimensions.values():
if dim["weight"] > 0:
dim["average"] = dim["score"] / dim["weight"]
else:
dim["average"] = 0
return {
"total_score": round(total_score, 2),
"weighted_score": round(weighted_score, 2),
"dimensions": dimensions
}

View File

@@ -0,0 +1,111 @@
"""
员工服务层
"""
from typing import Optional, List
from sqlalchemy import select, func
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import selectinload
from app.models.models import Staff, Department
from app.schemas.schemas import StaffCreate, StaffUpdate
class StaffService:
"""员工服务"""
@staticmethod
async def get_list(
db: AsyncSession,
department_id: Optional[int] = None,
status: Optional[str] = None,
keyword: Optional[str] = None,
page: int = 1,
page_size: int = 20
) -> tuple[List[Staff], int]:
"""获取员工列表"""
query = select(Staff).options(selectinload(Staff.department))
if department_id:
query = query.where(Staff.department_id == department_id)
if status:
query = query.where(Staff.status == status)
if keyword:
query = query.where(
(Staff.name.ilike(f"%{keyword}%")) |
(Staff.employee_id.ilike(f"%{keyword}%"))
)
# 统计总数
count_query = select(func.count()).select_from(query.subquery())
total = await db.scalar(count_query)
# 分页
query = query.order_by(Staff.id.desc())
query = query.offset((page - 1) * page_size).limit(page_size)
result = await db.execute(query)
staff_list = result.scalars().all()
return staff_list, total or 0
@staticmethod
async def get_by_id(db: AsyncSession, staff_id: int) -> Optional[Staff]:
"""根据ID获取员工"""
result = await db.execute(
select(Staff)
.options(selectinload(Staff.department))
.where(Staff.id == staff_id)
)
return result.scalar_one_or_none()
@staticmethod
async def get_by_employee_id(db: AsyncSession, employee_id: str) -> Optional[Staff]:
"""根据工号获取员工"""
result = await db.execute(
select(Staff).where(Staff.employee_id == employee_id)
)
return result.scalar_one_or_none()
@staticmethod
async def create(db: AsyncSession, staff_data: StaffCreate) -> Staff:
"""创建员工"""
staff = Staff(**staff_data.model_dump())
db.add(staff)
await db.flush()
await db.refresh(staff)
return staff
@staticmethod
async def update(db: AsyncSession, staff_id: int, staff_data: StaffUpdate) -> Optional[Staff]:
"""更新员工"""
staff = await StaffService.get_by_id(db, staff_id)
if not staff:
return None
update_data = staff_data.model_dump(exclude_unset=True)
for key, value in update_data.items():
setattr(staff, key, value)
await db.flush()
await db.refresh(staff)
return staff
@staticmethod
async def delete(db: AsyncSession, staff_id: int) -> bool:
"""删除员工"""
staff = await StaffService.get_by_id(db, staff_id)
if not staff:
return False
await db.delete(staff)
return True
@staticmethod
async def get_by_department(db: AsyncSession, department_id: int) -> List[Staff]:
"""获取科室下所有员工"""
result = await db.execute(
select(Staff)
.where(Staff.department_id == department_id, Staff.status == "active")
.order_by(Staff.name)
)
return list(result.scalars().all())

View File

@@ -0,0 +1,299 @@
"""
统计服务层 - BSC 维度分析、绩效统计
"""
from typing import Optional, List, Dict, Any
from datetime import datetime
from sqlalchemy import select, func, and_
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import selectinload
from app.models.models import (
Assessment, AssessmentDetail, Indicator, Department, Staff,
BSCDimension, AssessmentStatus
)
class StatsService:
"""统计服务"""
@staticmethod
async def get_bsc_dimension_stats(
db: AsyncSession,
department_id: Optional[int] = None,
period_year: int = None,
period_month: int = None
) -> Dict[str, Any]:
"""获取 BSC 维度统计"""
dimensions = {
BSCDimension.FINANCIAL: {'score': 0, 'weight': 0, 'indicators': 0},
BSCDimension.CUSTOMER: {'score': 0, 'weight': 0, 'indicators': 0},
BSCDimension.INTERNAL_PROCESS: {'score': 0, 'weight': 0, 'indicators': 0},
BSCDimension.LEARNING_GROWTH: {'score': 0, 'weight': 0, 'indicators': 0},
}
# 构建查询条件
conditions = [
Assessment.status == AssessmentStatus.FINALIZED
]
if period_year:
conditions.append(Assessment.period_year == period_year)
if period_month:
conditions.append(Assessment.period_month == period_month)
if department_id:
conditions.append(Assessment.staff_id.has(Staff.department_id == department_id))
# 查询各维度得分
result = await db.execute(
select(
Indicator.bs_dimension,
func.sum(AssessmentDetail.score * Indicator.weight).label('total_score'),
func.sum(Indicator.weight).label('total_weight'),
func.count(AssessmentDetail.id).label('indicator_count')
)
.join(AssessmentDetail, AssessmentDetail.indicator_id == Indicator.id)
.join(Assessment, Assessment.id == AssessmentDetail.assessment_id)
.where(and_(*conditions))
.group_by(Indicator.bs_dimension)
)
for row in result.fetchall():
dim = row.bs_dimension
if dim in dimensions:
dimensions[dim] = {
'score': float(row.total_score) if row.total_score else 0,
'weight': float(row.total_weight) if row.total_weight else 0,
'indicators': row.indicator_count,
'average': (float(row.total_score) / float(row.total_weight)) if row.total_weight else 0
}
return {
'dimensions': dimensions,
'period': f"{period_year}{period_month}" if period_year and period_month else "全部"
}
@staticmethod
async def get_department_stats(
db: AsyncSession,
period_year: int = None,
period_month: int = None
) -> List[Dict[str, Any]]:
"""获取科室绩效统计"""
conditions = [
Assessment.status == AssessmentStatus.FINALIZED
]
if period_year:
conditions.append(Assessment.period_year == period_year)
if period_month:
conditions.append(Assessment.period_month == period_month)
result = await db.execute(
select(
Department.id,
Department.name,
Department.dept_type,
Staff.id.label('staff_id'),
Staff.name.label('staff_name'),
Assessment.total_score,
Assessment.weighted_score
)
.join(Staff, Staff.department_id == Department.id)
.join(Assessment, Assessment.staff_id == Staff.id)
.where(and_(*conditions))
.order_by(Department.id, Assessment.weighted_score.desc())
)
# 按科室汇总
dept_stats = {}
for row in result.fetchall():
dept_id = row.id
if dept_id not in dept_stats:
dept_stats[dept_id] = {
'department_id': dept_id,
'department_name': row.name,
'dept_type': row.dept_type,
'staff_count': 0,
'total_score': 0,
'avg_score': 0,
'max_score': 0,
'min_score': None,
'staff_list': []
}
dept_stats[dept_id]['staff_count'] += 1
dept_stats[dept_id]['total_score'] += row.weighted_score or 0
dept_stats[dept_id]['staff_list'].append({
'staff_id': row.staff_id,
'staff_name': row.staff_name,
'score': row.weighted_score
})
if row.weighted_score:
if row.weighted_score > dept_stats[dept_id]['max_score']:
dept_stats[dept_id]['max_score'] = row.weighted_score
if dept_stats[dept_id]['min_score'] is None or row.weighted_score < dept_stats[dept_id]['min_score']:
dept_stats[dept_id]['min_score'] = row.weighted_score
# 计算平均分
result_list = []
for dept in dept_stats.values():
if dept['staff_count'] > 0:
dept['avg_score'] = dept['total_score'] / dept['staff_count']
result_list.append(dept)
# 按平均分排序
result_list.sort(key=lambda x: x['avg_score'], reverse=True)
return result_list
@staticmethod
async def get_trend_stats(
db: AsyncSession,
department_id: Optional[int] = None,
period_year: int = None,
months: int = 6
) -> List[Dict[str, Any]]:
"""获取趋势统计(月度)"""
conditions = [
Assessment.status == AssessmentStatus.FINALIZED
]
if period_year:
# 查询最近 months 个月的数据
from datetime import datetime
current_month = datetime.now().month
start_month = current_month - months + 1
if start_month < 1:
# 跨年份
conditions.append(
((Assessment.period_year == period_year - 1) & (Assessment.period_month >= start_month + 12)) |
((Assessment.period_year == period_year) & (Assessment.period_month <= current_month))
)
else:
conditions.append(Assessment.period_year == period_year)
conditions.append(Assessment.period_month >= start_month)
conditions.append(Assessment.period_month <= current_month)
if department_id:
conditions.append(Assessment.staff_id.has(Staff.department_id == department_id))
result = await db.execute(
select(
Assessment.period_month,
func.avg(Assessment.total_score).label('avg_score'),
func.avg(Assessment.weighted_score).label('avg_weighted_score'),
func.count(Assessment.id).label('count')
)
.join(Staff, Staff.id == Assessment.staff_id)
.where(and_(*conditions))
.group_by(Assessment.period_month)
.order_by(Assessment.period_month)
)
return [
{
'month': row.period_month,
'avg_score': float(row.avg_score) if row.avg_score else 0,
'avg_weighted_score': float(row.avg_weighted_score) if row.avg_weighted_score else 0,
'count': row.count
}
for row in result.fetchall()
]
@staticmethod
async def get_ranking_stats(
db: AsyncSession,
period_year: int = None,
period_month: int = None,
limit: int = 10
) -> List[Dict[str, Any]]:
"""获取绩效排名"""
conditions = [
Assessment.status == AssessmentStatus.FINALIZED
]
if period_year:
conditions.append(Assessment.period_year == period_year)
if period_month:
conditions.append(Assessment.period_month == period_month)
result = await db.execute(
select(
Staff.id,
Staff.name,
Staff.employee_id,
Department.name.label('dept_name'),
Assessment.total_score,
Assessment.weighted_score
)
.join(Department, Department.id == Staff.department_id)
.join(Assessment, Assessment.staff_id == Staff.id)
.where(and_(*conditions))
.order_by(Assessment.weighted_score.desc())
.limit(limit)
)
return [
{
'staff_id': row.id,
'staff_name': row.name,
'employee_id': row.employee_id,
'department': row.dept_name,
'total_score': float(row.total_score) if row.total_score else 0,
'weighted_score': float(row.weighted_score) if row.weighted_score else 0,
'rank': idx + 1
}
for idx, row in enumerate(result.fetchall())
]
@staticmethod
async def get_completion_stats(
db: AsyncSession,
indicator_id: Optional[int] = None,
period_year: int = None,
period_month: int = None
) -> Dict[str, Any]:
"""获取指标完成度统计"""
conditions = [
Assessment.status == AssessmentStatus.FINALIZED
]
if period_year:
conditions.append(Assessment.period_year == period_year)
if period_month:
conditions.append(Assessment.period_month == period_month)
query = select(
Indicator.id,
Indicator.name,
Indicator.code,
Indicator.target_value,
Indicator.max_score,
func.avg(AssessmentDetail.score).label('avg_score'),
func.max(AssessmentDetail.score).label('max_score'),
func.min(AssessmentDetail.score).label('min_score'),
func.count(AssessmentDetail.id).label('count')
).join(AssessmentDetail, AssessmentDetail.indicator_id == Indicator.id)
if indicator_id:
conditions.append(Indicator.id == indicator_id)
result = await db.execute(
query.where(and_(*conditions))
.group_by(Indicator.id, Indicator.name, Indicator.code, Indicator.target_value, Indicator.max_score)
)
indicators = []
for row in result.fetchall():
completion_rate = 0
if row.target_value and row.avg_score:
completion_rate = (float(row.avg_score) / float(row.target_value)) * 100 if row.target_value else 0
indicators.append({
'indicator_id': row.id,
'indicator_name': row.name,
'indicator_code': row.code,
'target_value': float(row.target_value) if row.target_value else None,
'max_score': float(row.max_score) if row.max_score else 0,
'avg_score': float(row.avg_score) if row.avg_score else 0,
'completion_rate': min(completion_rate, 100), # 最高 100%
'count': row.count
})
return {'indicators': indicators}

View File

@@ -0,0 +1,520 @@
"""
满意度调查服务
功能:
1. 调查问卷管理 - 问卷CRUD、题目管理
2. 调查响应处理 - 提交回答、计算得分
3. 满意度统计 - 科室满意度、趋势分析
"""
from typing import Optional, List, Dict, Any
from datetime import datetime
from sqlalchemy import select, func, and_, or_
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import selectinload
import json
from app.models.models import (
Survey, SurveyQuestion, SurveyResponse, SurveyAnswer,
SurveyStatus, SurveyType, QuestionType,
Department
)
class SurveyService:
"""满意度调查服务"""
# ==================== 问卷管理 ====================
@staticmethod
async def get_survey_list(
db: AsyncSession,
survey_type: Optional[SurveyType] = None,
status: Optional[SurveyStatus] = None,
page: int = 1,
page_size: int = 20
) -> tuple[List[Survey], int]:
"""获取问卷列表"""
query = select(Survey).options(
selectinload(Survey.questions)
)
if survey_type:
query = query.where(Survey.survey_type == survey_type)
if status:
query = query.where(Survey.status == status)
# 统计总数
count_query = select(func.count()).select_from(query.subquery())
total = await db.scalar(count_query)
# 分页
query = query.order_by(Survey.created_at.desc())
query = query.offset((page - 1) * page_size).limit(page_size)
result = await db.execute(query)
surveys = result.scalars().all()
return list(surveys), total or 0
@staticmethod
async def get_survey_by_id(db: AsyncSession, survey_id: int) -> Optional[Survey]:
"""获取问卷详情"""
result = await db.execute(
select(Survey)
.options(
selectinload(Survey.questions)
)
.where(Survey.id == survey_id)
)
return result.scalar_one_or_none()
@staticmethod
async def create_survey(
db: AsyncSession,
survey_name: str,
survey_code: str,
survey_type: SurveyType,
description: Optional[str] = None,
target_departments: Optional[List[int]] = None,
is_anonymous: bool = True,
created_by: Optional[int] = None
) -> Survey:
"""创建问卷"""
survey = Survey(
survey_name=survey_name,
survey_code=survey_code,
survey_type=survey_type,
description=description,
target_departments=json.dumps(target_departments) if target_departments else None,
is_anonymous=is_anonymous,
created_by=created_by,
status=SurveyStatus.DRAFT
)
db.add(survey)
await db.flush()
await db.refresh(survey)
return survey
@staticmethod
async def update_survey(
db: AsyncSession,
survey_id: int,
**kwargs
) -> Optional[Survey]:
"""更新问卷"""
survey = await SurveyService.get_survey_by_id(db, survey_id)
if not survey:
return None
# 只有草稿状态可以修改
if survey.status != SurveyStatus.DRAFT:
raise ValueError("只有草稿状态的问卷可以修改")
for key, value in kwargs.items():
if hasattr(survey, key):
if key == "target_departments" and isinstance(value, list):
value = json.dumps(value)
setattr(survey, key, value)
await db.flush()
await db.refresh(survey)
return survey
@staticmethod
async def publish_survey(db: AsyncSession, survey_id: int) -> Optional[Survey]:
"""发布问卷"""
survey = await SurveyService.get_survey_by_id(db, survey_id)
if not survey:
return None
if survey.status != SurveyStatus.DRAFT:
raise ValueError("只有草稿状态的问卷可以发布")
# 检查是否有题目
if survey.total_questions == 0:
raise ValueError("问卷没有题目,无法发布")
survey.status = SurveyStatus.PUBLISHED
survey.start_date = datetime.utcnow()
await db.flush()
await db.refresh(survey)
return survey
@staticmethod
async def close_survey(db: AsyncSession, survey_id: int) -> Optional[Survey]:
"""结束问卷"""
survey = await SurveyService.get_survey_by_id(db, survey_id)
if not survey:
return None
if survey.status != SurveyStatus.PUBLISHED:
raise ValueError("只有已发布的问卷可以结束")
survey.status = SurveyStatus.CLOSED
survey.end_date = datetime.utcnow()
await db.flush()
await db.refresh(survey)
return survey
@staticmethod
async def delete_survey(db: AsyncSession, survey_id: int) -> bool:
"""删除问卷"""
survey = await SurveyService.get_survey_by_id(db, survey_id)
if not survey:
return False
if survey.status == SurveyStatus.PUBLISHED:
raise ValueError("发布中的问卷无法删除")
await db.delete(survey)
await db.flush()
return True
# ==================== 题目管理 ====================
@staticmethod
async def add_question(
db: AsyncSession,
survey_id: int,
question_text: str,
question_type: QuestionType,
options: Optional[List[Dict]] = None,
score_max: int = 5,
is_required: bool = True
) -> SurveyQuestion:
"""添加题目"""
survey = await SurveyService.get_survey_by_id(db, survey_id)
if not survey:
raise ValueError("问卷不存在")
if survey.status != SurveyStatus.DRAFT:
raise ValueError("只有草稿状态的问卷可以添加题目")
# 获取最大排序号
result = await db.execute(
select(func.max(SurveyQuestion.sort_order))
.where(SurveyQuestion.survey_id == survey_id)
)
max_order = result.scalar() or 0
question = SurveyQuestion(
survey_id=survey_id,
question_text=question_text,
question_type=question_type,
options=json.dumps(options, ensure_ascii=False) if options else None,
score_max=score_max,
is_required=is_required,
sort_order=max_order + 1
)
db.add(question)
# 更新问卷题目数
survey.total_questions += 1
await db.flush()
await db.refresh(question)
return question
@staticmethod
async def update_question(
db: AsyncSession,
question_id: int,
**kwargs
) -> Optional[SurveyQuestion]:
"""更新题目"""
result = await db.execute(
select(SurveyQuestion).where(SurveyQuestion.id == question_id)
)
question = result.scalar_one_or_none()
if not question:
return None
# 检查问卷状态
survey = await SurveyService.get_survey_by_id(db, question.survey_id)
if survey and survey.status != SurveyStatus.DRAFT:
raise ValueError("只有草稿状态的问卷题目可以修改")
for key, value in kwargs.items():
if hasattr(question, key):
if key == "options" and isinstance(value, list):
value = json.dumps(value, ensure_ascii=False)
setattr(question, key, value)
await db.flush()
await db.refresh(question)
return question
@staticmethod
async def delete_question(db: AsyncSession, question_id: int) -> bool:
"""删除题目"""
result = await db.execute(
select(SurveyQuestion).where(SurveyQuestion.id == question_id)
)
question = result.scalar_one_or_none()
if not question:
return False
# 检查问卷状态
survey = await SurveyService.get_survey_by_id(db, question.survey_id)
if survey and survey.status != SurveyStatus.DRAFT:
raise ValueError("只有草稿状态的问卷题目可以删除")
# 更新问卷题目数
if survey:
survey.total_questions -= 1
await db.delete(question)
await db.flush()
return True
# ==================== 提交回答 ====================
@staticmethod
async def submit_response(
db: AsyncSession,
survey_id: int,
department_id: Optional[int],
answers: List[Dict[str, Any]],
respondent_type: str = "patient",
respondent_id: Optional[int] = None,
respondent_phone: Optional[str] = None,
ip_address: Optional[str] = None,
user_agent: Optional[str] = None
) -> SurveyResponse:
"""提交问卷回答"""
survey = await SurveyService.get_survey_by_id(db, survey_id)
if not survey:
raise ValueError("问卷不存在")
if survey.status != SurveyStatus.PUBLISHED:
raise ValueError("问卷未发布或已结束")
# 计算得分
total_score = 0.0
max_score = 0.0
# 创建回答记录
response = SurveyResponse(
survey_id=survey_id,
department_id=department_id,
respondent_type=respondent_type,
respondent_id=respondent_id,
respondent_phone=respondent_phone,
ip_address=ip_address,
user_agent=user_agent
)
db.add(response)
await db.flush()
# 处理每个回答
for answer_data in answers:
question_id = answer_data.get("question_id")
answer_value = answer_data.get("answer_value")
# 获取题目
q_result = await db.execute(
select(SurveyQuestion).where(SurveyQuestion.id == question_id)
)
question = q_result.scalar_one_or_none()
if not question:
continue
# 计算得分
score = 0.0
if question.question_type == QuestionType.SCORE:
# 评分题:直接取分值
try:
score = float(answer_value)
except (ValueError, TypeError):
score = 0
max_score += question.score_max
elif question.question_type == QuestionType.SINGLE_CHOICE:
# 单选题:根据选项得分
if question.options:
options = json.loads(question.options)
for opt in options:
if opt.get("value") == answer_value:
score = opt.get("score", 0)
break
max_score += 5 # 假设单选题最高5分
elif question.question_type == QuestionType.MULTIPLE_CHOICE:
# 多选题:累加选中选项得分
if question.options:
options = json.loads(question.options)
selected = answer_value.split(",") if answer_value else []
for opt in options:
if opt.get("value") in selected:
score += opt.get("score", 0)
max_score += 5
total_score += score
# 创建回答明细
answer = SurveyAnswer(
response_id=response.id,
question_id=question_id,
answer_value=str(answer_value) if answer_value else None,
score=score
)
db.add(answer)
# 更新回答记录得分
response.total_score = total_score
response.max_score = max_score if max_score > 0 else 1
response.satisfaction_rate = (total_score / response.max_score * 100) if response.max_score > 0 else 0
await db.flush()
await db.refresh(response)
return response
# ==================== 满意度统计 ====================
@staticmethod
async def get_department_satisfaction(
db: AsyncSession,
survey_id: Optional[int] = None,
department_id: Optional[int] = None,
period_year: Optional[int] = None,
period_month: Optional[int] = None
) -> List[Dict[str, Any]]:
"""获取科室满意度统计"""
query = select(
Department.id.label("department_id"),
Department.name.label("department_name"),
func.count(SurveyResponse.id).label("response_count"),
func.avg(SurveyResponse.satisfaction_rate).label("avg_satisfaction"),
func.sum(SurveyResponse.total_score).label("total_score"),
func.sum(SurveyResponse.max_score).label("max_score")
).join(
SurveyResponse, SurveyResponse.department_id == Department.id
)
conditions = []
if survey_id:
conditions.append(SurveyResponse.survey_id == survey_id)
if department_id:
conditions.append(Department.id == department_id)
if period_year and period_month:
from sqlalchemy import extract
conditions.append(extract('year', SurveyResponse.submitted_at) == period_year)
conditions.append(extract('month', SurveyResponse.submitted_at) == period_month)
if conditions:
query = query.where(and_(*conditions))
query = query.group_by(Department.id, Department.name)
query = query.order_by(func.avg(SurveyResponse.satisfaction_rate).desc())
result = await db.execute(query)
return [
{
"department_id": row.department_id,
"department_name": row.department_name,
"response_count": row.response_count,
"avg_satisfaction": round(float(row.avg_satisfaction), 2) if row.avg_satisfaction else 0,
"total_score": float(row.total_score) if row.total_score else 0,
"max_score": float(row.max_score) if row.max_score else 0
}
for row in result.fetchall()
]
@staticmethod
async def get_satisfaction_trend(
db: AsyncSession,
department_id: int,
months: int = 6
) -> List[Dict[str, Any]]:
"""获取满意度趋势"""
from sqlalchemy import extract
from datetime import datetime
current_date = datetime.now()
current_year = current_date.year
current_month = current_date.month
query = select(
extract('year', SurveyResponse.submitted_at).label("year"),
extract('month', SurveyResponse.submitted_at).label("month"),
func.count(SurveyResponse.id).label("response_count"),
func.avg(SurveyResponse.satisfaction_rate).label("avg_satisfaction")
).where(
SurveyResponse.department_id == department_id
).group_by(
extract('year', SurveyResponse.submitted_at),
extract('month', SurveyResponse.submitted_at)
).order_by(
extract('year', SurveyResponse.submitted_at),
extract('month', SurveyResponse.submitted_at)
)
result = await db.execute(query)
return [
{
"year": int(row.year),
"month": int(row.month),
"period": f"{int(row.year)}{int(row.month)}",
"response_count": row.response_count,
"avg_satisfaction": round(float(row.avg_satisfaction), 2) if row.avg_satisfaction else 0
}
for row in result.fetchall()
]
@staticmethod
async def get_question_stats(db: AsyncSession, survey_id: int) -> List[Dict[str, Any]]:
"""获取问卷各题目统计"""
# 获取问卷题目
questions_result = await db.execute(
select(SurveyQuestion)
.where(SurveyQuestion.survey_id == survey_id)
.order_by(SurveyQuestion.sort_order)
)
questions = questions_result.scalars().all()
stats = []
for question in questions:
# 统计该题目的回答
answer_result = await db.execute(
select(
func.count(SurveyAnswer.id).label("count"),
func.avg(SurveyAnswer.score).label("avg_score"),
func.sum(SurveyAnswer.score).label("total_score")
).where(SurveyAnswer.question_id == question.id)
)
row = answer_result.fetchone()
question_stat = {
"question_id": question.id,
"question_text": question.question_text,
"question_type": question.question_type.value,
"response_count": row.count if row else 0,
"avg_score": round(float(row.avg_score), 2) if row and row.avg_score else 0,
"total_score": float(row.total_score) if row and row.total_score else 0,
"max_possible_score": question.score_max
}
# 如果是选择题,统计各选项占比
if question.question_type in [QuestionType.SINGLE_CHOICE, QuestionType.MULTIPLE_CHOICE]:
if question.options:
options = json.loads(question.options)
option_stats = []
for opt in options:
count_result = await db.execute(
select(func.count(SurveyAnswer.id))
.where(SurveyAnswer.question_id == question.id)
.where(SurveyAnswer.answer_value.contains(opt.get("value")))
)
opt_count = count_result.scalar() or 0
option_stats.append({
"option": opt.get("label"),
"value": opt.get("value"),
"count": opt_count
})
question_stat["option_stats"] = option_stats
stats.append(question_stat)
return stats

View File

@@ -0,0 +1,293 @@
"""
指标模板服务层
"""
import json
from typing import Optional, List, Dict, Any
from sqlalchemy import select, func, delete
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import selectinload
from app.models.models import (
IndicatorTemplate, TemplateIndicator, Indicator,
TemplateType, BSCDimension
)
from app.schemas.schemas import (
IndicatorTemplateCreate, IndicatorTemplateUpdate,
TemplateIndicatorCreate, TemplateIndicatorUpdate
)
class TemplateService:
"""指标模板服务"""
@staticmethod
async def get_list(
db: AsyncSession,
template_type: Optional[str] = None,
is_active: Optional[bool] = None,
page: int = 1,
page_size: int = 20
) -> tuple[List[Dict], int]:
"""获取模板列表"""
query = select(IndicatorTemplate)
if template_type:
query = query.where(IndicatorTemplate.template_type == template_type)
if is_active is not None:
query = query.where(IndicatorTemplate.is_active == is_active)
# 统计总数
count_query = select(func.count()).select_from(query.subquery())
total = await db.scalar(count_query)
# 分页
query = query.order_by(IndicatorTemplate.template_type, IndicatorTemplate.id)
query = query.offset((page - 1) * page_size).limit(page_size)
result = await db.execute(query)
templates = result.scalars().all()
# 获取每个模板的指标数量
template_list = []
for t in templates:
indicator_count = await db.scalar(
select(func.count()).where(TemplateIndicator.template_id == t.id)
)
template_dict = {
"id": t.id,
"template_name": t.template_name,
"template_code": t.template_code,
"template_type": t.template_type.value,
"description": t.description,
"dimension_weights": t.dimension_weights,
"assessment_cycle": t.assessment_cycle,
"is_active": t.is_active,
"indicator_count": indicator_count or 0,
"created_at": t.created_at,
"updated_at": t.updated_at
}
template_list.append(template_dict)
return template_list, total or 0
@staticmethod
async def get_by_id(db: AsyncSession, template_id: int) -> Optional[IndicatorTemplate]:
"""根据 ID 获取模板"""
result = await db.execute(
select(IndicatorTemplate)
.options(selectinload(IndicatorTemplate.indicators).selectinload(TemplateIndicator.indicator))
.where(IndicatorTemplate.id == template_id)
)
return result.scalar_one_or_none()
@staticmethod
async def get_by_code(db: AsyncSession, template_code: str) -> Optional[IndicatorTemplate]:
"""根据编码获取模板"""
result = await db.execute(
select(IndicatorTemplate).where(IndicatorTemplate.template_code == template_code)
)
return result.scalar_one_or_none()
@staticmethod
async def create(
db: AsyncSession,
template_data: IndicatorTemplateCreate
) -> IndicatorTemplate:
"""创建模板"""
# 创建模板
template = IndicatorTemplate(
template_name=template_data.template_name,
template_code=template_data.template_code,
template_type=template_data.template_type,
description=template_data.description,
dimension_weights=template_data.dimension_weights,
assessment_cycle=template_data.assessment_cycle
)
db.add(template)
await db.flush()
# 添加指标关联
if template_data.indicators:
for idx, ind_data in enumerate(template_data.indicators):
ti = TemplateIndicator(
template_id=template.id,
indicator_id=ind_data.indicator_id,
category=ind_data.category,
target_value=ind_data.target_value,
target_unit=ind_data.target_unit,
weight=ind_data.weight,
scoring_method=ind_data.scoring_method,
scoring_params=ind_data.scoring_params,
sort_order=ind_data.sort_order or idx,
remark=ind_data.remark
)
db.add(ti)
await db.commit()
await db.refresh(template)
return template
@staticmethod
async def update(
db: AsyncSession,
template_id: int,
template_data: IndicatorTemplateUpdate
) -> Optional[IndicatorTemplate]:
"""更新模板"""
template = await TemplateService.get_by_id(db, template_id)
if not template:
return None
update_data = template_data.model_dump(exclude_unset=True)
for key, value in update_data.items():
setattr(template, key, value)
await db.commit()
await db.refresh(template)
return template
@staticmethod
async def delete(db: AsyncSession, template_id: int) -> bool:
"""删除模板"""
template = await TemplateService.get_by_id(db, template_id)
if not template:
return False
await db.delete(template)
await db.commit()
return True
@staticmethod
async def add_indicator(
db: AsyncSession,
template_id: int,
indicator_data: TemplateIndicatorCreate
) -> Optional[TemplateIndicator]:
"""添加模板指标"""
# 检查模板是否存在
template = await db.execute(
select(IndicatorTemplate).where(IndicatorTemplate.id == template_id)
)
if not template.scalar_one_or_none():
return None
# 检查指标是否已存在
existing = await db.execute(
select(TemplateIndicator).where(
TemplateIndicator.template_id == template_id,
TemplateIndicator.indicator_id == indicator_data.indicator_id
)
)
if existing.scalar_one_or_none():
return None
# 获取最大排序
max_order = await db.scalar(
select(func.max(TemplateIndicator.sort_order)).where(
TemplateIndicator.template_id == template_id
)
)
ti = TemplateIndicator(
template_id=template_id,
indicator_id=indicator_data.indicator_id,
category=indicator_data.category,
target_value=indicator_data.target_value,
target_unit=indicator_data.target_unit,
weight=indicator_data.weight,
scoring_method=indicator_data.scoring_method,
scoring_params=indicator_data.scoring_params,
sort_order=indicator_data.sort_order or (max_order or 0) + 1,
remark=indicator_data.remark
)
db.add(ti)
await db.commit()
await db.refresh(ti)
return ti
@staticmethod
async def update_indicator(
db: AsyncSession,
template_id: int,
indicator_id: int,
indicator_data: TemplateIndicatorUpdate
) -> Optional[TemplateIndicator]:
"""更新模板指标"""
result = await db.execute(
select(TemplateIndicator).where(
TemplateIndicator.template_id == template_id,
TemplateIndicator.indicator_id == indicator_id
)
)
ti = result.scalar_one_or_none()
if not ti:
return None
update_data = indicator_data.model_dump(exclude_unset=True)
for key, value in update_data.items():
setattr(ti, key, value)
await db.commit()
await db.refresh(ti)
return ti
@staticmethod
async def remove_indicator(
db: AsyncSession,
template_id: int,
indicator_id: int
) -> bool:
"""移除模板指标"""
result = await db.execute(
select(TemplateIndicator).where(
TemplateIndicator.template_id == template_id,
TemplateIndicator.indicator_id == indicator_id
)
)
ti = result.scalar_one_or_none()
if not ti:
return False
await db.delete(ti)
await db.commit()
return True
@staticmethod
async def get_template_indicators(
db: AsyncSession,
template_id: int
) -> List[TemplateIndicator]:
"""获取模板的所有指标"""
result = await db.execute(
select(TemplateIndicator)
.options(selectinload(TemplateIndicator.indicator))
.where(TemplateIndicator.template_id == template_id)
.order_by(TemplateIndicator.sort_order)
)
return result.scalars().all()
@staticmethod
def get_template_type_label(template_type: str) -> str:
"""获取模板类型标签"""
type_map = {
"general": "通用模板",
"surgical": "手术临床科室",
"nonsurgical_ward": "非手术有病房科室",
"nonsurgical_noward": "非手术无病房科室",
"medical_tech": "医技科室",
"nursing": "护理单元",
"admin": "行政科室",
"logistics": "后勤科室"
}
return type_map.get(template_type, template_type)
@staticmethod
def get_dimension_label(dimension: str) -> str:
"""获取维度标签"""
dimension_map = {
"financial": "财务管理",
"customer": "顾客服务",
"internal_process": "内部流程",
"learning_growth": "学习与成长"
}
return dimension_map.get(dimension, dimension)

View File

@@ -0,0 +1,33 @@
"""
工具函数模块
"""
from datetime import datetime
from typing import Optional
def get_current_period() -> tuple[int, int]:
"""获取当前考核周期(年、月)"""
now = datetime.now()
return now.year, now.month
def format_period(year: int, month: int) -> str:
"""格式化考核周期"""
return f"{year}{month:02d}"
def calculate_score_level(score: float) -> str:
"""计算绩效等级"""
if score >= 90:
return "优秀"
elif score >= 80:
return "良好"
elif score >= 60:
return "合格"
else:
return "不合格"
def generate_employee_id(department_code: str, sequence: int) -> str:
"""生成员工工号"""
return f"{department_code}{sequence:04d}"