1062 lines
37 KiB
PL/PgSQL
1062 lines
37 KiB
PL/PgSQL
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; |