-- ============================================================================ -- 浼樺寲娲惧崟 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_type, 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锛氫慨澶嶆椂闂村啿绐佹鏌ヨ繃浜庝弗鏍肩殑闂锛屼粎褰撹鍗曟湁鏄庣‘鏃堕棿鑼冨洿鏃舵墠妫€鏌ユ椂闂村啿绐?;