BEGIN; -- ===================================================================================== -- RPC: rpc_homecare_auto_dispatch -- Version: v2_secure_dispatch -- Purpose: -- 1. 消费者支付成功后,由数据库自动派单 -- 2. 禁止前端直接 INSERT hss_service_assignments -- 3. 支持派单失败原因返回、失败记录、重新派单 -- 4. 增加权限校验、支付校验、并发锁、重复派单保护 -- -- Dispatch rules: -- - 订单必须属于当前登录用户 -- - 订单必须已经支付 payment_status = 2 -- - 订单状态必须仍为 created -- - 服务人员必须在线、启用、未删除 -- - 可选匹配:站点、资质、服务半径、预约时间冲突 -- -- Return fields: -- success BOOLEAN -- code TEXT 前端用于分支判断 -- message TEXT 前端弹窗 content 直接使用 -- display_type TEXT modal / toast -- retryable BOOLEAN 是否允许显示“重新派单” -- dispatch_status TEXT -- ===================================================================================== -- ===================================================================================== -- 1. 收紧前端直接写入派单表的权限 -- ===================================================================================== DROP POLICY IF EXISTS hss_service_assignments_order_owner_insert ON public.hss_service_assignments; -- ===================================================================================== -- 2. 补充服务订单派单字段 -- ===================================================================================== ALTER TABLE public.hss_service_orders ADD COLUMN IF NOT EXISTS payment_status SMALLINT NOT NULL DEFAULT 1, ADD COLUMN IF NOT EXISTS dispatch_status TEXT NOT NULL DEFAULT 'pending', ADD COLUMN IF NOT EXISTS dispatch_error_code TEXT, ADD COLUMN IF NOT EXISTS dispatch_error_message TEXT, ADD COLUMN IF NOT EXISTS dispatch_failed_at TIMESTAMPTZ, ADD COLUMN IF NOT EXISTS dispatch_attempt_count INTEGER NOT NULL DEFAULT 0, -- 订单需要的服务资质编码,例如 BASIC_CARE、PROFESSIONAL_NURSING ADD COLUMN IF NOT EXISTS required_qualification_code TEXT, -- 指定服务站点,为空时不限定站点 ADD COLUMN IF NOT EXISTS dispatch_station_id TEXT, -- 用户服务地址经纬度;用于距离筛选,不负责地址文字转换 ADD COLUMN IF NOT EXISTS service_lat NUMERIC(10,7), ADD COLUMN IF NOT EXISTS service_lng NUMERIC(10,7), -- 用户预约服务时间;为空时表示立即上门或未指定时间 ADD COLUMN IF NOT EXISTS scheduled_start_at TIMESTAMPTZ, ADD COLUMN IF NOT EXISTS scheduled_end_at TIMESTAMPTZ; UPDATE public.hss_service_orders SET dispatch_status = 'pending' WHERE dispatch_status IS NULL; UPDATE public.hss_service_orders SET dispatch_attempt_count = 0 WHERE dispatch_attempt_count IS NULL; DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_constraint WHERE conname = 'chk_hss_service_orders_dispatch_status' AND conrelid = 'public.hss_service_orders'::regclass ) THEN ALTER TABLE public.hss_service_orders ADD CONSTRAINT chk_hss_service_orders_dispatch_status CHECK ( dispatch_status IN ('pending', 'dispatching', 'assigned', 'failed') ) NOT VALID; END IF; IF NOT EXISTS ( SELECT 1 FROM pg_constraint WHERE conname = 'chk_hss_service_orders_service_lat' AND conrelid = 'public.hss_service_orders'::regclass ) THEN ALTER TABLE public.hss_service_orders ADD CONSTRAINT chk_hss_service_orders_service_lat CHECK ( service_lat IS NULL OR service_lat BETWEEN -90 AND 90 ) NOT VALID; END IF; IF NOT EXISTS ( SELECT 1 FROM pg_constraint WHERE conname = 'chk_hss_service_orders_service_lng' AND conrelid = 'public.hss_service_orders'::regclass ) THEN ALTER TABLE public.hss_service_orders ADD CONSTRAINT chk_hss_service_orders_service_lng CHECK ( service_lng IS NULL OR service_lng BETWEEN -180 AND 180 ) NOT VALID; END IF; IF NOT EXISTS ( SELECT 1 FROM pg_constraint WHERE conname = 'chk_hss_service_orders_schedule_time' AND conrelid = 'public.hss_service_orders'::regclass ) THEN ALTER TABLE public.hss_service_orders ADD CONSTRAINT chk_hss_service_orders_schedule_time CHECK ( scheduled_start_at IS NULL OR scheduled_end_at IS NULL OR scheduled_end_at > scheduled_start_at ) NOT VALID; END IF; END; $$; -- ===================================================================================== -- 3. 补充服务人员派单能力字段 -- ===================================================================================== ALTER TABLE public.ml_delivery_staff ADD COLUMN IF NOT EXISTS current_lat NUMERIC(10,7), ADD COLUMN IF NOT EXISTS current_lng NUMERIC(10,7), -- 允许接单的最大直线距离,单位:公里 ADD COLUMN IF NOT EXISTS dispatch_radius_km NUMERIC(8,2) NOT NULL DEFAULT 20, -- 用于公平派单,优先选择更久没有接到任务的人 ADD COLUMN IF NOT EXISTS last_dispatched_at TIMESTAMPTZ; DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_constraint WHERE conname = 'chk_ml_delivery_staff_dispatch_radius' AND conrelid = 'public.ml_delivery_staff'::regclass ) THEN ALTER TABLE public.ml_delivery_staff ADD CONSTRAINT chk_ml_delivery_staff_dispatch_radius CHECK ( dispatch_radius_km > 0 AND dispatch_radius_km <= 200 ) NOT VALID; END IF; END; $$; -- ===================================================================================== -- 4. 补充派单记录字段 -- ===================================================================================== ALTER TABLE public.hss_service_assignments ADD COLUMN IF NOT EXISTS deleted_at TIMESTAMPTZ, -- 派单当时服务人员与用户之间的计算距离 ADD COLUMN IF NOT EXISTS dispatch_distance_km NUMERIC(8,3), -- 派单时使用的资质要求快照 ADD COLUMN IF NOT EXISTS qualification_code_snapshot TEXT; -- ===================================================================================== -- 5. 补充订单日志触发者字段 -- 原表 operator_id 可能已有 NOT NULL 约束,所以继续保留兼容写入; -- 新增 triggered_by_user_id 用于明确“由哪个消费者触发系统派单”。 -- ===================================================================================== ALTER TABLE public.hss_service_order_status_logs ADD COLUMN IF NOT EXISTS triggered_by_user_id UUID; -- ===================================================================================== -- 6. 服务人员资质表 -- 如果该表此前已经存在,会补充以下标准字段。 -- ===================================================================================== CREATE TABLE IF NOT EXISTS public.hc_worker_qualifications ( id TEXT PRIMARY KEY, staff_id TEXT NOT NULL, qualification_code TEXT NOT NULL, qualification_name TEXT NOT NULL DEFAULT '', certificate_no TEXT NOT NULL DEFAULT '', status SMALLINT NOT NULL DEFAULT 1, valid_from TIMESTAMPTZ, valid_until TIMESTAMPTZ, created_at TIMESTAMPTZ NOT NULL DEFAULT now(), updated_at TIMESTAMPTZ NOT NULL DEFAULT now(), deleted_at TIMESTAMPTZ ); ALTER TABLE public.hc_worker_qualifications ADD COLUMN IF NOT EXISTS staff_id TEXT, ADD COLUMN IF NOT EXISTS qualification_code TEXT, ADD COLUMN IF NOT EXISTS qualification_name TEXT NOT NULL DEFAULT '', ADD COLUMN IF NOT EXISTS certificate_no TEXT NOT NULL DEFAULT '', ADD COLUMN IF NOT EXISTS status SMALLINT NOT NULL DEFAULT 1, ADD COLUMN IF NOT EXISTS valid_from TIMESTAMPTZ, ADD COLUMN IF NOT EXISTS valid_until TIMESTAMPTZ, ADD COLUMN IF NOT EXISTS created_at TIMESTAMPTZ NOT NULL DEFAULT now(), ADD COLUMN IF NOT EXISTS updated_at TIMESTAMPTZ NOT NULL DEFAULT now(), ADD COLUMN IF NOT EXISTS deleted_at TIMESTAMPTZ; ALTER TABLE public.hc_worker_qualifications ENABLE ROW LEVEL SECURITY; REVOKE ALL ON TABLE public.hc_worker_qualifications FROM anon; REVOKE ALL ON TABLE public.hc_worker_qualifications FROM authenticated; CREATE INDEX IF NOT EXISTS idx_hc_worker_qualifications_dispatch_match ON public.hc_worker_qualifications ( staff_id, qualification_code, status, valid_until ) WHERE deleted_at IS NULL; -- ===================================================================================== -- 7. 新增派单尝试日志表 -- 专门记录成功或失败派单,不强行复用订单状态日志。 -- ===================================================================================== CREATE TABLE IF NOT EXISTS public.hss_service_dispatch_attempt_logs ( id TEXT PRIMARY KEY, order_id TEXT NOT NULL, requested_by_user_id UUID, selected_staff_id TEXT, selected_station_id TEXT, success BOOLEAN NOT NULL DEFAULT FALSE, result_code TEXT NOT NULL, result_message TEXT NOT NULL, retryable BOOLEAN NOT NULL DEFAULT FALSE, filters_snapshot JSONB NOT NULL DEFAULT '{}'::jsonb, created_at TIMESTAMPTZ NOT NULL DEFAULT now() ); ALTER TABLE public.hss_service_dispatch_attempt_logs ENABLE ROW LEVEL SECURITY; REVOKE ALL ON TABLE public.hss_service_dispatch_attempt_logs FROM anon; REVOKE ALL ON TABLE public.hss_service_dispatch_attempt_logs FROM authenticated; CREATE INDEX IF NOT EXISTS idx_hss_dispatch_attempt_logs_order_time ON public.hss_service_dispatch_attempt_logs ( order_id, created_at DESC ); -- ===================================================================================== -- 8. 派单性能索引 -- ===================================================================================== CREATE INDEX IF NOT EXISTS idx_hss_service_orders_dispatch_query ON public.hss_service_orders ( user_id, payment_status, status, dispatch_status, created_at DESC ) WHERE deleted_at IS NULL; CREATE INDEX IF NOT EXISTS idx_ml_delivery_staff_dispatch_candidate ON public.ml_delivery_staff ( online_status, status, is_active, station_id, last_dispatched_at ) WHERE deleted_at IS NULL; CREATE INDEX IF NOT EXISTS idx_hss_service_assignments_staff_active ON public.hss_service_assignments ( staff_id, status, assigned_at DESC ) WHERE deleted_at IS NULL AND status IN ('assigned', 'accepted', 'departed', 'arrived', 'serving', 'in_service'); -- ===================================================================================== -- 9. 创建活动派单唯一索引前,先检查是否已有脏数据 -- 如果同一订单已经存在多个有效派单,会中止脚本,避免自动误删业务数据。 -- ===================================================================================== DO $$ DECLARE v_duplicate_orders TEXT; BEGIN SELECT string_agg(t.order_id::text, ', ') INTO v_duplicate_orders FROM ( SELECT order_id FROM public.hss_service_assignments WHERE deleted_at IS NULL AND status IN ('assigned', 'accepted', 'departed', 'arrived', 'serving', 'in_service') GROUP BY order_id HAVING COUNT(*) > 1 LIMIT 20 ) t; IF v_duplicate_orders IS NOT NULL THEN RAISE EXCEPTION '存在同一订单的多条有效派单记录,请先人工核对并关闭重复记录后重新执行。订单ID: %', v_duplicate_orders; END IF; END; $$; CREATE UNIQUE INDEX IF NOT EXISTS uq_hss_service_assignments_active_order ON public.hss_service_assignments (order_id) WHERE deleted_at IS NULL AND status IN ('assigned', 'accepted', 'departed', 'arrived', 'serving', 'in_service'); -- ===================================================================================== -- 10. 创建安全自动派单 RPC -- ===================================================================================== CREATE OR REPLACE FUNCTION public.rpc_homecare_auto_dispatch( 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. 支付与订单状态校验 -- payment_status: -- 1 = 待支付 -- 2 = 已支付 -- 3 = 已退款 -- 4 = 支付失败 -- 5 = 支付超时关闭 -- ========================================================================= 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 <> 'created' 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. 查找可派单服务人员 -- -- 筛选规则: -- 1. 正常、启用、在线、绑定用户 -- 2. 订单指定站点时,必须属于该站点 -- 3. 订单指定资质时,必须持有有效资质 -- 4. 订单包含经纬度时,必须在服务半径内 -- 5. 不得与当前有效任务发生服务时间冲突 -- 6. 锁住被选中的服务人员,防止并发订单同时选中同一人 -- ========================================================================= 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 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 ( v_order.scheduled_start_at IS NULL OR v_order.scheduled_end_at IS NULL OR existing_order.scheduled_start_at IS NULL OR existing_order.scheduled_end_at IS NULL OR 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 SKIP LOCKED; 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 := 'sa-' || floor(extract(epoch FROM v_now) * 1000)::BIGINT::TEXT || '-' || upper(substr(md5(p_order_id || ':' || v_candidate.id::TEXT || ':' || random()::TEXT), 1, 10)); INSERT INTO public.hss_service_assignments ( id, order_id, staff_id, station_id, status, assigned_at, dispatch_distance_km, qualification_code_snapshot, created_at, updated_at ) VALUES ( v_assignment_id, p_order_id, v_candidate.id, v_candidate.station_id, 'assigned', v_now, v_candidate.distance_km, v_order.required_qualification_code, v_now, v_now ); -- ========================================================================= -- J. 更新订单与服务人员状态 -- ========================================================================= UPDATE public.hss_service_orders SET status = 'assigned', dispatch_status = 'assigned', current_assignment_id = v_assignment_id, current_staff_id = v_candidate.id, dispatch_error_code = NULL, dispatch_error_message = NULL, dispatch_failed_at = NULL, updated_at = v_now WHERE id = p_order_id; UPDATE public.ml_delivery_staff SET last_dispatched_at = v_now, updated_at = v_now WHERE id = v_candidate.id; -- ========================================================================= -- K. 写入订单状态日志 -- ========================================================================= v_status_log_id := 'slog-' || floor(extract(epoch FROM v_now) * 1000)::BIGINT::TEXT || '-' || upper(substr(md5(p_order_id || ':status:' || random()::TEXT), 1, 10)); INSERT INTO public.hss_service_order_status_logs ( id, order_id, from_status, to_status, operator_id, operator_role, triggered_by_user_id, remark, created_at ) VALUES ( v_status_log_id, p_order_id, 'created', 'assigned', v_current_user_id, 'system', v_current_user_id, '消费者支付成功后触发系统自动派单', v_now ); -- ========================================================================= -- L. 写入派单成功尝试日志 -- ========================================================================= v_attempt_log_id := 'dalog-' || floor(extract(epoch FROM v_now) * 1000)::BIGINT::TEXT || '-' || upper(substr(md5(p_order_id || ':success:' || 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, v_candidate.id::TEXT, COALESCE(v_candidate.station_id::TEXT, ''), TRUE, 'DISPATCH_ASSIGNED', '系统已为您匹配服务人员', FALSE, 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, 'dispatch_distance_km', v_candidate.distance_km ), v_now ); -- ========================================================================= -- M. 返回派单成功结果 -- ========================================================================= 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, 'station_id', COALESCE(v_candidate.station_id::TEXT, ''), 'dispatch_distance_km', v_candidate.distance_km ); EXCEPTION WHEN unique_violation THEN -- 极端并发场景下,唯一索引阻止重复派单后,将结果转换为前端可处理的返回值。 UPDATE public.hss_service_orders SET dispatch_status = 'failed', dispatch_error_code = 'DISPATCH_CONFLICT_RETRY', dispatch_error_message = '派单请求较多,请重新尝试', dispatch_failed_at = clock_timestamp(), updated_at = clock_timestamp() WHERE id = p_order_id AND status = 'created'; RETURN jsonb_build_object( 'success', FALSE, 'code', 'DISPATCH_CONFLICT_RETRY', 'message', '派单请求较多,请重新尝试', 'display_type', 'modal', 'retryable', TRUE, 'dispatch_status', 'failed', 'order_id', p_order_id ); END; $function$; -- ===================================================================================== -- 11. 限制 RPC 调用权限 -- ===================================================================================== REVOKE ALL ON FUNCTION public.rpc_homecare_auto_dispatch(TEXT) FROM PUBLIC; REVOKE ALL ON FUNCTION public.rpc_homecare_auto_dispatch(TEXT) FROM anon; GRANT EXECUTE ON FUNCTION public.rpc_homecare_auto_dispatch(TEXT) TO authenticated; COMMENT ON FUNCTION public.rpc_homecare_auto_dispatch(TEXT) IS '居家服务订单支付成功后的安全自动派单RPC:校验用户、支付、状态、资质、站点、距离与时间冲突,并返回结构化失败原因'; COMMIT;