Files
medical-mall/mall_sql/migrations/20260610_optimize_dispatch_conflict_check_FINAL.sql

565 lines
20 KiB
PL/PgSQL
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
-- ============================================================================
-- 浼樺寲娲惧崟 RPC锛氫慨澶嶆椂闂村啿绐佹鏌ヨ繃浜庝弗鏍肩殑闂
-- 鎵ц鏃堕棿: 2026-06-10
-- 闂: 褰撹鍗?scheduled_start_at/scheduled_end_at 涓?NULL 鏃讹紝
-- 浠讳綍鏈夋椿璺冧换鍔$殑浜哄憳閮戒細琚帓闄わ紝鍗充娇鏃堕棿鏍规湰涓嶅啿绐?
-- ============================================================================
CREATE OR REPLACE FUNCTION public.rpc_homecare_auto_dispatch_optimized(
p_order_id TEXT DEFAULT NULL
)
RETURNS JSONB
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = ''
AS $function$
DECLARE
v_now TIMESTAMPTZ := clock_timestamp();
v_current_user_id UUID;
v_order public.hss_service_orders%ROWTYPE;
v_existing_assignment RECORD;
v_candidate RECORD;
v_candidate_found BOOLEAN := FALSE;
v_assignment_id TEXT;
v_status_log_id TEXT;
v_attempt_log_id TEXT;
v_failure_code TEXT;
v_failure_message TEXT;
v_retryable BOOLEAN := FALSE;
BEGIN
-- A. 鍩虹鍙傛暟涓庤韩浠芥牎楠岋紙鍚屽師鍑芥暟锛?
IF p_order_id IS NULL OR btrim(p_order_id) = '' THEN
RETURN jsonb_build_object(
'success', FALSE,
'code', 'ORDER_ID_REQUIRED',
'message', '璁㈠崟ID涓嶈兘涓虹┖',
'display_type', 'toast',
'retryable', FALSE
);
END IF;
IF auth.uid() IS NULL THEN
RETURN jsonb_build_object(
'success', FALSE,
'code', 'UNAUTHENTICATED',
'message', '鐧诲綍鐘舵€佸凡澶辨晥锛岃閲嶆柊鐧诲綍鍚庡啀鎿嶄綔',
'display_type', 'modal',
'retryable', FALSE
);
END IF;
SELECT u.id
INTO v_current_user_id
FROM public.ak_users u
WHERE u.auth_id = auth.uid()
LIMIT 1;
IF v_current_user_id IS NULL THEN
RETURN jsonb_build_object(
'success', FALSE,
'code', 'USER_PROFILE_NOT_FOUND',
'message', '褰撳墠璐︽埛淇℃伅寮傚父锛岃閲嶆柊鐧诲綍鎴栬仈绯诲鏈?,
'display_type', 'modal',
'retryable', FALSE
);
END IF;
-- B. 閿佸畾璁㈠崟
SELECT o.*
INTO v_order
FROM public.hss_service_orders o
WHERE o.id = p_order_id
AND o.deleted_at IS NULL
FOR UPDATE OF o;
IF NOT FOUND THEN
RETURN jsonb_build_object(
'success', FALSE,
'code', 'ORDER_NOT_FOUND',
'message', '',
'display_type', 'modal',
'retryable', FALSE
);
END IF;
IF v_order.user_id IS DISTINCT FROM v_current_user_id THEN
RETURN jsonb_build_object(
'success', FALSE,
'code', 'ORDER_ACCESS_DENIED',
'message', '',
'display_type', 'modal',
'retryable', FALSE
);
END IF;
-- C. 骞傜瓑澶勭悊
IF v_order.status = 'assigned'
AND v_order.current_assignment_id IS NOT NULL THEN
RETURN jsonb_build_object(
'success', TRUE,
'code', 'ALREADY_ASSIGNED',
'message', '?,
'display_type', 'none',
'retryable', FALSE,
'dispatch_status', 'assigned',
'order_id', p_order_id,
'assignment_id', v_order.current_assignment_id,
'staff_id', v_order.current_staff_id
);
END IF;
-- D. 鏀粯涓庤鍗曠姸鎬佹牎楠?
IF COALESCE(v_order.payment_status, 1) <> 2 THEN
RETURN jsonb_build_object(
'success', FALSE,
'code', 'ORDER_NOT_PAID',
'message', '璁㈠崟灏氭湭瀹屾垚鏀粯锛屾殏涓嶈兘瀹夋帓鏈嶅姟浜哄憳',
'display_type', 'modal',
'retryable', FALSE,
'dispatch_status', COALESCE(v_order.dispatch_status, 'pending'),
'order_id', p_order_id
);
END IF;
IF v_order.status NOT IN ('created', 'paid') THEN
RETURN jsonb_build_object(
'success', FALSE,
'code', 'ORDER_STATUS_NOT_DISPATCHABLE',
'message', '褰撳墠璁㈠崟鐘舵€佷笉鍏佽閲嶆柊娲惧崟',
'display_type', 'modal',
'retryable', FALSE,
'dispatch_status', COALESCE(v_order.dispatch_status, 'pending'),
'order_id', p_order_id
);
END IF;
-- E. 鍏煎鍘嗗彶鏁版嵁
SELECT
a.id,
a.staff_id,
a.station_id
INTO v_existing_assignment
FROM public.hss_service_assignments a
WHERE a.order_id = p_order_id
AND a.deleted_at IS NULL
AND a.status IN ('assigned', 'accepted', 'departed', 'arrived', 'serving', 'in_service')
ORDER BY a.assigned_at DESC
LIMIT 1;
IF FOUND THEN
UPDATE public.hss_service_orders
SET status = 'assigned',
dispatch_status = 'assigned',
current_assignment_id = v_existing_assignment.id,
current_staff_id = v_existing_assignment.staff_id,
dispatch_error_code = NULL,
dispatch_error_message = NULL,
dispatch_failed_at = NULL,
updated_at = v_now
WHERE id = p_order_id;
RETURN jsonb_build_object(
'success', TRUE,
'code', 'ALREADY_ASSIGNED_RECOVERED',
'message', '璁㈠崟宸插瓨鍦ㄦ湁鏁堟淳鍗曡褰曪紝绯荤粺宸插畬鎴愮姸鎬佸悓姝?,
'display_type', 'none',
'retryable', FALSE,
'dispatch_status', 'assigned',
'order_id', p_order_id,
'assignment_id', v_existing_assignment.id,
'staff_id', v_existing_assignment.staff_id,
'station_id', COALESCE(v_existing_assignment.station_id::text, '')
);
END IF;
-- F. 杩涘叆娲惧崟涓姸鎬?
UPDATE public.hss_service_orders
SET dispatch_status = 'dispatching',
dispatch_attempt_count = COALESCE(dispatch_attempt_count, 0) + 1,
dispatch_error_code = NULL,
dispatch_error_message = NULL,
dispatch_failed_at = NULL,
updated_at = v_now
WHERE id = p_order_id;
-- G. 銆愭牳蹇冧紭鍖栥€戞煡鎵惧彲娲惧崟鏈嶅姟浜哄憳
-- 鍏抽敭淇锛氬彧鏈夊綋璁㈠崟鏈夋槑纭殑鏃堕棿鑼冨洿鏃讹紝鎵嶆鏌ユ椂闂村啿绐?
-- 濡傛灉璁㈠崟娌℃湁 scheduled_start_at/scheduled_end_at锛屽垯涓嶆鏌ユ椂闂村啿绐?
SELECT
s.id,
s.station_id,
distance_calc.distance_km
INTO v_candidate
FROM public.ml_delivery_staff s
CROSS JOIN LATERAL (
SELECT
CASE
WHEN v_order.service_lat IS NULL
OR v_order.service_lng IS NULL
OR s.current_lat IS NULL
OR s.current_lng IS NULL
THEN NULL
ELSE (
6371 * 2 * asin(
sqrt(
power(
sin(
radians((s.current_lat - v_order.service_lat)::DOUBLE PRECISION) / 2
),
2
)
+
cos(radians(v_order.service_lat::DOUBLE PRECISION))
* cos(radians(s.current_lat::DOUBLE PRECISION))
* power(
sin(
radians((s.current_lng - v_order.service_lng)::DOUBLE PRECISION) / 2
),
2
)
)
)
)::NUMERIC(8,3)
END AS distance_km
) distance_calc
WHERE s.deleted_at IS NULL
AND s.status = 1
AND COALESCE(s.is_active, TRUE) = TRUE
AND s.online_status = 'online'
AND s.uid IS NOT NULL
AND (
v_order.dispatch_station_id IS NULL
OR s.station_id::TEXT = v_order.dispatch_station_id
)
AND (
v_order.required_qualification_code IS NULL
OR EXISTS (
SELECT 1
FROM public.hc_worker_qualifications q
WHERE q.staff_id::TEXT = s.id::TEXT
AND q.qualification_code = v_order.required_qualification_code
AND q.deleted_at IS NULL
AND q.status::TEXT IN ('1', 'active', 'approved', 'valid')
AND (q.valid_from IS NULL OR q.valid_from <= v_now)
AND (q.valid_until IS NULL OR q.valid_until >= v_now)
)
)
AND (
v_order.service_lat IS NULL
OR v_order.service_lng IS NULL
OR (
s.current_lat IS NOT NULL
AND s.current_lng IS NOT NULL
AND distance_calc.distance_km <= COALESCE(s.dispatch_radius_km, 20)
)
)
-- 銆愪慨澶嶇偣銆戝彧鏈夊綋璁㈠崟鏈夋槑纭椂闂磋寖鍥存椂锛屾墠妫€鏌ユ椂闂村啿绐?
AND (
v_order.scheduled_start_at IS NULL
OR v_order.scheduled_end_at IS NULL
OR NOT EXISTS (
SELECT 1
FROM public.hss_service_assignments existing_assignment
JOIN public.hss_service_orders existing_order
ON existing_order.id = existing_assignment.order_id
WHERE existing_assignment.staff_id::TEXT = s.id::TEXT
AND existing_assignment.deleted_at IS NULL
AND existing_assignment.status IN (
'assigned',
'accepted',
'departed',
'arrived',
'serving',
'in_service'
)
AND existing_order.scheduled_start_at IS NOT NULL
AND existing_order.scheduled_end_at IS NOT NULL
AND tstzrange(
existing_order.scheduled_start_at,
existing_order.scheduled_end_at,
'[)'
) && tstzrange(
v_order.scheduled_start_at,
v_order.scheduled_end_at,
'[)'
)
)
)
ORDER BY
CASE
WHEN v_order.dispatch_station_id IS NOT NULL
AND s.station_id::TEXT = v_order.dispatch_station_id
THEN 0
ELSE 1
END,
distance_calc.distance_km ASC NULLS LAST,
COALESCE(s.last_dispatched_at, '-infinity'::TIMESTAMPTZ) ASC,
COALESCE(s.updated_at, s.created_at) DESC
LIMIT 1
FOR UPDATE OF s;
v_candidate_found := FOUND;
-- H. 娌℃湁鎵惧埌鍊欓€変汉鍛樻椂锛岃瘑鍒叿浣撳け璐ュ師鍥?
IF NOT v_candidate_found THEN
IF NOT EXISTS (
SELECT 1
FROM public.ml_delivery_staff s
WHERE s.deleted_at IS NULL
AND s.status = 1
AND COALESCE(s.is_active, TRUE) = TRUE
AND s.online_status = 'online'
AND s.uid IS NOT NULL
) THEN
v_failure_code := 'NO_ONLINE_STAFF';
v_failure_message := '';
v_retryable := TRUE;
ELSIF v_order.dispatch_station_id IS NOT NULL
AND NOT EXISTS (
SELECT 1
FROM public.ml_delivery_staff s
WHERE s.deleted_at IS NULL
AND s.status = 1
AND COALESCE(s.is_active, TRUE) = TRUE
AND s.online_status = 'online'
AND s.uid IS NOT NULL
AND s.station_id::TEXT = v_order.dispatch_station_id
) THEN
v_failure_code := 'NO_STAFF_IN_SERVICE_STATION';
v_failure_message := '?;
v_retryable := TRUE;
ELSIF v_order.required_qualification_code IS NOT NULL
AND NOT EXISTS (
SELECT 1
FROM public.ml_delivery_staff s
WHERE s.deleted_at IS NULL
AND s.status = 1
AND COALESCE(s.is_active, TRUE) = TRUE
AND s.online_status = 'online'
AND s.uid IS NOT NULL
AND (
v_order.dispatch_station_id IS NULL
OR s.station_id::TEXT = v_order.dispatch_station_id
)
AND EXISTS (
SELECT 1
FROM public.hc_worker_qualifications q
WHERE q.staff_id::TEXT = s.id::TEXT
AND q.qualification_code = v_order.required_qualification_code
AND q.deleted_at IS NULL
AND q.status::TEXT IN ('1', 'active', 'approved', 'valid')
AND (q.valid_from IS NULL OR q.valid_from <= v_now)
AND (q.valid_until IS NULL OR q.valid_until >= v_now)
)
) THEN
v_failure_code := 'NO_QUALIFIED_STAFF';
v_failure_message := '褰撳墠鏆傛棤鍏峰璇ユ湇鍔¤祫璐ㄧ殑鏈嶅姟浜哄憳锛岃绋嶅悗閲嶈瘯鎴栬仈绯诲鏈?;
v_retryable := TRUE;
ELSIF v_order.service_lat IS NOT NULL
AND v_order.service_lng IS NOT NULL
AND NOT EXISTS (
SELECT 1
FROM public.ml_delivery_staff s
CROSS JOIN LATERAL (
SELECT
(
6371 * 2 * asin(
sqrt(
power(
sin(
radians((s.current_lat - v_order.service_lat)::DOUBLE PRECISION) / 2
),
2
)
+
cos(radians(v_order.service_lat::DOUBLE PRECISION))
* cos(radians(s.current_lat::DOUBLE PRECISION))
* power(
sin(
radians((s.current_lng - v_order.service_lng)::DOUBLE PRECISION) / 2
),
2
)
)
)
)::NUMERIC(8,3) AS distance_km
) distance_calc
WHERE s.deleted_at IS NULL
AND s.status = 1
AND COALESCE(s.is_active, TRUE) = TRUE
AND s.online_status = 'online'
AND s.uid IS NOT NULL
AND s.current_lat IS NOT NULL
AND s.current_lng IS NOT NULL
AND (
v_order.dispatch_station_id IS NULL
OR s.station_id::TEXT = v_order.dispatch_station_id
)
AND (
v_order.required_qualification_code IS NULL
OR EXISTS (
SELECT 1
FROM public.hc_worker_qualifications q
WHERE q.staff_id::TEXT = s.id::TEXT
AND q.qualification_code = v_order.required_qualification_code
AND q.deleted_at IS NULL
AND q.status::TEXT IN ('1', 'active', 'approved', 'valid')
AND (q.valid_from IS NULL OR q.valid_from <= v_now)
AND (q.valid_until IS NULL OR q.valid_until >= v_now)
)
)
AND distance_calc.distance_km <= COALESCE(s.dispatch_radius_km, 20)
) THEN
v_failure_code := 'NO_NEARBY_STAFF';
v_failure_message := '';
v_retryable := TRUE;
ELSE
v_failure_code := 'ALL_ELIGIBLE_STAFF_BUSY';
v_failure_message := '';
v_retryable := TRUE;
END IF;
UPDATE public.hss_service_orders
SET dispatch_status = 'failed',
dispatch_error_code = v_failure_code,
dispatch_error_message = v_failure_message,
dispatch_failed_at = v_now,
updated_at = v_now
WHERE id = p_order_id;
v_attempt_log_id :=
'dalog-'
|| floor(extract(epoch FROM v_now) * 1000)::BIGINT::TEXT
|| '-'
|| upper(substr(md5(p_order_id || ':' || random()::TEXT), 1, 10));
INSERT INTO public.hss_service_dispatch_attempt_logs (
id,
order_id,
requested_by_user_id,
selected_staff_id,
selected_station_id,
success,
result_code,
result_message,
retryable,
filters_snapshot,
created_at
) VALUES (
v_attempt_log_id,
p_order_id,
v_current_user_id,
NULL,
v_order.dispatch_station_id,
FALSE,
v_failure_code,
v_failure_message,
v_retryable,
jsonb_build_object(
'required_qualification_code', v_order.required_qualification_code,
'dispatch_station_id', v_order.dispatch_station_id,
'service_lat', v_order.service_lat,
'service_lng', v_order.service_lng,
'scheduled_start_at', v_order.scheduled_start_at,
'scheduled_end_at', v_order.scheduled_end_at
),
v_now
);
RETURN jsonb_build_object(
'success', FALSE,
'code', v_failure_code,
'message', v_failure_message,
'display_type', 'modal',
'retryable', v_retryable,
'dispatch_status', 'failed',
'order_id', p_order_id
);
END IF;
-- I. 娲惧崟鎴愬姛锛屽垱寤烘淳鍗曡褰?
v_assignment_id :=
'dsa-'
|| floor(extract(epoch FROM v_now) * 1000)::BIGINT::TEXT
|| '-'
|| upper(substr(md5(p_order_id || ':' || v_candidate.id::TEXT), 1, 10));
INSERT INTO public.hss_service_assignments (
id,
order_id,
staff_id,
station_id,
status,
assigned_at,
created_at
) VALUES (
v_assignment_id,
p_order_id,
v_candidate.id,
v_candidate.station_id,
'assigned',
v_now,
v_now
);
UPDATE public.hss_service_orders
SET status = 'assigned',
dispatch_status = 'assigned',
current_assignment_id = v_assignment_id,
current_staff_id = v_candidate.id,
updated_at = v_now
WHERE id = p_order_id;
v_status_log_id :=
'slg-'
|| floor(extract(epoch FROM v_now) * 1000)::BIGINT::TEXT
|| '-'
|| upper(substr(md5(p_order_id || ':assigned'), 1, 10));
INSERT INTO public.hss_service_order_status_logs (
id,
order_id,
from_status,
to_status,
operator_role,
operator_id,
remark,
created_at
) VALUES (
v_status_log_id,
p_order_id,
'created',
'assigned',
'system',
v_current_user_id,
'',
v_now
);
RETURN jsonb_build_object(
'success', TRUE,
'code', 'DISPATCH_ASSIGNED',
'message', '?,
'display_type', 'none',
'retryable', FALSE,
'dispatch_status', 'assigned',
'order_id', p_order_id,
'assignment_id', v_assignment_id,
'staff_id', v_candidate.id::TEXT,
'station_id', COALESCE(v_candidate.station_id::TEXT, '')
);
END;
$function$;
COMMENT ON FUNCTION public.rpc_homecare_auto_dispatch_optimized(TEXT) IS '浼樺寲鐗堟淳鍗昍PC锛氫慨澶嶆椂闂村啿绐佹鏌ヨ繃浜庝弗鏍肩殑闂锛屼粎褰撹鍗曟湁鏄庣鏃堕棿鑼冨洿鏃舵墠妫€鏌ユ椂闂村啿绐?;