add backend source code
This commit is contained in:
0
backend/app/__init__.py
Normal file
0
backend/app/__init__.py
Normal file
0
backend/app/api/__init__.py
Normal file
0
backend/app/api/__init__.py
Normal file
17
backend/app/api/v1/__init__.py
Normal file
17
backend/app/api/v1/__init__.py
Normal 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)
|
||||
165
backend/app/api/v1/assessments.py
Normal file
165
backend/app/api/v1/assessments.py
Normal 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
135
backend/app/api/v1/auth.py
Normal 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
|
||||
}
|
||||
107
backend/app/api/v1/departments.py
Normal file
107
backend/app/api/v1/departments.py
Normal 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": "删除成功"}
|
||||
216
backend/app/api/v1/finance.py
Normal file
216
backend/app/api/v1/finance.py
Normal 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": "删除成功"}
|
||||
141
backend/app/api/v1/indicators.py
Normal file
141
backend/app/api/v1/indicators.py
Normal 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
163
backend/app/api/v1/menus.py
Normal 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": "初始化成功"
|
||||
}
|
||||
309
backend/app/api/v1/performance_plans.py
Normal file
309
backend/app/api/v1/performance_plans.py
Normal 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": "删除成功"
|
||||
}
|
||||
155
backend/app/api/v1/salary.py
Normal file
155
backend/app/api/v1/salary.py
Normal 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
123
backend/app/api/v1/staff.py
Normal 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
241
backend/app/api/v1/stats.py
Normal 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
|
||||
}
|
||||
401
backend/app/api/v1/surveys.py
Normal file
401
backend/app/api/v1/surveys.py
Normal 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
|
||||
271
backend/app/api/v1/templates.py
Normal file
271
backend/app/api/v1/templates.py
Normal 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}
|
||||
}
|
||||
0
backend/app/core/__init__.py
Normal file
0
backend/app/core/__init__.py
Normal file
46
backend/app/core/config.py
Normal file
46
backend/app/core/config.py
Normal file
@@ -0,0 +1,46 @@
|
||||
"""
|
||||
系统配置模块
|
||||
"""
|
||||
from pydantic_settings import BaseSettings
|
||||
from typing import Optional
|
||||
from functools import lru_cache
|
||||
|
||||
|
||||
class Settings(BaseSettings):
|
||||
"""系统配置"""
|
||||
|
||||
# 应用配置
|
||||
APP_NAME: str = "医院绩效考核管理系统"
|
||||
APP_VERSION: str = "1.0.0"
|
||||
DEBUG: bool = True
|
||||
API_PREFIX: str = "/api/v1"
|
||||
|
||||
# 数据库配置
|
||||
DATABASE_URL: str = "postgresql+asyncpg://postgres:postgres@localhost:5432/hospital_performance"
|
||||
DATABASE_POOL_SIZE: int = 20
|
||||
DATABASE_MAX_OVERFLOW: int = 10
|
||||
|
||||
# JWT配置
|
||||
SECRET_KEY: str = "your-secret-key-change-in-production-min-32-chars"
|
||||
ALGORITHM: str = "HS256"
|
||||
ACCESS_TOKEN_EXPIRE_MINUTES: int = 60 * 8 # 8小时
|
||||
|
||||
# 跨域配置
|
||||
CORS_ORIGINS: list[str] = ["http://localhost:3000", "http://localhost:5173"]
|
||||
|
||||
# 分页配置
|
||||
DEFAULT_PAGE_SIZE: int = 20
|
||||
MAX_PAGE_SIZE: int = 100
|
||||
|
||||
class Config:
|
||||
env_file = ".env"
|
||||
case_sensitive = True
|
||||
|
||||
|
||||
@lru_cache()
|
||||
def get_settings() -> Settings:
|
||||
"""获取配置单例"""
|
||||
return Settings()
|
||||
|
||||
|
||||
settings = get_settings()
|
||||
38
backend/app/core/database.py
Normal file
38
backend/app/core/database.py
Normal file
@@ -0,0 +1,38 @@
|
||||
"""
|
||||
数据库连接模块
|
||||
"""
|
||||
from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine, async_sessionmaker
|
||||
from sqlalchemy.orm import DeclarativeBase
|
||||
from app.core.config import settings
|
||||
|
||||
|
||||
# 创建异步引擎
|
||||
engine = create_async_engine(
|
||||
settings.DATABASE_URL,
|
||||
echo=settings.DEBUG,
|
||||
)
|
||||
|
||||
# 创建异步会话工厂
|
||||
async_session_maker = async_sessionmaker(
|
||||
engine,
|
||||
class_=AsyncSession,
|
||||
expire_on_commit=False,
|
||||
)
|
||||
|
||||
|
||||
class Base(DeclarativeBase):
|
||||
"""数据库模型基类"""
|
||||
pass
|
||||
|
||||
|
||||
async def get_db() -> AsyncSession:
|
||||
"""获取数据库会话依赖"""
|
||||
async with async_session_maker() as session:
|
||||
try:
|
||||
yield session
|
||||
await session.commit()
|
||||
except Exception:
|
||||
await session.rollback()
|
||||
raise
|
||||
finally:
|
||||
await session.close()
|
||||
114
backend/app/core/init_db.py
Normal file
114
backend/app/core/init_db.py
Normal file
@@ -0,0 +1,114 @@
|
||||
"""
|
||||
数据库初始化脚本
|
||||
创建初始管理员用户和示例数据
|
||||
"""
|
||||
import asyncio
|
||||
from sqlalchemy import select
|
||||
from app.core.database import async_session_maker
|
||||
from app.core.security import get_password_hash
|
||||
from app.models.models import User, Department, Staff, Indicator, DeptType, StaffStatus, IndicatorType
|
||||
|
||||
|
||||
async def init_admin_user():
|
||||
"""创建初始管理员用户"""
|
||||
async with async_session_maker() as db:
|
||||
# 检查是否已存在admin用户
|
||||
result = await db.execute(select(User).where(User.username == "admin"))
|
||||
if result.scalar_one_or_none():
|
||||
print("管理员用户已存在")
|
||||
return
|
||||
|
||||
# 创建admin用户
|
||||
admin = User(
|
||||
username="admin",
|
||||
password_hash=get_password_hash("admin123"),
|
||||
role="admin",
|
||||
is_active=True
|
||||
)
|
||||
db.add(admin)
|
||||
await db.flush()
|
||||
print("创建管理员用户: admin / admin123")
|
||||
|
||||
|
||||
async def init_departments():
|
||||
"""创建示例科室"""
|
||||
async with async_session_maker() as db:
|
||||
result = await db.execute(select(Department))
|
||||
if result.scalars().first():
|
||||
print("科室数据已存在")
|
||||
return
|
||||
|
||||
departments = [
|
||||
Department(name="内科", code="NK001", dept_type=DeptType.CLINICAL, level=1, sort_order=1),
|
||||
Department(name="外科", code="WK001", dept_type=DeptType.CLINICAL, level=1, sort_order=2),
|
||||
Department(name="中医科", code="ZYK001", dept_type=DeptType.CLINICAL, level=1, sort_order=3),
|
||||
Department(name="检验科", code="JYK001", dept_type=DeptType.MEDICAL_TECH, level=1, sort_order=4),
|
||||
Department(name="放射科", code="FSK001", dept_type=DeptType.MEDICAL_TECH, level=1, sort_order=5),
|
||||
Department(name="财务科", code="CWK001", dept_type=DeptType.ADMIN, level=1, sort_order=6),
|
||||
Department(name="办公室", code="BGS001", dept_type=DeptType.ADMIN, level=1, sort_order=7),
|
||||
]
|
||||
for dept in departments:
|
||||
db.add(dept)
|
||||
await db.flush()
|
||||
print(f"创建了 {len(departments)} 个科室")
|
||||
|
||||
|
||||
async def init_indicators():
|
||||
"""创建示例考核指标"""
|
||||
async with async_session_maker() as db:
|
||||
result = await db.execute(select(Indicator))
|
||||
if result.scalars().first():
|
||||
print("指标数据已存在")
|
||||
return
|
||||
|
||||
indicators = [
|
||||
Indicator(name="门诊量", code="MZL001", indicator_type=IndicatorType.QUANTITY, weight=1.5, max_score=100, unit="人次"),
|
||||
Indicator(name="住院量", code="ZYL001", indicator_type=IndicatorType.QUANTITY, weight=1.2, max_score=100, unit="人次"),
|
||||
Indicator(name="诊断准确率", code="ZDZQL001", indicator_type=IndicatorType.QUALITY, weight=2.0, max_score=100, unit="%"),
|
||||
Indicator(name="患者满意度", code="HZMYD001", indicator_type=IndicatorType.SERVICE, weight=1.5, max_score=100, unit="%"),
|
||||
Indicator(name="医疗成本控制", code="YLCBKZ001", indicator_type=IndicatorType.COST, weight=1.0, max_score=100, unit="%"),
|
||||
Indicator(name="工作效率", code="GZXL001", indicator_type=IndicatorType.EFFICIENCY, weight=1.0, max_score=100, unit="%"),
|
||||
]
|
||||
for ind in indicators:
|
||||
db.add(ind)
|
||||
await db.flush()
|
||||
print(f"创建了 {len(indicators)} 个考核指标")
|
||||
|
||||
|
||||
async def init_sample_staff():
|
||||
"""创建示例员工"""
|
||||
async with async_session_maker() as db:
|
||||
result = await db.execute(select(Staff))
|
||||
if result.scalars().first():
|
||||
print("员工数据已存在")
|
||||
return
|
||||
|
||||
# 获取科室ID
|
||||
dept_result = await db.execute(select(Department))
|
||||
departments = {d.code: d.id for d in dept_result.scalars().all()}
|
||||
|
||||
staff_list = [
|
||||
Staff(employee_id="EMP001", name="张三", department_id=departments.get("NK001"), position="主治医师", title="副主任医师", base_salary=8000, performance_ratio=1.2, status=StaffStatus.ACTIVE),
|
||||
Staff(employee_id="EMP002", name="李四", department_id=departments.get("WK001"), position="住院医师", title="主治医师", base_salary=7000, performance_ratio=1.0, status=StaffStatus.ACTIVE),
|
||||
Staff(employee_id="EMP003", name="王五", department_id=departments.get("ZYK001"), position="主任医师", title="主任医师", base_salary=10000, performance_ratio=1.5, status=StaffStatus.ACTIVE),
|
||||
Staff(employee_id="EMP004", name="赵六", department_id=departments.get("JYK001"), position="检验师", title="主管检验师", base_salary=6000, performance_ratio=1.0, status=StaffStatus.ACTIVE),
|
||||
Staff(employee_id="EMP005", name="钱七", department_id=departments.get("CWK001"), position="会计", title="会计师", base_salary=5000, performance_ratio=0.8, status=StaffStatus.ACTIVE),
|
||||
]
|
||||
for staff in staff_list:
|
||||
db.add(staff)
|
||||
await db.flush()
|
||||
print(f"创建了 {len(staff_list)} 个员工")
|
||||
|
||||
|
||||
async def main():
|
||||
"""初始化所有数据"""
|
||||
print("开始初始化数据库...")
|
||||
await init_departments()
|
||||
await init_indicators()
|
||||
await init_sample_staff()
|
||||
await init_admin_user()
|
||||
print("数据库初始化完成!")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
64
backend/app/core/logging_config.py
Normal file
64
backend/app/core/logging_config.py
Normal file
@@ -0,0 +1,64 @@
|
||||
"""
|
||||
Logging configuration module
|
||||
"""
|
||||
import logging
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from logging.handlers import RotatingFileHandler
|
||||
from datetime import datetime
|
||||
|
||||
# Use absolute path - backend directory is parent of app/core
|
||||
BACKEND_DIR = Path(__file__).resolve().parent.parent.parent
|
||||
LOG_DIR = BACKEND_DIR / "logs"
|
||||
|
||||
# Ensure logs directory exists
|
||||
LOG_DIR.mkdir(exist_ok=True)
|
||||
|
||||
# Log file paths
|
||||
current_date = datetime.now().strftime('%Y%m%d')
|
||||
LOG_FILE = LOG_DIR / f"app_{current_date}.log"
|
||||
ERROR_LOG_FILE = LOG_DIR / f"error_{current_date}.log"
|
||||
|
||||
# Log format
|
||||
LOG_FORMAT = "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
|
||||
DATE_FORMAT = "%Y-%m-%d %H:%M:%S"
|
||||
|
||||
# Create logger
|
||||
logger = logging.getLogger("hospital_performance")
|
||||
logger.setLevel(logging.DEBUG)
|
||||
|
||||
# Clear existing handlers
|
||||
logger.handlers.clear()
|
||||
|
||||
# Console handler (INFO level)
|
||||
console_handler = logging.StreamHandler(sys.stdout)
|
||||
console_handler.setLevel(logging.INFO)
|
||||
console_handler.setFormatter(logging.Formatter(LOG_FORMAT, DATE_FORMAT))
|
||||
logger.addHandler(console_handler)
|
||||
|
||||
# File handler (DEBUG level, rotating)
|
||||
file_handler = RotatingFileHandler(
|
||||
LOG_FILE,
|
||||
maxBytes=10*1024*1024, # 10MB
|
||||
backupCount=7, # Keep 7 backups
|
||||
encoding="utf-8"
|
||||
)
|
||||
file_handler.setLevel(logging.DEBUG)
|
||||
file_handler.setFormatter(logging.Formatter(LOG_FORMAT, DATE_FORMAT))
|
||||
logger.addHandler(file_handler)
|
||||
|
||||
# Error file handler (ERROR level)
|
||||
error_handler = RotatingFileHandler(
|
||||
ERROR_LOG_FILE,
|
||||
maxBytes=10*1024*1024,
|
||||
backupCount=7,
|
||||
encoding="utf-8"
|
||||
)
|
||||
error_handler.setLevel(logging.ERROR)
|
||||
error_handler.setFormatter(logging.Formatter(LOG_FORMAT, DATE_FORMAT))
|
||||
logger.addHandler(error_handler)
|
||||
|
||||
|
||||
def get_logger(name: str) -> logging.Logger:
|
||||
"""Get child logger"""
|
||||
return logger.getChild(name)
|
||||
109
backend/app/core/security.py
Normal file
109
backend/app/core/security.py
Normal file
@@ -0,0 +1,109 @@
|
||||
"""
|
||||
安全认证模块
|
||||
"""
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Any, Optional, Annotated
|
||||
from jose import jwt, JWTError
|
||||
import bcrypt
|
||||
from fastapi import Depends, HTTPException, status
|
||||
from fastapi.security import OAuth2PasswordBearer
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select
|
||||
|
||||
from app.core.config import settings
|
||||
from app.core.database import get_db
|
||||
from app.models.models import User
|
||||
|
||||
|
||||
# 密码加密直接使用 bcrypt
|
||||
|
||||
# OAuth2 密码模式
|
||||
oauth2_scheme = OAuth2PasswordBearer(tokenUrl=f"{settings.API_PREFIX}/auth/login")
|
||||
|
||||
|
||||
def verify_password(plain_password: str, hashed_password: str) -> bool:
|
||||
"""验证密码"""
|
||||
return bcrypt.checkpw(plain_password.encode('utf-8'), hashed_password.encode('utf-8'))
|
||||
|
||||
|
||||
def get_password_hash(password: str) -> str:
|
||||
"""生成密码哈希"""
|
||||
return bcrypt.hashpw(password.encode('utf-8'), bcrypt.gensalt()).decode('utf-8')
|
||||
|
||||
|
||||
def create_access_token(subject: str | Any, expires_delta: Optional[timedelta] = None) -> str:
|
||||
"""创建访问令牌"""
|
||||
if expires_delta:
|
||||
expire = datetime.utcnow() + expires_delta
|
||||
else:
|
||||
expire = datetime.utcnow() + timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES)
|
||||
|
||||
to_encode = {"exp": expire, "sub": str(subject)}
|
||||
encoded_jwt = jwt.encode(to_encode, settings.SECRET_KEY, algorithm=settings.ALGORITHM)
|
||||
return encoded_jwt
|
||||
|
||||
|
||||
def decode_token(token: str) -> Optional[dict]:
|
||||
"""解码令牌"""
|
||||
try:
|
||||
payload = jwt.decode(token, settings.SECRET_KEY, algorithms=[settings.ALGORITHM])
|
||||
return payload
|
||||
except JWTError:
|
||||
return None
|
||||
|
||||
|
||||
async def get_current_user(
|
||||
token: Annotated[str, Depends(oauth2_scheme)],
|
||||
db: AsyncSession = Depends(get_db)
|
||||
) -> User:
|
||||
"""从JWT获取当前用户"""
|
||||
credentials_exception = HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="无法验证凭据",
|
||||
headers={"WWW-Authenticate": "Bearer"},
|
||||
)
|
||||
|
||||
payload = decode_token(token)
|
||||
if payload is None:
|
||||
raise credentials_exception
|
||||
|
||||
user_id: str = payload.get("sub")
|
||||
if user_id is None:
|
||||
raise credentials_exception
|
||||
|
||||
result = await db.execute(
|
||||
select(User).where(User.id == int(user_id))
|
||||
)
|
||||
user = result.scalar_one_or_none()
|
||||
|
||||
if user is None:
|
||||
raise credentials_exception
|
||||
|
||||
return user
|
||||
|
||||
|
||||
async def get_current_active_user(
|
||||
current_user: Annotated[User, Depends(get_current_user)]
|
||||
) -> User:
|
||||
"""验证当前用户是否激活"""
|
||||
if not current_user.is_active:
|
||||
raise HTTPException(status_code=400, detail="用户已被禁用")
|
||||
return current_user
|
||||
|
||||
|
||||
async def get_current_admin_user(
|
||||
current_user: Annotated[User, Depends(get_current_active_user)]
|
||||
) -> User:
|
||||
"""验证当前用户是否为管理员"""
|
||||
if current_user.role != "admin":
|
||||
raise HTTPException(status_code=403, detail="需要管理员权限")
|
||||
return current_user
|
||||
|
||||
|
||||
async def get_current_manager_user(
|
||||
current_user: Annotated[User, Depends(get_current_active_user)]
|
||||
) -> User:
|
||||
"""验证当前用户是否为管理员或经理"""
|
||||
if current_user.role not in ("admin", "manager"):
|
||||
raise HTTPException(status_code=403, detail="需要管理员或经理权限")
|
||||
return current_user
|
||||
91
backend/app/main.py
Normal file
91
backend/app/main.py
Normal 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"
|
||||
)
|
||||
2
backend/app/models/__init__.py
Normal file
2
backend/app/models/__init__.py
Normal file
@@ -0,0 +1,2 @@
|
||||
from app.models.models import * # noqa
|
||||
from app.models.finance import * # noqa
|
||||
78
backend/app/models/finance.py
Normal file
78
backend/app/models/finance.py
Normal 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
|
||||
588
backend/app/models/models.py
Normal file
588
backend/app/models/models.py
Normal 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"),
|
||||
)
|
||||
1
backend/app/schemas/__init__.py
Normal file
1
backend/app/schemas/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
from app.schemas.schemas import * # noqa
|
||||
742
backend/app/schemas/schemas.py
Normal file
742
backend/app/schemas/schemas.py
Normal 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
|
||||
0
backend/app/scripts/__init__.py
Normal file
0
backend/app/scripts/__init__.py
Normal file
275
backend/app/scripts/init_templates.py
Normal file
275
backend/app/scripts/init_templates.py
Normal 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())
|
||||
15
backend/app/services/__init__.py
Normal file
15
backend/app/services/__init__.py
Normal 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",
|
||||
]
|
||||
262
backend/app/services/assessment_service.py
Normal file
262
backend/app/services/assessment_service.py
Normal 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
|
||||
149
backend/app/services/department_service.py
Normal file
149
backend/app/services/department_service.py
Normal 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
|
||||
265
backend/app/services/dimension_weight_service.py
Normal file
265
backend/app/services/dimension_weight_service.py
Normal 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
|
||||
367
backend/app/services/finance_service.py
Normal file
367
backend/app/services/finance_service.py
Normal 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
|
||||
196
backend/app/services/indicator_service.py
Normal file
196
backend/app/services/indicator_service.py
Normal 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
|
||||
}
|
||||
]
|
||||
136
backend/app/services/menu_service.py
Normal file
136
backend/app/services/menu_service.py
Normal 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()
|
||||
341
backend/app/services/performance_plan_service.py
Normal file
341
backend/app/services/performance_plan_service.py
Normal 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
|
||||
259
backend/app/services/salary_service.py
Normal file
259
backend/app/services/salary_service.py
Normal 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
|
||||
441
backend/app/services/scoring_service.py
Normal file
441
backend/app/services/scoring_service.py
Normal 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
|
||||
}
|
||||
111
backend/app/services/staff_service.py
Normal file
111
backend/app/services/staff_service.py
Normal 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())
|
||||
299
backend/app/services/stats_service.py
Normal file
299
backend/app/services/stats_service.py
Normal 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}
|
||||
520
backend/app/services/survey_service.py
Normal file
520
backend/app/services/survey_service.py
Normal 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
|
||||
293
backend/app/services/template_service.py
Normal file
293
backend/app/services/template_service.py
Normal 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)
|
||||
33
backend/app/utils/__init__.py
Normal file
33
backend/app/utils/__init__.py
Normal 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}"
|
||||
Reference in New Issue
Block a user