Files
medical-mall/mall_sql/migrations/20260601_hss_auto_dispatch_rpc_v2.sql

1062 lines
37 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.
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;