前面我们聊过康复报告生成和智能排班系统,这次把视角再拉高一层——从单一功能模块扩展到康复机构的全链路运营数字化。一个真实的康复机构每天要处理预约排班、训练记录、进度追踪、费用结算、医保对账等大量事务,各个环节彼此关联。下面这套系统尝试把这些流程串起来,代码量较大,覆盖核心数据模型、业务流程引擎、费用计算、报表导出等完整链路。
"""
康复机构全链路运营管理系统
===========================
模块覆盖:
1. 患者档案管理(PatientProfile)
2. 疗程计划制定(TreatmentPlan)
3. 预约与排班(Appointment & Schedule)
4. 训练会话记录(SessionRecord)
5. 进度评估与报告(ProgressAssessment)
6. 费用结算与医保对账(Billing & Insurance)
7. 运营报表导出(Reporting)
"""
import csv
import json
import uuid
import logging
import statistics
from dataclasses import dataclass, field, asdict
from datetime import datetime, date, timedelta, time
from decimal import Decimal, ROUND_HALF_UP
from enum import Enum
from typing import List, Dict, Optional, Tuple, Any, Set
from collections import defaultdict
from pathlib import Path
# ==================== 日志配置 ====================
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
)
logger = logging.getLogger(__name__)
# ==================== 枚举与常量 ====================
class ConditionType(Enum): 28088.baike.tongsou.com
ARTICULATION = "articulation_disorder" # 构音障碍
FLUENCY = "fluency_disorder" # 流畅性障碍(口吃)
VOICE = "voice_disorder" # 嗓音障碍
LANGUAGE_DELAY = "language_development_delay" # 语言发育迟缓
HEARING = "hearing_impairment" # 听力障碍相关
SWALLOWING = "swallowing_disorder" # 吞咽障碍
OTHER = "other"
class SeverityLevel(Enum): 28087.baike.tongsou.com
MILD = "mild" # 轻度
MODERATE = "moderate" # 中度
SEVERE = "severe" # 重度
PROFOUND = "profound" # 极重度
class SessionStatus(Enum): 28086.baike.tongsou.com
SCHEDULED = "scheduled" # 已预约
CONFIRMED = "confirmed" # 已确认
COMPLETED = "completed" # 已完成
CANCELLED = "cancelled" # 已取消
NO_SHOW = "no_show" # 未到
class PaymentMethod(Enum): 28085.baike.tongsou.com
CASH = "cash"
CARD = "card"
WECHAT = "wechat"
ALIPAY = "alipay"
INSURANCE = "insurance" # 医保结算
PACKAGE = "package" # 套餐扣次
class InvoiceType(Enum): 28084.baike.tongsou.com
PERSONAL = "personal" # 个人发票
COMPANY = "company" # 企业发票
# 收费标准(示例)
SESSION_PRICES = {
ConditionType.ARTICULATION: Decimal("200.00"),
ConditionType.FLUENCY: Decimal("220.00"),
ConditionType.VOICE: Decimal("250.00"),
ConditionType.LANGUAGE_DELAY: Decimal("180.00"),
ConditionType.HEARING: Decimal("200.00"),
ConditionType.SWALLOWING: Decimal("300.00"),
ConditionType.OTHER: Decimal("200.00"),
}
# 医保报销比例(示例)
INSURANCE_REIMBURSE_RATE = Decimal("0.70")
# ==================== 数据模型 ====================
@dataclass
class ContactInfo: 28083.baike.tongsou.com
"""联系方式"""
phone: str
email: Optional[str] = None
address: Optional[str] = None
emergency_contact: Optional[str] = None
emergency_phone: Optional[str] = None
@dataclass
class GuardianInfo: 28082.baike.tongsou.com
"""监护人信息(未成年患者)"""
name: str
relation: str # 与患者关系:父亲/母亲/其他
phone: str
id_number: Optional[str] = None
@dataclass
class MedicalHistory: 28081.baike.tongsou.com
"""病史信息"""
diagnosis: str
diagnosis_date: Optional[date] = None
referring_doctor: Optional[str] = None
hospital: Optional[str] = None
previous_therapy: bool = False
previous_therapy_details: Optional[str] = None
comorbidities: List[str] = field(default_factory=list)
medications: List[str] = field(default_factory=list)
allergies: List[str] = field(default_factory=list)
@dataclass
class PatientProfile:
"""患者档案"""
patient_id: str
name: str
gender: str
birth_date: date
age: int
condition_type: ConditionType
severity: SeverityLevel
contact: ContactInfo
guardian: Optional[GuardianInfo] = None
medical_history: Optional[MedicalHistory] = None
insurance_number: Optional[str] = None # 医保卡号
insurance_type: Optional[str] = None # 医保类型
notes: str = ""
registered_date: date = field(default_factory=date.today)
status: str = "active" # active / paused / discharged
@property
def is_minor(self) -> bool: 28080.baike.tongsou.com
return self.age < 18
def age_at(self, ref_date: date) -> int: 28079.baike.tongsou.com
delta = ref_date.year - self.birth_date.year
if (ref_date.month, ref_date.day) < (self.birth_date.month, self.birth_date.day):
delta -= 1
return delta
def to_dict(self) -> Dict[str, Any]: 28078.baike.tongsou.com
d = asdict(self)
d["birth_date"] = self.birth_date.isoformat(28077.baike.tongsou.com)
d["registered_date"] = self.registered_date.isoformat(27058.baike.tongsou.com)
d["condition_type"] = self.condition_type.value
d["severity"] = self.severity.value
return d
@dataclass
class TrainingGoal: 27057.baike.tongsou.com
"""训练目标"""
goal_id: str
description: str
domain: str # 如"语音清晰度"、"构音运动"、"语言理解"
baseline_value: Optional[float] = None # 基线值
target_value: Optional[float] = None # 目标值
unit: str = "" # 单位,如"%"、"分"
deadline: Optional[date] = None
achieved: bool = False
achieved_date: Optional[date] = None
@dataclass
class TreatmentPlan: 26069.baike.tongsou.com
"""疗程计划"""
plan_id: str
patient_id: str
therapist_id: str
start_date: date
end_date: date
session_frequency: int # 每周次数
estimated_total_sessions: int # 预估总次数
goals: List[TrainingGoal] = field(default_factory=list)
recommended_methods: List[str] = field(default_factory=list)
notes: str = ""
status: str = "active" # active / completed / terminated
def to_dict(self) -> Dict[str, Any]: 26065.baike.tongsou.com
d = asdict(self)
d["start_date"] = self.start_date.isoformat()
d["end_date"] = self.end_date.isoformat()
d["goals"] = [asdict(g) for g in self.goals]
for g in d["goals"]: 26066.baike.tongsou.com
if g.get("deadline"): 26062.baike.tongsou.com
g["deadline"] = g["deadline"].isoformat()
if g.get("achieved_date"): 26060.baike.tongsou.com
g["achieved_date"] = g["achieved_date"].isoformat()
return d
@dataclass
class Appointment: 26059.baike.tongsou.com
"""预约记录"""
appointment_id: str
patient_id: str
therapist_id: str
room_id: str
scheduled_time: datetime
duration_minutes: int
session_type: str = "individual"
status: SessionStatus = SessionStatus.SCHEDULED
notes: str = ""
created_at: datetime = field(default_factory=datetime.now)
def to_dict(self) -> Dict[str, Any]: 26058.baike.tongsou.com
d = asdict(self)
d["scheduled_time"] = self.scheduled_time.isoformat()
d["created_at"] = self.created_at.isoformat()
d["status"] = self.status.value
return d
@dataclass
class SessionRecord: 26055.baike.tongsou.com
"""训练会话记录"""
record_id: str
appointment_id: str
patient_id: str
therapist_id: str
session_date: datetime
duration_actual: int # 实际时长(分钟)
content_covered: List[str] # 训练内容
patient_performance: str # 患者表现描述
homework_assigned: List[str] = field(default_factory=list)
next_session_focus: str = ""
attachments: List[str] = field(default_factory=list) # 录音/视频文件路径
def to_dict(self) -> Dict[str, Any]: 26057.baike.tongsou.com
d = asdict(self)
d["session_date"] = self.session_date.isoformat(26054.baike.tongsou.com)
return d
@dataclass
class ProgressAssessment: 26053.baike.tongsou.com
"""进度评估"""
assessment_id: str
patient_id: str
therapist_id: str
assessment_date: date
assessment_type: str # "中期评估" / "结课评估" / "月度评估"
scores: Dict[str, float] # 各项评分
summary: str
goal_updates: List[Dict[str, Any]] = field(default_factory=list)
recommendations: List[str] = field(default_factory=list)
def to_dict(self) -> Dict[str, Any]: 26052.baike.tongsou.com
d = asdict(self)
d["assessment_date"] = self.assessment_date.isoformat(26051.baike.tongsou.com)
return d
@dataclass
class BillingItem: 26050.baike.tongsou.com
"""账单明细"""
item_id: str
patient_id: str
description: str
session_date: Optional[date] = None
unit_price: Decimal
quantity: int = 1
discount: Decimal = Decimal("0.00")
subtotal: Decimal = field(init=False)
def __post_init__(self): 26049.baike.tongsou.com
self.subtotal = (self.unit_price * self.quantity) - self.discount
def to_dict(self) -> Dict[str, Any]: 26048.baike.tongsou.com
return {
"item_id": self.item_id,
"patient_id": self.patient_id,
"description": self.description,
"session_date": self.session_date.isoformat() if self.session_date else None,
"unit_price": str(self.unit_price),
"quantity": self.quantity,
"discount": str(self.discount),
"subtotal": str(self.subtotal),
}
@dataclass
class Invoice: 26047.baike.tongsou.com
"""发票"""
invoice_id: str
patient_id: str
invoice_type: InvoiceType
company_name: Optional[str] = None
tax_id: Optional[str] = None
items: List[BillingItem] = field(default_factory=list)
total_amount: Decimal = field(init=False)
issued_date: date = field(default_factory=date.today)
status: str = "issued" # issued / cancelled
def __post_init__(self): 26046.baike.tongsou.com
self.total_amount = sum(item.subtotal for item in self.items)
def to_dict(self) -> Dict[str, Any]: 26043.baike.tongsou.com
return {
"invoice_id": self.invoice_id,
"patient_id": self.patient_id,
"invoice_type": self.invoice_type.value,
"company_name": self.company_name,
"tax_id": self.tax_id,
"items": [item.to_dict() for item in self.items],
"total_amount": str(self.total_amount),
"issued_date": self.issued_date.isoformat(26042.baike.tongsou.com),
"status": self.status,
}
@dataclass
class InsuranceClaim: 26041.baike.tongsou.com
"""医保报销申请"""
claim_id: str
patient_id: str
invoice_id: str
claim_amount: Decimal
reimbursed_amount: Decimal = Decimal("0.00")
status: str = "pending" # pending / approved / rejected / paid
submitted_date: date = field(default_factory=date.today)
processed_date: Optional[date] = None
rejection_reason: Optional[str] = None
def to_dict(self) -> Dict[str, Any]: 26040.baike.tongsou.com
return {
"claim_id": self.claim_id,
"patient_id": self.patient_id,
"invoice_id": self.invoice_id,
"claim_amount": str(self.claim_amount),
"reimbursed_amount": str(self.reimbursed_amount),
"status": self.status,
"submitted_date": self.submitted_date.isoformat(26039.baike.tongsou.com),
"processed_date": self.processed_date.isoformat() if self.processed_date else None,
"rejection_reason": self.rejection_reason,
}
@dataclass
class PaymentRecord: 26038.baike.tongsou.com
"""支付记录"""
payment_id: str
patient_id: str
invoice_id: str
amount: Decimal
method: PaymentMethod
paid_at: datetime = field(default_factory=datetime.now)
transaction_id: Optional[str] = None
notes: str = ""
def to_dict(self) -> Dict[str, Any]: 26037.baike.tongsou.com
return {
"payment_id": self.payment_id,
"patient_id": self.patient_id,
"invoice_id": self.invoice_id,
"amount": str(self.amount),
"method": self.method.value,
"paid_at": self.paid_at.isoformat(),
"transaction_id": self.transaction_id,
"notes": self.notes,
}
# ==================== 患者管理服务 ====================
class PatientService: 26036.baike.tongsou.com
"""患者档案管理"""
def __init__(self): 26035.baike.tongsou.com
self.patients: Dict[str, PatientProfile] = {}
self.logger = logging.getLogger(__name__)
def register_patient(self, profile: PatientProfile) -> str:
if profile.patient_id in self.patients: 26034.baike.tongsou.com
raise ValueError(f"患者ID {profile.patient_id} 已存在")
self.patients[profile.patient_id] = profile
self.logger.info(f"新患者注册: {profile.name} ({profile.patient_id})")
return profile.patient_id
def get_patient(self, patient_id: str) -> Optional[PatientProfile]:
return self.patients.get(patient_id)
def update_patient(self, patient_id: str, **kwargs) -> bool: 26033.baike.tongsou.com
patient = self.patients.get(patient_id)
if not patient: 26032.baike.tongsou.com
return False
for key, value in kwargs.items(): 26031.baike.tongsou.com
if hasattr(patient, key):
setattr(patient, key, value)
self.logger.info(f"患者信息更新: {patient_id}")
return True
def list_patients(
self,
condition_type: Optional[ConditionType] = None,
status: str = "active",
) -> List[PatientProfile]: 26030.baike.tongsou.com
results = list(self.patients.values())
if condition_type:
results = [p for p in results if p.condition_type == condition_type]
if status:
results = [p for p in results if p.status == status]
return results
def export_patients_csv(self, filepath: str) -> int:
patients = list(self.patients.values())
if not patients: 26029.baike.tongsou.com
return 0
keys = ["patient_id", "name", "gender", "age", "condition_type",
"severity", "status", "registered_date"]
with open(filepath, "w", newline="", encoding="utf-8-sig") as f:
writer = csv.DictWriter(f, fieldnames=keys)
writer.writeheader(26026.baike.tongsou.com)
for p in patients: 26028.baike.tongsou.com
row = {k: getattr(p, k) for k in keys}
row["condition_type"] = row["condition_type"].value
row["severity"] = row["severity"].value
row["registered_date"] = row["registered_date"].isoformat(26025.baike.tongsou.com)
writer.writerow(row)
self.logger.info(f"导出 {len(patients)} 名患者到 {filepath}")
return len(patients)
# ==================== 疗程计划服务 ====================
class TreatmentPlanService:
"""疗程计划管理"""
def __init__(self): baike.kuaisou.com/sitemap
self.plans: Dict[str, TreatmentPlan] = {}
self.plans_by_patient: Dict[str, List[str]] = defaultdict(list)
self.logger = logging.getLogger(__name__)
def create_plan(
self,
patient_id: str,
therapist_id: str,
start_date: date,
weeks: int,
frequency: int,
goals: Optional[List[TrainingGoal]] = None,
) -> TreatmentPlan: 26145.baike.tongsou.com
plan_id = f"PLAN-{uuid.uuid4().hex[:8].upper()}"
end_date = start_date + timedelta(weeks=weeks)
estimated = weeks * frequency
plan = TreatmentPlan(
plan_id=plan_id,
patient_id=patient_id,
therapist_id=therapist_id,
start_date=start_date,
end_date=end_date,
session_frequency=frequency,
estimated_total_sessions=estimated,
goals=goals or [],
)
self.plans[plan_id] = plan
self.plans_by_patient[patient_id].append(plan_id)
self.logger.info(
f"创建疗程计划 {plan_id} for 患者 {patient_id}, "
f"{weeks}周 x {frequency}次/周 = {estimated}次"
)
return plan
def get_plan(self, plan_id: str) -> Optional[TreatmentPlan]:
return self.plans.get(plan_id)
def get_active_plan(self, patient_id: str) -> Optional[TreatmentPlan]:
for pid in self.plans_by_patient.get(patient_id, []):
plan = self.plans[pid]
if plan.status == "active": 22021.baike.tongsou.com
return plan
return None
def add_goal(self, plan_id: str, goal: TrainingGoal) -> bool:
plan = self.plans.get(plan_id)
if not plan: 22016.baike.tongsou.com
return False
plan.goals.append(goal)
return True
def complete_plan(self, plan_id: str) -> bool: 27054.baike.tongsou.com
plan = self.plans.get(plan_id)
if not plan:
return False
plan.status = "completed"
self.logger.info(f"疗程计划 {plan_id} 已完成")
return True
# ==================== 预约与排班服务 ====================
class AppointmentService: 27053.baike.tongsou.com
"""预约与排班管理"""
def __init__(self): 27027.baike.tongsou.com
self.appointments: Dict[str, Appointment] = {}
self.appointments_by_patient: Dict[str, List[str]] = defaultdict(list)
self.appointments_by_therapist: Dict[str, List[str]] = defaultdict(list)
self.logger = logging.getLogger(__name__)
def book_appointment(
self,
patient_id: str,
therapist_id: str,
room_id: str,
scheduled_time: datetime,
duration: int = 45,
session_type: str = "individual",
) -> Appointment: 27025.baike.tongsou.com
appt_id = f"APPT-{uuid.uuid4().hex[:8].upper()}"
# 简单冲突检查
if self._has_conflict(therapist_id, room_id, scheduled_time, duration):
raise ValueError(
f"时间冲突: 治疗师 {therapist_id} 或治疗室 {room_id} "
f"在 {scheduled_time} 已被占用"
)
appt = Appointment(
appointment_id=appt_id,
patient_id=patient_id,
therapist_id=therapist_id,
room_id=room_id,
scheduled_time=scheduled_time,
duration_minutes=duration,
session_type=session_type,
status=SessionStatus.SCHEDULED,
)
self.appointments[appt_id] = appt
self.appointments_by_patient[patient_id].append(appt_id)
self.appointments_by_therapist[therapist_id].append(appt_id)
self.logger.info(f"预约创建: {appt_id} @ {scheduled_time}")
return appt
def _has_conflict(
self,
therapist_id: str,
room_id: str,
scheduled_time: datetime,
duration: int,
) -> bool: 27024.baike.tongsou.com
end_time = scheduled_time + timedelta(minutes=duration)
for appt in self.appointments.values(): 27016.baike.tongsou.com
if appt.status in (SessionStatus.CANCELLED, SessionStatus.NO_SHOW):
continue
appt_end = appt.scheduled_time + timedelta(minutes=appt.duration_minutes)
# 时间重叠判断
if scheduled_time < appt_end and appt.scheduled_time < end_time:
if appt.therapist_id == therapist_id or appt.room_id == room_id:
return True
return False
def confirm_appointment(self, appt_id: str) -> bool: 27014.baike.tongsou.com
appt = self.appointments.get(appt_id)
if not appt: 27013.baike.tongsou.com
return False
appt.status = SessionStatus.CONFIRMED
return True
def cancel_appointment(self, appt_id: str, reason: str = "") -> bool:
appt = self.appointments.get(appt_id)
if not appt: 27011.baike.tongsou.com
return False
appt.status = SessionStatus.CANCELLED
appt.notes = f"取消原因: {reason}"
self.logger.info(f"预约取消: {appt_id}, 原因: {reason}")
return True
def mark_completed(self, appt_id: str) -> bool:
appt = self.appointments.get(appt_id)
if not appt: 27009.baike.tongsou.com
return False
appt.status = SessionStatus.COMPLETED原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。