diff --git a/crates/erp-health/src/service/appointment_service.rs b/crates/erp-health/src/service/appointment_service.rs index 1cc2196..f082af9 100644 --- a/crates/erp-health/src/service/appointment_service.rs +++ b/crates/erp-health/src/service/appointment_service.rs @@ -154,10 +154,9 @@ pub async fn create_appointment( let txn = state.db.begin().await?; // 原子 CAS: 排班名额 +1 - // 注意: CAS 按 (tenant_id, doctor_id, schedule_date, start_time) 匹配。 - // 唯一索引在 (tenant_id, doctor_id, schedule_date, period_type) 上。 - // 这要求同一医生同一天同一 start_time 只有一个排班记录,否则 CAS 可能递增错误的行。 - // 当前业务逻辑保证: 每个医生每天每个时间段最多一条排班。 + // CAS 按 (tenant_id, doctor_id, schedule_date) + 时间范围匹配: + // 排班的 start_time <= 预约 start_time 且排班的 end_time >= 预约 end_time, + // 即预约时段落在排班时段内即可。rows_affected 必须恰好为 1(同一医生同一天不应有时段重叠)。 let cas_result = doctor_schedule::Entity::update_many() .col_expr( doctor_schedule::Column::CurrentAppointments, @@ -167,7 +166,8 @@ pub async fn create_appointment( .filter(doctor_schedule::Column::TenantId.eq(tenant_id)) .filter(doctor_schedule::Column::DoctorId.eq(doctor_id_val)) .filter(doctor_schedule::Column::ScheduleDate.eq(req.appointment_date)) - .filter(doctor_schedule::Column::StartTime.eq(req.start_time)) + .filter(doctor_schedule::Column::StartTime.lte(req.start_time)) + .filter(doctor_schedule::Column::EndTime.gte(req.end_time)) .filter( Condition::all() .add(doctor_schedule::Column::DeletedAt.is_null()) @@ -183,6 +183,16 @@ pub async fn create_appointment( txn.rollback().await?; return Err(HealthError::ScheduleFull); } + if cas_result.rows_affected > 1 { + txn.rollback().await?; + tracing::error!( + doctor_id = %doctor_id_val, + date = %req.appointment_date, + matched = cas_result.rows_affected, + "CAS matched multiple schedules — overlapping schedule data" + ); + return Err(HealthError::Validation("排班数据异常:存在重叠时段,请联系管理员".to_string())); + } let now = Utc::now(); let active = appointment::ActiveModel { @@ -554,7 +564,7 @@ pub async fn send_reminders( db: &sea_orm::DatabaseConnection, event_bus: &erp_core::events::EventBus, ) -> crate::error::HealthResult { - use chrono::{Local, NaiveDate}; + use chrono::Local; use serde_json::json; let tomorrow = Local::now().date_naive() + chrono::Duration::days(1);