add backend source code

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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