diff --git a/.pages-backup/pages.consumer.2026-06-08T00-49-54-379Z.json b/.pages-backup/pages.consumer.2026-06-08T00-49-54-379Z.json new file mode 100644 index 00000000..5edb6957 --- /dev/null +++ b/.pages-backup/pages.consumer.2026-06-08T00-49-54-379Z.json @@ -0,0 +1,470 @@ +{ + "pages": [ + { + "path": "pages/main/index", + "style": { + "navigationBarTitleText": "首页", + "navigationStyle": "custom", + "enablePullDownRefresh": false + } + }, + { + "path": "pages/user/boot", + "style": { + "navigationBarTitleText": "" + } + }, + { + "path": "pages/user/login", + "style": { + "navigationBarTitleText": "用户登录", + "navigationStyle": "custom" + } + }, + { + "path": "pages/user/register", + "style": { + "navigationBarTitleText": "注册" + } + }, + { + "path": "pages/user/forgot-password", + "style": { + "navigationBarTitleText": "忘记密码" + } + }, + { + "path": "pages/user/terms", + "style": { + "navigationBarTitleText": "用户协议与隐私政策" + } + }, + { + "path": "pages/user/center", + "style": { + "navigationBarTitleText": "用户中心" + } + }, + { + "path": "pages/user/profile", + "style": { + "navigationBarTitleText": "个人资料" + } + }, + { + "path": "pages/user/change-password", + "style": { + "navigationBarTitleText": "修改密码" + } + }, + { + "path": "pages/user/bind-phone", + "style": { + "navigationBarTitleText": "绑定手机" + } + }, + { + "path": "pages/user/bind-email", + "style": { + "navigationBarTitleText": "绑定邮箱" + } + }, + { + "path": "pages/main/messages", + "style": { + "navigationBarTitleText": "消息", + "enablePullDownRefresh": true + } + }, + { + "path": "pages/main/cart", + "style": { + "navigationBarTitleText": "购物车", + "navigationStyle": "custom" + } + }, + { + "path": "pages/main/profile", + "style": { + "navigationBarTitleText": "我的", + "navigationStyle": "custom" + } + }, + { + "path": "pages/main/category", + "style": { + "navigationBarTitleText": "分类", + "navigationStyle": "custom" + } + } + ], + "subPackages": [ + { + "root": "pages/mall/consumer", + "pages": [ + { + "path": "settings", + "style": { + "navigationBarTitleText": "设置" + } + }, + { + "path": "edit-profile", + "style": { + "navigationBarTitleText": "编辑资料" + } + }, + { + "path": "wallet", + "style": { + "navigationBarTitleText": "我的钱包" + } + }, + { + "path": "withdraw", + "style": { + "navigationBarTitleText": "余额提现" + } + }, + { + "path": "search", + "style": { + "navigationBarTitleText": "搜索", + "navigationStyle": "custom" + } + }, + { + "path": "product-detail", + "style": { + "navigationBarTitleText": "", + "navigationStyle": "custom" + } + }, + { + "path": "channel-detail", + "style": { + "navigationBarTitleText": "频道详情", + "navigationStyle": "custom" + } + }, + { + "path": "shop-detail", + "style": { + "navigationBarTitleText": "店铺详情" + } + }, + { + "path": "coupons", + "style": { + "navigationBarTitleText": "我的优惠券" + } + }, + { + "path": "favorites", + "style": { + "navigationBarTitleText": "我的收藏" + } + }, + { + "path": "footprint", + "style": { + "navigationBarTitleText": "我的足迹" + } + }, + { + "path": "address", + "style": { + "navigationBarTitleText": "地址" + } + }, + { + "path": "address-list", + "style": { + "navigationBarTitleText": "收货地址" + } + }, + { + "path": "address-edit", + "style": { + "navigationBarTitleText": "编辑地址" + } + }, + { + "path": "checkout", + "style": { + "navigationBarTitleText": "确认订单" + } + }, + { + "path": "payment", + "style": { + "navigationBarTitleText": "", + "navigationStyle": "custom" + } + }, + { + "path": "payment-success", + "style": { + "navigationBarTitleText": "支付成功", + "navigationStyle": "custom" + } + }, + { + "path": "orders", + "style": { + "navigationBarTitleText": "", + "navigationStyle": "custom", + "enablePullDownRefresh": true, + "backgroundColor": "#f5f5f5" + } + }, + { + "path": "order-detail", + "style": { + "navigationBarTitleText": "订单详情" + } + }, + { + "path": "logistics", + "style": { + "navigationBarTitleText": "物流详情" + } + }, + { + "path": "review", + "style": { + "navigationBarTitleText": "评价晒单" + } + }, + { + "path": "refund", + "style": { + "navigationBarTitleText": "退款/售后" + } + }, + { + "path": "apply-refund", + "style": { + "navigationBarTitleText": "申请售后" + } + }, + { + "path": "refund-review", + "style": { + "navigationBarTitleText": "服务评价" + } + }, + { + "path": "chat", + "style": { + "navigationBarTitleText": "客服聊天", + "navigationStyle": "custom" + } + }, + { + "path": "chat_new", + "style": { + "navigationBarTitleText": "客服聊天(新版)" + } + }, + { + "path": "subscription/plan-list", + "style": { + "navigationBarTitleText": "软件订阅" + } + }, + { + "path": "subscription/plan-detail", + "style": { + "navigationBarTitleText": "订阅详情" + } + }, + { + "path": "subscription/subscribe-checkout", + "style": { + "navigationBarTitleText": "确认订阅" + } + }, + { + "path": "subscription/my-subscriptions", + "style": { + "navigationBarTitleText": "我的订阅" + } + }, + { + "path": "subscription/followed-shops", + "style": { + "navigationBarTitleText": "关注店铺" + } + }, + { + "path": "points/index", + "style": { + "navigationBarTitleText": "积分管理" + } + }, + { + "path": "points/signin", + "style": { + "navigationBarTitleText": "签到" + } + }, + { + "path": "points/exchange", + "style": { + "navigationBarTitleText": "积分兑换" + } + }, + { + "path": "points/exchange-records", + "style": { + "navigationBarTitleText": "兑换记录" + } + }, + { + "path": "red-packets/index", + "style": { + "navigationBarTitleText": "我的红包" + } + }, + { + "path": "bank-cards/index", + "style": { + "navigationBarTitleText": "银行卡管理" + } + }, + { + "path": "bank-cards/add", + "style": { + "navigationBarTitleText": "添加银行卡" + } + }, + { + "path": "home-service/index", + "style": { + "navigationBarTitleText": "居家上门服务", + "navigationStyle": "custom" + } + }, + { + "path": "home-service/apply", + "style": { + "navigationBarTitleText": "提交服务申请", + "navigationStyle": "custom" + } + }, + { + "path": "home-service/service-detail", + "style": { + "navigationBarTitleText": "预约服务", + "navigationStyle": "custom" + } + }, + { + "path": "home-service/order-detail", + "style": { + "navigationBarTitleText": "服务单详情", + "navigationStyle": "custom" + } + }, + { + "path": "home-service/feedback", + "style": { + "navigationBarTitleText": "验收反馈", + "navigationStyle": "custom" + } + }, + { + "path": "bank-cards/verify", + "style": { + "navigationBarTitleText": "银行卡验证" + } + }, + { + "path": "balance/index", + "style": { + "navigationBarTitleText": "余额" + } + }, + { + "path": "my-reviews", + "style": { + "navigationBarTitleText": "我的评价" + } + }, + { + "path": "message-detail", + "style": { + "navigationBarTitleText": "消息详情" + } + }, + { + "path": "message-batch-delete", + "style": { + "navigationBarTitleText": "批量删除" + } + }, + { + "path": "member/index", + "style": { + "navigationBarTitleText": "会员中心" + } + }, + { + "path": "product-reviews", + "style": { + "navigationBarTitleText": "商品评价" + } + } + ] + } + ], + "tabBar": { + "color": "#999999", + "selectedColor": "#ff5000", + "backgroundColor": "#ffffff", + "borderStyle": "black", + "list": [ + { + "pagePath": "pages/main/index", + "text": "首页", + "iconPath": "static/tabbar/home.png", + "selectedIconPath": "static/tabbar/home-active.png" + }, + { + "pagePath": "pages/main/messages", + "text": "消息", + "iconPath": "static/tabbar/message.png", + "selectedIconPath": "static/tabbar/message.png" + }, + { + "pagePath": "pages/main/cart", + "text": "购物车", + "iconPath": "static/tabbar/cart.png", + "selectedIconPath": "static/tabbar/cart.png" + }, + { + "pagePath": "pages/main/profile", + "text": "我的", + "iconPath": "static/tabbar/user.png", + "selectedIconPath": "static/tabbar/user.png" + } + ] + }, + "globalStyle": { + "navigationBarTextStyle": "black", + "navigationBarTitleText": "mall", + "navigationBarBackgroundColor": "#FFFFFF", + "backgroundColor": "#F8F8F8" + }, + "condition": { + "current": 0, + "list": [ + { + "name": "consumer端", + "path": "pages/main/index", + "query": "role=consumer" + } + ] + } +} diff --git a/mall_sql/migrations/20260603_homecare_checkin_rpc_fixed.sql b/mall_sql/migrations/20260603_homecare_checkin_rpc_fixed.sql new file mode 100644 index 00000000..c0fbc43e --- /dev/null +++ b/mall_sql/migrations/20260603_homecare_checkin_rpc_fixed.sql @@ -0,0 +1,607 @@ +-- Homecare location-distance RPCs (precheck + submit) — Fixed for uuid types +-- Date: 2026-06-03 (original), 2026-06-08 (fixed for uuid alignment) +-- Purpose: move checkin distance flow to RPC-only for phase1 tables +-- Fixed: p_org_id, p_team_id, p_operator_id changed from text to uuid +-- to match hc_dispatch_assignments table schema + +create or replace function public.rpc_homecare_dispatch_assignment_upsert( + p_work_order_id uuid, + p_org_id uuid, -- ✅ fixed: was text, now uuid + p_team_id uuid, -- ✅ fixed: was text, now uuid + p_worker_id uuid, + p_service_latitude numeric, + p_service_longitude numeric, + p_service_coordinate_type text default 'gcj02', + p_operator_id uuid default null, -- ✅ fixed: was text, now uuid + p_dispatch_reason text default null +) +returns jsonb +language plpgsql +security definer +set search_path = public, app +as $$ +declare + v_prev_version integer := 0; + v_prev_id text; +begin + if p_work_order_id is null then + raise exception 'p_work_order_id is required'; + end if; + + if p_org_id is null then + raise exception 'p_org_id is required'; + end if; + + if p_service_latitude is null or p_service_longitude is null then + raise exception 'service coordinates are required'; + end if; + + if to_regclass('public.hc_dispatch_assignments') is null then + raise exception 'hc_dispatch_assignments table is required'; + end if; + + select id, assign_version + into v_prev_id, v_prev_version + from public.hc_dispatch_assignments + where work_order_id = p_work_order_id + and is_current = true + order by created_at desc + limit 1; + + if found then + update public.hc_dispatch_assignments + set is_current = false, + updated_at = now(), + updated_by = coalesce(p_operator_id, updated_by) + where id = v_prev_id; + end if; + + insert into public.hc_dispatch_assignments( + id, + work_order_id, + org_id, + team_id, + assign_version, + worker_id, + service_latitude, + service_longitude, + service_coordinate_type, + dispatch_status, + dispatch_reason, + is_current, + dispatched_at, + created_by, + updated_by, + created_at, + updated_at + ) values ( + 'dsp_' || gen_random_uuid()::text, + p_work_order_id, + p_org_id, + p_team_id, + v_prev_version + 1, + p_worker_id, + p_service_latitude, + p_service_longitude, + lower(coalesce(p_service_coordinate_type, 'gcj02')), + 'PENDING', + p_dispatch_reason, + true, + now(), + p_operator_id, + p_operator_id, + now(), + now() + ); + + return jsonb_build_object('ok', true, 'assignVersion', v_prev_version + 1); +end; +$$; + +create or replace function public.rpc_homecare_checkin_precheck( + p_work_order_id uuid, + p_worker_id uuid, + p_latitude numeric, + p_longitude numeric, + p_coordinate_type text default 'gcj02', + p_accuracy numeric default null, + p_reported_at timestamptz default null, + p_location_scene text default 'CHECKIN_PRECHECK' +) +returns jsonb +language plpgsql +security definer +set search_path = public, app +as $$ +declare + v_assignment public.hc_dispatch_assignments%rowtype; + v_radius_meters numeric := 200; + v_distance_meters numeric; + v_can_checkin boolean := false; + v_reason_code text := 'WORK_ORDER_NOT_ASSIGNABLE'; + v_scene text := upper(coalesce(p_location_scene, 'CHECKIN_PRECHECK')); + v_reported_at timestamptz := coalesce(p_reported_at, now()); + v_worker_location_accepted boolean := false; + v_service_location_ready boolean := false; + v_insert_location boolean := false; + v_lat1 double precision; + v_lng1 double precision; + v_lat2 double precision; + v_lng2 double precision; + v_a double precision; + v_c double precision; +begin + if p_work_order_id is null then + raise exception 'p_work_order_id is required'; + end if; + + if p_worker_id is null then + raise exception 'p_worker_id is required'; + end if; + + if p_latitude is null or p_longitude is null then + raise exception 'p_latitude and p_longitude are required'; + end if; + + if v_scene not in ('CHECKIN_PRECHECK', 'CHECKIN') then + raise exception 'invalid p_location_scene: %', v_scene; + end if; + + if to_regclass('public.hc_dispatch_assignments') is null then + raise exception 'hc_dispatch_assignments table is required'; + end if; + + if to_regclass('public.hc_worker_locations') is null then + raise exception 'hc_worker_locations table is required'; + end if; + + if to_regclass('public.sys_sla_config') is null then + raise exception 'sys_sla_config table is required'; + end if; + + select * + into v_assignment + from public.hc_dispatch_assignments + where work_order_id = p_work_order_id + and is_current = true + order by created_at desc + limit 1; + + if not found then + v_reason_code := 'SERVICE_LOCATION_MISSING'; + return jsonb_build_object( + 'distanceMeters', null, + 'allowedRadiusMeters', 0, + 'canCheckin', false, + 'reasonCode', v_reason_code, + 'workerLocationAccepted', false, + 'serviceLocationReady', false + ); + end if; + + if coalesce(v_assignment.dispatch_status, '') not in ('PENDING', 'ACCEPTED') then + v_reason_code := 'WORK_ORDER_NOT_ASSIGNABLE'; + return jsonb_build_object( + 'distanceMeters', null, + 'allowedRadiusMeters', 0, + 'canCheckin', false, + 'reasonCode', v_reason_code, + 'workerLocationAccepted', false, + 'serviceLocationReady', false + ); + end if; + + if v_assignment.worker_id is not null and v_assignment.worker_id <> p_worker_id then + v_reason_code := 'WORKER_NOT_MATCHED'; + return jsonb_build_object( + 'distanceMeters', null, + 'allowedRadiusMeters', 0, + 'canCheckin', false, + 'reasonCode', v_reason_code, + 'workerLocationAccepted', false, + 'serviceLocationReady', true + ); + end if; + + if v_assignment.service_latitude is null or v_assignment.service_longitude is null then + v_reason_code := 'SERVICE_LOCATION_MISSING'; + return jsonb_build_object( + 'distanceMeters', null, + 'allowedRadiusMeters', 0, + 'canCheckin', false, + 'reasonCode', v_reason_code, + 'workerLocationAccepted', false, + 'serviceLocationReady', false + ); + end if; + + v_service_location_ready := true; + + -- SLA lookup priority: WORK_ORDER > ORG > TEAM > GLOBAL + select config_value::numeric + into v_radius_meters + from public.sys_sla_config + where config_key = 'HOMECARE_CHECKIN_RADIUS_METERS' + and scope_type = 'WORK_ORDER' + and scope_id = p_work_order_id::text + and is_active = true + limit 1; + + if v_radius_meters is null and v_assignment.org_id is not null then + select config_value::numeric + into v_radius_meters + from public.sys_sla_config + where config_key = 'HOMECARE_CHECKIN_RADIUS_METERS' + and scope_type = 'ORG' + and scope_id = v_assignment.org_id::text + and is_active = true + limit 1; + end if; + + if v_radius_meters is null and v_assignment.team_id is not null then + select config_value::numeric + into v_radius_meters + from public.sys_sla_config + where config_key = 'HOMECARE_CHECKIN_RADIUS_METERS' + and scope_type = 'TEAM' + and scope_id = v_assignment.team_id::text + and is_active = true + limit 1; + end if; + + if v_radius_meters is null then + select config_value::numeric + into v_radius_meters + from public.sys_sla_config + where config_key = 'HOMECARE_CHECKIN_RADIUS_METERS' + and scope_type = 'GLOBAL' + and scope_id is null + and is_active = true + limit 1; + end if; + + v_radius_meters := coalesce(v_radius_meters, 200); + + -- Haversine distance + v_lat1 := radians(p_latitude::double precision); + v_lng1 := radians(p_longitude::double precision); + v_lat2 := radians(v_assignment.service_latitude::double precision); + v_lng2 := radians(v_assignment.service_longitude::double precision); + + v_a := power(sin((v_lat2 - v_lat1) / 2), 2) + + cos(v_lat1) * cos(v_lat2) * power(sin((v_lng2 - v_lng1) / 2), 2); + v_c := 2 * atan2(sqrt(v_a), sqrt(1 - v_a)); + v_distance_meters := round((6371000 * v_c)::numeric, 2); + + v_can_checkin := (v_distance_meters <= v_radius_meters); + v_reason_code := case when v_can_checkin then 'OK' else 'OUT_OF_RADIUS' end; + v_insert_location := (v_scene = 'CHECKIN_PRECHECK') or (v_scene = 'CHECKIN'); + + if v_insert_location then + insert into public.hc_worker_locations( + id, + work_order_id, + worker_id, + latitude, + longitude, + coordinate_type, + accuracy, + location_scene, + reported_at, + distance_meters, + created_at + ) values ( + 'loc_' || gen_random_uuid()::text, + p_work_order_id, + p_worker_id, + p_latitude, + p_longitude, + lower(coalesce(p_coordinate_type, 'gcj02')), + p_accuracy, + v_scene, + v_reported_at, + v_distance_meters, + now() + ); + + v_worker_location_accepted := true; + end if; + + return jsonb_build_object( + 'distanceMeters', v_distance_meters, + 'allowedRadiusMeters', v_radius_meters, + 'canCheckin', v_can_checkin, + 'reasonCode', v_reason_code, + 'workerLocationAccepted', v_worker_location_accepted, + 'serviceLocationReady', v_service_location_ready + ); +end; +$$; + +create or replace function public.rpc_homecare_checkin_submit( + p_work_order_id uuid, + p_worker_id uuid, + p_latitude numeric, + p_longitude numeric, + p_coordinate_type text default 'gcj02', + p_accuracy numeric default null, + p_reported_at timestamptz default null, + p_evidence_file_ids jsonb default '[]'::jsonb, + p_signature_payload text default null, + p_reason text default null +) +returns jsonb +language plpgsql +security definer +set search_path = public, app +as $$ +declare + v_precheck jsonb; + v_can_checkin boolean := false; + v_assignment public.hc_dispatch_assignments%rowtype; + v_assignment_found boolean := false; + v_max_photo_count numeric := 3; + v_requested_count integer := 0; + v_existing_count integer := 0; + v_mismatch_count integer := 0; + v_not_owned_count integer := 0; + v_not_ready_count integer := 0; +begin + if to_regclass('public.hc_work_order_confirmations') is null then + raise exception 'hc_work_order_confirmations table is required'; + end if; + + if p_signature_payload is null or length(trim(p_signature_payload)) = 0 then + return jsonb_build_object( + 'distanceMeters', null, + 'allowedRadiusMeters', 0, + 'canCheckin', false, + 'reasonCode', 'SIGNATURE_REQUIRED', + 'workerLocationAccepted', false, + 'serviceLocationReady', false + ); + end if; + + if length(trim(p_signature_payload)) < 8 then + return jsonb_build_object( + 'distanceMeters', null, + 'allowedRadiusMeters', 0, + 'canCheckin', false, + 'reasonCode', 'SIGNATURE_INVALID', + 'workerLocationAccepted', false, + 'serviceLocationReady', false + ); + end if; + + if p_evidence_file_ids is null or jsonb_typeof(p_evidence_file_ids) <> 'array' then + return jsonb_build_object( + 'distanceMeters', null, + 'allowedRadiusMeters', 0, + 'canCheckin', false, + 'reasonCode', 'EVIDENCE_FILE_NOT_EXIST', + 'workerLocationAccepted', false, + 'serviceLocationReady', false + ); + end if; + + v_requested_count := jsonb_array_length(p_evidence_file_ids); + if v_requested_count < 1 then + return jsonb_build_object( + 'distanceMeters', null, + 'allowedRadiusMeters', 0, + 'canCheckin', false, + 'reasonCode', 'EVIDENCE_FILE_NOT_EXIST', + 'workerLocationAccepted', false, + 'serviceLocationReady', false + ); + end if; + + select * + into v_assignment + from public.hc_dispatch_assignments + where work_order_id = p_work_order_id + and is_current = true + order by created_at desc + limit 1; + + v_assignment_found := found; + + select config_value::numeric + into v_max_photo_count + from public.sys_sla_config + where config_key = 'HOMECARE_CHECKIN_MAX_PHOTO_COUNT' + and scope_type = 'WORK_ORDER' + and scope_id = p_work_order_id::text + and is_active = true + limit 1; + + if v_max_photo_count is null and v_assignment_found and v_assignment.org_id is not null then + select config_value::numeric + into v_max_photo_count + from public.sys_sla_config + where config_key = 'HOMECARE_CHECKIN_MAX_PHOTO_COUNT' + and scope_type = 'ORG' + and scope_id = v_assignment.org_id::text + and is_active = true + limit 1; + end if; + + if v_max_photo_count is null and v_assignment_found and v_assignment.team_id is not null then + select config_value::numeric + into v_max_photo_count + from public.sys_sla_config + where config_key = 'HOMECARE_CHECKIN_MAX_PHOTO_COUNT' + and scope_type = 'TEAM' + and scope_id = v_assignment.team_id::text + and is_active = true + limit 1; + end if; + + if v_max_photo_count is null then + select config_value::numeric + into v_max_photo_count + from public.sys_sla_config + where config_key = 'HOMECARE_CHECKIN_MAX_PHOTO_COUNT' + and scope_type = 'GLOBAL' + and scope_id is null + and is_active = true + limit 1; + end if; + + v_max_photo_count := coalesce(v_max_photo_count, 3); + if v_requested_count > v_max_photo_count then + return jsonb_build_object( + 'distanceMeters', null, + 'allowedRadiusMeters', 0, + 'canCheckin', false, + 'reasonCode', 'PHOTO_COUNT_EXCEED_LIMIT', + 'workerLocationAccepted', false, + 'serviceLocationReady', false + ); + end if; + + select count(*) + into v_existing_count + from public.hc_evidence_files e + where e.id in (select jsonb_array_elements_text(p_evidence_file_ids)); + + if v_existing_count <> v_requested_count then + return jsonb_build_object( + 'distanceMeters', null, + 'allowedRadiusMeters', 0, + 'canCheckin', false, + 'reasonCode', 'EVIDENCE_FILE_NOT_EXIST', + 'workerLocationAccepted', false, + 'serviceLocationReady', false + ); + end if; + + select count(*) + into v_mismatch_count + from public.hc_evidence_files e + where e.id in (select jsonb_array_elements_text(p_evidence_file_ids)) + and e.work_order_id <> p_work_order_id; + + if v_mismatch_count > 0 then + return jsonb_build_object( + 'distanceMeters', null, + 'allowedRadiusMeters', 0, + 'canCheckin', false, + 'reasonCode', 'EVIDENCE_FILE_WORK_ORDER_MISMATCH', + 'workerLocationAccepted', false, + 'serviceLocationReady', false + ); + end if; + + select count(*) + into v_not_owned_count + from public.hc_evidence_files e + where e.id in (select jsonb_array_elements_text(p_evidence_file_ids)) + and e.work_order_id = p_work_order_id + and coalesce(e.uploader_id, '') <> p_worker_id::text; + + if v_not_owned_count > 0 then + return jsonb_build_object( + 'distanceMeters', null, + 'allowedRadiusMeters', 0, + 'canCheckin', false, + 'reasonCode', 'EVIDENCE_FILE_NOT_OWNED', + 'workerLocationAccepted', false, + 'serviceLocationReady', false + ); + end if; + + select count(*) + into v_not_ready_count + from public.hc_evidence_files e + where e.id in (select jsonb_array_elements_text(p_evidence_file_ids)) + and e.work_order_id = p_work_order_id + and (e.file_url is null or length(trim(e.file_url)) = 0); + + if v_not_ready_count > 0 then + return jsonb_build_object( + 'distanceMeters', null, + 'allowedRadiusMeters', 0, + 'canCheckin', false, + 'reasonCode', 'EVIDENCE_FILE_NOT_READY', + 'workerLocationAccepted', false, + 'serviceLocationReady', false + ); + end if; + + v_precheck := public.rpc_homecare_checkin_precheck( + p_work_order_id, + p_worker_id, + p_latitude, + p_longitude, + p_coordinate_type, + p_accuracy, + p_reported_at, + 'CHECKIN' + ); + + v_can_checkin := coalesce((v_precheck ->> 'canCheckin')::boolean, false); + if not v_can_checkin then + return v_precheck; + end if; + + insert into public.hc_work_order_confirmations( + id, + work_order_id, + confirmation_type, + status, + confirmed_by, + confirmed_at, + reason, + payload, + created_at, + updated_at + ) values ( + 'woc_' || gen_random_uuid()::text, + p_work_order_id, + 'ARRIVAL', + 'PENDING', + p_worker_id, + null, + coalesce(p_reason, 'worker_checkin_submitted'), + jsonb_build_object( + 'distanceMeters', (v_precheck ->> 'distanceMeters')::numeric, + 'allowedRadiusMeters', (v_precheck ->> 'allowedRadiusMeters')::numeric, + 'coordinateType', coalesce(lower(p_coordinate_type), 'gcj02'), + 'reportedAt', coalesce(p_reported_at, now()), + 'signatureProvided', (p_signature_payload is not null and length(trim(p_signature_payload)) > 0), + 'evidenceFileIds', coalesce(p_evidence_file_ids, '[]'::jsonb) + ), + now(), + now() + ); + + return v_precheck || jsonb_build_object('confirmationInserted', true); +end; +$$; + +-- ============================================================ +-- Grants (updated for uuid types) +-- ============================================================ + +revoke all on function public.rpc_homecare_checkin_precheck(uuid, uuid, numeric, numeric, text, numeric, timestamptz, text) from public; +grant execute on function public.rpc_homecare_checkin_precheck(uuid, uuid, numeric, numeric, text, numeric, timestamptz, text) to service_role; + +revoke all on function public.rpc_homecare_dispatch_assignment_upsert(uuid, uuid, uuid, uuid, numeric, numeric, text, uuid, text) from public; +grant execute on function public.rpc_homecare_dispatch_assignment_upsert(uuid, uuid, uuid, uuid, numeric, numeric, text, uuid, text) to service_role; + +revoke all on function public.rpc_homecare_checkin_submit(uuid, uuid, numeric, numeric, text, numeric, timestamptz, jsonb, text, text) from public; +grant execute on function public.rpc_homecare_checkin_submit(uuid, uuid, numeric, numeric, text, numeric, timestamptz, jsonb, text, text) to service_role; + +-- ============================================================ +-- Comments +-- ============================================================ + +comment on function public.rpc_homecare_checkin_precheck(uuid, uuid, numeric, numeric, text, numeric, timestamptz, text) +is 'Homecare checkin distance precheck RPC with SLA lookup and optional location snapshot insert'; + +comment on function public.rpc_homecare_dispatch_assignment_upsert(uuid, uuid, uuid, uuid, numeric, numeric, text, uuid, text) +is 'Homecare dispatch assignment upsert RPC; rotates current assignment and snapshots service location for distance checkin (uuid types)'; + +comment on function public.rpc_homecare_checkin_submit(uuid, uuid, numeric, numeric, text, numeric, timestamptz, jsonb, text, text) +is 'Homecare checkin submit RPC; re-runs distance precheck and writes confirmation snapshot when checkin is allowed'; diff --git a/mall_sql/migrations/20260605_homecare_migration_fixed.sql b/mall_sql/migrations/20260605_homecare_migration_fixed.sql new file mode 100644 index 00000000..11ff0395 --- /dev/null +++ b/mall_sql/migrations/20260605_homecare_migration_fixed.sql @@ -0,0 +1,916 @@ +-- ============================================================ +-- Homecare Migration SQL - Phase 1 (Location & Checkin) +-- Date: 2026-06-05 +-- Purpose: Formal migration for homecare location distance phase 1 +-- Scope: Create/alter tables, indexes, foreign keys, seed data +-- ID Type: All user/org/team/elder IDs unified to uuid +-- Checkin Radius: 50 meters (phase 1) +-- ============================================================ + +BEGIN; + +-- Needed for gen_random_uuid() defaults used by UUID primary keys. +CREATE EXTENSION IF NOT EXISTS pgcrypto; + +-- ============================================================ +-- Part 1: Create/Alter Foundation Tables +-- ============================================================ + +-- Table: public.hc_work_order_events +-- Source: 20260518_homecare_foundation.sql +CREATE TABLE IF NOT EXISTS public.hc_work_order_events ( + id text primary key, + work_order_id uuid not null, + org_id uuid not null, + team_id uuid, + worker_id uuid, + action text not null, + from_status text not null, + to_status text not null, + operator_id uuid not null, + reason text, + payload jsonb, + created_at timestamptz not null default now() +); + +-- Table: public.hc_work_order_exceptions +-- Source: 20260518_homecare_foundation.sql +CREATE TABLE IF NOT EXISTS public.hc_work_order_exceptions ( + id text primary key, + work_order_id uuid not null, + org_id uuid not null, + team_id uuid, + worker_id uuid, + exception_type text not null, + description text not null, + status text not null default 'PENDING' check (status in ('PENDING', 'HANDLED')), + decision text, + reason text, + created_by uuid not null, + handled_by uuid, + evidence_urls jsonb not null default '[]'::jsonb, + created_at timestamptz not null default now(), + handled_at timestamptz +); + +-- Table: public.hc_evidence_files +-- Source: 20260518_homecare_foundation.sql +-- Phase 1 Update: Add fields for checkin evidence support +CREATE TABLE IF NOT EXISTS public.hc_evidence_files ( + id text primary key, + work_order_id uuid not null, + org_id uuid not null, + owner_id uuid, + uploader_id uuid not null, + file_url text not null, + mime_type text, + evidence_type text, + -- Phase 1新增字段 + file_size_bytes bigint not null default 0, + upload_status text not null default 'TEMP' check (upload_status in ('TEMP', 'READY', 'BOUND', 'LOCKED', 'DELETED')), + storage_bucket text, + storage_path text, + file_hash text, + bound_action text check (bound_action in ('CHECKIN', 'CHECKOUT', 'EXCEPTION')), + bound_record_id text, + is_locked boolean not null default false, + locked_at timestamptz, + expires_at timestamptz, + updated_at timestamptz not null default now(), + created_at timestamptz not null default now() +); + +-- Table: public.hc_worker_qualifications +-- Source: 20260518_homecare_foundation.sql +CREATE TABLE IF NOT EXISTS public.hc_worker_qualifications ( + id text primary key, + org_id uuid not null, + worker_id uuid not null, + qualification_type text not null, + qualification_no text, + issue_org text, + valid_from date, + valid_to date, + review_status text not null default 'PENDING' check (review_status in ('PENDING', 'APPROVED', 'REJECTED', 'EXPIRED')), + reviewed_by uuid, + reviewed_at timestamptz, + file_urls jsonb not null default '[]'::jsonb, + created_at timestamptz not null default now(), + updated_at timestamptz not null default now() +); + +-- Table: public.hc_settlements +-- Source: 20260518_homecare_foundation.sql +CREATE TABLE IF NOT EXISTS public.hc_settlements ( + id uuid primary key default gen_random_uuid(), + work_order_id uuid not null unique, + org_id uuid not null, + finance_owner_id uuid, + status text not null default 'PENDING' check (status in ('PENDING', 'READY', 'CONFIRMED')), + amount numeric(12, 2) not null default 0, + currency text not null default 'CNY', + settled_at timestamptz, + created_at timestamptz not null default now(), + updated_at timestamptz not null default now() +); + +-- ============================================================ +-- Part 2: Business Closure Extension Tables +-- ============================================================ + +-- Table: public.hc_acceptances +-- Source: 20260527_homecare_business_closure_extensions.sql +CREATE TABLE IF NOT EXISTS public.hc_acceptances ( + id text primary key, + work_order_id uuid not null, + org_id uuid not null, + team_id uuid, + worker_id uuid, + status text not null default 'PENDING' check (status in ('PENDING', 'ACCEPTED', 'REJECTED', 'CLOSED')), + result text check (result in ('PASS', 'REJECT')), + rating smallint check (rating between 1 and 5), + tags jsonb not null default '[]'::jsonb, + comment text, + rejected_reason text, + accepted_by uuid, + accepted_at timestamptz, + created_by uuid, + updated_by uuid, + created_at timestamptz not null default now(), + updated_at timestamptz not null default now() +); + +-- Table: public.hc_acceptance_issues +-- Source: 20260527_homecare_business_closure_extensions.sql +CREATE TABLE IF NOT EXISTS public.hc_acceptance_issues ( + id text primary key, + acceptance_id text not null, + work_order_id uuid not null, + org_id uuid not null, + team_id uuid, + worker_id uuid, + issue_type text not null, + priority text, + description text not null, + evidence_urls jsonb not null default '[]'::jsonb, + status text not null default 'OPEN' check (status in ('OPEN', 'PROCESSING', 'RESOLVED', 'CLOSED')), + reporter_id uuid not null, + handler_id uuid, + resolution text, + resolved_at timestamptz, + created_at timestamptz not null default now(), + updated_at timestamptz not null default now() +); + +-- Table: public.hc_complaints +-- Source: 20260527_homecare_business_closure_extensions.sql +CREATE TABLE IF NOT EXISTS public.hc_complaints ( + id text primary key, + work_order_id uuid, + acceptance_issue_id text, + org_id uuid not null, + team_id uuid, + worker_id uuid, + complainant_id uuid not null, + complaint_type text not null, + description text not null, + evidence_urls jsonb not null default '[]'::jsonb, + status text not null default 'PENDING' check (status in ('PENDING', 'PROCESSING', 'RESOLVED', 'CLOSED')), + handler_id uuid, + resolution text, + resolved_at timestamptz, + created_by uuid, + created_at timestamptz not null default now(), + updated_at timestamptz not null default now() +); + +-- Table: public.hc_exception_actions +-- Source: 20260527_homecare_business_closure_extensions.sql +CREATE TABLE IF NOT EXISTS public.hc_exception_actions ( + id text primary key, + exception_id text not null, + work_order_id uuid not null, + org_id uuid not null, + team_id uuid, + worker_id uuid, + action_type text not null, + description text, + payload jsonb not null default '{}'::jsonb, + operator_id uuid not null, + created_at timestamptz not null default now() +); + +-- Table: public.hc_settlement_items +CREATE TABLE IF NOT EXISTS public.hc_settlement_items ( + id uuid primary key default gen_random_uuid(), + settlement_id uuid not null, + work_order_id uuid not null, + execution_record_id uuid, + item_name text not null, + unit_price numeric(12, 2) not null default 0, + quantity numeric(10, 2) not null default 1, + actual_amount numeric(12, 2) not null default 0, + deduction_amount numeric(12, 2) not null default 0, + self_pay_amount numeric(12, 2) not null default 0, + status text not null default 'PENDING' check (status in ('PENDING', 'APPROVED', 'REJECTED', 'WAIVED')), + created_at timestamptz not null default now(), + updated_at timestamptz not null default now() +); + +-- Table: public.hc_payments +-- Source: 20260527_homecare_business_closure_extensions.sql +CREATE TABLE IF NOT EXISTS public.hc_payments ( + id uuid primary key default gen_random_uuid(), + settlement_id uuid not null, + org_id uuid not null, + payer_id uuid, + payment_channel text not null, + transaction_id text, + amount numeric(12, 2) not null, + status text not null default 'PENDING' check (status in ('PENDING', 'PAID', 'FAILED', 'CANCELLED', 'REFUNDING', 'REFUNDED')), + callback_payload jsonb not null default '{}'::jsonb, + paid_at timestamptz, + failed_at timestamptz, + created_at timestamptz not null default now(), + updated_at timestamptz not null default now() +); + +-- Table: public.hc_refunds +-- Source: 20260527_homecare_business_closure_extensions.sql +CREATE TABLE IF NOT EXISTS public.hc_refunds ( + id uuid primary key default gen_random_uuid(), + payment_id uuid not null, + settlement_id uuid not null, + org_id uuid not null, + amount numeric(12, 2) not null, + reason text not null, + status text not null default 'PENDING' check (status in ('PENDING', 'PROCESSING', 'REFUNDED', 'FAILED', 'CLOSED')), + refund_transaction_id text, + created_by uuid, + refunded_at timestamptz, + created_at timestamptz not null default now(), + updated_at timestamptz not null default now() +); + +-- Table: public.hc_ledgers +-- Source: 20260527_homecare_business_closure_extensions.sql +CREATE TABLE IF NOT EXISTS public.hc_ledgers ( + id uuid primary key default gen_random_uuid(), + settlement_id uuid not null, + org_id uuid not null, + archive_period text not null, + ledger_type text not null default 'SETTLEMENT', + content jsonb not null default '{}'::jsonb, + archived_at timestamptz, + created_by uuid, + created_at timestamptz not null default now(), + updated_at timestamptz not null default now() +); + +-- Table: public.hc_archive_files +-- Source: 20260527_homecare_business_closure_extensions.sql +CREATE TABLE IF NOT EXISTS public.hc_archive_files ( + id uuid primary key default gen_random_uuid(), + ledger_id uuid not null, + org_id uuid not null, + file_url text not null, + file_name text, + file_size bigint, + mime_type text, + created_by uuid, + created_at timestamptz not null default now() +); + +-- Table: public.hc_consent_records +-- Source: 20260527_homecare_business_closure_extensions.sql +CREATE TABLE IF NOT EXISTS public.hc_consent_records ( + id text primary key, + org_id uuid not null, + elder_id uuid not null, + consent_type text not null, + consent_status text not null default 'GRANTED' check (consent_status in ('GRANTED', 'REVOKED', 'EXPIRED')), + consent_version text, + granted_by uuid, + granted_at timestamptz, + revoked_at timestamptz, + expires_at timestamptz, + evidence_urls jsonb not null default '[]'::jsonb, + created_at timestamptz not null default now(), + updated_at timestamptz not null default now() +); + +-- ============================================================ +-- Part 3: Location Distance Phase 1 Tables +-- ============================================================ + +-- Table: public.hc_dispatch_assignments +-- Source: 20260602_homecare_location_distance_phase1_tables.sql +CREATE TABLE IF NOT EXISTS public.hc_dispatch_assignments ( + id text primary key, + work_order_id uuid not null, + org_id uuid not null, + team_id uuid, + assign_version integer not null default 1 check (assign_version > 0), + worker_id uuid, + service_latitude numeric(10,7) not null, + service_longitude numeric(10,7) not null, + service_coordinate_type text not null default 'gcj02', + dispatch_status text not null default 'PENDING' + check (dispatch_status in ('PENDING', 'ACCEPTED', 'REJECTED', 'TIMEOUT', 'REASSIGNED', 'CANCELLED')), + dispatch_reason text, + is_current boolean not null default true, + dispatched_at timestamptz not null default now(), + accepted_at timestamptz, + rejected_at timestamptz, + timeout_at timestamptz, + reassigned_at timestamptz, + cancelled_at timestamptz, + created_by uuid, + updated_by uuid, + created_at timestamptz not null default now(), + updated_at timestamptz not null default now(), + constraint chk_hc_dispatch_assignments_lat_range check (service_latitude between -90 and 90), + constraint chk_hc_dispatch_assignments_lng_range check (service_longitude between -180 and 180), + constraint uq_hc_dispatch_assignments_version unique (work_order_id, assign_version) +); + +-- Table: public.hc_worker_locations +-- Source: 20260602_homecare_location_distance_phase1_tables.sql +CREATE TABLE IF NOT EXISTS public.hc_worker_locations ( + id text primary key, + work_order_id uuid not null, + worker_id uuid not null, + latitude numeric(10,7) not null, + longitude numeric(10,7) not null, + coordinate_type text not null default 'gcj02', + accuracy numeric(10,2), + location_scene text not null + check (location_scene in ('ON_THE_WAY', 'CHECKIN_PRECHECK', 'CHECKIN', 'CHECKOUT')), + reported_at timestamptz not null, + distance_meters numeric(10,2), + formatted_address text, + province text, + city text, + district text, + street text, + poi_title text, + geocode_provider text, + geocode_status text, + address_updated_at timestamptz, + created_at timestamptz not null default now(), + constraint chk_hc_worker_locations_lat_range check (latitude between -90 and 90), + constraint chk_hc_worker_locations_lng_range check (longitude between -180 and 180) +); + +-- Table: public.hc_work_order_confirmations +-- Source: 20260602_homecare_location_distance_phase1_tables.sql +CREATE TABLE IF NOT EXISTS public.hc_work_order_confirmations ( + id text primary key, + work_order_id uuid not null, + confirmation_type text not null default 'ARRIVAL', + status text not null default 'PENDING' + check (status in ('PENDING', 'CONFIRMED', 'REJECTED')), + confirmed_by uuid, + confirmed_at timestamptz, + reason text, + payload jsonb not null default '{}'::jsonb, + -- Phase 1预留:二期电子签名扩展字段 + signature_url text, + signature_hash text, + created_at timestamptz not null default now(), + updated_at timestamptz not null default now() +); + +-- Table: public.sys_sla_config +-- Source: 20260602_homecare_location_distance_phase1_tables.sql +CREATE TABLE IF NOT EXISTS public.sys_sla_config ( + id text primary key, + config_key text not null, + config_value text not null, + value_type text not null default 'string', + scope_type text not null default 'GLOBAL' + check (scope_type in ('WORK_ORDER', 'ORG', 'TEAM', 'GLOBAL')), + scope_id text, + is_active boolean not null default true, + description text, + updated_by uuid, + created_at timestamptz not null default now(), + updated_at timestamptz not null default now(), + constraint chk_sys_sla_config_scope check ( + (scope_type = 'GLOBAL' and scope_id is null) + or (scope_type <> 'GLOBAL') + ) +); + +-- ============================================================ +-- Part 4: Audit Extension Tables (if not exist) +-- ============================================================ + +-- Table: public.hc_state_transitions +CREATE TABLE IF NOT EXISTS public.hc_state_transitions ( + id text primary key, + work_order_id uuid not null, + from_status text not null, + to_status text not null, + triggered_by uuid not null, + reason text, + payload jsonb, + created_at timestamptz not null default now() +); + +-- Table: public.hc_audit_logs +CREATE TABLE IF NOT EXISTS public.hc_audit_logs ( + id text primary key, + work_order_id uuid, + org_id uuid, + actor_id uuid not null, + actor_role text not null, + action text not null, + resource_type text not null, + resource_id text not null, + details jsonb, + ip_address inet, + user_agent text, + created_at timestamptz not null default now() +); + +-- Table: public.hc_sensitive_access_logs +CREATE TABLE IF NOT EXISTS public.hc_sensitive_access_logs ( + id text primary key, + accessor_id uuid not null, + accessed_resource_type text not null, + accessed_resource_id text not null, + access_purpose text not null, + approved_by uuid, + created_at timestamptz not null default now() +); + + +-- ============================================================ +-- Part 4.5: Legacy Compatibility Fixes +-- ============================================================ +-- These fixes make reruns safe when older hc_* tables already exist. +-- CREATE TABLE IF NOT EXISTS does not upgrade existing table definitions, +-- so referenced columns and FK columns are normalized before indexes/FKs. + +-- Existing settlements table may already have id uuid in current database. +-- If settlement-related child tables already exist with text FK columns, +-- convert them to uuid before FK creation. This succeeds only when existing +-- values are empty or valid UUID strings. +DO $$ +BEGIN + IF to_regclass('public.hc_settlement_items') IS NOT NULL + AND EXISTS ( + SELECT 1 FROM information_schema.columns + WHERE table_schema='public' + AND table_name='hc_settlement_items' + AND column_name='settlement_id' + AND udt_name <> 'uuid' + ) + THEN + ALTER TABLE public.hc_settlement_items + ALTER COLUMN settlement_id TYPE uuid USING NULLIF(settlement_id, '')::uuid; + END IF; + + IF to_regclass('public.hc_payments') IS NOT NULL + AND EXISTS ( + SELECT 1 FROM information_schema.columns + WHERE table_schema='public' + AND table_name='hc_payments' + AND column_name='settlement_id' + AND udt_name <> 'uuid' + ) + THEN + ALTER TABLE public.hc_payments + ALTER COLUMN settlement_id TYPE uuid USING NULLIF(settlement_id, '')::uuid; + END IF; + + IF to_regclass('public.hc_refunds') IS NOT NULL + AND EXISTS ( + SELECT 1 FROM information_schema.columns + WHERE table_schema='public' + AND table_name='hc_refunds' + AND column_name='settlement_id' + AND udt_name <> 'uuid' + ) + THEN + ALTER TABLE public.hc_refunds + ALTER COLUMN settlement_id TYPE uuid USING NULLIF(settlement_id, '')::uuid; + END IF; + + IF to_regclass('public.hc_refunds') IS NOT NULL + AND EXISTS ( + SELECT 1 FROM information_schema.columns + WHERE table_schema='public' + AND table_name='hc_refunds' + AND column_name='payment_id' + AND udt_name <> 'uuid' + ) + THEN + ALTER TABLE public.hc_refunds + ALTER COLUMN payment_id TYPE uuid USING NULLIF(payment_id, '')::uuid; + END IF; + + IF to_regclass('public.hc_ledgers') IS NOT NULL + AND EXISTS ( + SELECT 1 FROM information_schema.columns + WHERE table_schema='public' + AND table_name='hc_ledgers' + AND column_name='settlement_id' + AND udt_name <> 'uuid' + ) + THEN + ALTER TABLE public.hc_ledgers + ALTER COLUMN settlement_id TYPE uuid USING NULLIF(settlement_id, '')::uuid; + END IF; + + IF to_regclass('public.hc_archive_files') IS NOT NULL + AND EXISTS ( + SELECT 1 FROM information_schema.columns + WHERE table_schema='public' + AND table_name='hc_archive_files' + AND column_name='ledger_id' + AND udt_name <> 'uuid' + ) + THEN + ALTER TABLE public.hc_archive_files + ALTER COLUMN ledger_id TYPE uuid USING NULLIF(ledger_id, '')::uuid; + END IF; +END $$; + +-- ============================================================ +-- Part 5: Indexes +-- ============================================================ + +-- hc_dispatch_assignments indexes +CREATE INDEX IF NOT EXISTS idx_hc_dispatch_assignments_wo_created + ON public.hc_dispatch_assignments (work_order_id, created_at DESC); +CREATE INDEX IF NOT EXISTS idx_hc_dispatch_assignments_current + ON public.hc_dispatch_assignments (work_order_id) WHERE is_current = true; + +-- Ensure each work order has at most one current assignment for distance/checkin RPC lookup. +CREATE UNIQUE INDEX IF NOT EXISTS uq_hc_dispatch_assignments_one_current + ON public.hc_dispatch_assignments (work_order_id) WHERE is_current = true; + +CREATE INDEX IF NOT EXISTS idx_hc_dispatch_assignments_worker_status + ON public.hc_dispatch_assignments (worker_id, dispatch_status, created_at DESC); + +-- hc_worker_locations indexes +CREATE INDEX IF NOT EXISTS idx_hc_worker_locations_wo_reported + ON public.hc_worker_locations (work_order_id, reported_at DESC); +CREATE INDEX IF NOT EXISTS idx_hc_worker_locations_worker_reported + ON public.hc_worker_locations (worker_id, reported_at DESC); + +-- hc_work_order_confirmations indexes +CREATE INDEX IF NOT EXISTS idx_hc_work_order_confirmations_wo_created + ON public.hc_work_order_confirmations (work_order_id, created_at DESC); +CREATE INDEX IF NOT EXISTS idx_hc_work_order_confirmations_status + ON public.hc_work_order_confirmations (status, updated_at DESC); + +-- hc_evidence_files indexes +CREATE INDEX IF NOT EXISTS idx_hc_evidence_files_wo_created + ON public.hc_evidence_files (work_order_id, created_at DESC); +CREATE INDEX IF NOT EXISTS idx_hc_evidence_files_upload_status + ON public.hc_evidence_files (upload_status) WHERE upload_status != 'DELETED'; + +-- sys_sla_config indexes +CREATE INDEX IF NOT EXISTS idx_sys_sla_config_key_scope + ON public.sys_sla_config (config_key, scope_type, scope_id, is_active) WHERE is_active = true; + +-- hc_work_order_exceptions indexes +CREATE INDEX IF NOT EXISTS idx_hc_work_order_exceptions_wo_created + ON public.hc_work_order_exceptions (work_order_id, created_at DESC); +CREATE INDEX IF NOT EXISTS idx_hc_work_order_exceptions_status + ON public.hc_work_order_exceptions (status, created_at DESC); + +-- hc_work_order_events indexes +CREATE INDEX IF NOT EXISTS idx_hc_work_order_events_wo_created + ON public.hc_work_order_events (work_order_id, created_at DESC); + +-- hc_settlements indexes +CREATE INDEX IF NOT EXISTS idx_hc_settlements_wo + ON public.hc_settlements (work_order_id); + +-- ============================================================ +-- Part 6: Foreign Keys +-- ============================================================ + +-- Make foreign key creation idempotent for reruns +ALTER TABLE public.hc_work_order_events + DROP CONSTRAINT IF EXISTS fk_hc_work_order_events_work_order, + DROP CONSTRAINT IF EXISTS fk_hc_work_order_events_worker, + DROP CONSTRAINT IF EXISTS fk_hc_work_order_events_operator; + +ALTER TABLE public.hc_work_order_exceptions + DROP CONSTRAINT IF EXISTS fk_hc_work_order_exceptions_work_order, + DROP CONSTRAINT IF EXISTS fk_hc_work_order_exceptions_worker, + DROP CONSTRAINT IF EXISTS fk_hc_work_order_exceptions_created_by, + DROP CONSTRAINT IF EXISTS fk_hc_work_order_exceptions_handled_by; + +ALTER TABLE public.hc_evidence_files + DROP CONSTRAINT IF EXISTS fk_hc_evidence_files_work_order, + DROP CONSTRAINT IF EXISTS fk_hc_evidence_files_uploader; + +ALTER TABLE public.hc_worker_qualifications + DROP CONSTRAINT IF EXISTS fk_hc_worker_qualifications_worker, + DROP CONSTRAINT IF EXISTS fk_hc_worker_qualifications_reviewed_by; + +ALTER TABLE public.hc_settlements + DROP CONSTRAINT IF EXISTS fk_hc_settlements_work_order; + +ALTER TABLE public.hc_acceptances + DROP CONSTRAINT IF EXISTS fk_hc_acceptances_work_order, + DROP CONSTRAINT IF EXISTS fk_hc_acceptances_worker, + DROP CONSTRAINT IF EXISTS fk_hc_acceptances_accepted_by; + +ALTER TABLE public.hc_acceptance_issues + DROP CONSTRAINT IF EXISTS fk_hc_acceptance_issues_acceptance, + DROP CONSTRAINT IF EXISTS fk_hc_acceptance_issues_work_order, + DROP CONSTRAINT IF EXISTS fk_hc_acceptance_issues_worker, + DROP CONSTRAINT IF EXISTS fk_hc_acceptance_issues_reporter, + DROP CONSTRAINT IF EXISTS fk_hc_acceptance_issues_handler; + +ALTER TABLE public.hc_complaints + DROP CONSTRAINT IF EXISTS fk_hc_complaints_work_order, + DROP CONSTRAINT IF EXISTS fk_hc_complaints_acceptance_issue, + DROP CONSTRAINT IF EXISTS fk_hc_complaints_worker, + DROP CONSTRAINT IF EXISTS fk_hc_complaints_complainant, + DROP CONSTRAINT IF EXISTS fk_hc_complaints_handler; + +ALTER TABLE public.hc_exception_actions + DROP CONSTRAINT IF EXISTS fk_hc_exception_actions_exception, + DROP CONSTRAINT IF EXISTS fk_hc_exception_actions_work_order, + DROP CONSTRAINT IF EXISTS fk_hc_exception_actions_worker, + DROP CONSTRAINT IF EXISTS fk_hc_exception_actions_operator; + +ALTER TABLE public.hc_settlement_items + DROP CONSTRAINT IF EXISTS fk_hc_settlement_items_settlement, + DROP CONSTRAINT IF EXISTS fk_hc_settlement_items_work_order, + DROP CONSTRAINT IF EXISTS fk_hc_settlement_items_execution_record; + +ALTER TABLE public.hc_payments + DROP CONSTRAINT IF EXISTS fk_hc_payments_settlement, + DROP CONSTRAINT IF EXISTS fk_hc_payments_payer; + +ALTER TABLE public.hc_refunds + DROP CONSTRAINT IF EXISTS fk_hc_refunds_payment, + DROP CONSTRAINT IF EXISTS fk_hc_refunds_settlement, + DROP CONSTRAINT IF EXISTS fk_hc_refunds_created_by; + +ALTER TABLE public.hc_ledgers + DROP CONSTRAINT IF EXISTS fk_hc_ledgers_settlement, + DROP CONSTRAINT IF EXISTS fk_hc_ledgers_created_by; + +ALTER TABLE public.hc_archive_files + DROP CONSTRAINT IF EXISTS fk_hc_archive_files_ledger, + DROP CONSTRAINT IF EXISTS fk_hc_archive_files_created_by; + +ALTER TABLE public.hc_consent_records + DROP CONSTRAINT IF EXISTS fk_hc_consent_records_elder, + DROP CONSTRAINT IF EXISTS fk_hc_consent_records_granted_by; + +ALTER TABLE public.hc_dispatch_assignments + DROP CONSTRAINT IF EXISTS fk_hc_dispatch_assignments_work_order, + DROP CONSTRAINT IF EXISTS fk_hc_dispatch_assignments_worker, + DROP CONSTRAINT IF EXISTS fk_hc_dispatch_assignments_created_by, + DROP CONSTRAINT IF EXISTS fk_hc_dispatch_assignments_updated_by; + +ALTER TABLE public.hc_worker_locations + DROP CONSTRAINT IF EXISTS fk_hc_worker_locations_work_order, + DROP CONSTRAINT IF EXISTS fk_hc_worker_locations_worker; + +ALTER TABLE public.hc_work_order_confirmations + DROP CONSTRAINT IF EXISTS fk_hc_work_order_confirmations_work_order, + DROP CONSTRAINT IF EXISTS fk_hc_work_order_confirmations_confirmed_by; + +ALTER TABLE public.hc_state_transitions + DROP CONSTRAINT IF EXISTS fk_hc_state_transitions_work_order, + DROP CONSTRAINT IF EXISTS fk_hc_state_transitions_triggered_by; + +ALTER TABLE public.hc_audit_logs + DROP CONSTRAINT IF EXISTS fk_hc_audit_logs_work_order, + DROP CONSTRAINT IF EXISTS fk_hc_audit_logs_actor; + +ALTER TABLE public.hc_sensitive_access_logs + DROP CONSTRAINT IF EXISTS fk_hc_sensitive_access_logs_accessor; + +-- hc_work_order_events foreign keys +ALTER TABLE public.hc_work_order_events + ADD CONSTRAINT fk_hc_work_order_events_work_order + FOREIGN KEY (work_order_id) REFERENCES public.ec_care_tasks(id) ON DELETE RESTRICT, + ADD CONSTRAINT fk_hc_work_order_events_worker + FOREIGN KEY (worker_id) REFERENCES public.ak_users(id) ON DELETE SET NULL, + ADD CONSTRAINT fk_hc_work_order_events_operator + FOREIGN KEY (operator_id) REFERENCES public.ak_users(id) ON DELETE RESTRICT; + +-- hc_work_order_exceptions foreign keys +ALTER TABLE public.hc_work_order_exceptions + ADD CONSTRAINT fk_hc_work_order_exceptions_work_order + FOREIGN KEY (work_order_id) REFERENCES public.ec_care_tasks(id) ON DELETE RESTRICT, + ADD CONSTRAINT fk_hc_work_order_exceptions_worker + FOREIGN KEY (worker_id) REFERENCES public.ak_users(id) ON DELETE SET NULL, + ADD CONSTRAINT fk_hc_work_order_exceptions_created_by + FOREIGN KEY (created_by) REFERENCES public.ak_users(id) ON DELETE RESTRICT, + ADD CONSTRAINT fk_hc_work_order_exceptions_handled_by + FOREIGN KEY (handled_by) REFERENCES public.ak_users(id) ON DELETE SET NULL; + +-- hc_evidence_files foreign keys +ALTER TABLE public.hc_evidence_files + ADD CONSTRAINT fk_hc_evidence_files_work_order + FOREIGN KEY (work_order_id) REFERENCES public.ec_care_tasks(id) ON DELETE RESTRICT, + ADD CONSTRAINT fk_hc_evidence_files_uploader + FOREIGN KEY (uploader_id) REFERENCES public.ak_users(id) ON DELETE RESTRICT; + +-- hc_worker_qualifications foreign keys +ALTER TABLE public.hc_worker_qualifications + ADD CONSTRAINT fk_hc_worker_qualifications_worker + FOREIGN KEY (worker_id) REFERENCES public.ak_users(id) ON DELETE RESTRICT, + ADD CONSTRAINT fk_hc_worker_qualifications_reviewed_by + FOREIGN KEY (reviewed_by) REFERENCES public.ak_users(id) ON DELETE SET NULL; + +-- hc_settlements foreign keys +ALTER TABLE public.hc_settlements + ADD CONSTRAINT fk_hc_settlements_work_order + FOREIGN KEY (work_order_id) REFERENCES public.ec_care_tasks(id) ON DELETE RESTRICT; + +-- hc_acceptances foreign keys +ALTER TABLE public.hc_acceptances + ADD CONSTRAINT fk_hc_acceptances_work_order + FOREIGN KEY (work_order_id) REFERENCES public.ec_care_tasks(id) ON DELETE RESTRICT, + ADD CONSTRAINT fk_hc_acceptances_worker + FOREIGN KEY (worker_id) REFERENCES public.ak_users(id) ON DELETE SET NULL, + ADD CONSTRAINT fk_hc_acceptances_accepted_by + FOREIGN KEY (accepted_by) REFERENCES public.ak_users(id) ON DELETE SET NULL; + +-- hc_acceptance_issues foreign keys +ALTER TABLE public.hc_acceptance_issues + ADD CONSTRAINT fk_hc_acceptance_issues_acceptance + FOREIGN KEY (acceptance_id) REFERENCES public.hc_acceptances(id) ON DELETE RESTRICT, + ADD CONSTRAINT fk_hc_acceptance_issues_work_order + FOREIGN KEY (work_order_id) REFERENCES public.ec_care_tasks(id) ON DELETE RESTRICT, + ADD CONSTRAINT fk_hc_acceptance_issues_worker + FOREIGN KEY (worker_id) REFERENCES public.ak_users(id) ON DELETE SET NULL, + ADD CONSTRAINT fk_hc_acceptance_issues_reporter + FOREIGN KEY (reporter_id) REFERENCES public.ak_users(id) ON DELETE RESTRICT, + ADD CONSTRAINT fk_hc_acceptance_issues_handler + FOREIGN KEY (handler_id) REFERENCES public.ak_users(id) ON DELETE SET NULL; + +-- hc_complaints foreign keys +ALTER TABLE public.hc_complaints + ADD CONSTRAINT fk_hc_complaints_work_order + FOREIGN KEY (work_order_id) REFERENCES public.ec_care_tasks(id) ON DELETE SET NULL, + ADD CONSTRAINT fk_hc_complaints_acceptance_issue + FOREIGN KEY (acceptance_issue_id) REFERENCES public.hc_acceptance_issues(id) ON DELETE SET NULL, + ADD CONSTRAINT fk_hc_complaints_worker + FOREIGN KEY (worker_id) REFERENCES public.ak_users(id) ON DELETE SET NULL, + ADD CONSTRAINT fk_hc_complaints_complainant + FOREIGN KEY (complainant_id) REFERENCES public.ak_users(id) ON DELETE RESTRICT, + ADD CONSTRAINT fk_hc_complaints_handler + FOREIGN KEY (handler_id) REFERENCES public.ak_users(id) ON DELETE SET NULL; + +-- hc_exception_actions foreign keys +ALTER TABLE public.hc_exception_actions + ADD CONSTRAINT fk_hc_exception_actions_exception + FOREIGN KEY (exception_id) REFERENCES public.hc_work_order_exceptions(id) ON DELETE RESTRICT, + ADD CONSTRAINT fk_hc_exception_actions_work_order + FOREIGN KEY (work_order_id) REFERENCES public.ec_care_tasks(id) ON DELETE RESTRICT, + ADD CONSTRAINT fk_hc_exception_actions_worker + FOREIGN KEY (worker_id) REFERENCES public.ak_users(id) ON DELETE SET NULL, + ADD CONSTRAINT fk_hc_exception_actions_operator + FOREIGN KEY (operator_id) REFERENCES public.ak_users(id) ON DELETE RESTRICT; + +-- hc_settlement_items foreign keys +ALTER TABLE public.hc_settlement_items + ADD CONSTRAINT fk_hc_settlement_items_settlement + FOREIGN KEY (settlement_id) REFERENCES public.hc_settlements(id) ON DELETE RESTRICT, + ADD CONSTRAINT fk_hc_settlement_items_work_order + FOREIGN KEY (work_order_id) REFERENCES public.ec_care_tasks(id) ON DELETE RESTRICT, + ADD CONSTRAINT fk_hc_settlement_items_execution_record + FOREIGN KEY (execution_record_id) REFERENCES public.ec_care_records(id) ON DELETE SET NULL; + +-- hc_payments foreign keys +ALTER TABLE public.hc_payments + ADD CONSTRAINT fk_hc_payments_settlement + FOREIGN KEY (settlement_id) REFERENCES public.hc_settlements(id) ON DELETE RESTRICT, + ADD CONSTRAINT fk_hc_payments_payer + FOREIGN KEY (payer_id) REFERENCES public.ak_users(id) ON DELETE SET NULL; + +-- hc_refunds foreign keys +ALTER TABLE public.hc_refunds + ADD CONSTRAINT fk_hc_refunds_payment + FOREIGN KEY (payment_id) REFERENCES public.hc_payments(id) ON DELETE RESTRICT, + ADD CONSTRAINT fk_hc_refunds_settlement + FOREIGN KEY (settlement_id) REFERENCES public.hc_settlements(id) ON DELETE RESTRICT, + ADD CONSTRAINT fk_hc_refunds_created_by + FOREIGN KEY (created_by) REFERENCES public.ak_users(id) ON DELETE SET NULL; + +-- hc_ledgers foreign keys +ALTER TABLE public.hc_ledgers + ADD CONSTRAINT fk_hc_ledgers_settlement + FOREIGN KEY (settlement_id) REFERENCES public.hc_settlements(id) ON DELETE RESTRICT, + ADD CONSTRAINT fk_hc_ledgers_created_by + FOREIGN KEY (created_by) REFERENCES public.ak_users(id) ON DELETE SET NULL; + +-- hc_archive_files foreign keys +ALTER TABLE public.hc_archive_files + ADD CONSTRAINT fk_hc_archive_files_ledger + FOREIGN KEY (ledger_id) REFERENCES public.hc_ledgers(id) ON DELETE RESTRICT, + ADD CONSTRAINT fk_hc_archive_files_created_by + FOREIGN KEY (created_by) REFERENCES public.ak_users(id) ON DELETE SET NULL; + +-- hc_consent_records foreign keys +ALTER TABLE public.hc_consent_records + ADD CONSTRAINT fk_hc_consent_records_elder + FOREIGN KEY (elder_id) REFERENCES public.ec_elders(id) ON DELETE RESTRICT, + ADD CONSTRAINT fk_hc_consent_records_granted_by + FOREIGN KEY (granted_by) REFERENCES public.ak_users(id) ON DELETE SET NULL; + +-- Location distance phase 1 foreign keys +ALTER TABLE public.hc_dispatch_assignments + ADD CONSTRAINT fk_hc_dispatch_assignments_work_order + FOREIGN KEY (work_order_id) REFERENCES public.ec_care_tasks(id) ON DELETE RESTRICT, + ADD CONSTRAINT fk_hc_dispatch_assignments_worker + FOREIGN KEY (worker_id) REFERENCES public.ak_users(id) ON DELETE SET NULL, + ADD CONSTRAINT fk_hc_dispatch_assignments_created_by + FOREIGN KEY (created_by) REFERENCES public.ak_users(id) ON DELETE SET NULL, + ADD CONSTRAINT fk_hc_dispatch_assignments_updated_by + FOREIGN KEY (updated_by) REFERENCES public.ak_users(id) ON DELETE SET NULL; + +ALTER TABLE public.hc_worker_locations + ADD CONSTRAINT fk_hc_worker_locations_work_order + FOREIGN KEY (work_order_id) REFERENCES public.ec_care_tasks(id) ON DELETE RESTRICT, + ADD CONSTRAINT fk_hc_worker_locations_worker + FOREIGN KEY (worker_id) REFERENCES public.ak_users(id) ON DELETE RESTRICT; + +ALTER TABLE public.hc_work_order_confirmations + ADD CONSTRAINT fk_hc_work_order_confirmations_work_order + FOREIGN KEY (work_order_id) REFERENCES public.ec_care_tasks(id) ON DELETE RESTRICT, + ADD CONSTRAINT fk_hc_work_order_confirmations_confirmed_by + FOREIGN KEY (confirmed_by) REFERENCES public.ak_users(id) ON DELETE SET NULL; + +-- Audit extension tables foreign keys +ALTER TABLE public.hc_state_transitions + ADD CONSTRAINT fk_hc_state_transitions_work_order + FOREIGN KEY (work_order_id) REFERENCES public.ec_care_tasks(id) ON DELETE RESTRICT, + ADD CONSTRAINT fk_hc_state_transitions_triggered_by + FOREIGN KEY (triggered_by) REFERENCES public.ak_users(id) ON DELETE RESTRICT; + +ALTER TABLE public.hc_audit_logs + ADD CONSTRAINT fk_hc_audit_logs_work_order + FOREIGN KEY (work_order_id) REFERENCES public.ec_care_tasks(id) ON DELETE SET NULL, + ADD CONSTRAINT fk_hc_audit_logs_actor + FOREIGN KEY (actor_id) REFERENCES public.ak_users(id) ON DELETE RESTRICT; + +ALTER TABLE public.hc_sensitive_access_logs + ADD CONSTRAINT fk_hc_sensitive_access_logs_accessor + FOREIGN KEY (accessor_id) REFERENCES public.ak_users(id) ON DELETE RESTRICT; + +-- ============================================================ +-- Part 7: Seed Data - SLA Config +-- ============================================================ + +-- Phase 1 SLA configuration +-- Checkin radius: 50 meters (phase 1 final) +INSERT INTO public.sys_sla_config (id, config_key, config_value, value_type, scope_type, description) +VALUES + ('sla_001', 'HOMECARE_CHECKIN_RADIUS_METERS', '50', 'integer', 'GLOBAL', '签到半径(一期50米)'), + ('sla_002', 'HOMECARE_LOCATION_REFRESH_SECONDS', '30', 'integer', 'GLOBAL', '定位刷新间隔(秒)'), + ('sla_003', 'HOMECARE_CHECKIN_MAX_PHOTO_COUNT', '3', 'integer', 'GLOBAL', '签到最大照片数'), + ('sla_004', 'HOMECARE_CHECKIN_MAX_PHOTO_SIZE_MB', '5', 'integer', 'GLOBAL', '单张最大照片大小(MB)') +ON CONFLICT (id) DO NOTHING; + +-- ============================================================ +-- Part 8: Comments +-- ============================================================ + +COMMENT ON TABLE public.hc_work_order_events IS '居家服务工单事件表 - 业务时间线展示'; +COMMENT ON TABLE public.hc_work_order_exceptions IS '居家服务工单异常表'; +COMMENT ON TABLE public.hc_evidence_files IS '居家服务证据文件表 - 签到照片、异常图片等'; +COMMENT ON TABLE public.hc_worker_qualifications IS '居家服务人员资质表'; +COMMENT ON TABLE public.hc_settlements IS '居家服务结算表'; +COMMENT ON TABLE public.hc_acceptances IS '居家服务确认表'; +COMMENT ON TABLE public.hc_acceptance_issues IS '居家服务确认问题表'; +COMMENT ON TABLE public.hc_complaints IS '居家服务投诉表'; +COMMENT ON TABLE public.hc_exception_actions IS '居家服务异常操作表'; +COMMENT ON TABLE public.hc_settlement_items IS '居家服务结算明细表'; +COMMENT ON TABLE public.hc_payments IS '居家服务支付表'; +COMMENT ON TABLE public.hc_refunds IS '居家服务退款表'; +COMMENT ON TABLE public.hc_ledgers IS '居家服务账簿表'; +COMMENT ON TABLE public.hc_archive_files IS '居家服务归档文件表'; +COMMENT ON TABLE public.hc_consent_records IS '居家服务同意记录表'; +COMMENT ON TABLE public.hc_dispatch_assignments IS '居家服务派单记录表 - 定位距离一期'; +COMMENT ON TABLE public.hc_worker_locations IS '居家服务人员定位表 - 定位距离一期'; +COMMENT ON TABLE public.hc_work_order_confirmations IS '居家服务工单确认表 - 定位距离一期'; +COMMENT ON TABLE public.sys_sla_config IS '居家服务SLA配置表'; +COMMENT ON TABLE public.hc_state_transitions IS '居家服务状态变迁表 - 审计'; +COMMENT ON TABLE public.hc_audit_logs IS '居家服务操作审计表'; +COMMENT ON TABLE public.hc_sensitive_access_logs IS '居家服务敏感访问日志表'; + +COMMENT ON COLUMN public.hc_evidence_files.upload_status IS '上传状态: TEMP/READY/BOUND/LOCKED/DELETED'; +COMMENT ON COLUMN public.hc_evidence_files.bound_action IS '绑定动作: CHECKIN/CHECKOUT/EXCEPTION'; +COMMENT ON COLUMN public.hc_worker_locations.location_scene IS '定位场景: ON_THE_WAY/CHECKIN_PRECHECK/CHECKIN/CHECKOUT'; +COMMENT ON COLUMN public.hc_work_order_confirmations.confirmation_type IS '确认类型: ARRIVAL(二期扩展CHECKOUT)'; +COMMENT ON COLUMN public.hc_work_order_confirmations.signature_url IS '二期电子签名图片URL(预留)'; +COMMENT ON COLUMN public.hc_work_order_confirmations.signature_hash IS '二期电子签名哈希(预留)'; + +COMMIT; diff --git a/pages/main/messages.uvue b/pages/main/messages.uvue index 9baf802b..7b0b81e2 100644 --- a/pages/main/messages.uvue +++ b/pages/main/messages.uvue @@ -1,659 +1,744 @@ -