add backend source code
This commit is contained in:
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)
|
||||
Reference in New Issue
Block a user