修复订单显示bug

This commit is contained in:
2026-06-10 20:20:47 +08:00
parent de62513987
commit 9fbc6f8cd1
45 changed files with 7514 additions and 2025 deletions

View File

@@ -0,0 +1,633 @@
BEGIN;
-- =====================================================================================
-- 20260609_backfill_ec_care_tasks_core_fields_SAFE.sql
-- Purpose:
-- 安全回填历史 ec_care_tasks 的核心展示字段,修复 delivery 端订单卡片/详情空白问题。
--
-- Risk Control:
-- 1. 先备份 ec_care_tasks
-- 2. 默认只匹配 6 小时内的候选记录,避免 48 小时窗口造成错配;
-- 3. 只使用“唯一候选”或“第一候选明显优于第二候选”的记录;
-- 4. 只补空字段,不覆盖已有有效值;
-- 5. 写入 backfill 审计表;
-- 6. 最后输出检查结果。
--
-- 注意:
-- 执行前请确认字段已存在。
-- 如果字段不存在,请先让后端补 ALTER TABLE ADD COLUMN。
-- =====================================================================================
-- =====================================================================================
-- 0. 执行前字段检查
-- =====================================================================================
DO $$
DECLARE
v_missing text;
BEGIN
SELECT string_agg(table_name || '.' || column_name, ', ')
INTO v_missing
FROM (
VALUES
('ec_care_tasks', 'id'),
('ec_care_tasks', 'user_id'),
('ec_care_tasks', 'assigned_to'),
('ec_care_tasks', 'request_id'),
('ec_care_tasks', 'service_catalog_id'),
('ec_care_tasks', 'service_name'),
('ec_care_tasks', 'service_category'),
('ec_care_tasks', 'service_snapshot_json'),
('ec_care_tasks', 'elder_name'),
('ec_care_tasks', 'elder_phone'),
('ec_care_tasks', 'contact_name'),
('ec_care_tasks', 'contact_phone'),
('ec_care_tasks', 'address_snapshot_json'),
('ec_care_tasks', 'address_snapshot'),
('ec_care_tasks', 'scheduled_at'),
('ec_care_tasks', 'appointment_time'),
('ec_care_tasks', 'remark'),
('ec_care_tasks', 'created_at'),
('ec_care_tasks', 'updated_at'),
('ec_service_requests', 'id'),
('ec_service_requests', 'user_id'),
('ec_service_requests', 'service_catalog_id'),
('ec_service_requests', 'service_name'),
('ec_service_requests', 'service_category'),
('ec_service_requests', 'elder_name'),
('ec_service_requests', 'elder_phone'),
('ec_service_requests', 'contact_name'),
('ec_service_requests', 'contact_phone'),
('ec_service_requests', 'address_snapshot_json'),
('ec_service_requests', 'address_snapshot'),
('ec_service_requests', 'scheduled_at'),
('ec_service_requests', 'remark'),
('ec_service_requests', 'created_at')
) AS required_cols(table_name, column_name)
WHERE NOT EXISTS (
SELECT 1
FROM information_schema.columns c
WHERE c.table_schema = 'public'
AND c.table_name = required_cols.table_name
AND c.column_name = required_cols.column_name
);
IF v_missing IS NOT NULL THEN
RAISE EXCEPTION '缺少必要字段,请先补字段后再执行回填:%', v_missing;
END IF;
END $$;
-- =====================================================================================
-- 1. 备份 ec_care_tasks
-- =====================================================================================
CREATE TABLE IF NOT EXISTS public.ec_care_tasks_backup_20260609_before_backfill AS
SELECT *
FROM public.ec_care_tasks;
-- =====================================================================================
-- 2. 创建回填审计表
-- =====================================================================================
CREATE TABLE IF NOT EXISTS public.ec_care_tasks_backfill_audit_20260609 (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
task_id uuid NOT NULL,
source_type text NOT NULL,
source_id text,
match_diff_seconds numeric,
candidate_count integer,
second_diff_seconds numeric,
old_snapshot jsonb,
new_snapshot jsonb,
created_at timestamptz NOT NULL DEFAULT now()
);
-- =====================================================================================
-- 3. 自表兜底address_snapshot_json / appointment_time
-- 只补空字段,不覆盖已有有效值。
-- =====================================================================================
WITH updated AS (
UPDATE public.ec_care_tasks t
SET
address_snapshot_json = COALESCE(
NULLIF(t.address_snapshot_json, '{}'::jsonb),
NULLIF(t.address_snapshot, '{}'::jsonb),
'{}'::jsonb
),
appointment_time = COALESCE(t.appointment_time, t.scheduled_at),
updated_at = now()
WHERE
(
t.address_snapshot_json IS NULL
OR t.address_snapshot_json = '{}'::jsonb
OR t.appointment_time IS NULL
)
RETURNING
t.id,
to_jsonb(t) AS new_snapshot
)
INSERT INTO public.ec_care_tasks_backfill_audit_20260609 (
task_id,
source_type,
source_id,
match_diff_seconds,
candidate_count,
second_diff_seconds,
old_snapshot,
new_snapshot
)
SELECT
u.id,
'SELF_FALLBACK',
NULL,
NULL,
NULL,
NULL,
NULL,
u.new_snapshot
FROM updated u;
-- =====================================================================================
-- 4. 从 ec_service_requests 安全回填
-- 风险控制:
-- - user_id 必须相同;
-- - 时间窗口从 48 小时缩小为 6 小时;
-- - 如果同一 task 有多个候选,必须满足:
-- a. 只有 1 个候选;或
-- b. 第一候选与第二候选至少相差 30 分钟;
-- - 只补空字段。
-- =====================================================================================
WITH task_targets AS (
SELECT
t.id,
t.user_id,
COALESCE(t.appointment_time, t.scheduled_at, t.created_at) AS task_time,
to_jsonb(t) AS old_snapshot
FROM public.ec_care_tasks t
WHERE
COALESCE(t.request_id::text, '') = ''
OR COALESCE(t.service_name, '') = ''
OR COALESCE(t.elder_name, '') = ''
OR COALESCE(t.contact_name, '') = ''
OR COALESCE(t.contact_phone, '') = ''
OR t.address_snapshot_json IS NULL
OR t.address_snapshot_json = '{}'::jsonb
),
request_candidates AS (
SELECT
tt.id AS task_id,
tt.old_snapshot,
r.id AS request_id,
r.service_catalog_id,
r.service_name,
r.service_category,
r.elder_name,
r.elder_phone,
r.contact_name,
r.contact_phone,
COALESCE(
NULLIF(r.address_snapshot_json, '{}'::jsonb),
NULLIF(r.address_snapshot, '{}'::jsonb),
'{}'::jsonb
) AS address_snapshot_json,
r.scheduled_at,
r.remark,
ABS(EXTRACT(EPOCH FROM (
COALESCE(r.scheduled_at, r.created_at) - tt.task_time
))) AS diff_seconds,
COUNT(*) OVER (
PARTITION BY tt.id
) AS candidate_count,
ROW_NUMBER() OVER (
PARTITION BY tt.id
ORDER BY
ABS(EXTRACT(EPOCH FROM (
COALESCE(r.scheduled_at, r.created_at) - tt.task_time
))),
r.created_at DESC
) AS rn,
LEAD(
ABS(EXTRACT(EPOCH FROM (
COALESCE(r.scheduled_at, r.created_at) - tt.task_time
)))
) OVER (
PARTITION BY tt.id
ORDER BY
ABS(EXTRACT(EPOCH FROM (
COALESCE(r.scheduled_at, r.created_at) - tt.task_time
))),
r.created_at DESC
) AS second_diff_seconds
FROM task_targets tt
JOIN public.ec_service_requests r
ON r.user_id = tt.user_id
AND ABS(EXTRACT(EPOCH FROM (
COALESCE(r.scheduled_at, r.created_at) - tt.task_time
))) <= 21600
),
safe_best_request AS (
SELECT *
FROM request_candidates
WHERE rn = 1
AND (
candidate_count = 1
OR second_diff_seconds IS NULL
OR second_diff_seconds - diff_seconds >= 1800
)
),
updated AS (
UPDATE public.ec_care_tasks t
SET
request_id = COALESCE(t.request_id, br.request_id),
service_catalog_id = COALESCE(
NULLIF(t.service_catalog_id, ''),
COALESCE(br.service_catalog_id, '')
),
service_name = COALESCE(
NULLIF(t.service_name, ''),
COALESCE(br.service_name, '')
),
service_category = COALESCE(
NULLIF(t.service_category, ''),
COALESCE(br.service_category, '')
),
service_snapshot_json = CASE
WHEN t.service_snapshot_json IS NULL OR t.service_snapshot_json = '{}'::jsonb THEN
jsonb_strip_nulls(jsonb_build_object(
'category', COALESCE(br.service_category, ''),
'name', COALESCE(br.service_name, ''),
'price', 0
))
ELSE t.service_snapshot_json
END,
elder_name = COALESCE(
NULLIF(t.elder_name, ''),
COALESCE(br.elder_name, '')
),
elder_phone = COALESCE(
NULLIF(t.elder_phone, ''),
COALESCE(br.elder_phone, '')
),
contact_name = COALESCE(
NULLIF(t.contact_name, ''),
COALESCE(br.contact_name, '')
),
contact_phone = COALESCE(
NULLIF(t.contact_phone, ''),
COALESCE(br.contact_phone, '')
),
address_snapshot_json = COALESCE(
NULLIF(t.address_snapshot_json, '{}'::jsonb),
NULLIF(t.address_snapshot, '{}'::jsonb),
NULLIF(br.address_snapshot_json, '{}'::jsonb),
'{}'::jsonb
),
scheduled_at = COALESCE(t.scheduled_at, br.scheduled_at),
appointment_time = COALESCE(
t.appointment_time,
t.scheduled_at,
br.scheduled_at
),
remark = COALESCE(
NULLIF(t.remark, ''),
COALESCE(br.remark, '')
),
updated_at = now()
FROM safe_best_request br
WHERE t.id = br.task_id
RETURNING
t.id,
br.request_id,
br.diff_seconds,
br.candidate_count,
br.second_diff_seconds,
br.old_snapshot,
to_jsonb(t) AS new_snapshot
)
INSERT INTO public.ec_care_tasks_backfill_audit_20260609 (
task_id,
source_type,
source_id,
match_diff_seconds,
candidate_count,
second_diff_seconds,
old_snapshot,
new_snapshot
)
SELECT
u.id,
'EC_SERVICE_REQUEST',
u.request_id::text,
u.diff_seconds,
u.candidate_count,
u.second_diff_seconds,
u.old_snapshot,
u.new_snapshot
FROM updated u;
-- =====================================================================================
-- 5. 如果 legacy 表存在,再从 hss_service_orders 安全回填剩余字段
-- 风险控制:
-- - 仅在 hss_service_orders 表存在时执行;
-- - user_id 必须相同;
-- - assigned_to / current_staff_id 如果能匹配则匹配;
-- - 时间窗口 6 小时;
-- - 候选记录必须唯一或第一候选明显优于第二候选;
-- - 只补空字段。
-- =====================================================================================
DO $$
BEGIN
IF to_regclass('public.hss_service_orders') IS NOT NULL THEN
WITH task_targets AS (
SELECT
t.id,
t.user_id,
t.assigned_to,
COALESCE(t.appointment_time, t.scheduled_at, t.created_at) AS task_time,
to_jsonb(t) AS old_snapshot
FROM public.ec_care_tasks t
WHERE
COALESCE(t.service_name, '') = ''
OR COALESCE(t.elder_name, '') = ''
OR COALESCE(t.contact_name, '') = ''
OR COALESCE(t.contact_phone, '') = ''
OR t.address_snapshot_json IS NULL
OR t.address_snapshot_json = '{}'::jsonb
OR COALESCE(t.remark, '') = ''
),
legacy_candidates AS (
SELECT
tt.id AS task_id,
tt.old_snapshot,
o.id AS legacy_order_id,
o.service_id,
o.service_name,
o.service_snapshot_json,
o.address_snapshot_json,
o.recipient_name,
o.recipient_phone,
o.contact_name,
o.contact_phone,
o.appointment_time,
o.remark,
ABS(EXTRACT(EPOCH FROM (
COALESCE(o.appointment_time, o.created_at) - tt.task_time
))) AS diff_seconds,
COUNT(*) OVER (
PARTITION BY tt.id
) AS candidate_count,
ROW_NUMBER() OVER (
PARTITION BY tt.id
ORDER BY
ABS(EXTRACT(EPOCH FROM (
COALESCE(o.appointment_time, o.created_at) - tt.task_time
))),
o.created_at DESC
) AS rn,
LEAD(
ABS(EXTRACT(EPOCH FROM (
COALESCE(o.appointment_time, o.created_at) - tt.task_time
)))
) OVER (
PARTITION BY tt.id
ORDER BY
ABS(EXTRACT(EPOCH FROM (
COALESCE(o.appointment_time, o.created_at) - tt.task_time
))),
o.created_at DESC
) AS second_diff_seconds
FROM task_targets tt
JOIN public.hss_service_orders o
ON o.deleted_at IS NULL
AND o.user_id = tt.user_id
AND (
tt.assigned_to IS NULL
OR o.current_staff_id = tt.assigned_to
)
AND ABS(EXTRACT(EPOCH FROM (
COALESCE(o.appointment_time, o.created_at) - tt.task_time
))) <= 21600
),
safe_best_legacy AS (
SELECT *
FROM legacy_candidates
WHERE rn = 1
AND (
candidate_count = 1
OR second_diff_seconds IS NULL
OR second_diff_seconds - diff_seconds >= 1800
)
),
updated AS (
UPDATE public.ec_care_tasks t
SET
service_catalog_id = COALESCE(
NULLIF(t.service_catalog_id, ''),
COALESCE(bl.service_id, '')
),
service_name = COALESCE(
NULLIF(t.service_name, ''),
COALESCE(bl.service_name, '')
),
service_snapshot_json = COALESCE(
NULLIF(t.service_snapshot_json, '{}'::jsonb),
NULLIF(bl.service_snapshot_json, '{}'::jsonb),
'{}'::jsonb
),
elder_name = COALESCE(
NULLIF(t.elder_name, ''),
COALESCE(bl.recipient_name, '')
),
elder_phone = COALESCE(
NULLIF(t.elder_phone, ''),
COALESCE(bl.recipient_phone, '')
),
contact_name = COALESCE(
NULLIF(t.contact_name, ''),
COALESCE(bl.contact_name, '')
),
contact_phone = COALESCE(
NULLIF(t.contact_phone, ''),
COALESCE(bl.contact_phone, '')
),
address_snapshot_json = COALESCE(
NULLIF(t.address_snapshot_json, '{}'::jsonb),
NULLIF(t.address_snapshot, '{}'::jsonb),
NULLIF(bl.address_snapshot_json, '{}'::jsonb),
'{}'::jsonb
),
scheduled_at = COALESCE(t.scheduled_at, bl.appointment_time),
appointment_time = COALESCE(
t.appointment_time,
t.scheduled_at,
bl.appointment_time
),
remark = COALESCE(
NULLIF(t.remark, ''),
COALESCE(bl.remark, '')
),
updated_at = now()
FROM safe_best_legacy bl
WHERE t.id = bl.task_id
RETURNING
t.id,
bl.legacy_order_id,
bl.diff_seconds,
bl.candidate_count,
bl.second_diff_seconds,
bl.old_snapshot,
to_jsonb(t) AS new_snapshot
)
INSERT INTO public.ec_care_tasks_backfill_audit_20260609 (
task_id,
source_type,
source_id,
match_diff_seconds,
candidate_count,
second_diff_seconds,
old_snapshot,
new_snapshot
)
SELECT
u.id,
'HSS_SERVICE_ORDER',
u.legacy_order_id::text,
u.diff_seconds,
u.candidate_count,
u.second_diff_seconds,
u.old_snapshot,
u.new_snapshot
FROM updated u;
ELSE
RAISE NOTICE 'hss_service_orders not exists, skip legacy backfill';
END IF;
END $$;
-- =====================================================================================
-- 6. 补齐 service_category
-- 仍然只补空字段。
-- =====================================================================================
WITH updated AS (
UPDATE public.ec_care_tasks t
SET
service_category = CASE
WHEN COALESCE(t.service_category, '') <> '' THEN t.service_category
WHEN COALESCE(t.service_snapshot_json ->> 'category', '') <> '' THEN t.service_snapshot_json ->> 'category'
WHEN COALESCE(t.service_name, '') LIKE '%护理%' THEN '居家服务'
WHEN COALESCE(t.service_name, '') LIKE '%随访%' THEN '健康随访'
ELSE COALESCE(t.service_category, '')
END,
updated_at = now()
WHERE COALESCE(t.service_category, '') = ''
RETURNING
t.id,
to_jsonb(t) AS new_snapshot
)
INSERT INTO public.ec_care_tasks_backfill_audit_20260609 (
task_id,
source_type,
source_id,
match_diff_seconds,
candidate_count,
second_diff_seconds,
old_snapshot,
new_snapshot
)
SELECT
u.id,
'SERVICE_CATEGORY_FALLBACK',
NULL,
NULL,
NULL,
NULL,
NULL,
u.new_snapshot
FROM updated u;
-- =====================================================================================
-- 7. 执行后检查
-- =====================================================================================
-- 7.1 回填审计统计
SELECT
source_type,
COUNT(*) AS affected_rows
FROM public.ec_care_tasks_backfill_audit_20260609
GROUP BY source_type
ORDER BY source_type;
-- 7.2 还有多少条仍然缺少核心展示字段
SELECT
COUNT(*) AS still_missing_core_rows
FROM public.ec_care_tasks t
WHERE
COALESCE(t.service_name, '') = ''
OR COALESCE(t.elder_name, '') = ''
OR COALESCE(t.contact_name, '') = ''
OR COALESCE(t.contact_phone, '') = ''
OR t.address_snapshot_json IS NULL
OR t.address_snapshot_json = '{}'::jsonb;
-- 7.3 抽样查看仍有问题的任务
SELECT
t.id,
t.request_id,
t.user_id,
t.assigned_to,
t.service_name,
t.elder_name,
t.contact_name,
t.contact_phone,
t.appointment_time,
t.created_at
FROM public.ec_care_tasks t
WHERE
COALESCE(t.service_name, '') = ''
OR COALESCE(t.elder_name, '') = ''
OR COALESCE(t.contact_name, '') = ''
OR COALESCE(t.contact_phone, '') = ''
OR t.address_snapshot_json IS NULL
OR t.address_snapshot_json = '{}'::jsonb
ORDER BY t.created_at DESC
LIMIT 20;
COMMIT;

View File

@@ -0,0 +1,135 @@
BEGIN;
-- =====================================================================================
-- 20260609_exclude_invalid_ec_care_tasks_from_delivery_rpc.sql
-- Purpose:
-- 过滤 delivery 订单列表中的“空壳 ec_care_tasks”。
--
-- Background:
-- 现场数据已确认存在一批 ec_care_tasks
-- - user_id 为 NULL
-- - request_id 为 NULL
-- - service_name / elder_name / contact_name / address_snapshot_json 全空
-- - assigned_to 存的是 ml_delivery_staff.uid而不是 ml_delivery_staff.id
--
-- 这类记录无法可靠回填业务信息,继续返回给前端只会形成空白卡片。
-- 正确做法是:在 RPC 层将其排除,只返回可展示的有效订单。
-- =====================================================================================
CREATE OR REPLACE FUNCTION public.delivery_is_valid_care_task(p_raw JSONB)
RETURNS BOOLEAN
LANGUAGE plpgsql
IMMUTABLE
AS $$
DECLARE
v_service_name TEXT := COALESCE(p_raw ->> 'service_name', '');
v_elder_name TEXT := COALESCE(p_raw ->> 'elder_name', '');
v_contact_name TEXT := COALESCE(p_raw ->> 'contact_name', '');
v_contact_phone TEXT := COALESCE(p_raw ->> 'contact_phone', '');
v_request_id TEXT := COALESCE(p_raw ->> 'request_id', '');
v_user_id TEXT := COALESCE(p_raw ->> 'user_id', '');
v_address JSONB := COALESCE(p_raw -> 'address_snapshot_json', p_raw -> 'address_snapshot', '{}'::jsonb);
BEGIN
IF v_service_name <> '' THEN
RETURN TRUE;
END IF;
IF v_elder_name <> '' THEN
RETURN TRUE;
END IF;
IF v_contact_name <> '' THEN
RETURN TRUE;
END IF;
IF v_contact_phone <> '' THEN
RETURN TRUE;
END IF;
IF v_request_id <> '' THEN
RETURN TRUE;
END IF;
IF v_user_id <> '' THEN
RETURN TRUE;
END IF;
IF v_address IS NOT NULL AND v_address <> '{}'::jsonb THEN
RETURN TRUE;
END IF;
RETURN FALSE;
END;
$$;
CREATE OR REPLACE FUNCTION public.rpc_delivery_order_list(
p_staff_id UUID DEFAULT NULL,
p_tab TEXT DEFAULT 'all',
p_keyword TEXT DEFAULT ''
)
RETURNS JSONB
LANGUAGE plpgsql
STABLE
SECURITY DEFINER
SET search_path = public
AS $$
DECLARE
v_user_id UUID := public.delivery_current_user_id();
v_staff_id UUID := COALESCE(p_staff_id, public.delivery_current_staff_id());
v_result JSONB := '[]'::jsonb;
v_task JSONB;
v_order JSONB;
v_order_id TEXT;
BEGIN
PERFORM public.delivery_assert_staff_access(v_staff_id);
IF public.delivery_table_exists('ec_care_tasks') THEN
BEGIN
FOR v_task IN
EXECUTE 'SELECT to_jsonb(t) FROM public.ec_care_tasks t WHERE ($1 IS NULL OR t.assigned_to = $1) ORDER BY t.created_at DESC'
USING v_user_id
LOOP
IF NOT public.delivery_is_valid_care_task(v_task) THEN
CONTINUE;
END IF;
v_order := public.delivery_get_care_order_json(v_task ->> 'id');
v_result := public.delivery_append_if_match(v_result, v_order, p_tab, p_keyword);
END LOOP;
EXCEPTION WHEN OTHERS THEN
NULL;
END;
END IF;
BEGIN
FOR v_order_id IN
SELECT o.id
FROM public.hss_service_orders o
WHERE o.deleted_at IS NULL
AND (v_staff_id IS NULL OR o.current_staff_id = v_staff_id::TEXT)
ORDER BY COALESCE(o.updated_at, o.created_at) DESC, o.created_at DESC
LOOP
BEGIN
v_order := public.delivery_get_legacy_order_json(v_order_id);
v_result := public.delivery_append_if_match(v_result, v_order, p_tab, p_keyword);
EXCEPTION WHEN OTHERS THEN
NULL;
END;
END LOOP;
EXCEPTION WHEN OTHERS THEN
NULL;
END;
RETURN COALESCE(v_result, '[]'::jsonb);
END;
$$;
REVOKE ALL ON FUNCTION public.delivery_is_valid_care_task(JSONB) FROM PUBLIC;
REVOKE ALL ON FUNCTION public.delivery_is_valid_care_task(JSONB) FROM anon;
GRANT EXECUTE ON FUNCTION public.delivery_is_valid_care_task(JSONB) TO authenticated;
REVOKE ALL ON FUNCTION public.rpc_delivery_order_list(UUID, TEXT, TEXT) FROM PUBLIC;
REVOKE ALL ON FUNCTION public.rpc_delivery_order_list(UUID, TEXT, TEXT) FROM anon;
GRANT EXECUTE ON FUNCTION public.rpc_delivery_order_list(UUID, TEXT, TEXT) TO authenticated;
COMMIT;

View File

@@ -0,0 +1,128 @@
BEGIN;
-- =====================================================================================
-- 20260609_fix_delivery_rpc_request_snapshot.sql
-- Purpose:
-- 修复 delivery RPC 在读取 ec_care_tasks 新链工单时仅返回状态、缺少服务快照的问题。
-- 当 ec_care_tasks 自身缺少 service_name / contact / address_snapshot_json 等字段时,
-- 自动回填 ec_service_requests 中的下单快照,再交由 delivery_build_order_json 输出前端字段。
-- =====================================================================================
CREATE OR REPLACE FUNCTION public.delivery_get_care_order_json(p_order_id TEXT)
RETURNS JSONB
LANGUAGE plpgsql
STABLE
SECURITY DEFINER
SET search_path = public
AS $$
DECLARE
v_user_id UUID := public.delivery_current_user_id();
v_raw JSONB;
v_request JSONB;
v_logs JSONB := '[]'::jsonb;
v_records JSONB := '[]'::jsonb;
v_evidence JSONB := '[]'::jsonb;
v_exception JSONB;
BEGIN
IF NOT public.delivery_table_exists('ec_care_tasks') THEN
RETURN NULL;
END IF;
BEGIN
EXECUTE 'SELECT to_jsonb(t) FROM public.ec_care_tasks t WHERE t.id = $1::uuid AND ($2 IS NULL OR t.assigned_to = $2) LIMIT 1'
INTO v_raw
USING p_order_id, v_user_id;
EXCEPTION WHEN OTHERS THEN
RETURN NULL;
END;
IF v_raw IS NULL THEN
RETURN NULL;
END IF;
BEGIN
IF public.delivery_table_exists('ec_service_requests')
AND COALESCE(v_raw ->> 'request_id', '') <> '' THEN
EXECUTE 'SELECT to_jsonb(r) FROM public.ec_service_requests r WHERE r.id = $1::uuid LIMIT 1'
INTO v_request
USING (v_raw ->> 'request_id');
END IF;
EXCEPTION WHEN OTHERS THEN
v_request := NULL;
END;
IF v_request IS NOT NULL THEN
v_raw := v_raw || jsonb_strip_nulls(
jsonb_build_object(
'service_name',
COALESCE(NULLIF(v_raw ->> 'service_name', ''), NULLIF(v_request ->> 'service_name', '')),
'service_category',
COALESCE(NULLIF(v_raw ->> 'service_category', ''), NULLIF(v_request ->> 'service_category', '')),
'elder_name',
COALESCE(NULLIF(v_raw ->> 'elder_name', ''), NULLIF(v_request ->> 'elder_name', '')),
'elder_phone',
COALESCE(NULLIF(v_raw ->> 'elder_phone', ''), NULLIF(v_request ->> 'elder_phone', '')),
'contact_name',
COALESCE(NULLIF(v_raw ->> 'contact_name', ''), NULLIF(v_request ->> 'contact_name', '')),
'contact_phone',
COALESCE(NULLIF(v_raw ->> 'contact_phone', ''), NULLIF(v_request ->> 'contact_phone', '')),
'remark',
COALESCE(NULLIF(v_raw ->> 'remark', ''), NULLIF(v_request ->> 'remark', '')),
'scheduled_at',
COALESCE(NULLIF(v_raw ->> 'scheduled_at', ''), NULLIF(v_request ->> 'scheduled_at', '')),
'appointment_time',
COALESCE(NULLIF(v_raw ->> 'appointment_time', ''), NULLIF(v_request ->> 'scheduled_at', '')),
'service_snapshot_json',
COALESCE(
NULLIF(v_raw -> 'service_snapshot_json', '{}'::jsonb),
NULLIF(v_request -> 'service_snapshot_json', '{}'::jsonb),
jsonb_build_object(
'category', COALESCE(v_request ->> 'service_category', ''),
'price', 0
)
),
'address_snapshot_json',
COALESCE(
NULLIF(v_raw -> 'address_snapshot_json', '{}'::jsonb),
NULLIF(v_raw -> 'address_snapshot', '{}'::jsonb),
NULLIF(v_request -> 'address_snapshot_json', '{}'::jsonb),
NULLIF(v_request -> 'address_snapshot', '{}'::jsonb)
)
)
);
END IF;
BEGIN
IF public.delivery_table_exists('hc_work_order_events') THEN
EXECUTE 'SELECT COALESCE(jsonb_agg(to_jsonb(e) ORDER BY e.created_at DESC), ''[]''::jsonb) FROM public.hc_work_order_events e WHERE e.task_id = $1::uuid'
INTO v_logs
USING p_order_id;
END IF;
IF public.delivery_table_exists('ec_care_records') THEN
EXECUTE 'SELECT COALESCE(jsonb_agg(to_jsonb(r) ORDER BY r.created_at DESC), ''[]''::jsonb) FROM public.ec_care_records r WHERE r.task_id = $1::uuid'
INTO v_records
USING p_order_id;
END IF;
IF public.delivery_table_exists('hc_evidence_files') THEN
EXECUTE 'SELECT COALESCE(jsonb_agg(to_jsonb(f) ORDER BY f.created_at DESC), ''[]''::jsonb) FROM public.hc_evidence_files f WHERE f.task_id = $1::uuid'
INTO v_evidence
USING p_order_id;
END IF;
IF public.delivery_table_exists('hc_work_order_exceptions') THEN
EXECUTE 'SELECT to_jsonb(x) FROM public.hc_work_order_exceptions x WHERE x.task_id = $1::uuid ORDER BY x.created_at DESC LIMIT 1'
INTO v_exception
USING p_order_id;
END IF;
EXCEPTION WHEN OTHERS THEN
NULL;
END;
RETURN public.delivery_build_order_json(v_raw, v_logs, v_records, v_evidence, v_exception, 'care');
END;
$$;
REVOKE ALL ON FUNCTION public.delivery_get_care_order_json(TEXT) FROM PUBLIC;
REVOKE ALL ON FUNCTION public.delivery_get_care_order_json(TEXT) FROM anon;
GRANT EXECUTE ON FUNCTION public.delivery_get_care_order_json(TEXT) TO authenticated;
COMMIT;

View File

@@ -0,0 +1,59 @@
BEGIN;
-- =====================================================================================
-- 20260609_restore_delivery_get_legacy_order_json.sql
-- Purpose:
-- 修复 rpc_delivery_order_detail 在 legacy 订单详情场景下报错:
-- function public.delivery_get_legacy_order_json(text) does not exist
-- =====================================================================================
CREATE OR REPLACE FUNCTION public.delivery_get_legacy_order_json(p_order_id TEXT)
RETURNS JSONB
LANGUAGE plpgsql
STABLE
SECURITY DEFINER
SET search_path = public
AS $$
DECLARE
v_staff_id UUID := public.delivery_current_staff_id();
v_raw JSONB;
v_logs JSONB := '[]'::jsonb;
v_records JSONB := '[]'::jsonb;
v_evidence JSONB := '[]'::jsonb;
BEGIN
SELECT to_jsonb(o)
INTO v_raw
FROM public.hss_service_orders o
WHERE o.id = p_order_id
AND o.deleted_at IS NULL
AND (v_staff_id IS NULL OR o.current_staff_id = v_staff_id)
LIMIT 1;
IF v_raw IS NULL THEN
RETURN NULL;
END IF;
SELECT COALESCE(jsonb_agg(to_jsonb(l) ORDER BY l.created_at DESC), '[]'::jsonb)
INTO v_logs
FROM public.hss_service_order_status_logs l
WHERE l.order_id = p_order_id;
SELECT COALESCE(jsonb_agg(to_jsonb(r) ORDER BY r.created_at DESC), '[]'::jsonb)
INTO v_records
FROM public.hss_service_execution_records r
WHERE r.order_id = p_order_id;
SELECT COALESCE(jsonb_agg(to_jsonb(e) ORDER BY e.created_at DESC), '[]'::jsonb)
INTO v_evidence
FROM public.hss_service_evidence_files e
WHERE e.order_id = p_order_id;
RETURN public.delivery_build_order_json(v_raw, v_logs, v_records, v_evidence, NULL, 'legacy');
END;
$$;
REVOKE ALL ON FUNCTION public.delivery_get_legacy_order_json(TEXT) FROM PUBLIC;
REVOKE ALL ON FUNCTION public.delivery_get_legacy_order_json(TEXT) FROM anon;
GRANT EXECUTE ON FUNCTION public.delivery_get_legacy_order_json(TEXT) TO authenticated;
COMMIT;

View File

@@ -0,0 +1,74 @@
BEGIN;
-- =====================================================================================
-- 20260609_rollback_exclude_invalid_ec_care_tasks.sql
-- Purpose:
-- 回滚 20260609_exclude_invalid_ec_care_tasks_from_delivery_rpc.sql
-- 恢复 rpc_delivery_order_list 到“不过滤 ec_care_tasks”的版本。
-- =====================================================================================
CREATE OR REPLACE FUNCTION public.rpc_delivery_order_list(
p_staff_id UUID DEFAULT NULL,
p_tab TEXT DEFAULT 'all',
p_keyword TEXT DEFAULT ''
)
RETURNS JSONB
LANGUAGE plpgsql
STABLE
SECURITY DEFINER
SET search_path = public
AS $$
DECLARE
v_user_id UUID := public.delivery_current_user_id();
v_staff_id UUID := COALESCE(p_staff_id, public.delivery_current_staff_id());
v_result JSONB := '[]'::jsonb;
v_task JSONB;
v_order JSONB;
v_order_id TEXT;
BEGIN
PERFORM public.delivery_assert_staff_access(v_staff_id);
IF public.delivery_table_exists('ec_care_tasks') THEN
BEGIN
FOR v_task IN
EXECUTE 'SELECT to_jsonb(t) FROM public.ec_care_tasks t WHERE ($1 IS NULL OR t.assigned_to = $1) ORDER BY t.created_at DESC'
USING v_user_id
LOOP
v_order := public.delivery_get_care_order_json(v_task ->> 'id');
v_result := public.delivery_append_if_match(v_result, v_order, p_tab, p_keyword);
END LOOP;
EXCEPTION WHEN OTHERS THEN
NULL;
END;
END IF;
BEGIN
FOR v_order_id IN
SELECT o.id
FROM public.hss_service_orders o
WHERE o.deleted_at IS NULL
AND (v_staff_id IS NULL OR o.current_staff_id = v_staff_id::TEXT)
ORDER BY COALESCE(o.updated_at, o.created_at) DESC, o.created_at DESC
LOOP
BEGIN
v_order := public.delivery_get_legacy_order_json(v_order_id);
v_result := public.delivery_append_if_match(v_result, v_order, p_tab, p_keyword);
EXCEPTION WHEN OTHERS THEN
NULL;
END;
END LOOP;
EXCEPTION WHEN OTHERS THEN
NULL;
END;
RETURN COALESCE(v_result, '[]'::jsonb);
END;
$$;
DROP FUNCTION IF EXISTS public.delivery_is_valid_care_task(JSONB);
REVOKE ALL ON FUNCTION public.rpc_delivery_order_list(UUID, TEXT, TEXT) FROM PUBLIC;
REVOKE ALL ON FUNCTION public.rpc_delivery_order_list(UUID, TEXT, TEXT) FROM anon;
GRANT EXECUTE ON FUNCTION public.rpc_delivery_order_list(UUID, TEXT, TEXT) TO authenticated;
COMMIT;

View File

@@ -0,0 +1,364 @@
-- =====================================================================================
-- 20260610_backfill_pending_orders_core_fields_schema_safe.sql
-- Purpose:
-- 修复 hss_service_orders 待接单/待确认订单核心展示字段为空的问题。
-- 兼容当前表结构中没有 address 列的情况,避免 42703: column address does not exist。
-- 同时修复 COALESCE text/numeric 混用导致的 42804。
--
-- Safety:
-- 1) 只处理 deleted_at IS NULL 且 status in ('pending_assignment','pending_accept') 的订单。
-- 2) 不覆盖已有非空/非 0 字段。
-- 3) 新增的列均为 nullable不设置 DEFAULT降低锁表和批量重写风险。
-- 4) JSON 数字使用安全转换,避免 '¥138'、'138元'、空字符串造成 cast 报错。
-- =====================================================================================
BEGIN;
-- ============================================================================
-- 0. 安全补齐展示字段列
-- 你的表当前没有 address 列,所以之前 SELECT/UPDATE address 会直接 42703。
-- 这里用 IF NOT EXISTS 补齐 nullable 展示列,不覆盖已有列。
-- ============================================================================
ALTER TABLE public.hss_service_orders
ADD COLUMN IF NOT EXISTS request_id UUID,
ADD COLUMN IF NOT EXISTS service_snapshot_json JSONB,
ADD COLUMN IF NOT EXISTS address_snapshot_json JSONB,
ADD COLUMN IF NOT EXISTS service_name TEXT,
ADD COLUMN IF NOT EXISTS service_category TEXT,
ADD COLUMN IF NOT EXISTS elder_name TEXT,
ADD COLUMN IF NOT EXISTS elder_phone TEXT,
ADD COLUMN IF NOT EXISTS contact_name TEXT,
ADD COLUMN IF NOT EXISTS contact_phone TEXT,
ADD COLUMN IF NOT EXISTS address TEXT,
ADD COLUMN IF NOT EXISTS address_detail TEXT,
ADD COLUMN IF NOT EXISTS full_address TEXT,
ADD COLUMN IF NOT EXISTS appointment_time TIMESTAMPTZ,
ADD COLUMN IF NOT EXISTS duration_minutes INTEGER,
ADD COLUMN IF NOT EXISTS price NUMERIC,
ADD COLUMN IF NOT EXISTS staff_income NUMERIC;
-- ============================================================================
-- 1. 临时安全转换函数:只在当前会话存在,不污染 public schema
-- ============================================================================
CREATE OR REPLACE FUNCTION pg_temp.hzb_safe_numeric(p_value TEXT)
RETURNS NUMERIC
LANGUAGE plpgsql
IMMUTABLE
AS $$
DECLARE
v_text TEXT;
v_match TEXT;
BEGIN
v_text := NULLIF(BTRIM(COALESCE(p_value, '')), '');
IF v_text IS NULL THEN
RETURN NULL;
END IF;
-- 支持 138、138.00、¥138、138元只抽取第一个数字片段。
v_match := SUBSTRING(v_text FROM '[-+]?[0-9]+[.]?[0-9]*');
IF v_match IS NULL OR v_match = '' THEN
RETURN NULL;
END IF;
RETURN v_match::NUMERIC;
EXCEPTION
WHEN invalid_text_representation OR numeric_value_out_of_range THEN
RETURN NULL;
END;
$$;
CREATE OR REPLACE FUNCTION pg_temp.hzb_safe_int(p_value TEXT)
RETURNS INTEGER
LANGUAGE plpgsql
IMMUTABLE
AS $$
DECLARE
v_num NUMERIC;
BEGIN
v_num := pg_temp.hzb_safe_numeric(p_value);
IF v_num IS NULL THEN
RETURN NULL;
END IF;
RETURN ROUND(v_num)::INTEGER;
EXCEPTION
WHEN invalid_text_representation OR numeric_value_out_of_range THEN
RETURN NULL;
END;
$$;
-- ============================================================================
-- 2. 前置检查:现在 address 列已存在,不会再 42703
-- ============================================================================
SELECT id, status, request_id, service_name, service_category,
elder_name, contact_name,
address, address_detail, full_address, address_snapshot_json,
appointment_time, scheduled_at,
price, staff_income, duration_minutes,
created_at
FROM public.hss_service_orders
WHERE deleted_at IS NULL
AND status IN ('pending_assignment', 'pending_accept')
ORDER BY created_at DESC
LIMIT 30;
-- ============================================================================
-- 3. 回填服务信息
-- ============================================================================
UPDATE public.hss_service_orders
SET service_name = COALESCE(
NULLIF(service_name, ''),
NULLIF(service_snapshot_json->>'name', ''),
NULLIF(service_snapshot_json->>'serviceName', ''),
NULLIF(service_snapshot_json->>'service_name', ''),
NULLIF(service_category, ''),
'居家服务订单'
)
WHERE deleted_at IS NULL
AND status IN ('pending_assignment', 'pending_accept')
AND NULLIF(service_name, '') IS NULL;
UPDATE public.hss_service_orders
SET service_category = COALESCE(
NULLIF(service_category, ''),
NULLIF(service_snapshot_json->>'category', ''),
NULLIF(service_snapshot_json->>'serviceCategory', ''),
NULLIF(service_snapshot_json->>'service_category', ''),
NULLIF(service_snapshot_json->>'name', ''),
'居家服务'
)
WHERE deleted_at IS NULL
AND status IN ('pending_assignment', 'pending_accept')
AND NULLIF(service_category, '') IS NULL;
-- ============================================================================
-- 4. 回填价格/收入:全程 NUMERIC不混用 TEXT
-- ============================================================================
UPDATE public.hss_service_orders
SET price = COALESCE(
NULLIF(price, 0),
pg_temp.hzb_safe_numeric(service_snapshot_json->>'price'),
pg_temp.hzb_safe_numeric(service_snapshot_json->>'servicePrice'),
pg_temp.hzb_safe_numeric(service_snapshot_json->>'service_price'),
0::NUMERIC
)
WHERE deleted_at IS NULL
AND status IN ('pending_assignment', 'pending_accept')
AND (price IS NULL OR price = 0);
UPDATE public.hss_service_orders
SET staff_income = COALESCE(
NULLIF(staff_income, 0),
NULLIF(price, 0),
pg_temp.hzb_safe_numeric(service_snapshot_json->>'staffIncome'),
pg_temp.hzb_safe_numeric(service_snapshot_json->>'staff_income'),
pg_temp.hzb_safe_numeric(service_snapshot_json->>'price'),
pg_temp.hzb_safe_numeric(service_snapshot_json->>'servicePrice'),
0::NUMERIC
)
WHERE deleted_at IS NULL
AND status IN ('pending_assignment', 'pending_accept')
AND (staff_income IS NULL OR staff_income = 0);
-- ============================================================================
-- 5. 回填老人/联系人信息
-- ============================================================================
UPDATE public.hss_service_orders
SET elder_name = COALESCE(
NULLIF(elder_name, ''),
NULLIF(service_snapshot_json->>'elderName', ''),
NULLIF(service_snapshot_json->>'elder_name', ''),
NULLIF(service_snapshot_json->>'recipientName', ''),
'服务对象待补充'
)
WHERE deleted_at IS NULL
AND status IN ('pending_assignment', 'pending_accept')
AND NULLIF(elder_name, '') IS NULL;
UPDATE public.hss_service_orders
SET elder_phone = COALESCE(
NULLIF(elder_phone, ''),
NULLIF(service_snapshot_json->>'elderPhone', ''),
NULLIF(service_snapshot_json->>'elder_phone', ''),
NULLIF(service_snapshot_json->>'recipientPhone', ''),
''
)
WHERE deleted_at IS NULL
AND status IN ('pending_assignment', 'pending_accept')
AND NULLIF(elder_phone, '') IS NULL;
UPDATE public.hss_service_orders
SET contact_name = COALESCE(
NULLIF(contact_name, ''),
NULLIF(service_snapshot_json->>'contactName', ''),
NULLIF(service_snapshot_json->>'contact_name', ''),
NULLIF(service_snapshot_json->>'emergencyContactName', ''),
'家属待补充'
)
WHERE deleted_at IS NULL
AND status IN ('pending_assignment', 'pending_accept')
AND NULLIF(contact_name, '') IS NULL;
UPDATE public.hss_service_orders
SET contact_phone = COALESCE(
NULLIF(contact_phone, ''),
NULLIF(service_snapshot_json->>'contactPhone', ''),
NULLIF(service_snapshot_json->>'contact_phone', ''),
NULLIF(service_snapshot_json->>'emergencyContactPhone', ''),
''
)
WHERE deleted_at IS NULL
AND status IN ('pending_assignment', 'pending_accept')
AND NULLIF(contact_phone, '') IS NULL;
-- ============================================================================
-- 6. 回填地址字段
-- 既回填 address/address_detail/full_address也同步写入 address_snapshot_json。
-- ============================================================================
UPDATE public.hss_service_orders
SET
address = COALESCE(
NULLIF(address, ''),
NULLIF(address_snapshot_json->>'fullAddress', ''),
NULLIF(address_snapshot_json->>'full_address', ''),
NULLIF(address_snapshot_json->>'address', ''),
NULLIF(address_snapshot_json->>'addressDetail', ''),
NULLIF(address_snapshot_json->>'address_detail', ''),
NULLIF(service_snapshot_json->>'address', ''),
NULLIF(service_snapshot_json->>'serviceAddress', ''),
'地址待补充'
),
address_detail = COALESCE(
NULLIF(address_detail, ''),
NULLIF(address_snapshot_json->>'addressDetail', ''),
NULLIF(address_snapshot_json->>'address_detail', ''),
NULLIF(address_snapshot_json->>'detail', ''),
NULLIF(service_snapshot_json->>'addressDetail', ''),
''
),
full_address = COALESCE(
NULLIF(full_address, ''),
NULLIF(address_snapshot_json->>'fullAddress', ''),
NULLIF(address_snapshot_json->>'full_address', ''),
NULLIF(address_snapshot_json->>'address', ''),
NULLIF(service_snapshot_json->>'address', ''),
NULLIF(service_snapshot_json->>'serviceAddress', ''),
'地址待补充'
)
WHERE deleted_at IS NULL
AND status IN ('pending_assignment', 'pending_accept')
AND (
NULLIF(address, '') IS NULL
OR NULLIF(full_address, '') IS NULL
OR address_snapshot_json IS NULL
);
UPDATE public.hss_service_orders
SET address_snapshot_json = jsonb_strip_nulls(
COALESCE(address_snapshot_json, '{}'::jsonb)
|| jsonb_build_object(
'address', NULLIF(address, ''),
'fullAddress', NULLIF(full_address, ''),
'addressDetail', NULLIF(address_detail, '')
)
)
WHERE deleted_at IS NULL
AND status IN ('pending_assignment', 'pending_accept')
AND (
address_snapshot_json IS NULL
OR NULLIF(address_snapshot_json->>'address', '') IS NULL
OR NULLIF(address_snapshot_json->>'fullAddress', '') IS NULL
);
-- ============================================================================
-- 7. 回填预约时间和时长
-- ============================================================================
UPDATE public.hss_service_orders
SET appointment_time = COALESCE(
appointment_time,
scheduled_at,
created_at
)
WHERE deleted_at IS NULL
AND status IN ('pending_assignment', 'pending_accept')
AND appointment_time IS NULL;
UPDATE public.hss_service_orders
SET duration_minutes = COALESCE(
NULLIF(duration_minutes, 0),
pg_temp.hzb_safe_int(service_snapshot_json->>'durationMinutes'),
pg_temp.hzb_safe_int(service_snapshot_json->>'duration_minutes'),
pg_temp.hzb_safe_int(service_snapshot_json->>'duration'),
90
)
WHERE deleted_at IS NULL
AND status IN ('pending_assignment', 'pending_accept')
AND (duration_minutes IS NULL OR duration_minutes = 0);
-- ============================================================================
-- 8. 后置检查
-- ============================================================================
SELECT id, status, request_id, service_name, service_category,
elder_name, contact_name,
address, address_detail, full_address, address_snapshot_json,
appointment_time, scheduled_at,
price, staff_income, duration_minutes,
created_at
FROM public.hss_service_orders
WHERE deleted_at IS NULL
AND status IN ('pending_assignment', 'pending_accept')
ORDER BY created_at DESC
LIMIT 30;
-- ============================================================================
-- 9. 统计仍缺核心展示字段的订单数
-- ============================================================================
DO $$
DECLARE
v_pending_assignment_count INTEGER;
v_pending_accept_count INTEGER;
v_core_missing_count INTEGER;
BEGIN
SELECT count(*) INTO v_pending_assignment_count
FROM public.hss_service_orders
WHERE deleted_at IS NULL
AND status = 'pending_assignment';
SELECT count(*) INTO v_pending_accept_count
FROM public.hss_service_orders
WHERE deleted_at IS NULL
AND status = 'pending_accept';
SELECT count(*) INTO v_core_missing_count
FROM public.hss_service_orders
WHERE deleted_at IS NULL
AND status IN ('pending_assignment', 'pending_accept')
AND (
NULLIF(service_name, '') IS NULL
OR NULLIF(address, '') IS NULL
OR appointment_time IS NULL
OR price IS NULL
OR staff_income IS NULL
);
RAISE NOTICE '=== pending 订单核心字段回填完成 ===';
RAISE NOTICE 'pending_assignment 订单数: %', v_pending_assignment_count;
RAISE NOTICE 'pending_accept 订单数: %', v_pending_accept_count;
RAISE NOTICE '核心字段仍缺失订单数: %', v_core_missing_count;
RAISE NOTICE '总计: %', v_pending_assignment_count + v_pending_accept_count;
END $$;
COMMIT;

View File

@@ -0,0 +1,765 @@
-- =====================================================================================
-- 20260610_fix_delivery_order_list_pending_blank.sql
-- Purpose:
-- 修复配送端"待接单"Tab 订单卡片显示空白的问题。
-- 根因:
-- 1. hss_service_orders 表缺少 request_id / price / staff_income / service_category 等列
-- 2. delivery_build_order_json 对空字段的 COALESCE 兜底不足,返回 "" / 0
-- 3. delivery_order_matches 对 pending tab 的过滤逻辑正确,但返回的订单核心字段为空
-- 4. 前端 enrichOrderWithRequestFallback 依赖 request_id 补全,但 hss_service_orders 无此列
-- 5. pending_assignment / pending_accept 不在原表 CHECK 约束中
-- =====================================================================================
BEGIN;
-- ============================================================================
-- 1. 为 hss_service_orders 补齐缺失的核心列
-- ============================================================================
ALTER TABLE public.hss_service_orders
ADD COLUMN IF NOT EXISTS request_id TEXT NOT NULL DEFAULT '',
ADD COLUMN IF NOT EXISTS service_category TEXT NOT NULL DEFAULT '',
ADD COLUMN IF NOT EXISTS price NUMERIC(10, 2) NOT NULL DEFAULT 0,
ADD COLUMN IF NOT EXISTS staff_income NUMERIC(10, 2) NOT NULL DEFAULT 0,
ADD COLUMN IF NOT EXISTS elder_name TEXT NOT NULL DEFAULT '',
ADD COLUMN IF NOT EXISTS elder_phone TEXT NOT NULL DEFAULT '',
ADD COLUMN IF NOT EXISTS contact_name TEXT NOT NULL DEFAULT '',
ADD COLUMN IF NOT EXISTS contact_phone TEXT NOT NULL DEFAULT '',
ADD COLUMN IF NOT EXISTS scheduled_at TIMESTAMPTZ,
ADD COLUMN IF NOT EXISTS dispatch_status TEXT NOT NULL DEFAULT 'pending',
ADD COLUMN IF NOT EXISTS dispatch_attempt_count INTEGER NOT NULL DEFAULT 0,
ADD COLUMN IF NOT EXISTS dispatch_error_code TEXT NOT NULL DEFAULT '',
ADD COLUMN IF NOT EXISTS dispatch_error_message TEXT NOT NULL DEFAULT '',
ADD COLUMN IF NOT EXISTS dispatch_failed_at TIMESTAMPTZ,
ADD COLUMN IF NOT EXISTS service_price NUMERIC(10, 2) NOT NULL DEFAULT 0,
ADD COLUMN IF NOT EXISTS duration_minutes INTEGER NOT NULL DEFAULT 90;
-- 扩展 CHECK 约束:允许 pending_assignment / pending_accept 状态
-- 由于 PostgreSQL 不支持直接修改 CHECK需要 DROP + CREATE
ALTER TABLE public.hss_service_orders DROP CONSTRAINT IF EXISTS chk_hss_service_orders_status;
ALTER TABLE public.hss_service_orders
ADD CONSTRAINT chk_hss_service_orders_status CHECK (
status IN (
'created', 'paid', 'assigned', 'accepted', 'rejected', 'departed', 'arrived',
'in_service', 'completed', 'pending_acceptance', 'accepted_by_user',
'reviewed', 'settled', 'cancelled', 'exception',
'pending_assignment', 'pending_accept',
'waiting_departure', 'on_the_way', 'serving', 'pending_confirm',
'pending_submit', 'abnormal', 'exception_pending', 'archived'
)
);
-- ============================================================================
-- 2. 修复 delivery_build_order_json所有展示字段必须有兜底值
-- ============================================================================
DROP FUNCTION IF EXISTS public.delivery_build_order_json(JSONB, JSONB, JSONB, JSONB, JSONB, TEXT);
CREATE OR REPLACE FUNCTION public.delivery_build_order_json(
p_raw JSONB,
p_logs JSONB DEFAULT '[]'::jsonb,
p_records JSONB DEFAULT '[]'::jsonb,
p_evidence JSONB DEFAULT '[]'::jsonb,
p_exception JSONB DEFAULT NULL,
p_source TEXT DEFAULT 'legacy'
)
RETURNS JSONB
LANGUAGE plpgsql
IMMUTABLE
AS $$
DECLARE
v_service JSONB := COALESCE(p_raw -> 'service_snapshot_json', jsonb_build_object('category', COALESCE(p_raw ->> 'service_category', ''), 'price', COALESCE((p_raw ->> 'service_price')::NUMERIC, COALESCE((p_raw ->> 'price')::NUMERIC, 0))));
v_address JSONB := COALESCE(p_raw -> 'address_snapshot_json', COALESCE(p_raw -> 'address_snapshot', '{}'::jsonb));
v_raw_status TEXT := COALESCE(p_raw ->> 'status', 'pending_assignment');
v_normalized_status TEXT;
v_front_status TEXT;
v_checkin_record JSONB;
v_service_record JSONB;
v_service_items JSONB;
v_record_json JSONB;
v_timeline JSONB;
v_status_logs JSONB;
v_evidence_list JSONB;
v_service_name TEXT;
v_address_text TEXT;
v_address_detail TEXT;
v_full_address TEXT;
v_appointment_time TEXT;
v_staff_income NUMERIC;
v_price NUMERIC;
BEGIN
-- 优先从 service_snapshot_json 获取 category/price
IF v_service ->> 'category' = '' THEN
v_service := v_service || jsonb_build_object(
'category', COALESCE(NULLIF(p_raw ->> 'service_category', ''), '居家服务'),
'price', COALESCE(
NULLIF(p_raw ->> 'price', '')::NUMERIC,
NULLIF(p_raw ->> 'service_price', '')::NUMERIC,
0
)
);
END IF;
IF p_source = 'care' THEN
IF COALESCE(p_raw ->> 'accepted_by_family_at', '') <> '' THEN
v_normalized_status := 'completed';
ELSIF COALESCE(p_raw ->> 'acceptance_pending_at', '') <> '' THEN
v_normalized_status := 'pending_acceptance';
ELSIF COALESCE(p_raw ->> 'service_started_at', '') <> '' THEN
v_normalized_status := 'in_service';
ELSIF COALESCE(p_raw ->> 'checked_in_at', '') <> '' THEN
v_normalized_status := 'arrived';
ELSIF COALESCE(p_raw ->> 'departed_at', '') <> '' THEN
v_normalized_status := 'departed';
ELSIF COALESCE(p_raw ->> 'accepted_at', '') <> '' THEN
v_normalized_status := 'accepted';
ELSE
v_normalized_status := CASE v_raw_status
WHEN 'ORDER_ACCEPTED' THEN 'accepted'
WHEN 'ORDER_CHECKED_IN' THEN 'arrived'
WHEN 'ORDER_IN_SERVICE' THEN 'in_service'
WHEN 'ACCEPTANCE_PENDING' THEN 'pending_acceptance'
WHEN 'ORDER_EXCEPTION' THEN 'exception'
WHEN 'ORDER_REJECTED' THEN 'rejected'
WHEN 'ORDER_CANCELLED' THEN 'cancelled'
WHEN 'ORDER_COMPLETED' THEN 'completed'
ELSE 'pending_assignment'
END;
END IF;
ELSE
v_normalized_status := lower(v_raw_status);
END IF;
v_front_status := public.delivery_front_status(v_normalized_status, p_raw);
v_timeline := public.delivery_build_timeline(p_logs);
v_status_logs := public.delivery_build_status_logs(p_logs, COALESCE(p_raw ->> 'id', ''));
v_evidence_list := public.delivery_build_evidence(p_evidence, COALESCE(p_raw ->> 'id', ''));
SELECT item INTO v_checkin_record
FROM jsonb_array_elements(COALESCE(p_records, '[]'::jsonb)) item
WHERE COALESCE(item ->> 'record_type', item ->> 'care_record_type', '') = 'checkin'
ORDER BY COALESCE(item ->> 'created_at', '') DESC
LIMIT 1;
SELECT item INTO v_service_record
FROM jsonb_array_elements(COALESCE(p_records, '[]'::jsonb)) item
WHERE COALESCE(item ->> 'record_type', item ->> 'care_record_type', '') <> 'checkin'
AND COALESCE(item ->> 'record_type', item ->> 'care_record_type', '') <> 'review'
ORDER BY COALESCE(item ->> 'created_at', '') DESC
LIMIT 1;
IF v_service_record IS NULL THEN
v_service_record := v_checkin_record;
END IF;
v_service_items := COALESCE(v_service_record -> 'service_items_json', public.delivery_default_service_items(COALESCE(p_raw ->> 'id', ''), COALESCE(p_raw ->> 'service_name', '')));
IF v_service_record IS NULL THEN
v_record_json := NULL;
ELSE
v_record_json := jsonb_build_object(
'id', COALESCE(v_service_record ->> 'id', ''),
'orderId', COALESCE(p_raw ->> 'id', ''),
'startTime', COALESCE(v_service_record ->> 'started_at', v_service_record ->> 'service_started_at', ''),
'endTime', COALESCE(v_service_record ->> 'finished_at', v_service_record ->> 'service_finished_at', ''),
'actualDurationMinutes', COALESCE((v_service_record ->> 'duration_minutes')::INTEGER, (v_service_record ->> 'actual_duration_minutes')::INTEGER, 0),
'serviceItems', COALESCE(v_service_items, '[]'::jsonb),
'serviceContent', COALESCE((SELECT jsonb_agg(elem ->> 'name') FROM jsonb_array_elements(COALESCE(v_service_items, '[]'::jsonb)) elem WHERE COALESCE((elem ->> 'completed')::BOOLEAN, false)), '[]'::jsonb),
'processNote', COALESCE(v_service_record ->> 'summary', v_service_record ->> 'content', ''),
'elderStatus', '',
'healthMetrics', jsonb_build_object('bloodPressure', '', 'heartRate', '', 'bloodSugar', '', 'bloodOxygen', ''),
'materialsUsed', '',
'abnormalNote', '',
'photos', COALESCE((SELECT jsonb_agg(item ->> 'file_url') FROM jsonb_array_elements(COALESCE(p_evidence, '[]'::jsonb)) item WHERE COALESCE(item ->> 'phase', '') = 'service'), '[]'::jsonb),
'staffRemark', COALESCE(v_service_record ->> 'remark', ''),
'familyConfirmation', jsonb_build_object('method', 'none', 'code', '', 'signatureName', '', 'signatureUrl', '', 'confirmedAt', ''),
'createdAt', COALESCE(v_service_record ->> 'created_at', ''),
'updatedAt', COALESCE(v_service_record ->> 'updated_at', '')
);
END IF;
-- 【核心修复】serviceName 多级兜底
v_service_name := COALESCE(
NULLIF(p_raw ->> 'service_name', ''),
COALESCE(v_service ->> 'category', ''),
COALESCE(p_raw ->> 'service_category', ''),
'居家服务订单'
);
-- 【核心修复】address 多级兜底
v_address_text := COALESCE(
NULLIF(v_address ->> 'fullAddress', ''),
NULLIF(v_address ->> 'full_address', ''),
NULLIF(v_address ->> 'address', ''),
NULLIF(v_address ->> 'name', ''),
''
);
v_address_detail := COALESCE(
NULLIF(v_address ->> 'detailAddress', ''),
NULLIF(v_address ->> 'detail_address', ''),
''
);
IF v_address_text = '' THEN
v_address_text := '地址待补充';
END IF;
v_full_address := v_address_text;
IF v_address_detail != '' AND v_full_address !~ v_address_detail THEN
v_full_address := v_full_address || ' ' || v_address_detail;
END IF;
-- 【核心修复】appointmentTime 多级兜底
v_appointment_time := COALESCE(
NULLIF(p_raw ->> 'appointment_time', ''),
NULLIF(p_raw ->> 'scheduled_at', ''),
NULLIF(p_raw ->> 'appointment_start_time', ''),
COALESCE(to_char((p_raw ->> 'created_at')::timestamptz, 'YYYY-MM-DD HH24:MI:SS'), ''),
'时间待补充'
);
-- 【核心修复】staffIncome / price 多级兜底
v_price := COALESCE(
NULLIF(v_service ->> 'price', '')::NUMERIC,
NULLIF(p_raw ->> 'price', '')::NUMERIC,
NULLIF(p_raw ->> 'service_price', '')::NUMERIC,
0
);
v_staff_income := COALESCE(
NULLIF(p_raw ->> 'staff_income', '')::NUMERIC,
v_price,
0
);
RETURN jsonb_build_object(
'id', COALESCE(p_raw ->> 'id', ''),
'orderNo', COALESCE(NULLIF(p_raw ->> 'task_no', ''), p_raw ->> 'order_no', ''),
'serviceType', COALESCE(NULLIF(v_service ->> 'category', ''), '居家服务'),
'serviceName', v_service_name,
'serviceCategory', COALESCE(NULLIF(p_raw ->> 'service_category', ''), COALESCE(v_service ->> 'category', '')),
'serviceItems', COALESCE(v_service_items, '[]'::jsonb),
'elderId', COALESCE(p_raw ->> 'elder_id', p_raw ->> 'user_id', ''),
'elderName', COALESCE(
NULLIF(p_raw ->> 'elder_name', ''),
NULLIF(p_raw ->> 'recipient_name', ''),
'服务对象待补充'
),
'elderNameMasked', COALESCE(
NULLIF(p_raw ->> 'elder_name', ''),
NULLIF(p_raw ->> 'recipient_name', ''),
'服务对象待补充'
),
'elderGender', '',
'elderAge', 0,
'elderPhone', COALESCE(
NULLIF(p_raw ->> 'elder_phone', ''),
NULLIF(p_raw ->> 'recipient_phone', ''),
''
),
'elderPhoneMasked', COALESCE(
NULLIF(p_raw ->> 'elder_phone', ''),
NULLIF(p_raw ->> 'recipient_phone', ''),
''
),
'fullElderName', COALESCE(
NULLIF(p_raw ->> 'elder_name', ''),
NULLIF(p_raw ->> 'recipient_name', ''),
'服务对象待补充'
),
'fullPhone', COALESCE(
NULLIF(p_raw ->> 'elder_phone', ''),
NULLIF(p_raw ->> 'recipient_phone', ''),
''
),
'contactRelation', '家属',
'addressSummary', v_address_text,
'address', v_address_text,
'addressDetail', v_address_detail,
'fullAddress', v_full_address,
'latitude', COALESCE(NULLIF(v_address ->> 'latitude', '')::NUMERIC, 0),
'longitude', COALESCE(NULLIF(v_address ->> 'longitude', '')::NUMERIC, 0),
'appointmentTime', v_appointment_time,
'appointmentStartTime', v_appointment_time,
'appointmentEndTime', v_appointment_time,
'duration', COALESCE(
NULLIF(p_raw ->> 'duration_minutes', '')::INTEGER,
90
),
'estimatedDuration', COALESCE(
NULLIF(p_raw ->> 'duration_minutes', '')::INTEGER,
90
),
'price', v_price,
'staffIncome', v_staff_income,
'distance', '',
'actualStartTime', COALESCE(p_raw ->> 'service_started_at', ''),
'actualEndTime', COALESCE(NULLIF(p_raw ->> 'completed_at', ''), p_raw ->> 'service_completed_at', ''),
'status', v_front_status,
'statusText', public.delivery_status_text(v_front_status),
'statusTone', public.delivery_status_tone(v_front_status),
'riskTags', '[]'::jsonb,
'healthTags', '[]'::jsonb,
'careLevel', COALESCE(v_service ->> 'category', ''),
'needFamilyPresent', false,
'needMaterials', false,
'remark', COALESCE(p_raw ->> 'remark', ''),
'merchantId', COALESCE(p_raw ->> 'merchant_id', ''),
'merchantName', COALESCE(p_raw ->> 'merchant_name', '')
) || jsonb_build_object(
'deliveryStaffId', COALESCE(NULLIF(p_raw ->> 'current_staff_id', ''), p_raw ->> 'assigned_to', ''),
'deliveryStaffName', COALESCE(p_raw ->> 'delivery_staff_name', ''),
'acceptTime', COALESCE(p_raw ->> 'accepted_at', ''),
'departTime', COALESCE(p_raw ->> 'departed_at', ''),
'arriveTime', COALESCE(NULLIF(p_raw ->> 'arrived_at', ''), p_raw ->> 'checked_in_at', ''),
'checkinTime', COALESCE(NULLIF(p_raw ->> 'checked_in_at', ''), p_raw ->> 'arrived_at', ''),
'startServiceTime', COALESCE(p_raw ->> 'service_started_at', ''),
'finishTime', COALESCE(NULLIF(p_raw ->> 'completed_at', ''), p_raw ->> 'service_completed_at', ''),
'cancelReason', COALESCE(p_raw ->> 'cancel_reason', ''),
'exceptionType', COALESCE(p_exception ->> 'exception_type', ''),
'exceptionDesc', COALESCE(p_exception ->> 'description', p_exception ->> 'remark', ''),
'evidenceList', COALESCE(v_evidence_list, '[]'::jsonb),
'signatureUrl', '',
'signatureName', '',
'satisfactionStatus', CASE WHEN v_front_status = 'pending_acceptance' THEN '待验收' WHEN v_front_status = 'completed' THEN '已验收' ELSE '待评价' END,
'settlementStatus', CASE WHEN v_front_status = 'completed' THEN '已结算' ELSE '待确认' END,
'archiveStatus', '未归档',
'createdAt', COALESCE(p_raw ->> 'created_at', ''),
'updatedAt', COALESCE(p_raw ->> 'updated_at', ''),
'contactName', COALESCE(
NULLIF(p_raw ->> 'contact_name', ''),
'家属待补充'
),
'contactPhone', COALESCE(NULLIF(p_raw ->> 'contact_phone', ''), ''),
'requestId', COALESCE(NULLIF(p_raw ->> 'request_id', ''), '')
) || jsonb_build_object(
'notices', '[]'::jsonb,
'timeline', COALESCE(v_timeline, '[]'::jsonb),
'statusLog', COALESCE(v_status_logs, '[]'::jsonb),
'serviceSummary', COALESCE(v_service_record ->> 'summary', v_service_record ->> 'content', ''),
'progressNote', COALESCE(v_service_record ->> 'remark', ''),
'distanceKm', '',
'allowCheckinRadiusMeters', 100,
'lastLocation', CASE
WHEN v_checkin_record IS NULL THEN NULL
ELSE jsonb_build_object(
'latitude', COALESCE((v_checkin_record ->> 'latitude')::NUMERIC, (v_checkin_record ->> 'checkin_latitude')::NUMERIC, 0),
'longitude', COALESCE((v_checkin_record ->> 'longitude')::NUMERIC, (v_checkin_record ->> 'checkin_longitude')::NUMERIC, 0),
'address', COALESCE(v_checkin_record ->> 'location_text', v_checkin_record ->> 'checkin_address', ''),
'time', COALESCE(v_checkin_record ->> 'checked_in_at', v_checkin_record ->> 'checkin_time', '')
)
END,
'trackPoints', COALESCE(v_service_record -> 'track_points_json', '[]'::jsonb),
'serviceRecord', v_record_json,
'abnormalReport', public.delivery_build_abnormal(p_exception, COALESCE(p_raw ->> 'id', ''))
);
END;
$$;
REVOKE ALL ON FUNCTION public.delivery_build_order_json(JSONB, JSONB, JSONB, JSONB, JSONB, TEXT) FROM PUBLIC;
REVOKE ALL ON FUNCTION public.delivery_build_order_json(JSONB, JSONB, JSONB, JSONB, JSONB, TEXT) FROM anon;
GRANT EXECUTE ON FUNCTION public.delivery_build_order_json(JSONB, JSONB, JSONB, JSONB, JSONB, TEXT) TO authenticated;
-- ============================================================================
-- 3. 修复 delivery_get_legacy_order_json确保所有状态统一返回完整字段
-- ============================================================================
CREATE OR REPLACE FUNCTION public.delivery_get_legacy_order_json(p_order_id TEXT)
RETURNS JSONB
LANGUAGE plpgsql
STABLE
SECURITY DEFINER
SET search_path = public
AS $$
DECLARE
v_staff_id UUID := public.delivery_current_staff_id();
v_raw JSONB;
v_logs JSONB := '[]'::jsonb;
v_records JSONB := '[]'::jsonb;
v_evidence JSONB := '[]'::jsonb;
BEGIN
SELECT to_jsonb(o)
INTO v_raw
FROM public.hss_service_orders o
WHERE o.id = p_order_id
AND o.deleted_at IS NULL
AND (v_staff_id IS NULL OR o.current_staff_id = v_staff_id)
LIMIT 1;
IF v_raw IS NULL THEN
RETURN NULL;
END IF;
SELECT COALESCE(jsonb_agg(to_jsonb(l) ORDER BY l.created_at DESC), '[]'::jsonb)
INTO v_logs
FROM public.hss_service_order_status_logs l
WHERE l.order_id = p_order_id;
SELECT COALESCE(jsonb_agg(to_jsonb(r) ORDER BY r.created_at DESC), '[]'::jsonb)
INTO v_records
FROM public.hss_service_execution_records r
WHERE r.order_id = p_order_id;
SELECT COALESCE(jsonb_agg(to_jsonb(e) ORDER BY e.created_at DESC), '[]'::jsonb)
INTO v_evidence
FROM public.hss_service_evidence_files e
WHERE e.order_id = p_order_id;
RETURN public.delivery_build_order_json(v_raw, v_logs, v_records, v_evidence, NULL, 'legacy');
END;
$$;
REVOKE ALL ON FUNCTION public.delivery_get_legacy_order_json(TEXT) FROM PUBLIC;
REVOKE ALL ON FUNCTION public.delivery_get_legacy_order_json(TEXT) FROM anon;
GRANT EXECUTE ON FUNCTION public.delivery_get_legacy_order_json(TEXT) TO authenticated;
-- ============================================================================
-- 4. 修复 rpc_delivery_order_listpending tab 必须包含 pending_assignment + pending_accept
-- ============================================================================
CREATE OR REPLACE FUNCTION public.rpc_delivery_order_list(
p_staff_id UUID DEFAULT NULL,
p_tab TEXT DEFAULT 'all',
p_keyword TEXT DEFAULT ''
)
RETURNS JSONB
LANGUAGE plpgsql
STABLE
SECURITY DEFINER
SET search_path = pg_catalog, public, pg_temp
AS $$
DECLARE
v_user_id UUID := public.delivery_current_user_id();
v_staff_id UUID := COALESCE(p_staff_id, public.delivery_current_staff_id());
v_result JSONB := '[]'::jsonb;
v_order JSONB;
v_order_id TEXT;
v_order_key TEXT;
v_seen_order_ids TEXT[] := ARRAY[]::TEXT[];
v_max_rows INTEGER := 200;
BEGIN
IF v_user_id IS NULL THEN
RAISE EXCEPTION USING
ERRCODE = '42501',
MESSAGE = 'delivery user context is required';
END IF;
IF v_staff_id IS NULL THEN
RAISE EXCEPTION USING
ERRCODE = '42501',
MESSAGE = 'delivery staff context is required';
END IF;
PERFORM public.delivery_assert_staff_access(v_staff_id);
-- 查询 hss_service_orders
IF public.delivery_table_exists('hss_service_orders') THEN
FOR v_order_id IN
SELECT o.id::text
FROM public.hss_service_orders o
WHERE o.deleted_at IS NULL
AND o.current_staff_id = v_staff_id
AND o.status IN (
'pending_assignment',
'pending_accept',
'accepted',
'waiting_departure',
'departed',
'on_the_way',
'arrived',
'in_service',
'serving',
'pending_acceptance',
'completed',
'settled',
'archived',
'abnormal',
'exception_pending'
)
ORDER BY COALESCE(o.updated_at, o.created_at) DESC, o.created_at DESC
LIMIT v_max_rows
LOOP
v_order := public.delivery_get_legacy_order_json(v_order_id);
IF v_order IS NULL THEN
CONTINUE;
END IF;
v_order_key := COALESCE(v_order ->> 'id', v_order_id);
IF v_order_key = ANY(v_seen_order_ids) THEN
CONTINUE;
END IF;
v_seen_order_ids := array_append(v_seen_order_ids, v_order_key);
v_result := public.delivery_append_if_match(
COALESCE(v_result, '[]'::jsonb),
v_order,
COALESCE(p_tab, 'all'),
COALESCE(p_keyword, '')
);
END LOOP;
END IF;
-- 兼容 ec_care_tasks 新链
IF public.delivery_table_exists('ec_care_tasks') THEN
BEGIN
FOR v_order_id IN EXECUTE
'SELECT t.id::text
FROM public.ec_care_tasks t
WHERE t.assigned_to = $1
AND t.status NOT IN (''ORDER_COMPLETED'', ''ORDER_CANCELLED'')
ORDER BY t.created_at DESC
LIMIT $2'
USING v_user_id, v_max_rows
LOOP
v_order := public.delivery_get_care_order_json(v_order_id);
IF v_order IS NULL THEN
CONTINUE;
END IF;
v_order_key := COALESCE(v_order ->> 'id', v_order_id);
IF v_order_key = ANY(v_seen_order_ids) THEN
CONTINUE;
END IF;
v_seen_order_ids := array_append(v_seen_order_ids, v_order_key);
v_result := public.delivery_append_if_match(
COALESCE(v_result, '[]'::jsonb),
v_order,
COALESCE(p_tab, 'all'),
COALESCE(p_keyword, '')
);
END LOOP;
EXCEPTION
WHEN undefined_table OR undefined_column OR undefined_function THEN
RAISE NOTICE 'Skip legacy ec_care_tasks compatibility query: %', SQLERRM;
END;
END IF;
RETURN COALESCE(v_result, '[]'::jsonb);
END;
$$;
REVOKE ALL ON FUNCTION public.rpc_delivery_order_list(UUID, TEXT, TEXT) FROM PUBLIC;
REVOKE ALL ON FUNCTION public.rpc_delivery_order_list(UUID, TEXT, TEXT) FROM anon;
GRANT EXECUTE ON FUNCTION public.rpc_delivery_order_list(UUID, TEXT, TEXT) TO authenticated;
-- ============================================================================
-- 5. 修复 rpc_delivery_dashboard同步更新
-- ============================================================================
CREATE OR REPLACE FUNCTION public.rpc_delivery_dashboard(p_staff_id UUID DEFAULT NULL)
RETURNS JSONB
LANGUAGE plpgsql
STABLE
SECURITY DEFINER
SET search_path = pg_catalog, public, pg_temp
AS $$
DECLARE
v_user_id UUID := public.delivery_current_user_id();
v_staff_id UUID := COALESCE(p_staff_id, public.delivery_current_staff_id());
v_orders JSONB := '[]'::jsonb;
v_pending_assignment_count INTEGER := 0;
v_pending_accept_count INTEGER := 0;
v_today_order_count INTEGER := 0;
v_pending_depart_count INTEGER := 0;
v_serving_count INTEGER := 0;
v_completed_count INTEGER := 0;
v_exception_count INTEGER := 0;
v_next_order JSONB;
v_item JSONB;
v_order_id TEXT;
v_order_key TEXT;
v_seen_order_ids TEXT[] := ARRAY[]::TEXT[];
v_max_rows INTEGER := 200;
BEGIN
IF v_user_id IS NULL THEN
RAISE EXCEPTION USING
ERRCODE = '42501',
MESSAGE = 'delivery user context is required';
END IF;
IF v_staff_id IS NULL THEN
RAISE EXCEPTION USING
ERRCODE = '42501',
MESSAGE = 'delivery staff context is required';
END IF;
PERFORM public.delivery_assert_staff_access(v_staff_id);
IF public.delivery_table_exists('hss_service_orders') THEN
FOR v_order_id IN
SELECT o.id::text
FROM public.hss_service_orders o
WHERE o.deleted_at IS NULL
AND o.current_staff_id = v_staff_id
AND o.status IN (
'pending_assignment',
'pending_accept',
'accepted',
'waiting_departure',
'departed',
'on_the_way',
'arrived',
'in_service',
'serving',
'pending_acceptance',
'completed',
'settled',
'archived',
'abnormal',
'exception_pending'
)
ORDER BY COALESCE(o.updated_at, o.created_at) DESC, o.created_at DESC
LIMIT v_max_rows
LOOP
v_item := public.delivery_get_legacy_order_json(v_order_id);
IF v_item IS NULL THEN
CONTINUE;
END IF;
v_order_key := COALESCE(v_item ->> 'id', v_order_id);
IF v_order_key = ANY(v_seen_order_ids) THEN
CONTINUE;
END IF;
v_seen_order_ids := array_append(v_seen_order_ids, v_order_key);
v_orders := COALESCE(v_orders, '[]'::jsonb) || jsonb_build_array(v_item);
END LOOP;
END IF;
IF public.delivery_table_exists('ec_care_tasks') THEN
BEGIN
FOR v_order_id IN EXECUTE
'SELECT t.id::text
FROM public.ec_care_tasks t
WHERE t.assigned_to = $1
AND t.status NOT IN (''ORDER_COMPLETED'', ''ORDER_CANCELLED'')
ORDER BY t.created_at DESC
LIMIT $2'
USING v_user_id, v_max_rows
LOOP
v_item := public.delivery_get_care_order_json(v_order_id);
IF v_item IS NULL THEN
CONTINUE;
END IF;
v_order_key := COALESCE(v_item ->> 'id', v_order_id);
IF v_order_key = ANY(v_seen_order_ids) THEN
CONTINUE;
END IF;
v_seen_order_ids := array_append(v_seen_order_ids, v_order_key);
v_orders := COALESCE(v_orders, '[]'::jsonb) || jsonb_build_array(v_item);
END LOOP;
EXCEPTION
WHEN undefined_table OR undefined_column OR undefined_function THEN
RAISE NOTICE 'Skip legacy ec_care_tasks compatibility query: %', SQLERRM;
END;
END IF;
FOR v_item IN
SELECT value
FROM jsonb_array_elements(COALESCE(v_orders, '[]'::jsonb))
LOOP
IF v_item IS NULL THEN
CONTINUE;
END IF;
IF COALESCE(v_item ->> 'status', '') = 'pending_assignment' THEN
v_pending_assignment_count := v_pending_assignment_count + 1;
END IF;
IF COALESCE(v_item ->> 'status', '') = 'pending_accept' THEN
v_pending_accept_count := v_pending_accept_count + 1;
END IF;
IF COALESCE(v_item ->> 'status', '') IN (
'pending_assignment',
'pending_accept',
'accepted',
'waiting_departure',
'departed',
'on_the_way',
'arrived',
'in_service',
'serving',
'pending_acceptance'
) THEN
v_today_order_count := v_today_order_count + 1;
END IF;
IF COALESCE(v_item ->> 'status', '') IN (
'accepted',
'waiting_departure',
'departed',
'on_the_way',
'arrived'
) THEN
v_pending_depart_count := v_pending_depart_count + 1;
END IF;
IF COALESCE(v_item ->> 'status', '') IN ('in_service', 'serving') THEN
v_serving_count := v_serving_count + 1;
END IF;
IF COALESCE(v_item ->> 'status', '') IN ('completed', 'pending_acceptance', 'settled', 'archived') THEN
v_completed_count := v_completed_count + 1;
END IF;
IF COALESCE(v_item ->> 'status', '') IN ('abnormal', 'exception_pending') THEN
v_exception_count := v_exception_count + 1;
END IF;
IF v_next_order IS NULL
AND COALESCE(v_item ->> 'status', '') NOT IN (
'completed',
'settled',
'archived',
'cancelled',
'rejected',
'abnormal',
'exception_pending'
) THEN
v_next_order := v_item;
END IF;
END LOOP;
RETURN jsonb_build_object(
'pendingAssignmentCount', v_pending_assignment_count,
'pendingAcceptCount', v_pending_accept_count,
'todayOrderCount', v_today_order_count,
'pendingDepartCount', v_pending_depart_count,
'servingCount', v_serving_count,
'completedCount', v_completed_count,
'exceptionCount', v_exception_count,
'expectedIncome', 0,
'onlineStatus', COALESCE(public.delivery_build_staff_info(v_staff_id) ->> 'onlineStatus', 'resting'),
'nextOrder', v_next_order,
'recentOrders', COALESCE(
(
SELECT jsonb_agg(value)
FROM (
SELECT value
FROM jsonb_array_elements(COALESCE(v_orders, '[]'::jsonb))
LIMIT 5
) q
),
'[]'::jsonb
)
);
END;
$$;
REVOKE ALL ON FUNCTION public.rpc_delivery_dashboard(UUID) FROM PUBLIC;
REVOKE ALL ON FUNCTION public.rpc_delivery_dashboard(UUID) FROM anon;
GRANT EXECUTE ON FUNCTION public.rpc_delivery_dashboard(UUID) TO authenticated;
COMMIT;