From 9fbc6f8cd13ada162c990e1adbd7647cd95266ba Mon Sep 17 00:00:00 2001 From: huangzhenbao <17818024429@163.com> Date: Wed, 10 Jun 2026 20:20:47 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E8=AE=A2=E5=8D=95=E6=98=BE?= =?UTF-8?q?=E7=A4=BAbug?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...ges.delivery.2026-05-27T11-47-43-929Z.json | 478 ++++++ ...ges.delivery.2026-05-28T00-41-26-093Z.json | 131 ++ ...ges.delivery.2026-05-29T00-45-58-350Z.json | 131 ++ ...ges.delivery.2026-06-01T00-44-50-634Z.json | 131 ++ ...ges.delivery.2026-06-01T01-24-54-473Z.json | 131 ++ ...ges.delivery.2026-06-03T00-42-16-888Z.json | 131 ++ ...ges.delivery.2026-06-04T08-08-29-520Z.json | 131 ++ ...ges.delivery.2026-06-05T00-38-54-603Z.json | 131 ++ ...ges.delivery.2026-06-08T00-50-39-789Z.json | 131 ++ ...ges.delivery.2026-06-09T01-28-15-988Z.json | 131 ++ ...ges.delivery.2026-06-09T01-56-20-996Z.json | 131 ++ ...ges.delivery.2026-06-10T00-46-43-283Z.json | 131 ++ ...ges.delivery.2026-06-10T01-07-09-629Z.json | 131 ++ ak/config.uts | 5 +- api/delivery.uts | 541 ++++++- config/current-client.uts | 2 +- docs/AUTH_CHECKIN_API_GUIDE.md | 528 +++++++ ...609_backfill_ec_care_tasks_core_fields.sql | 633 ++++++++ ...nvalid_ec_care_tasks_from_delivery_rpc.sql | 135 ++ ...0609_fix_delivery_rpc_request_snapshot.sql | 128 ++ ...restore_delivery_get_legacy_order_json.sql | 59 + ...rollback_exclude_invalid_ec_care_tasks.sql | 74 + ...10_backfill_pending_orders_core_fields.sql | 364 +++++ .../20260610_fix_delivery_pending_blank.sql | 765 +++++++++ .../scripts/create_delivery_staff_final.sql | 143 ++ .../scripts/create_delivery_staff_safe.sql | 90 ++ mall_sql/scripts/create_homecare_auth_user.js | 94 ++ mall_sql/scripts/seed_homecare_worker.sql | 269 ++++ pages.json | 417 +---- .../consumer/home-service/order-detail.uvue | 122 +- pages/mall/delivery/orders/checkin.uvue | 284 +++- pages/mall/delivery/orders/detail.uvue | 10 +- pages/mall/delivery/orders/index.uvue | 8 +- pages/mall/delivery/orders/route.uvue | 3 +- pages/mall/delivery/service-record/index.uvue | 41 +- pages/user/login.uvue | 16 +- services/deliveryService.uts | 160 +- services/homeServiceService.uts | 42 +- services/serviceOrderService.uts | 1380 +++++++---------- utils/homeServiceBookingTime.uts | 16 +- utils/homecareAuth.uts | 255 +++ utils/pagesMode.uts | 2 +- utils/utils.uts | 25 + 报错信息.txt | 640 -------- 验收清单.md | 238 +++ 45 files changed, 7514 insertions(+), 2025 deletions(-) create mode 100644 .pages-backup/pages.delivery.2026-05-27T11-47-43-929Z.json create mode 100644 .pages-backup/pages.delivery.2026-05-28T00-41-26-093Z.json create mode 100644 .pages-backup/pages.delivery.2026-05-29T00-45-58-350Z.json create mode 100644 .pages-backup/pages.delivery.2026-06-01T00-44-50-634Z.json create mode 100644 .pages-backup/pages.delivery.2026-06-01T01-24-54-473Z.json create mode 100644 .pages-backup/pages.delivery.2026-06-03T00-42-16-888Z.json create mode 100644 .pages-backup/pages.delivery.2026-06-04T08-08-29-520Z.json create mode 100644 .pages-backup/pages.delivery.2026-06-05T00-38-54-603Z.json create mode 100644 .pages-backup/pages.delivery.2026-06-08T00-50-39-789Z.json create mode 100644 .pages-backup/pages.delivery.2026-06-09T01-28-15-988Z.json create mode 100644 .pages-backup/pages.delivery.2026-06-09T01-56-20-996Z.json create mode 100644 .pages-backup/pages.delivery.2026-06-10T00-46-43-283Z.json create mode 100644 .pages-backup/pages.delivery.2026-06-10T01-07-09-629Z.json create mode 100644 docs/AUTH_CHECKIN_API_GUIDE.md create mode 100644 mall_sql/migrations/20260609_backfill_ec_care_tasks_core_fields.sql create mode 100644 mall_sql/migrations/20260609_exclude_invalid_ec_care_tasks_from_delivery_rpc.sql create mode 100644 mall_sql/migrations/20260609_fix_delivery_rpc_request_snapshot.sql create mode 100644 mall_sql/migrations/20260609_restore_delivery_get_legacy_order_json.sql create mode 100644 mall_sql/migrations/20260609_rollback_exclude_invalid_ec_care_tasks.sql create mode 100644 mall_sql/migrations/20260610_backfill_pending_orders_core_fields.sql create mode 100644 mall_sql/migrations/20260610_fix_delivery_pending_blank.sql create mode 100644 mall_sql/scripts/create_delivery_staff_final.sql create mode 100644 mall_sql/scripts/create_delivery_staff_safe.sql create mode 100644 mall_sql/scripts/create_homecare_auth_user.js create mode 100644 mall_sql/scripts/seed_homecare_worker.sql create mode 100644 utils/homecareAuth.uts delete mode 100644 报错信息.txt create mode 100644 验收清单.md diff --git a/.pages-backup/pages.delivery.2026-05-27T11-47-43-929Z.json b/.pages-backup/pages.delivery.2026-05-27T11-47-43-929Z.json new file mode 100644 index 00000000..4d3aaac5 --- /dev/null +++ b/.pages-backup/pages.delivery.2026-05-27T11-47-43-929Z.json @@ -0,0 +1,478 @@ +{ + "pages": [ + { + "path": "pages/main/index", + "style": { + "navigationBarTitleText": "首页", + "navigationStyle": "custom", + "enablePullDownRefresh": false + } + }, + { + "path": "pages/user/boot", + "style": { + "navigationBarTitleText": "" + } + }, + { + "path": "pages/user/login", + "style": { + "navigationBarTitleText": "登录", + "navigationBarBackgroundColor": "#ffffff", + "navigationBarTextStyle": "black" + } + }, + { + "path": "pages/user/register", + "style": { + "navigationBarTitleText": "注册" + } + }, + { + "path": "pages/user/forgot-password", + "style": { + "navigationBarTitleText": "忘记密码" + } + }, + { + "path": "pages/user/terms", + "style": { + "navigationBarTitleText": "用户协议与隐私政策", + "navigationStyle": "custom" + } + }, + { + "path": "pages/user/login", + "style": { + "navigationBarTitleText": "服务人员登录", + "navigationStyle": "custom" + } + }, + { + "path": "pages/mall/delivery/home/index", + "style": { + "navigationBarTitleText": "工作台", + "navigationStyle": "custom" + } + }, + { + "path": "pages/mall/delivery/orders/index", + "style": { + "navigationBarTitleText": "工单列表", + "navigationStyle": "custom" + } + }, + { + "path": "pages/mall/delivery/orders/detail", + "style": { + "navigationBarTitleText": "订单详情", + "navigationStyle": "custom" + } + }, + { + "path": "pages/mall/delivery/orders/route", + "style": { + "navigationBarTitleText": "出发与导航", + "navigationStyle": "custom" + } + }, + { + "path": "pages/mall/delivery/orders/checkin", + "style": { + "navigationBarTitleText": "到岗签到", + "navigationStyle": "custom" + } + }, + { + "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": "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/.pages-backup/pages.delivery.2026-05-28T00-41-26-093Z.json b/.pages-backup/pages.delivery.2026-05-28T00-41-26-093Z.json new file mode 100644 index 00000000..22447dbf --- /dev/null +++ b/.pages-backup/pages.delivery.2026-05-28T00-41-26-093Z.json @@ -0,0 +1,131 @@ +{ + "pages": [ + { + "path": "pages/user/register", + "style": { + "navigationBarTitleText": "服务人员注册", + "navigationStyle": "custom" + } + }, + { + "path": "pages/user/terms", + "style": { + "navigationBarTitleText": "用户协议与隐私政策", + "navigationStyle": "custom" + } + }, + { + "path": "pages/user/login", + "style": { + "navigationBarTitleText": "服务人员登录", + "navigationStyle": "custom" + } + }, + { + "path": "pages/mall/delivery/home/index", + "style": { + "navigationBarTitleText": "工作台", + "navigationStyle": "custom" + } + }, + { + "path": "pages/mall/delivery/orders/index", + "style": { + "navigationBarTitleText": "工单列表", + "navigationStyle": "custom" + } + }, + { + "path": "pages/mall/delivery/orders/detail", + "style": { + "navigationBarTitleText": "订单详情", + "navigationStyle": "custom" + } + }, + { + "path": "pages/mall/delivery/orders/route", + "style": { + "navigationBarTitleText": "出发与导航", + "navigationStyle": "custom" + } + }, + { + "path": "pages/mall/delivery/orders/checkin", + "style": { + "navigationBarTitleText": "到岗签到", + "navigationStyle": "custom" + } + }, + { + "path": "pages/mall/delivery/service-record/index", + "style": { + "navigationBarTitleText": "服务记录", + "navigationStyle": "custom" + } + }, + { + "path": "pages/mall/delivery/orders/exception", + "style": { + "navigationBarTitleText": "异常上报", + "navigationStyle": "custom" + } + }, + { + "path": "pages/mall/delivery/profile/index", + "style": { + "navigationBarTitleText": "我的", + "navigationStyle": "custom" + } + }, + { + "path": "pages/mall/delivery/profile/settings", + "style": { + "navigationBarTitleText": "设置", + "navigationStyle": "custom" + } + } + ], + "subPackages": [], + "globalStyle": { + "navigationBarTextStyle": "black", + "navigationBarTitleText": "delivery", + "navigationBarBackgroundColor": "#FFFFFF", + "backgroundColor": "#F3F7F9" + }, + "tabBar": { + "color": "#6B7280", + "selectedColor": "#0F766E", + "backgroundColor": "#FFFFFF", + "borderStyle": "black", + "list": [ + { + "pagePath": "pages/mall/delivery/home/index", + "text": "工作台", + "iconPath": "static/tabbar/home.png", + "selectedIconPath": "static/tabbar/home-active.png" + }, + { + "pagePath": "pages/mall/delivery/orders/index", + "text": "订单", + "iconPath": "static/tabbar/cart.png", + "selectedIconPath": "static/tabbar/cart.png" + }, + { + "pagePath": "pages/mall/delivery/profile/index", + "text": "我的", + "iconPath": "static/tabbar/user.png", + "selectedIconPath": "static/tabbar/user.png" + } + ] + }, + "condition": { + "current": 0, + "list": [ + { + "name": "delivery端", + "path": "pages/user/login", + "query": "mode=delivery&role=delivery" + } + ] + } +} diff --git a/.pages-backup/pages.delivery.2026-05-29T00-45-58-350Z.json b/.pages-backup/pages.delivery.2026-05-29T00-45-58-350Z.json new file mode 100644 index 00000000..22447dbf --- /dev/null +++ b/.pages-backup/pages.delivery.2026-05-29T00-45-58-350Z.json @@ -0,0 +1,131 @@ +{ + "pages": [ + { + "path": "pages/user/register", + "style": { + "navigationBarTitleText": "服务人员注册", + "navigationStyle": "custom" + } + }, + { + "path": "pages/user/terms", + "style": { + "navigationBarTitleText": "用户协议与隐私政策", + "navigationStyle": "custom" + } + }, + { + "path": "pages/user/login", + "style": { + "navigationBarTitleText": "服务人员登录", + "navigationStyle": "custom" + } + }, + { + "path": "pages/mall/delivery/home/index", + "style": { + "navigationBarTitleText": "工作台", + "navigationStyle": "custom" + } + }, + { + "path": "pages/mall/delivery/orders/index", + "style": { + "navigationBarTitleText": "工单列表", + "navigationStyle": "custom" + } + }, + { + "path": "pages/mall/delivery/orders/detail", + "style": { + "navigationBarTitleText": "订单详情", + "navigationStyle": "custom" + } + }, + { + "path": "pages/mall/delivery/orders/route", + "style": { + "navigationBarTitleText": "出发与导航", + "navigationStyle": "custom" + } + }, + { + "path": "pages/mall/delivery/orders/checkin", + "style": { + "navigationBarTitleText": "到岗签到", + "navigationStyle": "custom" + } + }, + { + "path": "pages/mall/delivery/service-record/index", + "style": { + "navigationBarTitleText": "服务记录", + "navigationStyle": "custom" + } + }, + { + "path": "pages/mall/delivery/orders/exception", + "style": { + "navigationBarTitleText": "异常上报", + "navigationStyle": "custom" + } + }, + { + "path": "pages/mall/delivery/profile/index", + "style": { + "navigationBarTitleText": "我的", + "navigationStyle": "custom" + } + }, + { + "path": "pages/mall/delivery/profile/settings", + "style": { + "navigationBarTitleText": "设置", + "navigationStyle": "custom" + } + } + ], + "subPackages": [], + "globalStyle": { + "navigationBarTextStyle": "black", + "navigationBarTitleText": "delivery", + "navigationBarBackgroundColor": "#FFFFFF", + "backgroundColor": "#F3F7F9" + }, + "tabBar": { + "color": "#6B7280", + "selectedColor": "#0F766E", + "backgroundColor": "#FFFFFF", + "borderStyle": "black", + "list": [ + { + "pagePath": "pages/mall/delivery/home/index", + "text": "工作台", + "iconPath": "static/tabbar/home.png", + "selectedIconPath": "static/tabbar/home-active.png" + }, + { + "pagePath": "pages/mall/delivery/orders/index", + "text": "订单", + "iconPath": "static/tabbar/cart.png", + "selectedIconPath": "static/tabbar/cart.png" + }, + { + "pagePath": "pages/mall/delivery/profile/index", + "text": "我的", + "iconPath": "static/tabbar/user.png", + "selectedIconPath": "static/tabbar/user.png" + } + ] + }, + "condition": { + "current": 0, + "list": [ + { + "name": "delivery端", + "path": "pages/user/login", + "query": "mode=delivery&role=delivery" + } + ] + } +} diff --git a/.pages-backup/pages.delivery.2026-06-01T00-44-50-634Z.json b/.pages-backup/pages.delivery.2026-06-01T00-44-50-634Z.json new file mode 100644 index 00000000..22447dbf --- /dev/null +++ b/.pages-backup/pages.delivery.2026-06-01T00-44-50-634Z.json @@ -0,0 +1,131 @@ +{ + "pages": [ + { + "path": "pages/user/register", + "style": { + "navigationBarTitleText": "服务人员注册", + "navigationStyle": "custom" + } + }, + { + "path": "pages/user/terms", + "style": { + "navigationBarTitleText": "用户协议与隐私政策", + "navigationStyle": "custom" + } + }, + { + "path": "pages/user/login", + "style": { + "navigationBarTitleText": "服务人员登录", + "navigationStyle": "custom" + } + }, + { + "path": "pages/mall/delivery/home/index", + "style": { + "navigationBarTitleText": "工作台", + "navigationStyle": "custom" + } + }, + { + "path": "pages/mall/delivery/orders/index", + "style": { + "navigationBarTitleText": "工单列表", + "navigationStyle": "custom" + } + }, + { + "path": "pages/mall/delivery/orders/detail", + "style": { + "navigationBarTitleText": "订单详情", + "navigationStyle": "custom" + } + }, + { + "path": "pages/mall/delivery/orders/route", + "style": { + "navigationBarTitleText": "出发与导航", + "navigationStyle": "custom" + } + }, + { + "path": "pages/mall/delivery/orders/checkin", + "style": { + "navigationBarTitleText": "到岗签到", + "navigationStyle": "custom" + } + }, + { + "path": "pages/mall/delivery/service-record/index", + "style": { + "navigationBarTitleText": "服务记录", + "navigationStyle": "custom" + } + }, + { + "path": "pages/mall/delivery/orders/exception", + "style": { + "navigationBarTitleText": "异常上报", + "navigationStyle": "custom" + } + }, + { + "path": "pages/mall/delivery/profile/index", + "style": { + "navigationBarTitleText": "我的", + "navigationStyle": "custom" + } + }, + { + "path": "pages/mall/delivery/profile/settings", + "style": { + "navigationBarTitleText": "设置", + "navigationStyle": "custom" + } + } + ], + "subPackages": [], + "globalStyle": { + "navigationBarTextStyle": "black", + "navigationBarTitleText": "delivery", + "navigationBarBackgroundColor": "#FFFFFF", + "backgroundColor": "#F3F7F9" + }, + "tabBar": { + "color": "#6B7280", + "selectedColor": "#0F766E", + "backgroundColor": "#FFFFFF", + "borderStyle": "black", + "list": [ + { + "pagePath": "pages/mall/delivery/home/index", + "text": "工作台", + "iconPath": "static/tabbar/home.png", + "selectedIconPath": "static/tabbar/home-active.png" + }, + { + "pagePath": "pages/mall/delivery/orders/index", + "text": "订单", + "iconPath": "static/tabbar/cart.png", + "selectedIconPath": "static/tabbar/cart.png" + }, + { + "pagePath": "pages/mall/delivery/profile/index", + "text": "我的", + "iconPath": "static/tabbar/user.png", + "selectedIconPath": "static/tabbar/user.png" + } + ] + }, + "condition": { + "current": 0, + "list": [ + { + "name": "delivery端", + "path": "pages/user/login", + "query": "mode=delivery&role=delivery" + } + ] + } +} diff --git a/.pages-backup/pages.delivery.2026-06-01T01-24-54-473Z.json b/.pages-backup/pages.delivery.2026-06-01T01-24-54-473Z.json new file mode 100644 index 00000000..22447dbf --- /dev/null +++ b/.pages-backup/pages.delivery.2026-06-01T01-24-54-473Z.json @@ -0,0 +1,131 @@ +{ + "pages": [ + { + "path": "pages/user/register", + "style": { + "navigationBarTitleText": "服务人员注册", + "navigationStyle": "custom" + } + }, + { + "path": "pages/user/terms", + "style": { + "navigationBarTitleText": "用户协议与隐私政策", + "navigationStyle": "custom" + } + }, + { + "path": "pages/user/login", + "style": { + "navigationBarTitleText": "服务人员登录", + "navigationStyle": "custom" + } + }, + { + "path": "pages/mall/delivery/home/index", + "style": { + "navigationBarTitleText": "工作台", + "navigationStyle": "custom" + } + }, + { + "path": "pages/mall/delivery/orders/index", + "style": { + "navigationBarTitleText": "工单列表", + "navigationStyle": "custom" + } + }, + { + "path": "pages/mall/delivery/orders/detail", + "style": { + "navigationBarTitleText": "订单详情", + "navigationStyle": "custom" + } + }, + { + "path": "pages/mall/delivery/orders/route", + "style": { + "navigationBarTitleText": "出发与导航", + "navigationStyle": "custom" + } + }, + { + "path": "pages/mall/delivery/orders/checkin", + "style": { + "navigationBarTitleText": "到岗签到", + "navigationStyle": "custom" + } + }, + { + "path": "pages/mall/delivery/service-record/index", + "style": { + "navigationBarTitleText": "服务记录", + "navigationStyle": "custom" + } + }, + { + "path": "pages/mall/delivery/orders/exception", + "style": { + "navigationBarTitleText": "异常上报", + "navigationStyle": "custom" + } + }, + { + "path": "pages/mall/delivery/profile/index", + "style": { + "navigationBarTitleText": "我的", + "navigationStyle": "custom" + } + }, + { + "path": "pages/mall/delivery/profile/settings", + "style": { + "navigationBarTitleText": "设置", + "navigationStyle": "custom" + } + } + ], + "subPackages": [], + "globalStyle": { + "navigationBarTextStyle": "black", + "navigationBarTitleText": "delivery", + "navigationBarBackgroundColor": "#FFFFFF", + "backgroundColor": "#F3F7F9" + }, + "tabBar": { + "color": "#6B7280", + "selectedColor": "#0F766E", + "backgroundColor": "#FFFFFF", + "borderStyle": "black", + "list": [ + { + "pagePath": "pages/mall/delivery/home/index", + "text": "工作台", + "iconPath": "static/tabbar/home.png", + "selectedIconPath": "static/tabbar/home-active.png" + }, + { + "pagePath": "pages/mall/delivery/orders/index", + "text": "订单", + "iconPath": "static/tabbar/cart.png", + "selectedIconPath": "static/tabbar/cart.png" + }, + { + "pagePath": "pages/mall/delivery/profile/index", + "text": "我的", + "iconPath": "static/tabbar/user.png", + "selectedIconPath": "static/tabbar/user.png" + } + ] + }, + "condition": { + "current": 0, + "list": [ + { + "name": "delivery端", + "path": "pages/user/login", + "query": "mode=delivery&role=delivery" + } + ] + } +} diff --git a/.pages-backup/pages.delivery.2026-06-03T00-42-16-888Z.json b/.pages-backup/pages.delivery.2026-06-03T00-42-16-888Z.json new file mode 100644 index 00000000..22447dbf --- /dev/null +++ b/.pages-backup/pages.delivery.2026-06-03T00-42-16-888Z.json @@ -0,0 +1,131 @@ +{ + "pages": [ + { + "path": "pages/user/register", + "style": { + "navigationBarTitleText": "服务人员注册", + "navigationStyle": "custom" + } + }, + { + "path": "pages/user/terms", + "style": { + "navigationBarTitleText": "用户协议与隐私政策", + "navigationStyle": "custom" + } + }, + { + "path": "pages/user/login", + "style": { + "navigationBarTitleText": "服务人员登录", + "navigationStyle": "custom" + } + }, + { + "path": "pages/mall/delivery/home/index", + "style": { + "navigationBarTitleText": "工作台", + "navigationStyle": "custom" + } + }, + { + "path": "pages/mall/delivery/orders/index", + "style": { + "navigationBarTitleText": "工单列表", + "navigationStyle": "custom" + } + }, + { + "path": "pages/mall/delivery/orders/detail", + "style": { + "navigationBarTitleText": "订单详情", + "navigationStyle": "custom" + } + }, + { + "path": "pages/mall/delivery/orders/route", + "style": { + "navigationBarTitleText": "出发与导航", + "navigationStyle": "custom" + } + }, + { + "path": "pages/mall/delivery/orders/checkin", + "style": { + "navigationBarTitleText": "到岗签到", + "navigationStyle": "custom" + } + }, + { + "path": "pages/mall/delivery/service-record/index", + "style": { + "navigationBarTitleText": "服务记录", + "navigationStyle": "custom" + } + }, + { + "path": "pages/mall/delivery/orders/exception", + "style": { + "navigationBarTitleText": "异常上报", + "navigationStyle": "custom" + } + }, + { + "path": "pages/mall/delivery/profile/index", + "style": { + "navigationBarTitleText": "我的", + "navigationStyle": "custom" + } + }, + { + "path": "pages/mall/delivery/profile/settings", + "style": { + "navigationBarTitleText": "设置", + "navigationStyle": "custom" + } + } + ], + "subPackages": [], + "globalStyle": { + "navigationBarTextStyle": "black", + "navigationBarTitleText": "delivery", + "navigationBarBackgroundColor": "#FFFFFF", + "backgroundColor": "#F3F7F9" + }, + "tabBar": { + "color": "#6B7280", + "selectedColor": "#0F766E", + "backgroundColor": "#FFFFFF", + "borderStyle": "black", + "list": [ + { + "pagePath": "pages/mall/delivery/home/index", + "text": "工作台", + "iconPath": "static/tabbar/home.png", + "selectedIconPath": "static/tabbar/home-active.png" + }, + { + "pagePath": "pages/mall/delivery/orders/index", + "text": "订单", + "iconPath": "static/tabbar/cart.png", + "selectedIconPath": "static/tabbar/cart.png" + }, + { + "pagePath": "pages/mall/delivery/profile/index", + "text": "我的", + "iconPath": "static/tabbar/user.png", + "selectedIconPath": "static/tabbar/user.png" + } + ] + }, + "condition": { + "current": 0, + "list": [ + { + "name": "delivery端", + "path": "pages/user/login", + "query": "mode=delivery&role=delivery" + } + ] + } +} diff --git a/.pages-backup/pages.delivery.2026-06-04T08-08-29-520Z.json b/.pages-backup/pages.delivery.2026-06-04T08-08-29-520Z.json new file mode 100644 index 00000000..22447dbf --- /dev/null +++ b/.pages-backup/pages.delivery.2026-06-04T08-08-29-520Z.json @@ -0,0 +1,131 @@ +{ + "pages": [ + { + "path": "pages/user/register", + "style": { + "navigationBarTitleText": "服务人员注册", + "navigationStyle": "custom" + } + }, + { + "path": "pages/user/terms", + "style": { + "navigationBarTitleText": "用户协议与隐私政策", + "navigationStyle": "custom" + } + }, + { + "path": "pages/user/login", + "style": { + "navigationBarTitleText": "服务人员登录", + "navigationStyle": "custom" + } + }, + { + "path": "pages/mall/delivery/home/index", + "style": { + "navigationBarTitleText": "工作台", + "navigationStyle": "custom" + } + }, + { + "path": "pages/mall/delivery/orders/index", + "style": { + "navigationBarTitleText": "工单列表", + "navigationStyle": "custom" + } + }, + { + "path": "pages/mall/delivery/orders/detail", + "style": { + "navigationBarTitleText": "订单详情", + "navigationStyle": "custom" + } + }, + { + "path": "pages/mall/delivery/orders/route", + "style": { + "navigationBarTitleText": "出发与导航", + "navigationStyle": "custom" + } + }, + { + "path": "pages/mall/delivery/orders/checkin", + "style": { + "navigationBarTitleText": "到岗签到", + "navigationStyle": "custom" + } + }, + { + "path": "pages/mall/delivery/service-record/index", + "style": { + "navigationBarTitleText": "服务记录", + "navigationStyle": "custom" + } + }, + { + "path": "pages/mall/delivery/orders/exception", + "style": { + "navigationBarTitleText": "异常上报", + "navigationStyle": "custom" + } + }, + { + "path": "pages/mall/delivery/profile/index", + "style": { + "navigationBarTitleText": "我的", + "navigationStyle": "custom" + } + }, + { + "path": "pages/mall/delivery/profile/settings", + "style": { + "navigationBarTitleText": "设置", + "navigationStyle": "custom" + } + } + ], + "subPackages": [], + "globalStyle": { + "navigationBarTextStyle": "black", + "navigationBarTitleText": "delivery", + "navigationBarBackgroundColor": "#FFFFFF", + "backgroundColor": "#F3F7F9" + }, + "tabBar": { + "color": "#6B7280", + "selectedColor": "#0F766E", + "backgroundColor": "#FFFFFF", + "borderStyle": "black", + "list": [ + { + "pagePath": "pages/mall/delivery/home/index", + "text": "工作台", + "iconPath": "static/tabbar/home.png", + "selectedIconPath": "static/tabbar/home-active.png" + }, + { + "pagePath": "pages/mall/delivery/orders/index", + "text": "订单", + "iconPath": "static/tabbar/cart.png", + "selectedIconPath": "static/tabbar/cart.png" + }, + { + "pagePath": "pages/mall/delivery/profile/index", + "text": "我的", + "iconPath": "static/tabbar/user.png", + "selectedIconPath": "static/tabbar/user.png" + } + ] + }, + "condition": { + "current": 0, + "list": [ + { + "name": "delivery端", + "path": "pages/user/login", + "query": "mode=delivery&role=delivery" + } + ] + } +} diff --git a/.pages-backup/pages.delivery.2026-06-05T00-38-54-603Z.json b/.pages-backup/pages.delivery.2026-06-05T00-38-54-603Z.json new file mode 100644 index 00000000..22447dbf --- /dev/null +++ b/.pages-backup/pages.delivery.2026-06-05T00-38-54-603Z.json @@ -0,0 +1,131 @@ +{ + "pages": [ + { + "path": "pages/user/register", + "style": { + "navigationBarTitleText": "服务人员注册", + "navigationStyle": "custom" + } + }, + { + "path": "pages/user/terms", + "style": { + "navigationBarTitleText": "用户协议与隐私政策", + "navigationStyle": "custom" + } + }, + { + "path": "pages/user/login", + "style": { + "navigationBarTitleText": "服务人员登录", + "navigationStyle": "custom" + } + }, + { + "path": "pages/mall/delivery/home/index", + "style": { + "navigationBarTitleText": "工作台", + "navigationStyle": "custom" + } + }, + { + "path": "pages/mall/delivery/orders/index", + "style": { + "navigationBarTitleText": "工单列表", + "navigationStyle": "custom" + } + }, + { + "path": "pages/mall/delivery/orders/detail", + "style": { + "navigationBarTitleText": "订单详情", + "navigationStyle": "custom" + } + }, + { + "path": "pages/mall/delivery/orders/route", + "style": { + "navigationBarTitleText": "出发与导航", + "navigationStyle": "custom" + } + }, + { + "path": "pages/mall/delivery/orders/checkin", + "style": { + "navigationBarTitleText": "到岗签到", + "navigationStyle": "custom" + } + }, + { + "path": "pages/mall/delivery/service-record/index", + "style": { + "navigationBarTitleText": "服务记录", + "navigationStyle": "custom" + } + }, + { + "path": "pages/mall/delivery/orders/exception", + "style": { + "navigationBarTitleText": "异常上报", + "navigationStyle": "custom" + } + }, + { + "path": "pages/mall/delivery/profile/index", + "style": { + "navigationBarTitleText": "我的", + "navigationStyle": "custom" + } + }, + { + "path": "pages/mall/delivery/profile/settings", + "style": { + "navigationBarTitleText": "设置", + "navigationStyle": "custom" + } + } + ], + "subPackages": [], + "globalStyle": { + "navigationBarTextStyle": "black", + "navigationBarTitleText": "delivery", + "navigationBarBackgroundColor": "#FFFFFF", + "backgroundColor": "#F3F7F9" + }, + "tabBar": { + "color": "#6B7280", + "selectedColor": "#0F766E", + "backgroundColor": "#FFFFFF", + "borderStyle": "black", + "list": [ + { + "pagePath": "pages/mall/delivery/home/index", + "text": "工作台", + "iconPath": "static/tabbar/home.png", + "selectedIconPath": "static/tabbar/home-active.png" + }, + { + "pagePath": "pages/mall/delivery/orders/index", + "text": "订单", + "iconPath": "static/tabbar/cart.png", + "selectedIconPath": "static/tabbar/cart.png" + }, + { + "pagePath": "pages/mall/delivery/profile/index", + "text": "我的", + "iconPath": "static/tabbar/user.png", + "selectedIconPath": "static/tabbar/user.png" + } + ] + }, + "condition": { + "current": 0, + "list": [ + { + "name": "delivery端", + "path": "pages/user/login", + "query": "mode=delivery&role=delivery" + } + ] + } +} diff --git a/.pages-backup/pages.delivery.2026-06-08T00-50-39-789Z.json b/.pages-backup/pages.delivery.2026-06-08T00-50-39-789Z.json new file mode 100644 index 00000000..22447dbf --- /dev/null +++ b/.pages-backup/pages.delivery.2026-06-08T00-50-39-789Z.json @@ -0,0 +1,131 @@ +{ + "pages": [ + { + "path": "pages/user/register", + "style": { + "navigationBarTitleText": "服务人员注册", + "navigationStyle": "custom" + } + }, + { + "path": "pages/user/terms", + "style": { + "navigationBarTitleText": "用户协议与隐私政策", + "navigationStyle": "custom" + } + }, + { + "path": "pages/user/login", + "style": { + "navigationBarTitleText": "服务人员登录", + "navigationStyle": "custom" + } + }, + { + "path": "pages/mall/delivery/home/index", + "style": { + "navigationBarTitleText": "工作台", + "navigationStyle": "custom" + } + }, + { + "path": "pages/mall/delivery/orders/index", + "style": { + "navigationBarTitleText": "工单列表", + "navigationStyle": "custom" + } + }, + { + "path": "pages/mall/delivery/orders/detail", + "style": { + "navigationBarTitleText": "订单详情", + "navigationStyle": "custom" + } + }, + { + "path": "pages/mall/delivery/orders/route", + "style": { + "navigationBarTitleText": "出发与导航", + "navigationStyle": "custom" + } + }, + { + "path": "pages/mall/delivery/orders/checkin", + "style": { + "navigationBarTitleText": "到岗签到", + "navigationStyle": "custom" + } + }, + { + "path": "pages/mall/delivery/service-record/index", + "style": { + "navigationBarTitleText": "服务记录", + "navigationStyle": "custom" + } + }, + { + "path": "pages/mall/delivery/orders/exception", + "style": { + "navigationBarTitleText": "异常上报", + "navigationStyle": "custom" + } + }, + { + "path": "pages/mall/delivery/profile/index", + "style": { + "navigationBarTitleText": "我的", + "navigationStyle": "custom" + } + }, + { + "path": "pages/mall/delivery/profile/settings", + "style": { + "navigationBarTitleText": "设置", + "navigationStyle": "custom" + } + } + ], + "subPackages": [], + "globalStyle": { + "navigationBarTextStyle": "black", + "navigationBarTitleText": "delivery", + "navigationBarBackgroundColor": "#FFFFFF", + "backgroundColor": "#F3F7F9" + }, + "tabBar": { + "color": "#6B7280", + "selectedColor": "#0F766E", + "backgroundColor": "#FFFFFF", + "borderStyle": "black", + "list": [ + { + "pagePath": "pages/mall/delivery/home/index", + "text": "工作台", + "iconPath": "static/tabbar/home.png", + "selectedIconPath": "static/tabbar/home-active.png" + }, + { + "pagePath": "pages/mall/delivery/orders/index", + "text": "订单", + "iconPath": "static/tabbar/cart.png", + "selectedIconPath": "static/tabbar/cart.png" + }, + { + "pagePath": "pages/mall/delivery/profile/index", + "text": "我的", + "iconPath": "static/tabbar/user.png", + "selectedIconPath": "static/tabbar/user.png" + } + ] + }, + "condition": { + "current": 0, + "list": [ + { + "name": "delivery端", + "path": "pages/user/login", + "query": "mode=delivery&role=delivery" + } + ] + } +} diff --git a/.pages-backup/pages.delivery.2026-06-09T01-28-15-988Z.json b/.pages-backup/pages.delivery.2026-06-09T01-28-15-988Z.json new file mode 100644 index 00000000..22447dbf --- /dev/null +++ b/.pages-backup/pages.delivery.2026-06-09T01-28-15-988Z.json @@ -0,0 +1,131 @@ +{ + "pages": [ + { + "path": "pages/user/register", + "style": { + "navigationBarTitleText": "服务人员注册", + "navigationStyle": "custom" + } + }, + { + "path": "pages/user/terms", + "style": { + "navigationBarTitleText": "用户协议与隐私政策", + "navigationStyle": "custom" + } + }, + { + "path": "pages/user/login", + "style": { + "navigationBarTitleText": "服务人员登录", + "navigationStyle": "custom" + } + }, + { + "path": "pages/mall/delivery/home/index", + "style": { + "navigationBarTitleText": "工作台", + "navigationStyle": "custom" + } + }, + { + "path": "pages/mall/delivery/orders/index", + "style": { + "navigationBarTitleText": "工单列表", + "navigationStyle": "custom" + } + }, + { + "path": "pages/mall/delivery/orders/detail", + "style": { + "navigationBarTitleText": "订单详情", + "navigationStyle": "custom" + } + }, + { + "path": "pages/mall/delivery/orders/route", + "style": { + "navigationBarTitleText": "出发与导航", + "navigationStyle": "custom" + } + }, + { + "path": "pages/mall/delivery/orders/checkin", + "style": { + "navigationBarTitleText": "到岗签到", + "navigationStyle": "custom" + } + }, + { + "path": "pages/mall/delivery/service-record/index", + "style": { + "navigationBarTitleText": "服务记录", + "navigationStyle": "custom" + } + }, + { + "path": "pages/mall/delivery/orders/exception", + "style": { + "navigationBarTitleText": "异常上报", + "navigationStyle": "custom" + } + }, + { + "path": "pages/mall/delivery/profile/index", + "style": { + "navigationBarTitleText": "我的", + "navigationStyle": "custom" + } + }, + { + "path": "pages/mall/delivery/profile/settings", + "style": { + "navigationBarTitleText": "设置", + "navigationStyle": "custom" + } + } + ], + "subPackages": [], + "globalStyle": { + "navigationBarTextStyle": "black", + "navigationBarTitleText": "delivery", + "navigationBarBackgroundColor": "#FFFFFF", + "backgroundColor": "#F3F7F9" + }, + "tabBar": { + "color": "#6B7280", + "selectedColor": "#0F766E", + "backgroundColor": "#FFFFFF", + "borderStyle": "black", + "list": [ + { + "pagePath": "pages/mall/delivery/home/index", + "text": "工作台", + "iconPath": "static/tabbar/home.png", + "selectedIconPath": "static/tabbar/home-active.png" + }, + { + "pagePath": "pages/mall/delivery/orders/index", + "text": "订单", + "iconPath": "static/tabbar/cart.png", + "selectedIconPath": "static/tabbar/cart.png" + }, + { + "pagePath": "pages/mall/delivery/profile/index", + "text": "我的", + "iconPath": "static/tabbar/user.png", + "selectedIconPath": "static/tabbar/user.png" + } + ] + }, + "condition": { + "current": 0, + "list": [ + { + "name": "delivery端", + "path": "pages/user/login", + "query": "mode=delivery&role=delivery" + } + ] + } +} diff --git a/.pages-backup/pages.delivery.2026-06-09T01-56-20-996Z.json b/.pages-backup/pages.delivery.2026-06-09T01-56-20-996Z.json new file mode 100644 index 00000000..22447dbf --- /dev/null +++ b/.pages-backup/pages.delivery.2026-06-09T01-56-20-996Z.json @@ -0,0 +1,131 @@ +{ + "pages": [ + { + "path": "pages/user/register", + "style": { + "navigationBarTitleText": "服务人员注册", + "navigationStyle": "custom" + } + }, + { + "path": "pages/user/terms", + "style": { + "navigationBarTitleText": "用户协议与隐私政策", + "navigationStyle": "custom" + } + }, + { + "path": "pages/user/login", + "style": { + "navigationBarTitleText": "服务人员登录", + "navigationStyle": "custom" + } + }, + { + "path": "pages/mall/delivery/home/index", + "style": { + "navigationBarTitleText": "工作台", + "navigationStyle": "custom" + } + }, + { + "path": "pages/mall/delivery/orders/index", + "style": { + "navigationBarTitleText": "工单列表", + "navigationStyle": "custom" + } + }, + { + "path": "pages/mall/delivery/orders/detail", + "style": { + "navigationBarTitleText": "订单详情", + "navigationStyle": "custom" + } + }, + { + "path": "pages/mall/delivery/orders/route", + "style": { + "navigationBarTitleText": "出发与导航", + "navigationStyle": "custom" + } + }, + { + "path": "pages/mall/delivery/orders/checkin", + "style": { + "navigationBarTitleText": "到岗签到", + "navigationStyle": "custom" + } + }, + { + "path": "pages/mall/delivery/service-record/index", + "style": { + "navigationBarTitleText": "服务记录", + "navigationStyle": "custom" + } + }, + { + "path": "pages/mall/delivery/orders/exception", + "style": { + "navigationBarTitleText": "异常上报", + "navigationStyle": "custom" + } + }, + { + "path": "pages/mall/delivery/profile/index", + "style": { + "navigationBarTitleText": "我的", + "navigationStyle": "custom" + } + }, + { + "path": "pages/mall/delivery/profile/settings", + "style": { + "navigationBarTitleText": "设置", + "navigationStyle": "custom" + } + } + ], + "subPackages": [], + "globalStyle": { + "navigationBarTextStyle": "black", + "navigationBarTitleText": "delivery", + "navigationBarBackgroundColor": "#FFFFFF", + "backgroundColor": "#F3F7F9" + }, + "tabBar": { + "color": "#6B7280", + "selectedColor": "#0F766E", + "backgroundColor": "#FFFFFF", + "borderStyle": "black", + "list": [ + { + "pagePath": "pages/mall/delivery/home/index", + "text": "工作台", + "iconPath": "static/tabbar/home.png", + "selectedIconPath": "static/tabbar/home-active.png" + }, + { + "pagePath": "pages/mall/delivery/orders/index", + "text": "订单", + "iconPath": "static/tabbar/cart.png", + "selectedIconPath": "static/tabbar/cart.png" + }, + { + "pagePath": "pages/mall/delivery/profile/index", + "text": "我的", + "iconPath": "static/tabbar/user.png", + "selectedIconPath": "static/tabbar/user.png" + } + ] + }, + "condition": { + "current": 0, + "list": [ + { + "name": "delivery端", + "path": "pages/user/login", + "query": "mode=delivery&role=delivery" + } + ] + } +} diff --git a/.pages-backup/pages.delivery.2026-06-10T00-46-43-283Z.json b/.pages-backup/pages.delivery.2026-06-10T00-46-43-283Z.json new file mode 100644 index 00000000..22447dbf --- /dev/null +++ b/.pages-backup/pages.delivery.2026-06-10T00-46-43-283Z.json @@ -0,0 +1,131 @@ +{ + "pages": [ + { + "path": "pages/user/register", + "style": { + "navigationBarTitleText": "服务人员注册", + "navigationStyle": "custom" + } + }, + { + "path": "pages/user/terms", + "style": { + "navigationBarTitleText": "用户协议与隐私政策", + "navigationStyle": "custom" + } + }, + { + "path": "pages/user/login", + "style": { + "navigationBarTitleText": "服务人员登录", + "navigationStyle": "custom" + } + }, + { + "path": "pages/mall/delivery/home/index", + "style": { + "navigationBarTitleText": "工作台", + "navigationStyle": "custom" + } + }, + { + "path": "pages/mall/delivery/orders/index", + "style": { + "navigationBarTitleText": "工单列表", + "navigationStyle": "custom" + } + }, + { + "path": "pages/mall/delivery/orders/detail", + "style": { + "navigationBarTitleText": "订单详情", + "navigationStyle": "custom" + } + }, + { + "path": "pages/mall/delivery/orders/route", + "style": { + "navigationBarTitleText": "出发与导航", + "navigationStyle": "custom" + } + }, + { + "path": "pages/mall/delivery/orders/checkin", + "style": { + "navigationBarTitleText": "到岗签到", + "navigationStyle": "custom" + } + }, + { + "path": "pages/mall/delivery/service-record/index", + "style": { + "navigationBarTitleText": "服务记录", + "navigationStyle": "custom" + } + }, + { + "path": "pages/mall/delivery/orders/exception", + "style": { + "navigationBarTitleText": "异常上报", + "navigationStyle": "custom" + } + }, + { + "path": "pages/mall/delivery/profile/index", + "style": { + "navigationBarTitleText": "我的", + "navigationStyle": "custom" + } + }, + { + "path": "pages/mall/delivery/profile/settings", + "style": { + "navigationBarTitleText": "设置", + "navigationStyle": "custom" + } + } + ], + "subPackages": [], + "globalStyle": { + "navigationBarTextStyle": "black", + "navigationBarTitleText": "delivery", + "navigationBarBackgroundColor": "#FFFFFF", + "backgroundColor": "#F3F7F9" + }, + "tabBar": { + "color": "#6B7280", + "selectedColor": "#0F766E", + "backgroundColor": "#FFFFFF", + "borderStyle": "black", + "list": [ + { + "pagePath": "pages/mall/delivery/home/index", + "text": "工作台", + "iconPath": "static/tabbar/home.png", + "selectedIconPath": "static/tabbar/home-active.png" + }, + { + "pagePath": "pages/mall/delivery/orders/index", + "text": "订单", + "iconPath": "static/tabbar/cart.png", + "selectedIconPath": "static/tabbar/cart.png" + }, + { + "pagePath": "pages/mall/delivery/profile/index", + "text": "我的", + "iconPath": "static/tabbar/user.png", + "selectedIconPath": "static/tabbar/user.png" + } + ] + }, + "condition": { + "current": 0, + "list": [ + { + "name": "delivery端", + "path": "pages/user/login", + "query": "mode=delivery&role=delivery" + } + ] + } +} diff --git a/.pages-backup/pages.delivery.2026-06-10T01-07-09-629Z.json b/.pages-backup/pages.delivery.2026-06-10T01-07-09-629Z.json new file mode 100644 index 00000000..22447dbf --- /dev/null +++ b/.pages-backup/pages.delivery.2026-06-10T01-07-09-629Z.json @@ -0,0 +1,131 @@ +{ + "pages": [ + { + "path": "pages/user/register", + "style": { + "navigationBarTitleText": "服务人员注册", + "navigationStyle": "custom" + } + }, + { + "path": "pages/user/terms", + "style": { + "navigationBarTitleText": "用户协议与隐私政策", + "navigationStyle": "custom" + } + }, + { + "path": "pages/user/login", + "style": { + "navigationBarTitleText": "服务人员登录", + "navigationStyle": "custom" + } + }, + { + "path": "pages/mall/delivery/home/index", + "style": { + "navigationBarTitleText": "工作台", + "navigationStyle": "custom" + } + }, + { + "path": "pages/mall/delivery/orders/index", + "style": { + "navigationBarTitleText": "工单列表", + "navigationStyle": "custom" + } + }, + { + "path": "pages/mall/delivery/orders/detail", + "style": { + "navigationBarTitleText": "订单详情", + "navigationStyle": "custom" + } + }, + { + "path": "pages/mall/delivery/orders/route", + "style": { + "navigationBarTitleText": "出发与导航", + "navigationStyle": "custom" + } + }, + { + "path": "pages/mall/delivery/orders/checkin", + "style": { + "navigationBarTitleText": "到岗签到", + "navigationStyle": "custom" + } + }, + { + "path": "pages/mall/delivery/service-record/index", + "style": { + "navigationBarTitleText": "服务记录", + "navigationStyle": "custom" + } + }, + { + "path": "pages/mall/delivery/orders/exception", + "style": { + "navigationBarTitleText": "异常上报", + "navigationStyle": "custom" + } + }, + { + "path": "pages/mall/delivery/profile/index", + "style": { + "navigationBarTitleText": "我的", + "navigationStyle": "custom" + } + }, + { + "path": "pages/mall/delivery/profile/settings", + "style": { + "navigationBarTitleText": "设置", + "navigationStyle": "custom" + } + } + ], + "subPackages": [], + "globalStyle": { + "navigationBarTextStyle": "black", + "navigationBarTitleText": "delivery", + "navigationBarBackgroundColor": "#FFFFFF", + "backgroundColor": "#F3F7F9" + }, + "tabBar": { + "color": "#6B7280", + "selectedColor": "#0F766E", + "backgroundColor": "#FFFFFF", + "borderStyle": "black", + "list": [ + { + "pagePath": "pages/mall/delivery/home/index", + "text": "工作台", + "iconPath": "static/tabbar/home.png", + "selectedIconPath": "static/tabbar/home-active.png" + }, + { + "pagePath": "pages/mall/delivery/orders/index", + "text": "订单", + "iconPath": "static/tabbar/cart.png", + "selectedIconPath": "static/tabbar/cart.png" + }, + { + "pagePath": "pages/mall/delivery/profile/index", + "text": "我的", + "iconPath": "static/tabbar/user.png", + "selectedIconPath": "static/tabbar/user.png" + } + ] + }, + "condition": { + "current": 0, + "list": [ + { + "name": "delivery端", + "path": "pages/user/login", + "query": "mode=delivery&role=delivery" + } + ] + } +} diff --git a/ak/config.uts b/ak/config.uts index caa36ea9..3f166dd1 100644 --- a/ak/config.uts +++ b/ak/config.uts @@ -92,7 +92,9 @@ type RuntimeProfile = { const consumerProfile: RuntimeProfile = { client: 'consumer', supaUrl: 'http://119.146.131.237:9126', - supaKey: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiYW5vbiIsImlzcyI6InN1cGFiYXNlLTEiLCJpYXQiOjE3Njk2NzY0OTgsImV4cCI6MTkyNzM1NjQ5OH0.ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890', + // 2026/6/8之前用的supaKey: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiYW5vbiIsImlzcyI6InN1cGFiYXNlLTEiLCJpYXQiOjE3Njk2NzY0OTgsImV4cCI6MTkyNzM1NjQ5OH0.ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890', + // 2026/6/8之后用的 + supaKey: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiYW5vbiIsImlzcyI6InN1cGFiYXNlIiwiaWF0IjoxNzgwOTA5MDIyLCJleHAiOjIwOTYyNjkwMjJ9.u2GtsRyUiml1sjIyIWMQQhKJQSPQl4J-RWZdIxNw3wQ', wsUrl: 'ws://192.168.1.61:18000/realtime/v1/websocket', pushServerUrl: 'http://192.168.1.62:7301', homeRedirect: '/pages/main/index', @@ -131,6 +133,7 @@ const deliveryProfile: RuntimeProfile = { pushServerUrl: consumerProfile.pushServerUrl, homeRedirect: '/pages/mall/delivery/home/index', taborPage: '/pages/mall/delivery/home/index', + // 2026-06-09: 改回 true,开发测试阶段允许 mock 回退,避免无档案阻塞登录 testMode: true, } diff --git a/api/delivery.uts b/api/delivery.uts index 2cdd7ebd..48e670b7 100644 --- a/api/delivery.uts +++ b/api/delivery.uts @@ -1,4 +1,5 @@ import type { + DeliveryAbnormalReportType, DeliveryCertificateType, DeliveryCheckinPayloadType, DeliveryDashboardType, @@ -11,10 +12,12 @@ import type { DeliveryLoginResultType, DeliveryMessageType, DeliveryOrderQueryType, + DeliveryOrderStatus, DeliveryOrderType, DeliveryProgressPayloadType, DeliveryRecordType, DeliveryEvidenceRecordType, + DeliveryServiceRecordType, DeliveryServiceItemType } from '@/types/delivery.uts' import supa, { ensureSupabaseReady } from '@/components/supadb/aksupainstance.uts' @@ -235,6 +238,26 @@ function buildDeliveryInfoFromStaff(raw: any): DeliveryInfoType { } as DeliveryInfoType } +async function fetchCareRequestById(requestId: string): Promise { + if (requestId == '') { + return null + } + try { + await ensureSupabaseReady() + const response = await supa.from('ec_service_requests').select('*').eq('id', requestId).limit(1).execute() + if (response.error != null || response.data == null) { + return null + } + const rows = response.data as Array + if (rows.length > 0) { + return rows[0] + } + } catch (error) { + console.warn('[delivery api] 读取 ec_service_requests 失败:', requestId, error) + } + return null +} + async function fetchDeliveryProfileFromRemote(userId: string): Promise { if (userId == '') { return null @@ -271,17 +294,19 @@ function isDelivererRole(userInfo: UserProfile | null): boolean { } function shouldBypassDeliveryRpc(): boolean { - if (IS_TEST_MODE !== true) { - return false - } - const forceRemote = uni.getStorageSync(DELIVERY_FORCE_REMOTE_KEY) - if (forceRemote === true) { - return false - } - if (typeof forceRemote == 'string' && (forceRemote == 'true' || forceRemote == '1')) { - return false - } - return true + // FIX: 强制走真实 RPC,不走 mock fallback + return false + // if (IS_TEST_MODE !== true) { + // return false + // } + // const forceRemote = uni.getStorageSync(DELIVERY_FORCE_REMOTE_KEY) + // if (forceRemote === true) { + // return false + // } + // if (typeof forceRemote == 'string' && (forceRemote == 'true' || forceRemote == '1')) { + // return false + // } + // return true } async function callDeliveryRpc(functionName: string, params: UTSJSONObject): Promise { @@ -293,6 +318,7 @@ async function callDeliveryRpc(functionName: string, params: UTSJSONObject): Pro } try { await ensureSupabaseReady() + console.warn('[delivery api] RPC request:', functionName, JSON.stringify(params)) const res: any = await supa.rpc(functionName, params) if (res?.status === 404) { markMissingDeliveryRpc(functionName) @@ -302,6 +328,7 @@ async function callDeliveryRpc(functionName: string, params: UTSJSONObject): Pro if (res?.error != null) { throw res.error } + console.warn('[delivery api] RPC response:', functionName, JSON.stringify(res.data)) return res.data } catch (error) { console.warn('[delivery api] RPC 调用失败,回退 mock:' + functionName, error) @@ -309,11 +336,460 @@ async function callDeliveryRpc(functionName: string, params: UTSJSONObject): Pro } } +function rpcStr(item: any, key: string): string { + if (item == null) return '' + if (typeof item.getString === 'function') { + const v = item.getString(key) + return v != null ? v : '' + } + const v = item[key] + return v != null && v !== undefined ? String(v) : '' +} + +function rpcNum(item: any, key: string): number { + if (item == null) return 0 + if (typeof item.getNumber === 'function') { + const v = item.getNumber(key) + return v != null ? v : 0 + } + const v = item[key] + if (typeof v === 'number') return v + const parsed = Number(v) + return isNaN(parsed) ? 0 : parsed +} + +function rpcObj(item: any, key: string): any { + if (item == null) return null + if (typeof item.getJSONObj === 'function') { + const v = item.getJSONObj(key) + return v != null ? v : null + } + return item[key] ?? null +} + +function mapRpcOrderItem(item: any): DeliveryOrderType { + const addressObj = rpcObj(item, 'address_snapshot_json') ?? {} + const serviceObj = rpcObj(item, 'service_snapshot_json') ?? {} + + const order = {} as DeliveryOrderType + order.id = rpcStr(item, 'id') + order.orderNo = rpcStr(item, 'order_no') + order.serviceType = rpcStr(serviceObj, 'category') || '居家服务' + order.serviceName = rpcStr(item, 'service_name') + order.serviceCategory = rpcStr(serviceObj, 'category') + order.serviceItems = [] as Array + order.elderId = '' + order.elderName = rpcStr(item, 'recipient_name') + order.elderNameMasked = order.elderName + order.elderGender = '' + order.elderAge = 0 + order.elderPhone = rpcStr(item, 'recipient_phone') + order.elderPhoneMasked = order.elderPhone + order.fullElderName = order.elderName + order.fullPhone = order.elderPhone + order.contactRelation = '家属' + order.contactName = rpcStr(item, 'contact_name') + order.contactPhone = rpcStr(item, 'contact_phone') + order.addressSummary = rpcStr(addressObj, 'fullAddress') || rpcStr(addressObj, 'full_address') + order.address = order.addressSummary + order.addressDetail = rpcStr(addressObj, 'detailAddress') || rpcStr(addressObj, 'detail_address') + order.fullAddress = order.address + ' ' + order.addressDetail + order.latitude = rpcNum(addressObj, 'latitude') + order.longitude = rpcNum(addressObj, 'longitude') + order.appointmentTime = rpcStr(item, 'appointment_time') + order.appointmentStartTime = order.appointmentTime + order.appointmentEndTime = order.appointmentTime + order.duration = 90 + order.estimatedDuration = 90 + order.price = rpcNum(serviceObj, 'price') + order.staffIncome = order.price + order.distance = '' + order.actualStartTime = rpcStr(item, 'service_started_at') + order.actualEndTime = rpcStr(item, 'completed_at') + order.status = rpcStr(item, 'status') as DeliveryOrderStatus + order.statusText = '' + order.statusTone = '' + order.riskTags = [] as Array + order.healthTags = [] as Array + order.careLevel = '' + order.needFamilyPresent = false + order.needMaterials = false + order.remark = rpcStr(item, 'remark') + order.merchantId = '' + order.merchantName = '' + order.deliveryStaffId = rpcStr(item, 'current_staff_id') + order.deliveryStaffName = '' + order.acceptTime = rpcStr(item, 'accepted_at') + order.rejectTime = '' + order.departTime = rpcStr(item, 'departed_at') + order.arriveTime = rpcStr(item, 'arrived_at') + order.checkinTime = rpcStr(item, 'checked_in_at') || rpcStr(item, 'arrived_at') + order.startServiceTime = order.actualStartTime + order.finishTime = rpcStr(item, 'completed_at') || rpcStr(item, 'service_completed_at') + order.cancelReason = rpcStr(item, 'cancel_reason') + order.exceptionType = '' + order.exceptionDesc = '' + order.evidenceList = [] as Array + order.signatureUrl = '' + order.signatureName = '' + order.satisfactionStatus = '待评价' + order.settlementStatus = '待结算' + order.archiveStatus = '未归档' + order.createdAt = rpcStr(item, 'created_at') + order.updatedAt = rpcStr(item, 'updated_at') + order.notices = [] as Array + order.timeline = [] as Array + order.statusLog = [] as Array + order.serviceSummary = '' + order.progressNote = '' + order.distanceKm = '' + order.allowCheckinRadiusMeters = 100 + order.lastLocation = null + order.trackPoints = [] as Array + order.serviceRecord = null + order.abnormalReport = null + + return order +} + +function rpcBoolCompat(item: any, key: string): boolean { + if (item == null) return false + if (typeof item.getBoolean === 'function') { + const v = item.getBoolean(key) + return v != null ? v : false + } + return item[key] === true +} + +function rpcArrayCompat(item: any, key: string): Array { + if (item == null) return [] as Array + const value = item[key] + if (Array.isArray(value)) { + return value + } + return [] as Array +} + +function rpcStrCompat(item: any, keys: Array): string { + for (let i = 0; i < keys.length; i++) { + const value = rpcStr(item, keys[i]) + if (value != '') { + return value + } + } + return '' +} + +function rpcNumCompat(item: any, keys: Array): number { + for (let i = 0; i < keys.length; i++) { + const value = rpcNum(item, keys[i]) + if (value != 0) { + return value + } + } + return 0 +} + +function mapRpcTimelineCompat(list: Array): Array { + const result = [] as Array + for (let i = 0; i < list.length; i++) { + const item = list[i] + result.push({ + id: rpcStrCompat(item, ['id']), + title: rpcStrCompat(item, ['title']), + time: rpcStrCompat(item, ['time', 'createdAt', 'created_at']), + description: rpcStrCompat(item, ['description', 'remark']) + } as DeliveryTimelineItemType) + } + return result +} + +function mapRpcOrderItemCompat(item: any): DeliveryOrderType { + const legacy = mapRpcOrderItem(item) + const addressObj = rpcObj(item, 'address_snapshot_json') ?? rpcObj(item, 'addressSnapshotJson') ?? {} + const serviceObj = rpcObj(item, 'service_snapshot_json') ?? rpcObj(item, 'serviceSnapshotJson') ?? {} + const order = legacy + const addressSummary = rpcStrCompat(item, ['addressSummary', 'address']) != '' ? rpcStrCompat(item, ['addressSummary', 'address']) : rpcStrCompat(addressObj, ['fullAddress', 'full_address', 'address', 'name']) + const addressDetail = rpcStrCompat(item, ['addressDetail']) != '' ? rpcStrCompat(item, ['addressDetail']) : rpcStrCompat(addressObj, ['detailAddress', 'detail_address']) + + order.id = rpcStrCompat(item, ['id']) + order.orderNo = rpcStrCompat(item, ['orderNo', 'order_no', 'task_no']) + order.serviceType = rpcStrCompat(item, ['serviceType']) + if (order.serviceType == '') { + order.serviceType = rpcStrCompat(serviceObj, ['category']) + } + if (order.serviceType == '') { + order.serviceType = '居家服务' + } + order.serviceName = rpcStrCompat(item, ['serviceName', 'service_name']) + if (order.serviceName == '') { + order.serviceName = rpcStrCompat(serviceObj, ['name', 'serviceName']) + } + if (order.serviceName == '') { + order.serviceName = rpcStrCompat(item, ['serviceCategory']) != '' ? rpcStrCompat(item, ['serviceCategory']) : '居家服务订单' + } + order.serviceCategory = rpcStrCompat(item, ['serviceCategory']) + if (order.serviceCategory == '') { + order.serviceCategory = rpcStrCompat(serviceObj, ['category']) + } + order.serviceItems = rpcArrayCompat(item, 'serviceItems') as Array + order.elderId = rpcStrCompat(item, ['elderId', 'elder_id', 'user_id']) + order.elderName = rpcStrCompat(item, ['elderName', 'recipient_name', 'elder_name']) + if (order.elderName == '') { + order.elderName = rpcStrCompat(item, ['contactName', 'contact_name']) != '' ? rpcStrCompat(item, ['contactName', 'contact_name']) : '服务对象待补充' + } + order.elderNameMasked = order.elderName + order.elderGender = rpcStrCompat(item, ['elderGender']) + order.elderAge = rpcNumCompat(item, ['elderAge']) + order.elderPhone = rpcStrCompat(item, ['elderPhone', 'recipient_phone', 'elder_phone']) + order.elderPhoneMasked = order.elderPhone + order.fullElderName = order.elderName + order.fullPhone = order.elderPhone + order.contactRelation = rpcStrCompat(item, ['contactRelation']) + if (order.contactRelation == '') { + order.contactRelation = '家属' + } + order.contactName = rpcStrCompat(item, ['contactName', 'contact_name']) + order.contactPhone = rpcStrCompat(item, ['contactPhone', 'contact_phone']) + order.addressSummary = addressSummary + order.address = rpcStrCompat(item, ['address']) != '' ? rpcStrCompat(item, ['address']) : addressSummary + if (order.address == '') { + order.address = rpcStrCompat(addressObj, ['fullAddress', 'full_address', 'address', 'name']) + } + if (order.address == '') { + order.address = '地址待补充' + } + order.addressDetail = addressDetail + order.fullAddress = rpcStrCompat(item, ['fullAddress']) + if (order.fullAddress == '') { + order.fullAddress = order.address + } + if (order.addressDetail != '' && order.fullAddress.indexOf(order.addressDetail) < 0) { + order.fullAddress = order.fullAddress + ' ' + order.addressDetail + } + order.latitude = rpcNumCompat(item, ['latitude']) + if (order.latitude == 0) { + order.latitude = rpcNumCompat(addressObj, ['latitude']) + } + order.longitude = rpcNumCompat(item, ['longitude']) + if (order.longitude == 0) { + order.longitude = rpcNumCompat(addressObj, ['longitude']) + } + order.appointmentTime = rpcStrCompat(item, ['appointmentTime', 'appointment_time', 'scheduled_at']) + if (order.appointmentTime == '') { + order.appointmentTime = rpcStrCompat(item, ['createdAt', 'created_at']) + } + if (order.appointmentTime == '') { + order.appointmentTime = '时间待补充' + } + order.appointmentStartTime = rpcStrCompat(item, ['appointmentStartTime']) + if (order.appointmentStartTime == '') { + order.appointmentStartTime = order.appointmentTime + } + order.appointmentEndTime = rpcStrCompat(item, ['appointmentEndTime']) + if (order.appointmentEndTime == '') { + order.appointmentEndTime = order.appointmentTime + } + order.duration = rpcNumCompat(item, ['duration', 'duration_minutes']) + if (order.duration == 0) { + order.duration = 90 + } + order.estimatedDuration = rpcNumCompat(item, ['estimatedDuration', 'duration', 'duration_minutes']) + if (order.estimatedDuration == 0) { + order.estimatedDuration = order.duration + } + order.price = rpcNumCompat(item, ['price']) + if (order.price == 0) { + order.price = rpcNumCompat(serviceObj, ['price']) + } + order.staffIncome = rpcNumCompat(item, ['staffIncome']) + if (order.staffIncome == 0) { + order.staffIncome = rpcNumCompat(item, ['price']) + } + if (order.staffIncome == 0) { + order.staffIncome = rpcNumCompat(serviceObj, ['price']) + } + if (order.staffIncome == 0) { + order.staffIncome = 0 + } + order.distance = rpcStrCompat(item, ['distance']) + order.actualStartTime = rpcStrCompat(item, ['actualStartTime', 'service_started_at']) + order.actualEndTime = rpcStrCompat(item, ['actualEndTime', 'completed_at', 'service_completed_at']) + order.status = rpcStrCompat(item, ['status']) as DeliveryOrderStatus + order.statusText = rpcStrCompat(item, ['statusText']) + order.statusTone = rpcStrCompat(item, ['statusTone']) + order.riskTags = rpcArrayCompat(item, 'riskTags') as Array + order.healthTags = rpcArrayCompat(item, 'healthTags') as Array + order.careLevel = rpcStrCompat(item, ['careLevel']) + order.needFamilyPresent = rpcBoolCompat(item, 'needFamilyPresent') + order.needMaterials = rpcBoolCompat(item, 'needMaterials') + order.remark = rpcStrCompat(item, ['remark']) + order.merchantId = rpcStrCompat(item, ['merchantId', 'merchant_id']) + order.merchantName = rpcStrCompat(item, ['merchantName', 'merchant_name']) + order.deliveryStaffId = rpcStrCompat(item, ['deliveryStaffId', 'current_staff_id', 'assigned_to']) + order.deliveryStaffName = rpcStrCompat(item, ['deliveryStaffName', 'delivery_staff_name']) + order.acceptTime = rpcStrCompat(item, ['acceptTime', 'accepted_at']) + order.departTime = rpcStrCompat(item, ['departTime', 'departed_at']) + order.arriveTime = rpcStrCompat(item, ['arriveTime', 'arrived_at']) + order.checkinTime = rpcStrCompat(item, ['checkinTime', 'checked_in_at', 'arrived_at']) + order.startServiceTime = rpcStrCompat(item, ['startServiceTime', 'service_started_at']) + order.finishTime = rpcStrCompat(item, ['finishTime', 'completed_at', 'service_completed_at']) + order.cancelReason = rpcStrCompat(item, ['cancelReason', 'cancel_reason']) + order.exceptionType = rpcStrCompat(item, ['exceptionType']) + order.exceptionDesc = rpcStrCompat(item, ['exceptionDesc']) + order.evidenceList = rpcArrayCompat(item, 'evidenceList') as Array + order.signatureUrl = rpcStrCompat(item, ['signatureUrl']) + order.signatureName = rpcStrCompat(item, ['signatureName']) + order.satisfactionStatus = rpcStrCompat(item, ['satisfactionStatus']) + order.settlementStatus = rpcStrCompat(item, ['settlementStatus']) + order.archiveStatus = rpcStrCompat(item, ['archiveStatus']) + order.createdAt = rpcStrCompat(item, ['createdAt', 'created_at']) + order.updatedAt = rpcStrCompat(item, ['updatedAt', 'updated_at']) + order.notices = rpcArrayCompat(item, 'notices') as Array + order.timeline = mapRpcTimelineCompat(rpcArrayCompat(item, 'timeline')) + order.statusLog = rpcArrayCompat(item, 'statusLog') as Array + order.serviceSummary = rpcStrCompat(item, ['serviceSummary']) + order.progressNote = rpcStrCompat(item, ['progressNote']) + order.distanceKm = rpcStrCompat(item, ['distanceKm']) + order.allowCheckinRadiusMeters = rpcNumCompat(item, ['allowCheckinRadiusMeters']) + if (order.allowCheckinRadiusMeters == 0) { + order.allowCheckinRadiusMeters = 100 + } + order.lastLocation = rpcObj(item, 'lastLocation') as DeliveryLocationType | null + order.trackPoints = rpcArrayCompat(item, 'trackPoints') as Array + order.serviceRecord = rpcObj(item, 'serviceRecord') as DeliveryServiceRecordType | null + order.abnormalReport = rpcObj(item, 'abnormalReport') as DeliveryAbnormalReportType | null + console.warn('[delivery api] mapped order:', JSON.stringify({ + id: order.id, + orderNo: order.orderNo, + serviceName: order.serviceName, + elderName: order.elderName, + contactName: order.contactName, + address: order.address, + addressDetail: order.addressDetail, + appointmentTime: order.appointmentTime, + price: order.price, + staffIncome: order.staffIncome, + status: order.status, + statusText: order.statusText, + requestId: rpcStrCompat(item, ['request_id', 'requestId']) + })) + return order +} + +function needsRequestFallback(order: DeliveryOrderType): boolean { + return order.serviceName == '' || order.elderName == '' || order.address == '' || order.contactName == '' +} + +// 用于限制 "missing request_id" 日志输出的计数器 +let _missingRequestIdLogCount = 0 + +function fillOrderFromRequestSnapshot(order: DeliveryOrderType, requestItem: any): DeliveryOrderType { + const addressObj = readObjectField(requestItem, 'address_snapshot_json') ?? readObjectField(requestItem, 'address_snapshot') ?? null + const addressFull = addressObj != null ? (readStringField(addressObj, 'fullAddress') != '' ? readStringField(addressObj, 'fullAddress') : readStringField(addressObj, 'full_address')) : '' + const addressDetail = addressObj != null ? (readStringField(addressObj, 'detailAddress') != '' ? readStringField(addressObj, 'detailAddress') : readStringField(addressObj, 'detail_address')) : '' + if (order.serviceName == '') { + order.serviceName = readStringField(requestItem, 'service_name') + } + if (order.serviceCategory == '') { + order.serviceCategory = readStringField(requestItem, 'service_category') + } + if (order.serviceType == '' || order.serviceType == '居家服务') { + const requestCategory = readStringField(requestItem, 'service_category') + if (requestCategory != '') { + order.serviceType = requestCategory + } + } + if (order.elderName == '') { + order.elderName = readStringField(requestItem, 'elder_name') + order.elderNameMasked = order.elderName + order.fullElderName = order.elderName + } + if (order.elderPhone == '') { + order.elderPhone = readStringField(requestItem, 'elder_phone') + order.elderPhoneMasked = order.elderPhone + order.fullPhone = order.elderPhone + } + if (order.contactName == '') { + order.contactName = readStringField(requestItem, 'contact_name') + } + if (order.contactPhone == '') { + order.contactPhone = readStringField(requestItem, 'contact_phone') + } + if (order.address == '') { + order.address = addressFull + order.addressSummary = addressFull + } + if (order.addressDetail == '') { + order.addressDetail = addressDetail + } + if (order.fullAddress == '' || order.fullAddress == order.address) { + order.fullAddress = order.address + if (order.addressDetail != '' && order.fullAddress.indexOf(order.addressDetail) < 0) { + order.fullAddress = order.fullAddress + ' ' + order.addressDetail + } + } + if (order.latitude == 0 && addressObj != null) { + const latitude = readNumberField(addressObj, 'latitude') + order.latitude = latitude != null ? latitude : 0 + } + if (order.longitude == 0 && addressObj != null) { + const longitude = readNumberField(addressObj, 'longitude') + order.longitude = longitude != null ? longitude : 0 + } + if (order.appointmentTime == '') { + order.appointmentTime = readStringField(requestItem, 'scheduled_at') + order.appointmentStartTime = order.appointmentTime + order.appointmentEndTime = order.appointmentTime + } + if (order.remark == '') { + order.remark = readStringField(requestItem, 'remark') + } + return order +} + +async function enrichOrderWithRequestFallback(rawItem: any, order: DeliveryOrderType): Promise { + if (!needsRequestFallback(order)) { + return order + } + const requestId = rpcStrCompat(rawItem, ['request_id', 'requestId']) + if (requestId == '') { + // 限制日志输出:只输出前3个缺少 request_id 的订单 + _missingRequestIdLogCount = _missingRequestIdLogCount + 1 + if (_missingRequestIdLogCount <= 3) { + console.warn('[delivery api] request fallback skipped: missing request_id for order', order.id, '(count:', _missingRequestIdLogCount, ')') + } + return order + } + const requestItem = await fetchCareRequestById(requestId) + if (requestItem == null) { + console.warn('[delivery api] request fallback miss:', order.id, requestId) + return order + } + console.warn('[delivery api] order snapshot missing, fallback to ec_service_requests:', order.id, requestId) + const nextOrder = fillOrderFromRequestSnapshot(order, requestItem) + console.warn('[delivery api] request fallback merged:', JSON.stringify({ + id: nextOrder.id, + requestId, + serviceName: nextOrder.serviceName, + elderName: nextOrder.elderName, + contactName: nextOrder.contactName, + address: nextOrder.address, + addressDetail: nextOrder.addressDetail, + appointmentTime: nextOrder.appointmentTime + })) + return nextOrder +} + function normalizeRpcOrderList(data: any): Array | null { if (!Array.isArray(data)) { return null } - return data as Array + const result = [] as Array + for (let i = 0; i < data.length; i++) { + result.push(mapRpcOrderItemCompat(data[i])) + } + return result } function normalizeRpcMessages(data: any): Array | null { @@ -341,7 +817,7 @@ function normalizeRpcOrder(data: any): DeliveryOrderType | null { if (data == null) { return null } - return data as DeliveryOrderType + return mapRpcOrderItemCompat(data) } function normalizeRpcDashboard(data: any): DeliveryDashboardType | null { @@ -537,6 +1013,8 @@ function buildMockOrders(profile: DeliveryInfoType): Array { serviceItems: buildMockServiceItems('mock-order-001', '基础上门护理'), elderId: 'elder-001', elderNameMasked: '李奶奶', + elderGender: '女', + elderAge: 78, elderPhoneMasked: '138****1024', fullElderName: '李秀珍', fullPhone: '13800131024', @@ -593,6 +1071,8 @@ function buildMockOrders(profile: DeliveryInfoType): Array { serviceItems: buildMockServiceItems('mock-order-002', '康复训练'), elderId: 'elder-002', elderNameMasked: '张爷爷', + elderGender: '男', + elderAge: 82, elderPhoneMasked: '137****2233', fullElderName: '张志坤', fullPhone: '13700132233', @@ -649,6 +1129,8 @@ function buildMockOrders(profile: DeliveryInfoType): Array { serviceItems: buildMockServiceItems('mock-order-003', '慢病随访'), elderId: 'elder-003', elderNameMasked: '黄阿姨', + elderGender: '女', + elderAge: 68, elderPhoneMasked: '135****5566', fullElderName: '黄玉英', fullPhone: '13500135566', @@ -738,6 +1220,8 @@ function buildMockOrders(profile: DeliveryInfoType): Array { ] as Array, elderId: 'elder-004', elderNameMasked: '陈伯伯', + elderGender: '男', + elderAge: 75, elderPhoneMasked: '139****3301', fullElderName: '陈国辉', fullPhone: '13900133301', @@ -812,6 +1296,8 @@ function buildMockOrders(profile: DeliveryInfoType): Array { serviceItems: buildMockServiceItems('mock-order-005', '异常处理'), elderId: 'elder-005', elderNameMasked: '王阿婆', + elderGender: '女', + elderAge: 80, elderPhoneMasked: '134****7744', fullElderName: '王月兰', fullPhone: '13400137744', @@ -892,6 +1378,8 @@ function buildMockOrders(profile: DeliveryInfoType): Array { ] as Array, elderId: 'elder-006', elderNameMasked: '刘叔叔', + elderGender: '男', + elderAge: 70, elderPhoneMasked: '133****6655', fullElderName: '刘建华', fullPhone: '13300136655', @@ -1524,19 +2012,27 @@ export async function loginDelivery(payload: DeliveryLoginPayloadType): Promise< throw new Error('当前账号不是上门服务人员账号') } - const deliveryInfo = await fetchDeliveryProfileFromRemote(profile.id) + let deliveryInfo = await fetchDeliveryProfileFromRemote(profile.id) + let usesMock = false if (deliveryInfo == null) { - try { - await supa.signOut() - } catch (error) {} - throw new Error('当前账号未绑定服务人员档案') + if (IS_TEST_MODE) { + // 测试环境下无档案时自动回退 mock 档案,避免阻塞登录调试 + deliveryInfo = ensureMockProfile(profile.id) + usesMock = true + console.log('[loginDelivery] 未找到远程服务人员档案,已回退 mock 档案,userId:', profile.id) + } else { + try { + await supa.signOut() + } catch (error) {} + throw new Error('当前账号未绑定服务人员档案') + } } return { token: result.access_token, userInfo: profile, deliveryInfo, - usesMock: false + usesMock } } @@ -1567,6 +2063,9 @@ export async function getDeliveryOrdersByStaffId(staffId: string, query: Deliver } as UTSJSONObject) const orders = normalizeRpcOrderList(rpcData) if (orders != null) { + for (let i = 0; i < orders.length; i++) { + orders[i] = await enrichOrderWithRequestFallback(rpcData[i], orders[i]) + } return orders } return await fallbackGetOrders(staffId, query) @@ -1578,7 +2077,7 @@ export async function getDeliveryOrderDetailById(orderId: string): Promise} 登录结果 + */ +async function login(email, password) { + try { + const response = await axios.post(`${API_BASE}/auth/email-login`, { + email, + password + }); + + const { token, user } = response.data.data; + + // 保存 Token 到本地存储 + localStorage.setItem('auth_token', token); + localStorage.setItem('user_info', JSON.stringify(user)); + + // 设置 axios 默认 Header + axios.defaults.headers.common['Authorization'] = `Bearer ${token}`; + + return { success: true, user }; + } catch (error) { + if (error.response) { + console.error('登录失败:', error.response.data.msg); + return { success: false, message: error.response.data.msg }; + } + return { success: false, message: '网络错误' }; + } +} + +// 使用示例 +const result = await login('dispatcher@example.com', 'password123'); +if (result.success) { + console.log('登录成功,用户信息:', result.user); +} +``` + +### 1.6 测试账号 + +| 角色 | 邮箱 | 密码 | 说明 | +|------|------|------|------| +| 派单员 | `dispatcher@example.com` | `password123` | 具有派单权限 | +| 居家服务员 | `worker@example.com` | `password123` | 具有接单/签到权限 | + +--- + +## 二、距离预校验接口 + +### 2.1 接口概述 + +居家服务员到达服务地点前,调用此接口校验当前位置与服务地点的距离是否在允许范围内。 + +### 2.2 接口信息 + +``` +POST /homecare/checkin/precheck +Content-Type: application/json +Authorization: Bearer {token} +``` + +### 2.3 请求参数 + +| 参数名 | 类型 | 必填 | 说明 | +|--------|------|------|------| +| workOrderId | string | 是 | 工单 ID(UUID) | +| latitude | number | 是 | 当前纬度(GCJ02 坐标系) | +| longitude | number | 是 | 当前经度(GCJ02 坐标系) | +| coordinateType | string | 否 | 坐标系类型,默认 `gcj02` | +| accuracy | number | 否 | 定位精度(米) | +| reportedAt | string | 否 | 报告时间(ISO 8601 格式) | + +**请求示例:** + +```json +{ + "workOrderId": "a6450755-add4-4bdf-839f-165459ddff5d", + "latitude": 39.9042, + "longitude": 116.4074, + "coordinateType": "gcj02", + "accuracy": 10, + "reportedAt": "2026-06-09T10:30:00.000Z" +} +``` + +### 2.4 响应信息 + +#### ✅ 校验通过(200) + +```json +{ + "code": "OK", + "msg": "success", + "data": { + "distanceMeters": 15.5, + "allowedRadiusMeters": 50, + "canCheckin": true, + "reasonCode": "OK", + "workerLocationAccepted": true, + "serviceLocationReady": true + }, + "traceId": "abc123-def456-ghi789" +} +``` + +**字段说明:** + +| 字段 | 类型 | 说明 | +|------|------|------| +| distanceMeters | number | 当前位置与服务地点的距离(米) | +| allowedRadiusMeters | number | 允许的签到半径(默认 50 米) | +| canCheckin | boolean | 是否允许签到 | +| reasonCode | string | 原因代码:`OK`(通过)/ `OUT_OF_RADIUS`(超出范围)/ `SERVICE_LOCATION_MISSING`(服务地点缺失) | +| workerLocationAccepted | boolean | 人员位置是否已记录 | +| serviceLocationReady | boolean | 服务地点是否已配置 | + +#### ❌ 校验失败 + +**超出签到半径(400)** + +```json +{ + "code": "BAD_REQUEST", + "msg": "Checkin rejected", + "data": { + "distanceMeters": 150.0, + "allowedRadiusMeters": 50, + "canCheckin": false, + "reasonCode": "OUT_OF_RADIUS", + "workerLocationAccepted": true, + "serviceLocationReady": true + }, + "traceId": "abc123-def456-ghi789" +} +``` + +**服务地点未配置(400)** + +```json +{ + "code": "BAD_REQUEST", + "msg": "Checkin rejected", + "data": { + "distanceMeters": null, + "allowedRadiusMeters": 0, + "canCheckin": false, + "reasonCode": "SERVICE_LOCATION_MISSING", + "workerLocationAccepted": false, + "serviceLocationReady": false + }, + "traceId": "abc123-def456-ghi789" +} +``` + +**工单未分配(400)** + +```json +{ + "code": "BAD_REQUEST", + "msg": "Checkin rejected", + "data": { + "distanceMeters": null, + "allowedRadiusMeters": 0, + "canCheckin": false, + "reasonCode": "WORK_ORDER_NOT_ASSIGNABLE", + "workerLocationAccepted": false, + "serviceLocationReady": false + }, + "traceId": "abc123-def456-ghi789" +} +``` + +### 2.5 前端调用示例 + +```javascript +/** + * 签到距离预校验 + * @param {string} workOrderId - 工单 ID + * @param {number} latitude - 纬度 + * @param {number} longitude - 经度 + * @param {Object} options - 可选参数 + * @returns {Promise} 校验结果 + */ +async function checkinPrecheck(workOrderId, latitude, longitude, options = {}) { + const token = localStorage.getItem('auth_token'); + + if (!token) { + return { success: false, message: '请先登录' }; + } + + try { + const response = await axios.post( + `${API_BASE}/homecare/checkin/precheck`, + { + workOrderId, + latitude, + longitude, + coordinateType: options.coordinateType || 'gcj02', + accuracy: options.accuracy, + reportedAt: options.reportedAt || new Date().toISOString() + }, + { + headers: { + 'Authorization': `Bearer ${token}` + } + } + ); + + const { data } = response.data; + + if (data.canCheckin) { + console.log(`✅ 签到预校验通过`); + console.log(` 距离: ${data.distanceMeters} 米`); + console.log(` 允许半径: ${data.allowedRadiusMeters} 米`); + return { success: true, ...data }; + } else { + console.log(`❌ 签到预校验失败: ${data.reasonCode}`); + console.log(` 距离: ${data.distanceMeters} 米`); + console.log(` 允许半径: ${data.allowedRadiusMeters} 米`); + return { success: false, reasonCode: data.reasonCode, ...data }; + } + } catch (error) { + if (error.response) { + console.error('预校验请求失败:', error.response.data.msg); + return { success: false, message: error.response.data.msg }; + } + return { success: false, message: '网络错误' }; + } +} + +// 使用示例 1: 范围内签到 +const result1 = await checkinPrecheck( + 'a6450755-add4-4bdf-839f-165459ddff5d', + 39.9042, + 116.4074 +); +if (result1.success) { + // 显示"可以签到"按钮 +} + +// 使用示例 2: 超出范围签到 +const result2 = await checkinPrecheck( + 'a6450755-add4-4bdf-839f-165459ddff5d', + 39.9142, // 距离约 1.1 公里 + 116.4174 +); +if (!result2.success && result2.reasonCode === 'OUT_OF_RADIUS') { + // 显示"您超出签到范围"提示 +} +``` + +### 2.6 距离计算原理 + +后端使用 **Haversine 公式** 计算两点之间的球面距离: + +```javascript +function calculateDistance(lat1, lon1, lat2, lon2) { + const R = 6371000; // 地球半径(米) + const dLat = toRad(lat2 - lat1); + const dLon = toRad(lon2 - lon1); + const a = + Math.sin(dLat / 2) * Math.sin(dLat / 2) + + Math.cos(toRad(lat1)) * Math.cos(toRad(lat2)) * + Math.sin(dLon / 2) * Math.sin(dLon / 2); + const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); + return R * c; +} + +function toRad(degrees) { + return degrees * Math.PI / 180; +} +``` + +### 2.7 签到半径配置 + +签到半径通过 `sys_sla_config` 表配置,优先级如下: + +1. **工单级别** - `scope_type = 'WORK_ORDER'` +2. **组织级别** - `scope_type = 'ORG'` +3. **团队级别** - `scope_type = 'TEAM'` +4. **全局默认** - `scope_type = 'GLOBAL'`(默认 50 米) + +**查询配置 SQL:** + +```sql +SELECT config_value::numeric as radius_meters +FROM public.sys_sla_config +WHERE config_key = 'HOMECARE_CHECKIN_RADIUS_METERS' + AND scope_type = 'GLOBAL' + AND is_active = true +LIMIT 1; +``` + +--- + +## 三、完整签到流程 + +``` +┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ +│ 用户登录 │ ──► │ 管理员派单 │ ──► │ 居家服务员接单 │ ──► │ 签到预校验 │ +│ (邮箱登录) │ │ (派单接口) │ │ (接单接口) │ │ (距离预校验) │ +└─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘ + │ │ + │ ▼ + │ ┌─────────────┐ + │ │ 签到提交 │ + │ │ (证据文件) │ + │ └─────────────┘ + ▼ │ +┌─────────────┐ │ +│ 获取用户信息 │ │ +└─────────────┘ ▼ + ┌─────────────┐ + │ 数据库更新 │ + │ - ec_care_ │ + │ tasks │ + │ - hc_work_ │ + │ order_ │ + │ events │ + └─────────────┘ +``` + +--- + +## 四、错误码对照表 + +| 错误码 | HTTP 状态码 | 说明 | 处理建议 | +|--------|------------|------|---------| +| `OK` | 200 | 操作成功 | - | +| `UNAUTHORIZED` | 401 | 邮箱或密码错误 | 提示用户检查账号密码 | +| `NOT_FOUND` | 404 | 用户不存在 | 提示用户注册 | +| `OUT_OF_RADIUS` | 400 | 超出签到半径 | 提示用户靠近服务地点 | +| `SERVICE_LOCATION_MISSING` | 400 | 服务地点未配置 | 联系管理员配置服务地点 | +| `WORK_ORDER_NOT_ASSIGNABLE` | 400 | 工单未分配 | 等待管理员派单 | +| `WORKER_NOT_MATCHED` | 400 | 人员不匹配 | 当前用户非指定服务人员 | +| `SIGNATURE_REQUIRED` | 400 | 缺少签名 | 签到提交时需要电子签名 | +| `EVIDENCE_FILE_NOT_EXIST` | 400 | 证据文件不存在 | 签到提交时需要上传照片 | + +--- + +## 五、联调测试清单 + +### 5.1 登录接口测试 + +- [ ] 使用正确账号密码登录成功 +- [ ] 使用错误密码登录失败 +- [ ] 使用不存在的邮箱登录失败 +- [ ] 验证返回的 Token 格式正确 +- [ ] 验证 Token 可正常用于后续请求 + +### 5.2 距离预校验测试 + +- [ ] 范围内签到(0 米)- 应该通过 +- [ ] 范围内签到(50 米)- 应该通过 +- [ ] 超出范围签到(100 米)- 应该拒绝 +- [ ] 超出范围签到(1 公里)- 应该拒绝 +- [ ] 服务地点未配置 - 应该拒绝 +- [ ] 工单未分配 - 应该拒绝 +- [ ] 人员不匹配 - 应该拒绝 + +### 5.3 数据库验证 + +预校验接口调用后,验证以下数据写入: + +```sql +-- 1. 检查 hc_worker_locations 表(预校验会插入位置记录) +SELECT * FROM public.hc_worker_locations +WHERE work_order_id = 'a6450755-add4-4bdf-839f-165459ddff5d' +ORDER BY created_at DESC +LIMIT 5; + +-- 2. 检查 hc_dispatch_assignments 表(派单记录) +SELECT * FROM public.hc_dispatch_assignments +WHERE work_order_id = 'a6450755-add4-4bdf-839f-165459ddff5d' +ORDER BY created_at DESC +LIMIT 5; +``` + +--- + +## 六、常见问题 + +### Q1: 为什么预校验通过但签到提交失败? + +**A:** 签到提交需要额外的证据文件(照片)和电子签名。预校验只检查距离,签到提交会验证: +- 至少 1 个有效的证据文件 +- 有效的电子签名(至少 8 个字符) +- 证据文件与工单匹配 + +### Q2: 距离计算为什么有误差? + +**A:** 距离计算使用 Haversine 公式,基于 GCJ02 坐标系。实际误差来源: +- 手机 GPS 定位精度(通常 5-20 米) +- 坐标系转换误差 +- 地球曲率近似计算 + +### Q3: 如何修改签到半径? + +**A:** 修改 `sys_sla_config` 表中的配置: + +```sql +UPDATE public.sys_sla_config +SET config_value = '100' -- 改为 100 米 +WHERE config_key = 'HOMECARE_CHECKIN_RADIUS_METERS' + AND scope_type = 'GLOBAL' + AND is_active = true; +``` + +--- + +## 七、相关文档 + +- [居家服务 RPC 迁移文档](./居家服务方案/居家sql/新/rpc/README.md) +- [迁移脚本说明](./居家服务方案/居家sql/新/20260605_homecare_migration_fixed.sql) +- [SLA 配置说明](./居家服务方案/居家sql/新/rpc/README.md) + +--- + +**文档维护:** 如有接口变更,请及时更新此文档。 diff --git a/mall_sql/migrations/20260609_backfill_ec_care_tasks_core_fields.sql b/mall_sql/migrations/20260609_backfill_ec_care_tasks_core_fields.sql new file mode 100644 index 00000000..4c8974e8 --- /dev/null +++ b/mall_sql/migrations/20260609_backfill_ec_care_tasks_core_fields.sql @@ -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; \ No newline at end of file diff --git a/mall_sql/migrations/20260609_exclude_invalid_ec_care_tasks_from_delivery_rpc.sql b/mall_sql/migrations/20260609_exclude_invalid_ec_care_tasks_from_delivery_rpc.sql new file mode 100644 index 00000000..c4e6eb7f --- /dev/null +++ b/mall_sql/migrations/20260609_exclude_invalid_ec_care_tasks_from_delivery_rpc.sql @@ -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; diff --git a/mall_sql/migrations/20260609_fix_delivery_rpc_request_snapshot.sql b/mall_sql/migrations/20260609_fix_delivery_rpc_request_snapshot.sql new file mode 100644 index 00000000..3a7cd0f3 --- /dev/null +++ b/mall_sql/migrations/20260609_fix_delivery_rpc_request_snapshot.sql @@ -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; diff --git a/mall_sql/migrations/20260609_restore_delivery_get_legacy_order_json.sql b/mall_sql/migrations/20260609_restore_delivery_get_legacy_order_json.sql new file mode 100644 index 00000000..ae70e421 --- /dev/null +++ b/mall_sql/migrations/20260609_restore_delivery_get_legacy_order_json.sql @@ -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; diff --git a/mall_sql/migrations/20260609_rollback_exclude_invalid_ec_care_tasks.sql b/mall_sql/migrations/20260609_rollback_exclude_invalid_ec_care_tasks.sql new file mode 100644 index 00000000..c8d9f2f0 --- /dev/null +++ b/mall_sql/migrations/20260609_rollback_exclude_invalid_ec_care_tasks.sql @@ -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; diff --git a/mall_sql/migrations/20260610_backfill_pending_orders_core_fields.sql b/mall_sql/migrations/20260610_backfill_pending_orders_core_fields.sql new file mode 100644 index 00000000..b4c47c28 --- /dev/null +++ b/mall_sql/migrations/20260610_backfill_pending_orders_core_fields.sql @@ -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; \ No newline at end of file diff --git a/mall_sql/migrations/20260610_fix_delivery_pending_blank.sql b/mall_sql/migrations/20260610_fix_delivery_pending_blank.sql new file mode 100644 index 00000000..0a529eb7 --- /dev/null +++ b/mall_sql/migrations/20260610_fix_delivery_pending_blank.sql @@ -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_list:pending 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; diff --git a/mall_sql/scripts/create_delivery_staff_final.sql b/mall_sql/scripts/create_delivery_staff_final.sql new file mode 100644 index 00000000..cefb3b25 --- /dev/null +++ b/mall_sql/scripts/create_delivery_staff_final.sql @@ -0,0 +1,143 @@ +-- =================================================================== +-- 用途:为已存在的 auth 用户创建 ak_users + ml_delivery_staff 档案 +-- 特点:自动探测 ak_users 表结构(有 auth_id 则插 auth_id,无则插 id) +-- =================================================================== + +DO $$ +DECLARE + v_auth_id UUID; + v_ak_user_id UUID; + v_has_auth_id BOOLEAN := FALSE; + v_has_phone BOOLEAN := FALSE; + v_has_avatar_url BOOLEAN := FALSE; + v_has_status BOOLEAN := FALSE; + v_has_registration_source BOOLEAN := FALSE; + v_has_v2 BOOLEAN := FALSE; + v_email TEXT := 'homecare_worker@test.com'; + v_nickname TEXT := '居家服务员'; + v_phone TEXT := '13800138000'; +BEGIN + -- 1. 获取 auth.users.id + SELECT id INTO v_auth_id FROM auth.users WHERE email = v_email LIMIT 1; + IF v_auth_id IS NULL THEN + RAISE EXCEPTION 'auth.users 中不存在 %', v_email; + END IF; + + -- 2. 探测 ak_users 表结构 + SELECT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = 'public' AND table_name = 'ak_users' AND column_name = 'auth_id') + INTO v_has_auth_id; + SELECT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = 'public' AND table_name = 'ak_users' AND column_name = 'phone') + INTO v_has_phone; + SELECT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = 'public' AND table_name = 'ak_users' AND column_name = 'avatar_url') + INTO v_has_avatar_url; + SELECT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = 'public' AND table_name = 'ak_users' AND column_name = 'status') + INTO v_has_status; + SELECT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = 'public' AND table_name = 'ak_users' AND column_name = 'registration_source') + INTO v_has_registration_source; + + RAISE NOTICE 'ak_users 结构探测: auth_id=%, phone=%, avatar_url=%, status=%, registration_source=%', + v_has_auth_id, v_has_phone, v_has_avatar_url, v_has_status, v_has_registration_source; + + -- 3. 插入 ak_users(动态字段) + IF v_has_auth_id THEN + -- 有 auth_id 字段:id 用 gen_random_uuid,auth_id 关联 auth.users.id + INSERT INTO public.ak_users ( + id, auth_id, username, email, role, + phone, avatar_url, status, registration_source, + created_at, updated_at + ) VALUES ( + gen_random_uuid(), v_auth_id, v_nickname, v_email, 'delivery', + v_phone, 'https://api.dicebear.com/7.x/avataaars/svg?seed=homecare', 'active', 'web', + NOW(), NOW() + ) + ON CONFLICT (auth_id) DO UPDATE SET + username = EXCLUDED.username, + email = EXCLUDED.email, + role = EXCLUDED.role, + phone = EXCLUDED.phone, + avatar_url = EXCLUDED.avatar_url, + status = EXCLUDED.status, + updated_at = NOW() + RETURNING id INTO v_ak_user_id; + + RAISE NOTICE '✓ ak_users 已插入(含 auth_id),业务主键 id: %', v_ak_user_id; + ELSE + -- 无 auth_id 字段:id 直接等于 auth.users.id + INSERT INTO public.ak_users ( + id, username, email, role, + created_at, updated_at + ) VALUES ( + v_auth_id, v_nickname, v_email, 'delivery', + NOW(), NOW() + ) + ON CONFLICT (id) DO UPDATE SET + username = EXCLUDED.username, + email = EXCLUDED.email, + role = EXCLUDED.role, + updated_at = NOW() + RETURNING id INTO v_ak_user_id; + + RAISE NOTICE '✓ ak_users 已插入(id = auth_id),id: %', v_ak_user_id; + END IF; + + -- 4. 探测 ml_delivery_staff v2 字段 + SELECT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = 'public' AND table_name = 'ml_delivery_staff' AND column_name = 'online_status') + INTO v_has_v2; + + -- 5. 插入 ml_delivery_staff(uid 必须等于 ak_users.id / ak_users 业务主键) + IF v_has_v2 THEN + INSERT INTO public.ml_delivery_staff ( + id, uid, nickname, avatar, phone, + status, is_active, + staff_no, online_status, certificate_status, + certificate_expire_at, service_area, skills, + created_at, updated_at + ) VALUES ( + gen_random_uuid(), + v_ak_user_id, -- 关键:必须是 ak_users 的业务主键 id + v_nickname, + 'https://api.dicebear.com/7.x/avataaars/svg?seed=homecare', + v_phone, + 1, TRUE, + 'HC' || EXTRACT(YEAR FROM NOW()) || LPAD(FLOOR(RANDOM() * 10000)::TEXT, 4, '0'), + 'resting', 'valid', + (NOW() + INTERVAL '1 year')::DATE, + '北京市朝阳区', + '["基础护理", "康复训练", "上门照护"]'::jsonb, + NOW(), NOW() + ) + ON CONFLICT (uid) WHERE deleted_at IS NULL DO UPDATE SET + nickname = EXCLUDED.nickname, + status = EXCLUDED.status, + is_active = EXCLUDED.is_active, + updated_at = NOW(); + RAISE NOTICE '✓ ml_delivery_staff (v2) 已插入/更新'; + ELSE + INSERT INTO public.ml_delivery_staff ( + id, uid, nickname, avatar, phone, + status, is_active, created_at, updated_at + ) VALUES ( + gen_random_uuid(), + v_ak_user_id, -- 关键:必须是 ak_users 的业务主键 id + v_nickname, + 'https://api.dicebear.com/7.x/avataaars/svg?seed=homecare', + v_phone, + 1, TRUE, + NOW(), NOW() + ) + ON CONFLICT (uid) DO UPDATE SET + nickname = EXCLUDED.nickname, + status = EXCLUDED.status, + is_active = EXCLUDED.is_active, + updated_at = NOW(); + RAISE NOTICE '✓ ml_delivery_staff (v1) 已插入/更新'; + END IF; + + RAISE NOTICE '============================================'; + RAISE NOTICE '建档完成!'; + RAISE NOTICE 'auth.users.id: %', v_auth_id; + RAISE NOTICE 'ak_users.id: %', v_ak_user_id; + RAISE NOTICE 'ml_delivery_staff.uid: %', v_ak_user_id; + RAISE NOTICE '============================================'; + +END $$; diff --git a/mall_sql/scripts/create_delivery_staff_safe.sql b/mall_sql/scripts/create_delivery_staff_safe.sql new file mode 100644 index 00000000..10d3c075 --- /dev/null +++ b/mall_sql/scripts/create_delivery_staff_safe.sql @@ -0,0 +1,90 @@ +-- =================================================================== +-- 用途:为已存在的 auth 用户创建 ml_delivery_staff 服务人员档案(兼容 v1/v2) +-- 前提:auth.users 中已有 homecare_worker@test.com +-- =================================================================== + +DO $$ +DECLARE + v_uid UUID; + v_has_v2 BOOLEAN := FALSE; +BEGIN + -- 1. 通过邮箱找到用户 ID + SELECT id INTO v_uid FROM auth.users WHERE email = 'homecare_worker@test.com' LIMIT 1; + + IF v_uid IS NULL THEN + RAISE EXCEPTION 'auth.users 中不存在 homecare_worker@test.com,请先创建该用户'; + END IF; + + -- 2. 确保 ak_users 中有对应记录(role 必须是 delivery) + INSERT INTO public.ak_users (id, username, email, role, created_at, updated_at) + VALUES (v_uid, '居家服务员', 'homecare_worker@test.com', 'delivery', NOW(), NOW()) + ON CONFLICT (id) DO UPDATE SET + username = EXCLUDED.username, + role = EXCLUDED.role, + updated_at = NOW(); + + RAISE NOTICE '✓ ak_users 已就绪,uid: %', v_uid; + + -- 3. 探测 ml_delivery_staff 是否为 v2 结构(是否有 online_status 字段) + SELECT EXISTS ( + SELECT 1 FROM information_schema.columns + WHERE table_schema = 'public' + AND table_name = 'ml_delivery_staff' + AND column_name = 'online_status' + ) INTO v_has_v2; + + -- 4. 根据表结构版本执行对应插入 + IF v_has_v2 THEN + -- v2 结构(含 online_status, certificate_status, staff_no 等) + INSERT INTO public.ml_delivery_staff ( + id, uid, nickname, avatar, phone, + status, is_active, + staff_no, online_status, certificate_status, + certificate_expire_at, service_area, skills, + created_at, updated_at + ) VALUES ( + gen_random_uuid(), v_uid, '居家服务员', + 'https://api.dicebear.com/7.x/avataaars/svg?seed=homecare', + '13800138000', + 1, TRUE, + 'HC' || EXTRACT(YEAR FROM NOW()) || LPAD(FLOOR(RANDOM() * 10000)::TEXT, 4, '0'), + 'resting', 'valid', + (NOW() + INTERVAL '1 year')::DATE, + '北京市朝阳区', + '["基础护理", "康复训练", "上门照护"]'::jsonb, + NOW(), NOW() + ) + ON CONFLICT (uid) WHERE deleted_at IS NULL DO UPDATE SET + nickname = EXCLUDED.nickname, + status = EXCLUDED.status, + is_active = EXCLUDED.is_active, + updated_at = NOW(); + + RAISE NOTICE '✓ ml_delivery_staff (v2) 档案已创建/更新'; + ELSE + -- v1 结构(仅基础字段) + INSERT INTO public.ml_delivery_staff ( + id, uid, nickname, avatar, phone, + status, is_active, + created_at, updated_at + ) VALUES ( + gen_random_uuid(), v_uid, '居家服务员', + 'https://api.dicebear.com/7.x/avataaars/svg?seed=homecare', + '13800138000', + 1, TRUE, + NOW(), NOW() + ) + ON CONFLICT (uid) DO UPDATE SET + nickname = EXCLUDED.nickname, + status = EXCLUDED.status, + is_active = EXCLUDED.is_active, + updated_at = NOW(); + + RAISE NOTICE '✓ ml_delivery_staff (v1) 档案已创建/更新'; + END IF; + + RAISE NOTICE '============================================'; + RAISE NOTICE '建档完成!请重新编译小程序后登录测试。'; + RAISE NOTICE '============================================'; + +END $$; diff --git a/mall_sql/scripts/create_homecare_auth_user.js b/mall_sql/scripts/create_homecare_auth_user.js new file mode 100644 index 00000000..e3ca4062 --- /dev/null +++ b/mall_sql/scripts/create_homecare_auth_user.js @@ -0,0 +1,94 @@ +#!/usr/bin/env node + +/** + * 创建居家服务员 auth 用户(Supabase Admin API) + * 用法: + * 1. cd mall_sql/scripts + * 2. 设置环境变量 SUPABASE_URL 和 SUPABASE_SERVICE_ROLE_KEY + * 3. node create_homecare_auth_user.js + * + * 如果未安装 @supabase/supabase-js,先执行: + * npm install @supabase/supabase-js + */ + +const { createClient } = require('@supabase/supabase-js'); + +const SUPABASE_URL = process.env.SUPABASE_URL || 'http://119.146.131.237:9126'; +const SUPABASE_SERVICE_ROLE_KEY = process.env.SUPABASE_SERVICE_ROLE_KEY || ''; + +if (!SUPABASE_SERVICE_ROLE_KEY) { + console.error('❌ 请设置环境变量 SUPABASE_SERVICE_ROLE_KEY'); + console.error(' 该 key 可在 Supabase Dashboard -> Project Settings -> API -> service_role key 获取'); + process.exit(1); +} + +const supabase = createClient(SUPABASE_URL, SUPABASE_SERVICE_ROLE_KEY, { + auth: { autoRefreshToken: false, persistSession: false } +}); + +async function main() { + const email = 'homecare_worker@test.com'; + const password = 'Homecare123!'; + + console.log(`📝 正在创建/更新 auth 用户: ${email}`); + + // 先尝试查找现有用户 + const { data: existingUsers, error: listError } = await supabase.auth.admin.listUsers({ + page: 1, + perPage: 1000 + }); + + if (listError) { + console.error('❌ 查询用户列表失败:', listError.message); + process.exit(1); + } + + const existingUser = existingUsers.users.find(u => u.email === email); + + if (existingUser) { + console.log(`⚠️ 用户已存在 (ID: ${existingUser.id}),更新密码...`); + const { data: updateData, error: updateError } = await supabase.auth.admin.updateUserById( + existingUser.id, + { password, email_confirm: true } + ); + + if (updateError) { + console.error('❌ 更新用户失败:', updateError.message); + process.exit(1); + } + + console.log('✅ 密码已更新'); + console.log(` 用户ID: ${updateData.user.id}`); + console.log(` 邮箱: ${updateData.user.email}`); + } else { + console.log('🔨 创建新用户...'); + const { data: createData, error: createError } = await supabase.auth.admin.createUser({ + email, + password, + email_confirm: true, + user_metadata: { + name: '居家服务员', + role: 'delivery', + nickname: '居家服务员' + } + }); + + if (createError) { + console.error('❌ 创建用户失败:', createError.message); + process.exit(1); + } + + console.log('✅ 用户创建成功'); + console.log(` 用户ID: ${createData.user.id}`); + console.log(` 邮箱: ${createData.user.email}`); + } + + console.log('\n📋 下一步:'); + console.log(' 在 Supabase SQL Editor 中执行 seed_homecare_worker.sql'); + console.log(' 以创建 ak_users 和 ml_delivery_staff 记录。'); +} + +main().catch(err => { + console.error('❌ 未捕获错误:', err); + process.exit(1); +}); diff --git a/mall_sql/scripts/seed_homecare_worker.sql b/mall_sql/scripts/seed_homecare_worker.sql new file mode 100644 index 00000000..f1587bde --- /dev/null +++ b/mall_sql/scripts/seed_homecare_worker.sql @@ -0,0 +1,269 @@ +-- ===================================================================================== +-- 脚本:创建居家服务员测试账号及完整档案(一站式 SQL) +-- 用途:为 delivery 端登录提供真实的服务人员档案 +-- 执行环境:Supabase SQL Editor(需 postgres/superuser 权限) +-- 对应前端配置:医疗-delivery/ak/config.uts +-- - SUPA_URL: http://119.146.131.237:9126 +-- - 测试账号: homecare_worker@test.com / Homecare123! +-- ===================================================================================== + +-- 0. 确保 pgcrypto 扩展可用(用于 bcrypt 密码哈希) +CREATE EXTENSION IF NOT EXISTS pgcrypto; + +DO $$ +DECLARE + v_auth_id UUID; + v_instance_id UUID; + v_email TEXT := 'homecare_worker@test.com'; + v_password TEXT := 'Homecare123!'; + v_nickname TEXT := '居家服务员'; + v_phone TEXT := '13800138000'; +BEGIN + -- ------------------------------------------------------------------------- + -- 步骤 1:获取 instance_id(自托管 Supabase 通常只有一个 instance) + -- ------------------------------------------------------------------------- + SELECT id INTO v_instance_id FROM auth.instances LIMIT 1; + IF v_instance_id IS NULL THEN + v_instance_id := '00000000-0000-0000-0000-000000000000'::UUID; + END IF; + + -- ------------------------------------------------------------------------- + -- 步骤 2:创建/更新 auth.users(Supabase Auth 核心用户表) + -- 使用 crypt(..., gen_salt('bf')) 生成 bcrypt 哈希,与 gotrue 兼容 + -- ------------------------------------------------------------------------- + INSERT INTO auth.users ( + instance_id, + id, + aud, + role, + email, + encrypted_password, + email_confirmed_at, + confirmation_sent_at, + recovery_sent_at, + email_change_sent_at, + new_email, + invited_at, + confirmation_token, + recovery_token, + email_change_token_new, + email_change_token_current, + email_change, + email_change_confirm_status, + banned_until, + reauthentication_token, + reauthentication_sent_at, + is_super_admin, + created_at, + updated_at, + phone, + phone_confirmed_at, + phone_change, + phone_change_token, + phone_change_sent_at, + confirmed_at, + raw_app_meta_data, + raw_user_meta_data, + is_sso_user, + deleted_at, + is_anonymous + ) VALUES ( + v_instance_id, + gen_random_uuid(), + 'authenticated', + 'authenticated', + v_email, + crypt(v_password, gen_salt('bf')), + NOW(), + NULL, + NULL, + NULL, + NULL, + NULL, + '', + '', + '', + '', + '', + 0, + NULL, + '', + NULL, + FALSE, + NOW(), + NOW(), + NULL, + NULL, + NULL, + '', + NULL, + NOW(), + '{"provider":"email","providers":["email"]}'::jsonb, + '{"name":"居家服务员","role":"delivery","nickname":"居家服务员"}'::jsonb, + FALSE, + NULL, + FALSE + ) + ON CONFLICT (email) DO UPDATE SET + encrypted_password = crypt(v_password, gen_salt('bf')), + email_confirmed_at = COALESCE(auth.users.email_confirmed_at, NOW()), + confirmed_at = COALESCE(auth.users.confirmed_at, NOW()), + updated_at = NOW(), + raw_user_meta_data = '{"name":"居家服务员","role":"delivery","nickname":"居家服务员"}'::jsonb, + deleted_at = NULL, + is_anonymous = FALSE; + + -- 获取刚创建/更新的用户 ID + SELECT id INTO v_auth_id FROM auth.users WHERE email = v_email LIMIT 1; + + IF v_auth_id IS NULL THEN + RAISE EXCEPTION 'auth.users 插入失败,请检查权限或表结构'; + END IF; + + RAISE NOTICE '✓ auth.users 已创建/更新: % (ID: %)', v_email, v_auth_id; + + -- ------------------------------------------------------------------------- + -- 步骤 3:创建 auth.identities(Supabase Auth 身份表,某些版本必需) + -- ------------------------------------------------------------------------- + INSERT INTO auth.identities ( + id, + user_id, + identity_data, + provider, + created_at, + updated_at + ) VALUES ( + v_auth_id::TEXT, + v_auth_id, + jsonb_build_object('sub', v_auth_id::TEXT, 'email', v_email), + 'email', + NOW(), + NOW() + ) + ON CONFLICT (id) DO UPDATE SET + identity_data = jsonb_build_object('sub', v_auth_id::TEXT, 'email', v_email), + updated_at = NOW(); + + RAISE NOTICE '✓ auth.identities 已创建/更新'; + + -- ------------------------------------------------------------------------- + -- 步骤 4:创建 public.ak_users(业务用户资料表) + -- ------------------------------------------------------------------------- + INSERT INTO public.ak_users ( + id, + username, + email, + gender, + birthday, + height_cm, + weight_kg, + bio, + avatar_url, + preferred_language, + health_goal, + service_address, + emergency_contact, + chronic_notes, + care_preference, + role, + school_id, + grade_id, + class_id, + created_at, + updated_at + ) VALUES ( + v_auth_id, + v_nickname, + v_email, + 'unknown', + NULL, + 0, + 0, + '居家养老护理员,具备基础护理和康复训练资质', + 'https://api.dicebear.com/7.x/avataaars/svg?seed=homecare', + 'zh-CN', + '', + '', + '', + '', + '', + 'delivery', + '', + '', + '', + NOW(), + NOW() + ) + ON CONFLICT (id) DO UPDATE SET + username = EXCLUDED.username, + email = EXCLUDED.email, + role = EXCLUDED.role, + bio = EXCLUDED.bio, + avatar_url = EXCLUDED.avatar_url, + updated_at = NOW(); + + RAISE NOTICE '✓ public.ak_users 已插入/更新'; + + -- ------------------------------------------------------------------------- + -- 步骤 5:创建 public.ml_delivery_staff(服务人员档案表) + -- ------------------------------------------------------------------------- + INSERT INTO public.ml_delivery_staff ( + id, + uid, + nickname, + avatar, + phone, + status, + is_active, + station_id, + staff_no, + online_status, + certificate_status, + certificate_expire_at, + service_area, + skills, + created_at, + updated_at + ) VALUES ( + gen_random_uuid(), + v_auth_id, + v_nickname, + 'https://api.dicebear.com/7.x/avataaars/svg?seed=homecare', + v_phone, + 1, -- 状态: 1-启用 + TRUE, -- 是否激活 + NULL, -- 所属站点(可选) + 'HC' || EXTRACT(YEAR FROM NOW()) || LPAD(FLOOR(RANDOM() * 10000)::TEXT, 4, '0'), -- 工号 + 'resting', -- 在线状态 + 'valid', -- 资质状态 + (NOW() + INTERVAL '1 year')::DATE, -- 资质到期日 + '北京市朝阳区', + '["基础护理", "康复训练", "上门照护", "健康监测"]'::jsonb, + NOW(), + NOW() + ) + ON CONFLICT (uid) WHERE deleted_at IS NULL DO UPDATE SET + nickname = EXCLUDED.nickname, + phone = EXCLUDED.phone, + status = EXCLUDED.status, + is_active = EXCLUDED.is_active, + online_status = EXCLUDED.online_status, + certificate_status = EXCLUDED.certificate_status, + service_area = EXCLUDED.service_area, + skills = EXCLUDED.skills, + updated_at = NOW(); + + RAISE NOTICE '✓ public.ml_delivery_staff 已插入/更新'; + + -- ------------------------------------------------------------------------- + -- 完成 + -- ------------------------------------------------------------------------- + RAISE NOTICE '============================================'; + RAISE NOTICE '居家服务员账号初始化完成!'; + RAISE NOTICE '邮箱: %', v_email; + RAISE NOTICE '密码: %', v_password; + RAISE NOTICE '用户ID: %', v_auth_id; + RAISE NOTICE 'SUPA_URL: http://119.146.131.237:9126'; + RAISE NOTICE '============================================'; + +END $$; diff --git a/pages.json b/pages.json index b2ea96e6..57a9404b 100644 --- a/pages.json +++ b/pages.json @@ -1,36 +1,10 @@ { "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": "忘记密码" + "navigationBarTitleText": "服务人员注册", + "navigationStyle": "custom" } }, { @@ -70,394 +44,81 @@ } }, { - "path": "pages/main/messages", + "path": "pages/mall/delivery/service-record/index", "style": { - "navigationBarTitleText": "消息", - "enablePullDownRefresh": true - } - }, - { - "path": "pages/main/cart", - "style": { - "navigationBarTitleText": "购物车", + "navigationBarTitleText": "服务记录", "navigationStyle": "custom" } }, { - "path": "pages/main/profile", + "path": "pages/mall/delivery/orders/exception", + "style": { + "navigationBarTitleText": "服务记录", + "navigationStyle": "custom" + } + }, + { + "path": "pages/mall/delivery/orders/exception", + "style": { + "navigationBarTitleText": "异常上报", + "navigationStyle": "custom" + } + }, + { + "path": "pages/mall/delivery/profile/index", "style": { "navigationBarTitleText": "我的", "navigationStyle": "custom" } }, { - "path": "pages/main/category", + "path": "pages/mall/delivery/profile/settings", "style": { - "navigationBarTitleText": "分类", + "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": "member/index", - "style": { - "navigationBarTitleText": "会员中心" - } - }, - { - "path": "product-reviews", - "style": { - "navigationBarTitleText": "商品评价" - } - } - ] - } - ], + "subPackages": [], + "globalStyle": { + "navigationBarTextStyle": "black", + "navigationBarTitleText": "delivery", + "navigationBarBackgroundColor": "#FFFFFF", + "backgroundColor": "#F3F7F9" + }, "tabBar": { - "color": "#999999", - "selectedColor": "#ff5000", - "backgroundColor": "#ffffff", + "color": "#6B7280", + "selectedColor": "#0F766E", + "backgroundColor": "#FFFFFF", "borderStyle": "black", "list": [ { - "pagePath": "pages/main/index", - "text": "首页", + "pagePath": "pages/mall/delivery/home/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": "购物车", + "pagePath": "pages/mall/delivery/orders/index", + "text": "订单", "iconPath": "static/tabbar/cart.png", "selectedIconPath": "static/tabbar/cart.png" }, { - "pagePath": "pages/main/profile", + "pagePath": "pages/mall/delivery/profile/index", "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" + "name": "delivery端", + "path": "pages/user/login", + "query": "mode=delivery&role=delivery" } ] } diff --git a/pages/mall/consumer/home-service/order-detail.uvue b/pages/mall/consumer/home-service/order-detail.uvue index ee126bb2..a932c8df 100644 --- a/pages/mall/consumer/home-service/order-detail.uvue +++ b/pages/mall/consumer/home-service/order-detail.uvue @@ -86,6 +86,17 @@ 状态更新时间:{{ consumerViewState.statusUpdatedAt }} + + {{ consumerViewState.exceptionTitle }} + {{ consumerViewState.exceptionDesc }} + + 异常原因: + {{ consumerViewState.exceptionReason }} + + 状态更新时间:{{ consumerViewState.statusUpdatedAt }} + + + 返回首页 再次预约 @@ -105,6 +116,8 @@ @@ -150,6 +403,15 @@ onLoad((options) => { color: #16324f; } +.success-text { + color: #0f766e; + font-weight: bold; +} + +.warning-text { + color: #dc2626; +} + .button-stack { flex-direction: column; } @@ -166,6 +428,10 @@ onLoad((options) => { color: #ffffff; } +.primary-btn[disabled] { + background: #9ca3af; +} + .secondary-btn { background: #eaf2f0; color: #0f766e; @@ -179,4 +445,4 @@ onLoad((options) => { background: #f2f7f6; font-size: 28rpx; } - \ No newline at end of file + diff --git a/pages/mall/delivery/orders/detail.uvue b/pages/mall/delivery/orders/detail.uvue index b8bd1c83..305d4bcb 100644 --- a/pages/mall/delivery/orders/detail.uvue +++ b/pages/mall/delivery/orders/detail.uvue @@ -12,7 +12,7 @@ {{ item.title }} - {{ item.time }} + {{ formatDateTime(item.time) }} {{ item.description }} @@ -22,7 +22,7 @@ 服务信息 服务名称:{{ order.serviceName }} 服务类型:{{ order.serviceType }} - 预约时间:{{ order.appointmentTime }} + 预约时间:{{ formatDateTime(order.appointmentTime) }} 预计时长:{{ order.duration }} 分钟 服务价格:¥{{ order.price }} 预计收入:¥{{ order.staffIncome }} @@ -71,7 +71,7 @@ 异常记录 异常类型:{{ order.abnormalReport!.type }} 异常说明:{{ order.abnormalReport!.description }} - 发生时间:{{ order.abnormalReport!.occurredAt }} + 发生时间:{{ formatDateTime(order.abnormalReport!.occurredAt) }} @@ -98,6 +98,7 @@ import { import { getNextStepText, getPrimaryActionText } from '@/utils/deliveryCareUi.uts' import { requireDeliveryAuth } from '@/utils/deliveryAuth.uts' import { getDeliveryRouteParam } from '@/utils/deliveryRoute.uts' +import { formatDateTime } from '@/utils/utils.uts' const orderId = ref('') const order = ref(null) @@ -112,6 +113,7 @@ async function loadData() { return } order.value = await getServiceOrderDetail(orderId.value) + console.warn('[orders/detail] order detail:', JSON.stringify(order.value)) } function joinTags(tags: Array): string { @@ -316,4 +318,4 @@ onLoad((options) => { font-size: 26rpx; color: #ffffff; } - \ No newline at end of file + diff --git a/pages/mall/delivery/orders/index.uvue b/pages/mall/delivery/orders/index.uvue index 28502a78..ba7ddb89 100644 --- a/pages/mall/delivery/orders/index.uvue +++ b/pages/mall/delivery/orders/index.uvue @@ -16,7 +16,7 @@ {{ item.serviceName }} - {{ item.elderName }} · {{ item.appointmentTime }} + {{ item.elderName }} · {{ formatDateTime(item.appointmentTime) }} {{ item.statusText }} @@ -50,6 +50,7 @@ import { } from '@/services/deliveryService.uts' import { getDeliveryOrderTabs, getPrimaryActionText } from '@/utils/deliveryCareUi.uts' import { requireDeliveryAuth } from '@/utils/deliveryAuth.uts' +import { formatDateTime } from '@/utils/utils.uts' const tabs = getDeliveryOrderTabs() const currentTab = ref('pending') @@ -68,13 +69,16 @@ async function loadData() { } if (currentTab.value == 'pending') { orders.value = await getPendingServiceOrders() + console.warn('[orders/index] pending orders:', JSON.stringify(orders.value)) return } if (currentTab.value == 'history') { orders.value = await getHistoryServiceOrders() + console.warn('[orders/index] history orders:', JSON.stringify(orders.value)) return } orders.value = await getTodayServiceOrders() + console.warn('[orders/index] today orders:', JSON.stringify(orders.value)) } function consumeStoredTab(): void { @@ -342,4 +346,4 @@ onShow(() => { .empty-box { padding: 24rpx 0; } - \ No newline at end of file + diff --git a/pages/mall/delivery/orders/route.uvue b/pages/mall/delivery/orders/route.uvue index 8897fad1..90a0cbb5 100644 --- a/pages/mall/delivery/orders/route.uvue +++ b/pages/mall/delivery/orders/route.uvue @@ -2,7 +2,7 @@ 服务地址:{{ order.fullAddress }} - 预约时间:{{ order.appointmentStartTime }} + 预约时间:{{ formatDateTime(order.appointmentStartTime) }} 当前位置:{{ currentLocationText }} @@ -25,6 +25,7 @@ import type { DeliveryLocationType, DeliveryOrderType } from '@/types/delivery.u import { arriveOrder, getDeliveryOrderDetail, startDepart } from '@/services/deliveryService.uts' import { requireDeliveryAuth } from '@/utils/deliveryAuth.uts' import { getDeliveryRouteParam } from '@/utils/deliveryRoute.uts' +import { formatDateTime } from '@/utils/utils.uts' const orderId = ref('') const order = ref(null) diff --git a/pages/mall/delivery/service-record/index.uvue b/pages/mall/delivery/service-record/index.uvue index 6738f1f7..bd69d140 100644 --- a/pages/mall/delivery/service-record/index.uvue +++ b/pages/mall/delivery/service-record/index.uvue @@ -86,6 +86,43 @@ const photoCount = ref(0) const photosText = computed((): string => '已添加占位照片 ' + String(photoCount.value) + ' 张') +function formatDisplayTime(isoStr: string): string { + if (isoStr == '') { + return '' + } + // 如果已经是 YYYY-MM-DD HH:mm 格式,直接返回 + if (isoStr.indexOf('T') == -1 && isoStr.length >= 16) { + return isoStr.substring(0, 16) + } + // 处理 ISO 8601: 2026-05-19T03:00:00+08:00 + const parts = isoStr.split('T') + if (parts.length < 2) { + return isoStr + } + const datePart = parts[0] + let timePart = parts[1] + // 去掉时区后缀 + const plusIdx = timePart.indexOf('+') + if (plusIdx != -1) { + timePart = timePart.substring(0, plusIdx) + } + const zIdx = timePart.indexOf('Z') + if (zIdx != -1) { + timePart = timePart.substring(0, zIdx) + } + // 去掉毫秒 + const dotIdx = timePart.indexOf('.') + if (dotIdx != -1) { + timePart = timePart.substring(0, dotIdx) + } + // 取 HH:mm + const timeSegments = timePart.split(':') + if (timeSegments.length >= 2) { + return datePart + ' ' + timeSegments[0] + ':' + timeSegments[1] + } + return datePart + ' ' + timePart +} + function toggleItem(itemId: string, event: any) { for (let i = 0; i < serviceItems.value.length; i++) { if (serviceItems.value[i].id == itemId) { @@ -111,8 +148,8 @@ async function loadData() { order.value = await getServiceOrderDetail(orderId.value) if (order.value != null) { serviceItems.value = order.value.serviceItems - startTime.value = order.value.startServiceTime != null && order.value.startServiceTime != '' ? order.value.startServiceTime : order.value.appointmentTime - endTime.value = order.value.finishTime + startTime.value = formatDisplayTime(order.value.startServiceTime != null && order.value.startServiceTime != '' ? order.value.startServiceTime : order.value.appointmentTime) + endTime.value = formatDisplayTime(order.value.finishTime) processNote.value = order.value.serviceSummary staffRemark.value = order.value.progressNote if (order.value.serviceRecord != null) { diff --git a/pages/user/login.uvue b/pages/user/login.uvue index 1dd1c650..60392e25 100644 --- a/pages/user/login.uvue +++ b/pages/user/login.uvue @@ -173,10 +173,14 @@ const loginType = ref(0) // 默认账号密码(唯一来源,修改只改这里) // 必须在 account/password ref 之前声明,否则 ref 初始化时无法引用 // ───────────────────────────────────────────── -const CONSUMER_TEST_ACCOUNT = 'test@mall.com' -const CONSUMER_TEST_PASSWORD = 'Hf2152111' -const MERCHANT_TEST_ACCOUNT = 'test19@163.com' -const MERCHANT_TEST_PASSWORD = 'huang123456' +// const TEST_ACCOUNT = 'test@mall.com' // ← 旧账号(已停用) +// const TEST_PASSWORD = 'Hf2152111' // ← 旧密码(已停用) +const TEST_ACCOUNT = 'test20@163.com' +const TEST_PASSWORD = 'huang123456' + +// delivery 端默认账号(居家服务员) +const DELIVERY_TEST_ACCOUNT = 'homecare_worker@test.com' +const DELIVERY_TEST_PASSWORD = 'Homecare123!' // ✅ account/password 直接以常量作初始值,上线/刷新立即生效,不再依赖 onMounted 延迟赋值 const account = ref(CONSUMER_TEST_ACCOUNT) @@ -401,8 +405,8 @@ onMounted(() => { }) if (isDeliveryMode()) { - account.value = '' - password.value = '' + account.value = DELIVERY_TEST_ACCOUNT + password.value = DELIVERY_TEST_PASSWORD } else if (isMerchantMode()) { account.value = MERCHANT_TEST_ACCOUNT password.value = MERCHANT_TEST_PASSWORD diff --git a/services/deliveryService.uts b/services/deliveryService.uts index dcc1c42d..a79355f7 100644 --- a/services/deliveryService.uts +++ b/services/deliveryService.uts @@ -12,28 +12,11 @@ import { startDepartById, startServiceById } from '@/api/delivery.uts' -import { saveServiceRecord as saveRealServiceRecord } from '@/services/serviceOrderService.uts' import { - acceptCareOrder, - checkInCareOrder, - completeCareOrder, - getCareOrderDetail, - getCareRecords, - getDeliveryCareDashboard, - getDeliveryCareProfile, - getHistoryCareOrders, - getPendingCareOrders, - getTodayCareOrders, - markCareOrderArrived, - markCareOrderDeparted, - rejectCareOrder, - startCareService, - submitCareAbnormalReport, - submitCareServiceRecord, - updateCareOrderStatus, - updateDeliveryCareOnlineStatus -} from '@/mock/delivery-care.mock.uts' -import { requireDeliveryAuth } from '@/utils/deliveryAuth.uts' + getOrderDetail as getDirectServiceOrderDetail, + getOrdersByTab as getDirectOrdersByTab +} from '@/services/serviceOrderService.uts' +import { getUserInfo, requireDeliveryAuth, setDeliveryInfo } from '@/utils/deliveryAuth.uts' import type { DeliveryAbnormalReportType, DeliveryCheckinPayloadType, @@ -53,6 +36,87 @@ import type { DeliveryServiceRecordType } from '@/types/delivery.uts' +function nowText(): string { + return new Date().toISOString().replace('T', ' ').substring(0, 19) +} + +function emptyDashboard(): DeliveryDashboardType { + return { + pendingAssignmentCount: 0, + pendingAcceptCount: 0, + todayOrderCount: 0, + pendingDepartCount: 0, + servingCount: 0, + completedCount: 0, + exceptionCount: 0, + expectedIncome: 0, + onlineStatus: 'resting', + nextOrder: null, + recentOrders: [] as Array + } as DeliveryDashboardType +} + +async function getCurrentStaffIdOrEmpty(): Promise { + const authResult = await requireDeliveryAuth({ redirectOnFail: false, toastOnFail: false }) + if (!authResult.ok || authResult.deliveryInfo == null) { + return '' + } + return authResult.deliveryInfo.id +} + +function hasOrderCoreInfo(order: DeliveryOrderType | null): boolean { + if (order == null) { + return false + } + return order.serviceName != '' || order.elderName != '' || order.address != '' || order.contactName != '' +} + +function filterOrdersWithCoreInfo(orders: Array): Array { + const result = [] as Array + for (let i = 0; i < orders.length; i++) { + if (hasOrderCoreInfo(orders[i])) { + result.push(orders[i]) + } + } + return result +} + +function shouldFallbackOrders(orders: Array): boolean { + if (orders.length == 0) { + return false + } + for (let i = 0; i < orders.length; i++) { + if (hasOrderCoreInfo(orders[i])) { + return false + } + } + return true +} + +function mapOrderToRecord(order: DeliveryOrderType): DeliveryRecordType { + const isAccepted = order.status == 'completed' || order.status == 'pending_acceptance' || order.status == 'settled' || order.status == 'archived' + return { + id: order.id, + orderId: order.id, + orderNo: order.orderNo, + serviceName: order.serviceName, + elderName: order.fullElderName != '' ? order.fullElderName : order.elderName, + elderNameMasked: order.elderNameMasked != '' ? order.elderNameMasked : order.elderName, + status: order.status, + statusText: order.statusText, + appointmentStartTime: order.appointmentStartTime, + appointmentTime: order.appointmentTime, + actualStartTime: order.actualStartTime, + actualEndTime: order.actualEndTime != '' ? order.actualEndTime : order.finishTime, + staffIncome: order.staffIncome, + settlementStatus: order.settlementStatus != '' ? order.settlementStatus : '待结算', + acceptanceStatus: order.status, + exceptionDesc: order.exceptionDesc, + ratingText: isAccepted ? '已验收' : (order.status == 'pending_acceptance' ? '待验收' : '待补充'), + hasServiceRecord: order.serviceRecord != null + } as DeliveryRecordType +} + export async function loginDelivery(payload: DeliveryLoginPayloadType): Promise { return await loginDeliveryApi(payload) } @@ -228,19 +292,63 @@ export async function getDeliveryDashboardStats(): Promise> { - return await getDeliveryOrders({ tab: 'pending', keyword: '' } as DeliveryOrderQueryType) + const orders = await getDeliveryOrders({ tab: 'pending', keyword: '' } as DeliveryOrderQueryType) + const validOrders = filterOrdersWithCoreInfo(orders) + if (validOrders.length > 0) { + return validOrders + } + if (shouldFallbackOrders(orders)) { + console.warn('[deliveryService] pending orders missing core info, fallback to direct query') + const fallbackOrders = await getDirectOrdersByTab('pending') + if (fallbackOrders.length > 0) { + return fallbackOrders + } + } + return orders } export async function getTodayServiceOrders(): Promise> { - return await getDeliveryOrders({ tab: 'today', keyword: '' } as DeliveryOrderQueryType) + const orders = await getDeliveryOrders({ tab: 'today', keyword: '' } as DeliveryOrderQueryType) + const validOrders = filterOrdersWithCoreInfo(orders) + if (validOrders.length > 0) { + return validOrders + } + if (shouldFallbackOrders(orders)) { + console.warn('[deliveryService] today orders missing core info, fallback to direct query') + const fallbackOrders = await getDirectOrdersByTab('today') + if (fallbackOrders.length > 0) { + return fallbackOrders + } + } + return orders } export async function getHistoryServiceOrders(): Promise> { - return await getDeliveryOrders({ tab: 'history', keyword: '' } as DeliveryOrderQueryType) + const orders = await getDeliveryOrders({ tab: 'history', keyword: '' } as DeliveryOrderQueryType) + const validOrders = filterOrdersWithCoreInfo(orders) + if (validOrders.length > 0) { + return validOrders + } + if (shouldFallbackOrders(orders)) { + console.warn('[deliveryService] history orders missing core info, fallback to direct query') + const fallbackOrders = await getDirectOrdersByTab('history') + if (fallbackOrders.length > 0) { + return fallbackOrders + } + } + return orders } export async function getServiceOrderDetail(orderId: string): Promise { - return await getDeliveryOrderDetail(orderId) + const order = await getDeliveryOrderDetailById(orderId) + if (!hasOrderCoreInfo(order)) { + console.warn('[deliveryService] order detail missing core info, fallback to direct query:', orderId) + const fallbackOrder = await getDirectServiceOrderDetail(orderId) + if (hasOrderCoreInfo(fallbackOrder)) { + return fallbackOrder + } + } + return order } export async function acceptServiceOrder(orderId: string): Promise { @@ -294,4 +402,4 @@ export async function updateOrderStatus(orderId: string, nextStatus: DeliveryOrd export async function getAbnormalReport(orderId: string): Promise { const order = getCareOrderDetail(orderId) return order != null ? order.abnormalReport : null -} \ No newline at end of file +} diff --git a/services/homeServiceService.uts b/services/homeServiceService.uts index 1cdacef7..324a9108 100644 --- a/services/homeServiceService.uts +++ b/services/homeServiceService.uts @@ -739,7 +739,24 @@ async function listWorkerTaskIds(): Promise> { if (userId == '') { return [] as Array } - const careTaskResponse = await supa.from('ec_care_tasks').select('id').eq('assigned_to', userId).order('created_at', { ascending: false }).execute() + + // FIX: userId 是 auth.uid(),但 assigned_to / current_staff_id 存的是 ml_delivery_staff.id + // 需要先查出当前用户对应的 staff_id + const staffResponse = await supa.from('ml_delivery_staff') + .select('id') + .eq('uid', userId) + .single() + .execute() + + const staffId = (staffResponse.error == null && staffResponse.data != null) + ? readString(staffResponse.data as any, 'id') + : '' + + if (staffId == '') { + return [] as Array + } + + const careTaskResponse = await supa.from('ec_care_tasks').select('id').eq('assigned_to', staffId).order('created_at', { ascending: false }).execute() if (careTaskResponse.error == null && careTaskResponse.data != null) { const result = [] as Array const rows = careTaskResponse.data as Array @@ -753,16 +770,7 @@ async function listWorkerTaskIds(): Promise> { return result } } - let staffProfileId = '' - const staffResponse = await supa.from('ml_delivery_staff').select('id').eq('uid', userId).limit(1).execute() - if (staffResponse.error == null && staffResponse.data != null) { - const staffRows = staffResponse.data as Array - if (staffRows.length > 0) { - staffProfileId = readString(staffRows[0], 'id') - } - } - const legacyStaffId = staffProfileId != '' ? staffProfileId : userId - const legacyResponse = await supa.from('hss_service_orders').select('id').eq('current_staff_id', legacyStaffId).order('created_at', { ascending: false }).execute() + const legacyResponse = await supa.from('hss_service_orders').select('id').eq('current_staff_id', staffId).order('created_at', { ascending: false }).execute() if (legacyResponse.error != null || legacyResponse.data == null) { return [] as Array } @@ -783,7 +791,8 @@ async function isCareTask(taskId: string): Promise { } async function completeWorkerTask(taskId: string): Promise { - // LEGACY/TODO: 已切换为调用 rpc_delivery_finish_service。 + // LEGACY/TODO: 本函数旧逻辑为前端直接 update ec_care_tasks + insert hc_work_order_events。 + // 已切换为调用 rpc_delivery_finish_service(delivery 端统一动作 RPC)。 const { data, error } = await supa.rpc('rpc_delivery_finish_service', { p_order_id: taskId, p_payload: {} as any @@ -953,7 +962,8 @@ export async function advanceWorkerTask(taskId: string): Promise { - // LEGACY/TODO: 已切换为调用 rpc_delivery_checkin_order + rpc_delivery_start_service。 + // LEGACY/TODO: 本函数旧逻辑为前端直接 insert ec_care_records + update ec_care_tasks + insert hc_work_order_events。 + // 已切换为调用 rpc_delivery_checkin_order 与 rpc_delivery_start_service。 const checkinResult = await supa.rpc('rpc_delivery_checkin_order', { p_order_id: taskId, p_payload: { @@ -975,7 +985,8 @@ export async function submitWorkerCheckIn(taskId: string, note: string): Promise } export async function submitWorkerServiceRecord(taskId: string, summary: string): Promise { - // LEGACY/TODO: 已切换为调用 rpc_delivery_save_progress。 + // LEGACY/TODO: 本函数旧逻辑为前端直接 upsert ec_care_records + update ec_care_tasks + insert hc_work_order_events。 + // 已切换为调用 rpc_delivery_save_progress。 const { data, error } = await supa.rpc('rpc_delivery_save_progress', { p_order_id: taskId, p_payload: { @@ -991,7 +1002,8 @@ export async function submitWorkerServiceRecord(taskId: string, summary: string) } export async function submitWorkerException(taskId: string, exceptionType: string, description: string): Promise { - // LEGACY/TODO: 已切换为调用 rpc_delivery_submit_exception。 + // LEGACY/TODO: 本函数旧逻辑为前端直接 insert hc_work_order_exceptions + update ec_care_tasks + insert hc_work_order_events。 + // 已切换为调用 rpc_delivery_submit_exception。 const { data, error } = await supa.rpc('rpc_delivery_submit_exception', { p_order_id: taskId, p_payload: { diff --git a/services/serviceOrderService.uts b/services/serviceOrderService.uts index 233f98c4..ee2a01d4 100644 --- a/services/serviceOrderService.uts +++ b/services/serviceOrderService.uts @@ -99,157 +99,60 @@ function buildId(prefix: string): string { return prefix + '-' + String(Date.now()) + '-' + String(Math.floor(Math.random() * 100000)).padStart(5, '0') } -function buildHex(length: number): string { - const chars = '0123456789abcdef' +function randomHex(len: number): string { let result = '' - for (let i = 0; i < length; i++) { - result += chars.charAt(Math.floor(Math.random() * chars.length)) + for (let i = 0; i < len; i++) { + result += Math.floor(Math.random() * 16).toString(16) } return result } -function buildUuidLike(): string { - const segment1 = buildHex(8) - const segment2 = buildHex(4) - const segment3 = '4' + buildHex(3) - const variants = '89ab' - const segment4 = variants.charAt(Math.floor(Math.random() * variants.length)) + buildHex(3) - const segment5 = buildHex(12) - return segment1 + '-' + segment2 + '-' + segment3 + '-' + segment4 + '-' + segment5 +function generateUuid(): string { + return randomHex(8) + '-' + randomHex(4) + '-4' + randomHex(3) + '-' + String(Math.floor(Math.random() * 4 + 8).toString(16)) + randomHex(3) + '-' + randomHex(12) } function buildOrderNo(): string { - const date = new Date() - const y = String(date.getFullYear()) - const m = String(date.getMonth() + 1).padStart(2, '0') - const d = String(date.getDate()).padStart(2, '0') - const h = String(date.getHours()).padStart(2, '0') - const i = String(date.getMinutes()).padStart(2, '0') - const s = String(date.getSeconds()).padStart(2, '0') - return 'HS' + y + m + d + h + i + s + String(Math.floor(Math.random() * 900) + 100) + const now = new Date() + const year = String(now.getFullYear()) + const month = String(now.getMonth() + 1).padStart(2, '0') + const day = String(now.getDate()).padStart(2, '0') + const random = String(Math.floor(Math.random() * 1000000)).padStart(6, '0') + return 'SO' + year + month + day + random } -function isUuidLike(value: string): boolean { - return /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-5][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}$/.test(value) +function normalizeAppointmentTime(time: string): string { + if (time == '') { + return new Date().toISOString() + } + const parsed = Date.parse(time) + if (!isNaN(parsed)) { + return new Date(parsed).toISOString() + } + return new Date().toISOString() } -function normalizeUuidOrNull(value: string): string | null { - if (value == '') { +function normalizeUuidOrNull(id: string): string | null { + if (id == '') { return null } - return isUuidLike(value) ? value : null -} - -function normalizeAppointmentTime(value: string): string | null { - const text = value.trim() - if (text == '') { - return null + if (id.indexOf('-') >= 0 && id.length == 36) { + return id } - const fullRangeMatch = text.match(/^(\d{4}-\d{2}-\d{2})\s+(\d{2}:\d{2})(\s*-\s*\d{2}:\d{2})?$/) - if (fullRangeMatch != null) { - const datePart = fullRangeMatch[1] ?? '' - const startTime = fullRangeMatch[2] ?? '' - const parsed = Date.parse(datePart + 'T' + startTime + ':00') - if (!isNaN(parsed)) { - return new Date(parsed).toISOString() - } - return datePart + ' ' + startTime - } - if (/^\d{4}-\d{2}-\d{2}(\s+(上午|下午|晚上))$/.test(text)) { - return text - } - if (/^\d{4}-\d{2}-\d{2}T/.test(text)) { - const parsed = Date.parse(text) - if (!isNaN(parsed)) { - const date = new Date(parsed) - const year = String(date.getFullYear()) - const month = String(date.getMonth() + 1).padStart(2, '0') - const day = String(date.getDate()).padStart(2, '0') - const hour = String(date.getHours()).padStart(2, '0') - const minute = String(date.getMinutes()).padStart(2, '0') - return year + '-' + month + '-' + day + ' ' + hour + ':' + minute - } - return text.replace('T', ' ') - } - const monthDayMatch = text.match(/(\d{2})\/(\d{2})/) - if (monthDayMatch != null) { - const month = monthDayMatch[1] ?? '' - const day = monthDayMatch[2] ?? '' - if (month == '' || day == '') { - return text - } - const year = String(new Date().getFullYear()) - const rangeMatch = text.match(/(\d{2}:\d{2})(\s*-\s*\d{2}:\d{2})?/) - if (rangeMatch != null) { - const startTime = rangeMatch[1] ?? '' - if (startTime != '') { - const parsed = Date.parse(year + '-' + month + '-' + day + 'T' + startTime + ':00') - if (!isNaN(parsed)) { - return new Date(parsed).toISOString() - } - return year + '-' + month + '-' + day + ' ' + startTime - } - } - const tailText = text.substring(text.indexOf(month + '/' + day) + 5).trim() - return year + '-' + month + '-' + day + (tailText != '' ? ' ' + tailText : '') - } - const explicitParsed = Date.parse(text) - if (!isNaN(explicitParsed) && /^\d{4}\//.test(text)) { - const date = new Date(explicitParsed) - const year = String(date.getFullYear()) - const month = String(date.getMonth() + 1).padStart(2, '0') - const day = String(date.getDate()).padStart(2, '0') - const hour = String(date.getHours()).padStart(2, '0') - const minute = String(date.getMinutes()).padStart(2, '0') - return year + '-' + month + '-' + day + ' ' + hour + ':' + minute - } - const year = String(new Date().getFullYear()) - return /^\d{2}-\d{2}\s+\d{2}:\d{2}/.test(text) ? year + '-' + text : text -} - -function safeParseObject(value: string): UTSJSONObject { - if (value == '') { - return JSON.parse('{}') as UTSJSONObject - } - return JSON.parse(value) as UTSJSONObject -} - -function safeParseArray(value: string): Array { - if (value == '') { - return [] as Array - } - const parsed = JSON.parse(value) as any - if (Array.isArray(parsed)) { - return parsed as Array - } - return [] as Array -} - -function safeJsonField(source: any, key: string): string { - const plain = JSON.parse(JSON.stringify(source)) as any - const value = plain[key] - if (value == null) { - return '' - } - return JSON.stringify(value) -} - -function safeFirstJsonField(source: any, keys: Array): string { - const plain = JSON.parse(JSON.stringify(source)) as any - for (let i = 0; i < keys.length; i++) { - const value = plain[keys[i]] - if (value != null) { - return JSON.stringify(value) - } - } - return '' + return null } function plainObject(source: any): any { + if (source instanceof UTSJSONObject) { + return source + } return JSON.parse(JSON.stringify(source)) as any } function readString(source: any, key: string): string { + if (source instanceof UTSJSONObject) { + const value = (source as UTSJSONObject).getString(key) + return value != null ? value : '' + } const value = plainObject(source)[key] if (value == null) { return '' @@ -258,6 +161,10 @@ function readString(source: any, key: string): string { } function readNumber(source: any, key: string): number { + if (source instanceof UTSJSONObject) { + const value = (source as UTSJSONObject).getNumber(key) + return value != null ? value : 0 + } const value = plainObject(source)[key] if (typeof value == 'number') { return value @@ -269,320 +176,6 @@ function readNumber(source: any, key: string): number { return 0 } -function hasMissingColumnError(error: any, columnName: string): boolean { - if (error == null || columnName == '') { - return false - } - const errorText = JSON.stringify(error).toLowerCase() - return errorText.indexOf('could not find the') >= 0 && errorText.indexOf(columnName.toLowerCase()) >= 0 -} - -let ecServiceRequestCreateUnavailable = false - -function shouldBypassEcServiceRequestCreate(error: any): boolean { - if (error == null) { - return false - } - return hasMissingColumnError(error, 'address_snapshot') - || hasMissingColumnError(error, 'address_snapshot_json') - || hasMissingColumnError(error, 'contact_name') - || hasMissingColumnError(error, 'contact_phone') -} - -export function shouldUseCareTaskPath(orderId: string): boolean { - return isUuidLike(orderId) -} - -function isStaffActive(staff: any): boolean { - const plain = plainObject(staff) - if (plain['deleted_at'] != null && String(plain['deleted_at']) != '') { - return false - } - if (plain['is_active'] === false) { - return false - } - return readNumber(staff, 'status') == 1 -} - -function getStaffPriority(staff: any): number { - let score = 0 - const onlineStatus = readString(staff, 'online_status') - if (onlineStatus == 'online') { - score += 30 - } else if (onlineStatus == 'resting' || onlineStatus == '') { - score += 20 - } else if (onlineStatus == 'busy') { - score += 10 - } - if (readString(staff, 'uid') != '') { - score += 5 - } - if (readString(staff, 'station_id') != '') { - score += 1 - } - return score -} - -async function getAutoAssignableStaff(): Promise { - try { - const rpcResponse: any = await supa.rpc(HOMECARE_DISPATCH_CANDIDATE_RPC, {} as any) - if (rpcResponse == null || rpcResponse.error != null || rpcResponse.data == null) { - return null - } - return rpcResponse.data - } catch (error) { - console.warn('getAutoAssignableStaff rpc failed', error) - return null - } -} - -function buildEcServiceRequestPayload(params: CreateServiceOrderParams, userId: string, requestId: string, createdAt: string, appointmentTime: string | null, useAddressSnapshot: boolean): any { - const payload = { - id: requestId, - user_id: userId, - service_catalog_id: params.service.id, - service_name: params.service.name, - service_category: params.service.category, - elder_name: params.recipientName, - elder_phone: params.recipientPhone, - elder_age: params.recipientAge, - elder_gender: params.recipientGender, - contact_name: params.contactName, - contact_phone: params.contactPhone, - scheduled_at: appointmentTime, - remark: params.remark, - status: 'ORDER_CREATED', - created_at: createdAt, - updated_at: createdAt - } as any - if (useAddressSnapshot) { - payload.address_snapshot = params.address as any - } else { - payload.address_snapshot_json = params.address as any - } - return payload -} - -function buildEcServiceRequestPayloadWithoutAddress(params: CreateServiceOrderParams, userId: string, requestId: string, createdAt: string, appointmentTime: string | null): any { - return { - id: requestId, - user_id: userId, - service_catalog_id: params.service.id, - service_name: params.service.name, - service_category: params.service.category, - elder_name: params.recipientName, - elder_phone: params.recipientPhone, - elder_age: params.recipientAge, - elder_gender: params.recipientGender, - contact_name: params.contactName, - contact_phone: params.contactPhone, - scheduled_at: appointmentTime, - remark: params.remark, - status: 'ORDER_CREATED', - created_at: createdAt, - updated_at: createdAt - } as any -} - -function buildEcCareTaskPayload(params: CreateServiceOrderParams, userId: string, requestId: string, taskId: string, taskNo: string, createdAt: string, appointmentTime: string | null, useAddressSnapshot: boolean): any { - const payload = { - id: taskId, - task_no: taskNo, - request_id: requestId, - user_id: userId, - service_catalog_id: params.service.id, - service_name: params.service.name, - service_category: params.service.category, - service_snapshot_json: buildServiceSnapshot(params), - elder_name: params.recipientName, - elder_phone: params.recipientPhone, - elder_age: params.recipientAge, - elder_gender: params.recipientGender, - contact_name: params.contactName, - contact_phone: params.contactPhone, - scheduled_at: appointmentTime, - remark: params.remark, - status: 'ORDER_CREATED', - created_at: createdAt, - updated_at: createdAt - } as any - if (useAddressSnapshot) { - payload.address_snapshot = params.address as any - } else { - payload.address_snapshot_json = params.address as any - } - return payload -} - -function buildEcCareTaskPayloadWithoutAddress(params: CreateServiceOrderParams, userId: string, requestId: string, taskId: string, taskNo: string, createdAt: string, appointmentTime: string | null): any { - return { - id: taskId, - task_no: taskNo, - request_id: requestId, - user_id: userId, - service_catalog_id: params.service.id, - service_name: params.service.name, - service_category: params.service.category, - service_snapshot_json: buildServiceSnapshot(params), - elder_name: params.recipientName, - elder_phone: params.recipientPhone, - elder_age: params.recipientAge, - elder_gender: params.recipientGender, - contact_name: params.contactName, - contact_phone: params.contactPhone, - scheduled_at: appointmentTime, - remark: params.remark, - status: 'ORDER_CREATED', - created_at: createdAt, - updated_at: createdAt - } as any -} - -function parseTimeline(item: any): ServiceOrderTimelineItemType { - return { - id: readString(item, 'id'), - orderId: readString(item, 'order_id'), - fromStatus: readString(item, 'from_status'), - toStatus: normalizeServiceOrderStatus(readString(item, 'to_status')), - operatorId: readString(item, 'operator_id'), - operatorRole: readString(item, 'operator_role'), - remark: readString(item, 'remark'), - createdAt: readString(item, 'created_at') - } - } - -function parseReview(item: any): ServiceReviewType { - return { - id: readString(item, 'id'), - orderId: readString(item, 'order_id'), - userId: readString(item, 'user_id'), - rating: readNumber(item, 'rating') == 0 ? 5 : readNumber(item, 'rating'), - tags: safeParseArray(safeJsonField(item, 'tags_json')), - content: readString(item, 'content'), - createdAt: readString(item, 'created_at') - } - } - -function parseExecutionRecord(item: any): ServiceExecutionRecordType { - return { - id: readString(item, 'id'), - orderId: readString(item, 'order_id'), - assignmentId: readString(item, 'assignment_id'), - checkinTime: readString(item, 'checkin_time'), - checkinLatitude: readNumber(item, 'checkin_latitude'), - checkinLongitude: readNumber(item, 'checkin_longitude'), - checkinAddress: readString(item, 'checkin_address'), - serviceStartedAt: readString(item, 'service_started_at'), - serviceFinishedAt: readString(item, 'service_finished_at'), - actualDurationMinutes: readNumber(item, 'actual_duration_minutes'), - serviceItemsJson: safeJsonField(item, 'service_items_json'), - summary: readString(item, 'summary'), - remark: readString(item, 'remark'), - trackPointsJson: safeJsonField(item, 'track_points_json'), - createdAt: readString(item, 'created_at'), - updatedAt: readString(item, 'updated_at') - } - } - -function parseEvidenceFile(item: any): ServiceEvidenceFileType { - return { - id: readString(item, 'id'), - orderId: readString(item, 'order_id'), - executionRecordId: readString(item, 'execution_record_id'), - phase: readString(item, 'phase'), - fileType: readString(item, 'file_type'), - storagePath: readString(item, 'storage_path'), - fileUrl: readString(item, 'file_url'), - latitude: readNumber(item, 'latitude'), - longitude: readNumber(item, 'longitude'), - capturedAt: readString(item, 'captured_at'), - createdAt: readString(item, 'created_at') - } - } - -function parseServiceOrder(item: any, logs: Array, review: ServiceReviewType | null): ServiceOrderType { - const addressSnapshot = safeFirstJsonField(item, ['address_snapshot_json', 'address_snapshot']) - const serviceSnapshot = safeJsonField(item, 'service_snapshot_json') - const addressObj = plainObject(safeParseObject(addressSnapshot)) - const serviceObj = plainObject(safeParseObject(serviceSnapshot)) - return { - id: readString(item, 'id'), - orderNo: readString(item, 'order_no'), - userId: readString(item, 'user_id'), - serviceId: readString(item, 'service_id'), - serviceName: readString(item, 'service_name'), - serviceSnapshot: { - serviceId: readString(serviceObj, 'serviceId') != '' ? readString(serviceObj, 'serviceId') : readString(item, 'service_id'), - serviceName: readString(serviceObj, 'serviceName') != '' ? readString(serviceObj, 'serviceName') : readString(item, 'service_name'), - category: readString(serviceObj, 'category'), - price: readNumber(serviceObj, 'price'), - durationText: readString(serviceObj, 'durationText'), - summary: readString(serviceObj, 'summary'), - tags: safeParseArray(safeJsonField(serviceObj, 'tags')), - suitableFor: readString(serviceObj, 'suitableFor') - }, - serviceAddressId: readString(item, 'service_address_id'), - addressSnapshot: { - addressId: readString(addressObj, 'addressId'), - contactName: readString(addressObj, 'contactName'), - contactPhone: readString(addressObj, 'contactPhone'), - province: readString(addressObj, 'province'), - city: readString(addressObj, 'city'), - district: readString(addressObj, 'district'), - detailAddress: readString(addressObj, 'detailAddress'), - fullAddress: readString(addressObj, 'fullAddress'), - latitude: readNumber(addressObj, 'latitude'), - longitude: readNumber(addressObj, 'longitude'), - coordinateType: readString(addressObj, 'coordinateType') == '' ? 'gcj02' : readString(addressObj, 'coordinateType'), - remark: readString(addressObj, 'remark') - }, - recipientName: readString(item, 'recipient_name'), - recipientPhone: readString(item, 'recipient_phone'), - recipientAge: readFirstNumber(item, ['recipient_age', 'elder_age']), - recipientGender: readFirstString(item, ['recipient_gender', 'elder_gender']), - contactName: readString(item, 'contact_name'), - contactPhone: readString(item, 'contact_phone'), - appointmentTime: readString(item, 'appointment_time'), - remark: readString(item, 'remark'), - status: normalizeServiceOrderStatus(readString(item, 'status')), - paymentStatus: readNumber(item, 'payment_status'), - payExpireAt: readString(item, 'pay_expire_at'), - currentAssignmentId: readString(item, 'current_assignment_id'), - currentStaffId: readString(item, 'current_staff_id'), - acceptedAt: readString(item, 'accepted_at'), - departedAt: readString(item, 'departed_at'), - arrivedAt: readString(item, 'arrived_at'), - serviceStartedAt: readString(item, 'service_started_at'), - completedAt: readString(item, 'completed_at'), - pendingAcceptanceAt: readString(item, 'pending_acceptance_at'), - acceptedByUserAt: readString(item, 'accepted_by_user_at'), - reviewedAt: readString(item, 'reviewed_at'), - createdAt: readString(item, 'created_at'), - updatedAt: readString(item, 'updated_at'), - staffName: '', - staffPhone: '', - logs, - executionRecord: null, - evidenceFiles: [] as Array, - review, - dispatchStatus: readString(item, 'dispatch_status'), - dispatchErrorCode: readString(item, 'dispatch_error_code'), - dispatchErrorMessage: readString(item, 'dispatch_error_message') - } - } - -async function insertLegacyStatusLog(orderId: string, fromStatus: string, toStatus: ServiceOrderStatus, operatorId: string, operatorRole: string, remark: string): Promise { - // LEGACY/TODO: 旧链路直接写 hss_service_order_status_logs 已停用。 - // 状态事件应由后端 RPC 统一写入 hc_work_order_events / hss_service_order_status_logs。 - console.warn('[LEGACY] insertLegacyStatusLog skipped for', orderId, fromStatus, toStatus, remark) - } - -async function insertWorkOrderEvent(taskId: string, fromStatus: string, toStatus: string, actorId: string, actorRole: string, action: string, remark: string): Promise { - // LEGACY/TODO: 前端直接 insert hc_work_order_events 已严格禁止。 - // 状态事件必须由后端 RPC 统一写入。 - console.warn('[LEGACY] insertWorkOrderEvent skipped for', taskId, fromStatus, toStatus, action, remark) - } - function readFirstString(source: any, keys: Array): string { for (let i = 0; i < keys.length; i++) { const value = readString(source, keys[i]) @@ -593,56 +186,281 @@ function readFirstString(source: any, keys: Array): string { return '' } -function readFirstNumber(source: any, keys: Array): number { - for (let i = 0; i < keys.length; i++) { - const value = readNumber(source, keys[i]) - if (value != 0) { - return value - } +function readJsonField(source: any, key: string): any { + if (source != null && typeof source.getJSONObj === 'function') { + const value = source.getJSONObj(key) + return value != null ? value : null } - return 0 + const value = plainObject(source)[key] + return value == null ? null : value } -function readJsonObjectField(source: any, keys: Array): any { - const plain = plainObject(source) - for (let i = 0; i < keys.length; i++) { - const value = plain[keys[i]] - if (value != null) { - return value - } +function hasMissingColumnError(error: any, columnName: string): boolean { + if (error == null || columnName == '') { + return false } - return null + const errorText = JSON.stringify(error).toLowerCase() + return errorText.indexOf('could not find the') >= 0 && errorText.indexOf(columnName.toLowerCase()) >= 0 +} + +let ecCareTaskCreateUnavailable = false + +function shouldBypassEcCareTaskCreate(error: any): boolean { + if (error == null) { + return false + } + return hasMissingColumnError(error, 'address_snapshot_json') + || hasMissingColumnError(error, 'service_snapshot_json') + || hasMissingColumnError(error, 'scheduled_at') + || hasMissingColumnError(error, 'appointment_time') + || hasMissingColumnError(error, 'task_no') + || hasMissingColumnError(error, 'service_catalog_id') + || hasMissingColumnError(error, 'elder_name') + || hasMissingColumnError(error, 'contact_name') +} + +function readServiceItems(source: any): Array { + const raw = readJsonField(source, 'service_items_json') + const result = [] as Array + if (raw == null) { + return result + } + const items = Array.isArray(raw) ? raw : plainObject(raw) + if (!Array.isArray(items)) { + return result + } + for (let i = 0; i < items.length; i++) { + result.push({ + id: readFirstString(items[i], ['id']) != '' ? readFirstString(items[i], ['id']) : buildId('svc-item'), + name: readFirstString(items[i], ['name', 'label']), + required: readString(items[i], 'required') != 'false', + completed: readString(items[i], 'completed') == 'true', + incompleteReason: readString(items[i], 'incompleteReason'), + remark: readString(items[i], 'remark'), + updatedAt: readString(items[i], 'updatedAt') + } as DeliveryServiceItemType) + } + return result } -function mapCareTaskRowToLegacyOrderRow(item: any): any { - const serviceSnapshotValue = readJsonObjectField(item, ['service_snapshot_json']) - const addressSnapshotValue = readJsonObjectField(item, ['address_snapshot', 'address_snapshot_json']) - let derivedStatus = readString(item, 'status') - if (readFirstString(item, ['accepted_by_family_at']) != '') { - derivedStatus = 'ACCEPTED' - } else if (readFirstString(item, ['acceptance_pending_at']) != '') { - derivedStatus = 'ACCEPTANCE_PENDING' - } else if (readString(item, 'service_started_at') != '') { - derivedStatus = 'ORDER_IN_SERVICE' - } else if (readFirstString(item, ['checked_in_at']) != '') { - derivedStatus = 'ORDER_CHECKED_IN' - } else if (readString(item, 'departed_at') != '') { - derivedStatus = 'departed' - } else if (readString(item, 'accepted_at') != '') { - derivedStatus = 'ORDER_ACCEPTED' - } else if (readFirstString(item, ['assigned_to']) != '') { - derivedStatus = 'ORDER_ASSIGNED' +function buildDefaultServiceItems(order: DeliveryOrderType): Array { + const names = [] as Array + if (order.serviceName.indexOf('护理') >= 0) { + names.push('上门签到确认') + names.push('基础护理执行') + names.push('生命体征记录') + names.push('家属沟通反馈') + } else if (order.serviceName.indexOf('随访') >= 0) { + names.push('上门签到确认') + names.push('慢病指标采集') + names.push('用药情况核对') + names.push('随访结论反馈') + } else { + names.push('上门签到确认') + names.push('服务项目执行') + names.push('服务过程记录') + names.push('完成情况确认') } - const serviceSnapshot = serviceSnapshotValue != null ? serviceSnapshotValue : { - serviceId: readFirstString(item, ['service_catalog_id', 'service_id']), - serviceName: readString(item, 'service_name'), - category: readFirstString(item, ['service_category', 'category']), - price: readFirstNumber(item, ['service_price', 'price']), - durationText: readFirstString(item, ['service_duration_text', 'duration_text']), - summary: readFirstString(item, ['service_summary', 'summary']), - tags: [] as Array, - suitableFor: readFirstString(item, ['suitable_for']) + const result = [] as Array + for (let i = 0; i < names.length; i++) { + result.push({ + id: order.id + '-item-' + String(i + 1), + name: names[i], + required: true, + completed: false, + incompleteReason: '', + remark: '', + updatedAt: '' + } as DeliveryServiceItemType) } + return result + } + +async function getCurrentStaffId(): Promise { + const userId = getCurrentUserId() + if (userId == '') { + return '' + } + const profile = await getDeliveryProfileByUserId(userId) + return profile != null ? profile.id : '' + } + +function getCurrentWorkerUserId(): string { + return getCurrentUserId() + } + +// ============================================================================= +// 居家服务 delivery 动作 RPC 统一调用封装 +// 说明:所有会改变履约状态的动作必须走后端 RPC,禁止前端直接 update/insert。 +// ============================================================================= + +function parseRpcOrderResult(data: any): DeliveryOrderType | null { + if (data == null) { + return null + } + const plain = plainObject(data) + if (Array.isArray(plain)) { + if (plain.length == 0) { + return null + } + return plain[0] as DeliveryOrderType + } + if (readString(plain, 'id') == '') { + return null + } + return plain as DeliveryOrderType +} + +async function callDeliveryActionRpc(rpcName: string, params: any): Promise { + try { + const { data, error } = await supa.rpc(rpcName, params as any) + if (error != null) { + console.error('[delivery-action] RPC failed:', rpcName, error) + return null + } + return parseRpcOrderResult(data) + } catch (e) { + console.error('[delivery-action] RPC exception:', rpcName, e) + return null + } +} + +// LEGACY: 以下两个函数为旧链路直接写表逻辑,已停用。新链路由 rpc_delivery_* 统一处理。 +// 保留函数体仅为了避免引用处编译报错,实际不再执行写操作。 +async function insertLegacyStatusLog(orderId: string, fromStatus: string, toStatus: ServiceOrderStatus, remark: string): Promise { + // LEGACY/TODO: 旧链路直接写 hss_service_order_status_logs 已停用。 + // 状态事件由后端 rpc_delivery_* 统一写入 hc_work_order_events / hss_service_order_status_logs。 + console.warn('[LEGACY] insertLegacyStatusLog skipped for', orderId, fromStatus, toStatus, remark) + } + +async function insertWorkOrderEvent(taskId: string, fromStatus: string, toStatus: string, action: string, remark: string): Promise { + // LEGACY/TODO: 前端直接 insert hc_work_order_events 已禁止。 + // 状态事件必须由后端 rpc_delivery_* 统一写入。 + console.warn('[LEGACY] insertWorkOrderEvent skipped for', taskId, fromStatus, toStatus, action, remark) + } + +function parseExecutionRecord(item: any): ServiceExecutionRecordType { + return { + id: '', + orderNo: '', + serviceType: '', + serviceName: '', + serviceCategory: '', + serviceItems: [] as Array, + elderId: '', + elderName: '', + elderNameMasked: '', + elderGender: '', + elderAge: 0, + elderPhone: '', + elderPhoneMasked: '', + fullElderName: '', + fullPhone: '', + contactRelation: '家属', + addressSummary: '', + address: '', + addressDetail: '', + fullAddress: '', + latitude: 0, + longitude: 0, + appointmentTime: '', + appointmentStartTime: '', + appointmentEndTime: '', + duration: 90, + estimatedDuration: 90, + price: 0, + staffIncome: 0, + distance: '', + actualStartTime: '', + actualEndTime: '', + status: 'pending_assignment' as any, + statusText: '', + statusTone: 'warning', + riskTags: [] as Array, + healthTags: [] as Array, + careLevel: '', + needFamilyPresent: false, + needMaterials: false, + remark: '', + merchantId: '', + merchantName: '', + deliveryStaffId: '', + deliveryStaffName: '', + acceptTime: '', + departTime: '', + arriveTime: '', + checkinTime: '', + finishTime: '', + cancelReason: '', + exceptionType: '', + exceptionDesc: '', + evidenceList: [] as Array, + signatureUrl: '', + signatureName: '', + satisfactionStatus: '', + settlementStatus: '', + archiveStatus: '', + createdAt: '', + updatedAt: '', + contactName: '', + contactPhone: '', + notices: [] as Array, + timeline: [] as Array, + statusLog: [] as Array, + serviceSummary: '', + progressNote: '', + distanceKm: '', + allowCheckinRadiusMeters: 100, + lastLocation: null, + trackPoints: [] as Array, + serviceRecord: null, + abnormalReport: null + } as DeliveryOrderType + } + +function deriveTaskStatus(item: any): string { + const rawStatus = readString(item, 'status') + if (readFirstString(item, ['accepted_by_family_at']) != '') return 'ACCEPTED' + if (readFirstString(item, ['acceptance_pending_at']) != '') return 'ACCEPTANCE_PENDING' + if (readFirstString(item, ['service_started_at']) != '') return 'ORDER_IN_SERVICE' + if (readFirstString(item, ['checked_in_at']) != '') return 'ORDER_CHECKED_IN' + if (readFirstString(item, ['departed_at']) != '') return 'departed' + if (readFirstString(item, ['accepted_at']) != '') return 'ORDER_ACCEPTED' + return rawStatus + } + +function statusToDeliveryStatus(status: ServiceOrderStatus, item: any): string { + if (status == 'assigned') return 'pending_assignment' + if (status == 'accepted') { + return readFirstString(item, ['departed_at']) != '' ? 'departed' : 'accepted' + } + if (status == 'departed') return 'departed' + if (status == 'arrived') return 'arrived' + if (status == 'in_service') return 'in_service' + if (status == 'pending_acceptance') return 'pending_acceptance' + if (status == 'reviewed' || status == 'accepted_by_user' || status == 'settled') return 'completed' + if (status == 'rejected') return 'rejected' + if (status == 'cancelled') return 'cancelled' + if (status == 'exception') return 'abnormal' + return 'pending_assignment' + } + +function statusTone(status: ServiceOrderStatus): string { + if (status == 'pending_acceptance' || status == 'assigned') return 'warning' + if (status == 'accepted' || status == 'departed' || status == 'arrived' || status == 'in_service') return 'primary' + if (status == 'accepted_by_user' || status == 'reviewed' || status == 'settled') return 'success' + if (status == 'rejected' || status == 'cancelled' || status == 'exception') return 'danger' + return 'warning' + } + +function hasOrderCoreInfo(order: DeliveryOrderType | null): boolean { + if (order == null) { + return false + } + return order.serviceName != '' || order.elderName != '' || order.address != '' || order.contactName != '' + } + +function mapCareTaskToLegacyShape(item: any): any { return { id: readString(item, 'id'), order_no: readFirstString(item, ['task_no', 'order_no']), @@ -950,195 +768,57 @@ export function buildAddressSnapshot(address: UserAddress, latitude: number, lon } } -export function getHomecareOrderDisplayStatus(order: ServiceOrderType): string { - if (order.paymentStatus == 1 && order.status == 'created' && isHomecarePaymentExpired(order)) { - return '已超时未支付' - } - if (order.paymentStatus == 1 && order.status == 'created') { - return '待付款' - } - if (order.paymentStatus == 2 && order.dispatchStatus == HOMECARE_DISPATCH_STATUS_PENDING) { - return '待派单' - } - if (order.paymentStatus == 2 && order.dispatchStatus == HOMECARE_DISPATCH_STATUS_DISPATCHING) { - return '正在派单' - } - if (order.paymentStatus == 2 && order.dispatchStatus == HOMECARE_DISPATCH_STATUS_FAILED) { - return '派单未成功' - } - if (order.paymentStatus == 2 && order.dispatchStatus == HOMECARE_DISPATCH_STATUS_ASSIGNED) { - return '已派单' - } - return getServiceOrderStatusText(order.status) +async function getLegacyServiceOrderDetail(orderId: string): Promise { + return await getOrderDetail(orderId) } -function isHomecarePaymentExpired(order: ServiceOrderType): boolean { - if (order.paymentStatus != 1 || order.status != 'created') { - return false +async function tryCreateCareTask(params: CreateServiceOrderParams): Promise { + // LEGACY/TODO: 本函数为前端直接 INSERT ec_care_tasks 的过渡逻辑。 + // 按新架构,履约工单应在支付完成后由后端 RPC 生成/激活,前端不得直接创建。 + // 当前因缺少后端接口(rpc_consumer_create_homecare_task 或支付回调自动创建)暂时保留。 + if (ecCareTaskCreateUnavailable) { + return null } - const payExpireAt = order.payExpireAt != null ? order.payExpireAt : '' - if (payExpireAt == '') { - return false + const userId = getCurrentUserId() + if (userId == '') { + return null } - const expireMs = Date.parse(payExpireAt) - if (isNaN(expireMs)) { - return false + const taskId = generateUuid() + const taskNo = buildOrderNo() + const now = nowIso() + const scheduledAt = normalizeAppointmentTime(params.appointmentTime) + const response = await supa.from('ec_care_tasks').insert({ + id: taskId, + task_no: taskNo, + user_id: userId, + service_catalog_id: params.service.id, + service_name: params.service.name, + service_category: params.service.category, + service_snapshot_json: params.service as any, + elder_name: params.recipientName, + elder_phone: params.recipientPhone, + contact_name: params.contactName, + contact_phone: params.contactPhone, + address_snapshot_json: params.address as any, + scheduled_at: scheduledAt, + appointment_time: scheduledAt, + remark: params.remark, + status: 'ORDER_CREATED', + created_at: now, + updated_at: now + }).execute() + if (shouldBypassEcCareTaskCreate(response.error)) { + ecCareTaskCreateUnavailable = true + console.error('[tryCreateCareTask] ec_care_tasks 缺少必要字段,已停用前端直接创建:', response.error) + return null } - return expireMs <= Date.now() -} - -export async function dispatchPaidHomecareOrder(orderId: string): Promise { - if (orderId == null || orderId.trim() == '') { - return { - success: false, - code: 'ORDER_ID_REQUIRED', - message: '订单信息异常,请返回后重试', - display_type: 'modal', - retryable: false - } + if (response.error != null) { + console.error('tryCreateCareTask failed', response.error) + return null } - - // LEGACY/TODO: rpc_homecare_auto_dispatch 当前只操作 hss_service_orders(旧交易链)。 - // 对于 ec_care_tasks 新链(UUID 格式订单 ID),不要调用旧 RPC,避免错误回写旧表。 - if (shouldUseCareTaskPath(orderId)) { - return { - success: true, - code: 'SYNC_IN_PROGRESS', - message: '付款成功,服务安排信息正在同步中,请稍后在我的服务中查看。', - display_type: 'toast', - retryable: false, - dispatch_status: HOMECARE_DISPATCH_STATUS_PENDING, - order_id: orderId - } - } - - const { data, error } = await supa.rpc('rpc_homecare_auto_dispatch', { - p_order_id: orderId - } as any) - - if (error != null) { - console.error('[homecare-dispatch] rpc failed:', error) - return { - success: false, - code: 'RPC_EXECUTION_FAILED', - message: '派单服务暂时异常,请稍后重试', - display_type: 'modal', - retryable: true, - dispatch_status: 'failed', - order_id: orderId - } - } - - if (data == null) { - return { - success: false, - code: 'RPC_EMPTY_RESULT', - message: '未获取到派单结果,请稍后重试', - display_type: 'modal', - retryable: true, - dispatch_status: 'failed', - order_id: orderId - } - } - - const result = plainObject(data) - const successValue = result['success'] - const isSuccess = successValue === true || successValue === 'true' || (typeof successValue === 'boolean' && successValue) - const code = readString(result, 'code') - const message = readString(result, 'message') - const displayType = readString(result, 'display_type') - const retryable = result['retryable'] === true || result['retryable'] === 'true' - const dispatchStatus = readString(result, 'dispatch_status') - - if (isSuccess || code == 'DISPATCH_ASSIGNED' || code == 'ALREADY_ASSIGNED' || code == 'ALREADY_ASSIGNED_RECOVERED') { - return { - success: true, - code: code != '' ? code : 'DISPATCH_ASSIGNED', - message: message != '' ? message : '系统已为您匹配服务人员', - display_type: displayType != '' ? displayType : 'none', - retryable: false, - dispatch_status: dispatchStatus != '' ? dispatchStatus : 'assigned', - order_id: readString(result, 'order_id'), - assignment_id: readString(result, 'assignment_id'), - staff_id: readString(result, 'staff_id'), - station_id: readString(result, 'station_id'), - dispatch_distance_km: readNumber(result, 'dispatch_distance_km') - } - } - - return { - success: false, - code: code != '' ? code : 'DISPATCH_FAILED', - message: message != '' ? message : '派单失败,请稍后重试', - display_type: displayType != '' ? displayType : 'modal', - retryable: retryable, - dispatch_status: dispatchStatus != '' ? dispatchStatus : 'failed', - order_id: readString(result, 'order_id') - } -} - -export function showHomecareDispatchFailureModal(orderId: string, result: HomecareDispatchResult, retryCallback: (id: string) => void): void { - const code = result.code - let title = '操作失败' - let allowRetry = false - - if (code == 'ORDER_ID_REQUIRED') { - title = '操作失败' - allowRetry = false - } else if (code == 'UNAUTHENTICATED') { - title = '请重新登录' - allowRetry = false - } else if (code == 'USER_PROFILE_NOT_FOUND') { - title = '账户异常' - allowRetry = false - } else if (code == 'ORDER_NOT_FOUND') { - title = '订单异常' - allowRetry = false - } else if (code == 'ORDER_ACCESS_DENIED') { - title = '无权操作' - allowRetry = false - } else if (code == 'ORDER_NOT_PAID') { - title = '暂不能派单' - allowRetry = false - } else if (code == 'ORDER_STATUS_NOT_DISPATCHABLE') { - title = '暂不能派单' - allowRetry = false - } else if (code == 'NO_ONLINE_STAFF' || code == 'NO_STAFF_IN_SERVICE_STATION' || code == 'NO_QUALIFIED_STAFF' || code == 'NO_NEARBY_STAFF' || code == 'ALL_ELIGIBLE_STAFF_BUSY') { - title = '暂未匹配成功' - allowRetry = true - } else if (code == 'DISPATCH_CONFLICT_RETRY') { - title = '请重新尝试' - allowRetry = true - } else if (code == 'RPC_EXECUTION_FAILED' || code == 'RPC_EMPTY_RESULT') { - title = '派单服务异常' - allowRetry = true - } else { - title = '操作失败' - allowRetry = result.retryable - } - - if (!allowRetry) { - uni.showModal({ - title: title, - content: result.message, - showCancel: false, - confirmText: '我知道了' - }) - return - } - - uni.showModal({ - title: title, - content: result.message, - showCancel: true, - cancelText: '稍后再试', - confirmText: '重新派单', - success: (res) => { - if (res.confirm) { - retryCallback(orderId) - } - } - }) + // 已移除前端直接写入 hc_work_order_events;事件应由后端 RPC 统一生成。 + console.warn('[LEGACY] tryCreateCareTask skipped direct insertWorkOrderEvent for', taskId) + return await getOrderDetail(taskId) as any } export async function createServiceOrder(params: CreateServiceOrderParams): Promise { @@ -1195,30 +875,71 @@ export async function createServiceOrder(params: CreateServiceOrderParams): Prom console.error('createServiceOrder failed', response.error) return null } - await insertLegacyStatusLog(orderId, '', 'created', userId, 'consumer', '创建服务订单') + await insertLegacyStatusLog(orderId, '', 'created', '创建服务订单') + const staffObj = await getAutoAssignableStaff() + if (staffObj != null) { + const plainStaff = plainObject(staffObj) + const assignmentId = buildId('sa') + await supa.from('hss_service_assignments').insert({ + id: assignmentId, + order_id: orderId, + staff_id: readString(plainStaff, 'id'), + station_id: readString(plainStaff, 'station_id') == '' ? null : readString(plainStaff, 'station_id'), + status: 'assigned', + assigned_at: now, + created_at: now, + updated_at: now + }).execute() + await supa.from('hss_service_orders').update({ + status: 'assigned', + current_assignment_id: assignmentId, + current_staff_id: readString(plainStaff, 'id'), + updated_at: now + }).eq('id', orderId).execute() + await insertLegacyStatusLog(orderId, 'created', 'assigned', '系统已自动派单') + } return await getLegacyServiceOrderDetail(orderId) } -export async function listConsumerServiceOrders(): Promise> { - const userId = getCurrentUserId() - if (userId == '') { - return [] as Array +export async function getOrdersByTab(tab: string): Promise> { + const staffId = await getCurrentStaffId() + if (staffId == '') { + return [] as Array } - const careTaskResponse = await supa.from('ec_care_tasks').select('*').eq('user_id', userId).order('created_at', { ascending: false }).execute() + + // FIX: assigned_to / current_staff_id 存的是 ml_delivery_staff.id,不是 auth.uid + const careTaskResponse = await supa.from('ec_care_tasks').select('*').eq('assigned_to', staffId).order('created_at', { ascending: false }).execute() if (careTaskResponse.error == null && careTaskResponse.data != null) { const rawTasks = careTaskResponse.data as Array - const taskResult = [] as Array + const result = [] as Array for (let i = 0; i < rawTasks.length; i++) { - const parsed = await getCareTaskDetail(readString(rawTasks[i], 'id')) - if (parsed != null) { - taskResult.push(parsed) + const parsed = await parseCareTaskOrder(readString(rawTasks[i], 'id'), rawTasks[i]) + let matched = true + if (tab == 'pending') { + matched = parsed.status == 'pending_assignment' + } else if (tab == 'today') { + matched = parsed.status == 'pending_assignment' || parsed.status == 'accepted' || parsed.status == 'departed' || parsed.status == 'arrived' || parsed.status == 'in_service' || parsed.status == 'pending_acceptance' + } else if (tab == 'history') { + matched = parsed.status == 'pending_acceptance' || parsed.status == 'completed' || parsed.status == 'abnormal' || parsed.status == 'cancelled' || parsed.status == 'rejected' + } + if (tab == 'all' || matched) { + result.push(parsed) } } - return taskResult + const validResult = [] as Array + for (let i = 0; i < result.length; i++) { + if (hasOrderCoreInfo(result[i])) { + validResult.push(result[i]) + } + } + if (validResult.length > 0) { + return validResult + } } - const response = await supa.from('hss_service_orders').select('*').eq('user_id', userId).order('created_at', { ascending: false }).execute() + + const response = await supa.from('hss_service_orders').select('*').eq('current_staff_id', staffId).order('created_at', { ascending: false }).execute() if (response.error != null || response.data == null) { - return [] as Array + return [] as Array } const list = response.data as any[] const result = [] as Array @@ -1229,139 +950,182 @@ export async function listConsumerServiceOrders(): Promise { - if (shouldUseCareTaskPath(orderId)) { - const careTask = await getCareTaskDetail(orderId) - if (careTask != null) { - return careTask - } - } - return await getLegacyServiceOrderDetail(orderId) - } - -export async function saveServiceRecord(orderId: string, record: DeliveryServiceRecordType): Promise { - const userId = getCurrentUserId() - if (userId == '') { - return null - } - const careTask = shouldUseCareTaskPath(orderId) ? await getCareTaskDetail(orderId) : null - if (careTask != null) { - // TODO/GAP: consumer 端保存服务记录(ec_care_tasks 新链)暂无后端 RPC。 - // 禁止前端直接 insert ec_care_records / update ec_care_tasks。 - // 如需启用,请后端补充 rpc_consumer_save_service_record(task_id, record)。 - console.warn('[GAP] saveServiceRecord 暂不可用(新链):缺少 consumer 保存服务记录 RPC') - uni.showToast({ title: '服务记录功能正在升级,请稍后重试', icon: 'none' }) - return await getCareTaskDetail(orderId) - } - const current = await getLegacyServiceOrderDetail(orderId) - if (current == null) { - return null - } - const savedAt = nowIso() - const serviceSummary = record.serviceSummary != '' ? record.serviceSummary : record.processNote - const saveResponse = await supa.from('hss_service_execution_records').insert({ - id: buildId('ser'), - order_id: orderId, - checkin_time: current.executionRecord != null ? current.executionRecord.checkin_time : savedAt, - checkin_latitude: current.executionRecord != null ? current.executionRecord.checkin_latitude : 0, - checkin_longitude: current.executionRecord != null ? current.executionRecord.checkin_longitude : 0, - checkin_address: current.executionRecord != null ? current.executionRecord.checkin_address : '', - service_content_json: record.serviceContent.length > 0 ? record.serviceContent as any : record.serviceItems.map(item => item.name) as any, - service_summary: serviceSummary, - completion_images_json: record.photos as any, - signature_image: '', - signature_name: record.familyConfirmation.familyMember, - created_at: savedAt, - updated_at: savedAt - }).execute() - if (saveResponse.error != null) { - return null - } - await supa.from('hss_service_orders').update({ - updated_at: savedAt - }).eq('id', orderId).execute() - return await getLegacyServiceOrderDetail(orderId) } -export async function confirmServiceOrder(orderId: string, rating: number, content: string, tags: Array): Promise { - const userId = getCurrentUserId() - if (userId == '') { +export async function getOrderDetail(orderId: string): Promise { + const careTaskId = normalizeUuidOrNull(orderId) + if (careTaskId != null) { + const careTaskResponse = await supa.from('ec_care_tasks').select('*').eq('id', careTaskId).limit(1).execute() + if (careTaskResponse.error == null && careTaskResponse.data != null) { + const rows = careTaskResponse.data as Array + if (rows.length > 0) { + return await parseCareTaskOrder(orderId, rows[0]) + } + } + } + const response = await supa.from('hss_service_orders').select('*').eq('id', orderId).single().execute() + if (response.error != null || response.data == null) { return null } - const careTask = shouldUseCareTaskPath(orderId) ? await getCareTaskDetail(orderId) : null - if (careTask != null) { - // TODO/GAP: consumer 确认验收(ec_care_tasks 新链)暂无后端 RPC。 - // 禁止前端直接 update ec_care_tasks / insert ec_care_records / insert hc_work_order_events。 - // 如需启用,请后端补充 rpc_consumer_confirm_acceptance(task_id, rating, feedback, tags)。 - console.warn('[GAP] confirmServiceOrder 暂不可用(新链):缺少 consumer 确认验收 RPC') - uni.showToast({ title: '验收功能正在升级,请稍后重试', icon: 'none' }) - return await getCareTaskDetail(orderId) - } - // LEGACY: 以下仍为旧 hss_service_orders 链路,仅用于兼容已有交易订单。 - const current = await getServiceOrderDetail(orderId) - if (current == null) { - return null - } - const acceptedAt = new Date().toISOString() - const updateResponse = await supa.from('hss_service_orders').update({ - status: 'accepted_by_user', - accepted_by_user_at: acceptedAt, - updated_at: acceptedAt - }).eq('id', orderId).execute() - if (updateResponse.error != null) { - return null - } - await insertLegacyStatusLog(orderId, current.status, 'accepted_by_user', userId, 'consumer', '用户确认验收') - await supa.from('hss_service_reviews').insert({ - id: buildId('srv'), - order_id: orderId, - user_id: userId, - rating, - tags_json: tags as any, - content, - created_at: acceptedAt - }).execute() - await supa.from('hss_service_orders').update({ - status: 'reviewed', - reviewed_at: acceptedAt, - updated_at: acceptedAt - }).eq('id', orderId).execute() - await insertLegacyStatusLog(orderId, 'accepted_by_user', 'reviewed', userId, 'consumer', '用户提交评价') - return await getLegacyServiceOrderDetail(orderId) + return await parseLegacyDeliveryOrder(orderId, response.data) } -export async function rejectServiceOrderAcceptance(orderId: string, content: string): Promise { - const userId = getCurrentUserId() - if (userId == '') { - return null - } - const careTask = shouldUseCareTaskPath(orderId) ? await getCareTaskDetail(orderId) : null - if (careTask != null) { - // TODO/GAP: consumer 拒绝验收(ec_care_tasks 新链)暂无后端 RPC。 - // 禁止前端直接 update ec_care_tasks / insert hc_work_order_exceptions。 - // 如需启用,请后端补充 rpc_consumer_reject_acceptance(task_id, feedback)。 - console.warn('[GAP] rejectServiceOrderAcceptance 暂不可用(新链):缺少 consumer 拒绝验收 RPC') - uni.showToast({ title: '验收功能正在升级,请稍后重试', icon: 'none' }) - return await getCareTaskDetail(orderId) - } - // LEGACY: 以下仍为旧 hss_service_orders 链路。 - const current = await getServiceOrderDetail(orderId) - if (current == null) { - return null - } - const updateResponse = await supa.from('hss_service_orders').update({ - status: 'exception', - updated_at: nowIso() - }).eq('id', orderId).execute() - if (updateResponse.error != null) { - return null - } - await insertLegacyStatusLog(orderId, current.status, 'exception', userId, 'consumer', content == '' ? '用户退回整改' : content) - return await getLegacyServiceOrderDetail(orderId) +async function updateLegacyOrderStatus(orderId: string, nextStatus: ServiceOrderStatus, updateData: UTSJSONObject, remark: string): Promise { + // LEGACY/TODO: 前端直接 update hss_service_orders.status 已禁止。 + // 请使用 rpc_delivery_* 系列 RPC 完成状态流转。 + console.warn('[LEGACY] updateLegacyOrderStatus skipped for', orderId, nextStatus, remark) + return await getOrderDetail(orderId) } -export async function getCurrentConsumerUser() { - return await getCurrentUser() +async function updateCareTask(orderId: string, nextStatus: string, updateData: any, action: string, remark: string): Promise { + // LEGACY/TODO: 前端直接 update ec_care_tasks.status + insert hc_work_order_events 已严格禁止。 + // 所有状态流转必须调用后端 rpc_delivery_* RPC。 + console.warn('[LEGACY] updateCareTask skipped for', orderId, nextStatus, action, remark) + return await getOrderDetail(orderId) + } + +export async function acceptOrder(orderId: string): Promise { + return await callDeliveryActionRpc('rpc_delivery_accept_order', { + p_order_id: orderId + }) + } + +export async function rejectOrder(orderId: string, reason: string): Promise { + return await callDeliveryActionRpc('rpc_delivery_reject_order', { + p_order_id: orderId, + p_reason: reason + }) + } + +export async function departOrder(orderId: string, location: DeliveryLocationType | null): Promise { + return await callDeliveryActionRpc('rpc_delivery_start_depart', { + p_order_id: orderId, + p_location: location != null ? { + latitude: location.latitude, + longitude: location.longitude, + address: location.address + } : {} as any + }) + } + +export async function arriveOrder(orderId: string, location: DeliveryLocationType | null): Promise { + return await callDeliveryActionRpc('rpc_delivery_arrive_order', { + p_order_id: orderId, + p_location: location != null ? { + latitude: location.latitude, + longitude: location.longitude, + address: location.address + } : {} as any + }) + } + +export async function checkinOrder(orderId: string, payload: DeliveryCheckinPayloadType): Promise { + return await callDeliveryActionRpc('rpc_delivery_checkin_order', { + p_order_id: orderId, + p_payload: { + location: payload.location != null ? { + latitude: payload.location.latitude, + longitude: payload.location.longitude, + address: payload.location.address + } : {} as any, + photos: payload.photos != null ? payload.photos : [] as Array, + note: payload.note != null ? payload.note : '' + } as any + }) + } + +export async function startService(orderId: string): Promise { + return await callDeliveryActionRpc('rpc_delivery_start_service', { + p_order_id: orderId + }) + } + +export async function saveServiceRecord(orderId: string, record: DeliveryServiceRecordType): Promise { + // 先保存服务进度 + const progressResult = await callDeliveryActionRpc('rpc_delivery_save_progress', { + p_order_id: orderId, + p_payload: { + items: record.serviceItems != null ? record.serviceItems as any : [] as any, + serviceSummary: record.processNote != null ? record.processNote : '', + progressNote: record.staffRemark != null ? record.staffRemark : '' + } as any + }) + if (progressResult == null) { + return null + } + // 再逐张上传证据照片 + if (record.photos != null && record.photos.length > 0) { + for (let i = 0; i < record.photos.length; i++) { + await callDeliveryActionRpc('rpc_delivery_upload_evidence', { + p_order_id: orderId, + p_phase: 'service', + p_file_url: record.photos[i], + p_latitude: 0, + p_longitude: 0 + }) + } + } + return await getOrderDetail(orderId) + } + +export async function finishOrder(orderId: string): Promise { + return await callDeliveryActionRpc('rpc_delivery_finish_service', { + p_order_id: orderId, + p_payload: {} as any + }) + } + +export async function submitException(orderId: string, payload: DeliveryExceptionPayloadType): Promise { + return await callDeliveryActionRpc('rpc_delivery_submit_exception', { + p_order_id: orderId, + p_payload: { + exceptionType: payload.exceptionType != null ? payload.exceptionType : 'other', + description: payload.description != null ? payload.description : '', + occurredAt: payload.occurredAt != null && payload.occurredAt != '' ? payload.occurredAt : nowIso(), + locationText: payload.locationText != null ? payload.locationText : '', + images: payload.images != null ? payload.images as any : [] as any, + needPlatformIntervention: payload.needPlatformIntervention == true, + requestCancelOrder: payload.requestCancelOrder == true, + requestReschedule: payload.requestReschedule == true + } as any + }) + } + +export async function changeOrderStatus(orderId: string, nextStatus: DeliveryOrderStatus, extraRemark: string = ''): Promise { + if (nextStatus == 'accepted') return await acceptOrder(orderId) + if (nextStatus == 'departed' || nextStatus == 'on_the_way') return await departOrder(orderId, null) + if (nextStatus == 'arrived' || nextStatus == 'checked_in') return await arriveOrder(orderId, null) + if (nextStatus == 'in_service' || nextStatus == 'serving') return await startService(orderId) + if (nextStatus == 'pending_acceptance' || nextStatus == 'pending_confirm' || nextStatus == 'pending_submit' || nextStatus == 'completed') return await finishOrder(orderId) + if (nextStatus == 'rejected') return await rejectOrder(orderId, extraRemark) + if (nextStatus == 'abnormal' || nextStatus == 'exception_pending') { + return await submitException(orderId, { + exceptionType: 'other', + description: extraRemark, + occurredAt: nowIso(), + locationText: '', + images: [] as Array, + needPlatformIntervention: false, + requestCancelOrder: false, + requestReschedule: false + }) + } + return await getOrderDetail(orderId) + } +export async function confirmServiceOrder(orderId: string, rating: number, feedback: string, tags: Array): Promise { + // TODO/GAP: consumer 确认验收暂无后端 RPC(ec_care_tasks 新链)。 + // 当前禁止前端直接 update ec_care_tasks / insert ec_care_records / insert hc_work_order_events。 + // 如需启用,请后端补充 rpc_consumer_confirm_acceptance(task_id, rating, feedback, tags)。 + console.warn('[GAP] confirmServiceOrder 暂不可用:缺少 consumer 新链确认验收 RPC') + uni.showToast({ title: '验收功能正在升级,请稍后重试', icon: 'none' }) + return await getOrderDetail(orderId) + } + +export async function rejectServiceOrderAcceptance(orderId: string, feedback: string): Promise { + // TODO/GAP: consumer 拒绝验收暂无后端 RPC(ec_care_tasks 新链)。 + // 当前禁止前端直接 update ec_care_tasks.status。 + // 如需启用,请后端补充 rpc_consumer_reject_acceptance(task_id, feedback)。 + console.warn('[GAP] rejectServiceOrderAcceptance 暂不可用:缺少 consumer 新链拒绝验收 RPC') + uni.showToast({ title: '验收功能正在升级,请稍后重试', icon: 'none' }) + return await getOrderDetail(orderId) } diff --git a/utils/homeServiceBookingTime.uts b/utils/homeServiceBookingTime.uts index 0113cb4e..2c741ade 100644 --- a/utils/homeServiceBookingTime.uts +++ b/utils/homeServiceBookingTime.uts @@ -170,11 +170,19 @@ export function buildCustomSlots(selectedDateKey: string, now: Date): Array 20 || (endDate.getHours() == 20 && endDate.getMinutes() > 0)) { + continue + } + const startAt = startDate.getTime() const endAt = endDate.getTime() const available = startAt >= minStartTime @@ -264,5 +272,9 @@ export function formatStandardAppointmentTime( if (day == null || slot == null) { return '' } - return new Date(slot.startAt).toISOString() + const d = new Date(day.timestamp) + const year = d.getFullYear() + const month = padLeft(d.getMonth() + 1, 2) + const dayNum = padLeft(d.getDate(), 2) + return year + '-' + month + '-' + dayNum + ' ' + slot.label } diff --git a/utils/homecareAuth.uts b/utils/homecareAuth.uts new file mode 100644 index 00000000..f1cc4329 --- /dev/null +++ b/utils/homecareAuth.uts @@ -0,0 +1,255 @@ +import { AkReq } from '@/uni_modules/ak-req/index.uts' +import supa from '@/components/supadb/aksupainstance.uts' + +const HOMECARE_API_BASE = 'http://localhost:4001' +const HOMECARE_TOKEN_KEY = 'homecare_auth_token' +const HOMECARE_USER_KEY = 'homecare_auth_user' + +export type HomecareLoginResult = { + success: boolean + message: string + token: string | null + user: UTSJSONObject | null +} + +export type HomecarePrecheckResult = { + success: boolean + message: string + distanceMeters: number | null + allowedRadiusMeters: number + canCheckin: boolean + reasonCode: string + workerLocationAccepted: boolean + serviceLocationReady: boolean +} + +export const reasonCodeMap: UTSJSONObject = new UTSJSONObject({ + OK: '已进入允许签到范围', + OUT_OF_RADIUS: '当前距离服务地点较远,请到达服务地址附近后再签到', + SERVICE_LOCATION_MISSING: '当前工单缺少服务地址坐标,请联系管理员处理', + WORK_ORDER_NOT_ASSIGNABLE: '当前工单未分配或状态不允许签到', + WORKER_NOT_MATCHED: '当前账号不是该工单的服务人员', + COORDINATE_TYPE_INVALID: '定位坐标类型异常,请重新定位', + SLA_CONFIG_MISSING: '签到规则配置缺失,请联系管理员处理' +}) + +export function getReasonText(code: string): string { + const text = reasonCodeMap.getString(code) + return text != null && text !== '' ? text : '未知原因: ' + code +} + +export function getHomecareToken(): string { + const stored = uni.getStorageSync(HOMECARE_TOKEN_KEY) as string | null + return stored != null ? stored : '' +} + +export function getHomecareUser(): UTSJSONObject | null { + const raw = uni.getStorageSync(HOMECARE_USER_KEY) as string | null + if (raw == null || raw === '') { + return null + } + try { + return JSON.parse(raw) as UTSJSONObject + } catch (e) { + return null + } +} + +export function saveHomecareToken(token: string): void { + uni.setStorageSync(HOMECARE_TOKEN_KEY, token) +} + +export function saveHomecareUser(user: UTSJSONObject): void { + uni.setStorageSync(HOMECARE_USER_KEY, JSON.stringify(user)) +} + +export function clearHomecareAuth(): void { + uni.removeStorageSync(HOMECARE_TOKEN_KEY) + uni.removeStorageSync(HOMECARE_USER_KEY) +} + +export async function emailLogin(email: string, password: string): Promise { + try { + const reqData = new UTSJSONObject() + reqData.set('email', email) + reqData.set('password', password) + + const res = await AkReq.request({ + url: HOMECARE_API_BASE + '/auth/email-login', + method: 'POST', + data: reqData, + contentType: 'application/json' + }) + + if (res.status >= 200 && res.status < 300 && res.data != null) { + const dataObj = res.data as UTSJSONObject + const dataInner = dataObj.getJSON('data') + if (dataInner != null) { + const token = dataInner.getString('token') + const user = dataInner.getJSON('user') + if (token != null && token !== '') { + saveHomecareToken(token) + if (user != null) { + saveHomecareUser(user) + } + return { + success: true, + message: '登录成功', + token: token, + user: user + } + } + } + } + + let msg = '登录失败' + if (res.data != null) { + const dataObj = res.data as UTSJSONObject + const m = dataObj.getString('msg') + if (m != null && m !== '') { + msg = m + } + } + return { + success: false, + message: msg, + token: null, + user: null + } + } catch (error) { + console.error('emailLogin error:', error) + return { + success: false, + message: '网络错误,请检查网络连接', + token: null, + user: null + } + } +} + +export async function checkinPrecheck( + workOrderId: string, + latitude: number, + longitude: number, + accuracy: number +): Promise { + console.warn('[CHECKIN RPC] ========== checkinPrecheck START ==========') + console.warn('[CHECKIN RPC] 参数: workOrderId=', workOrderId, ' lat=', latitude, ' lng=', longitude, ' accuracy=', accuracy) + + try { + // 获取当前用户 ID + console.warn('[CHECKIN RPC] 步骤 1: 获取 session') + const session = supa.getSession() + console.warn('[CHECKIN RPC] session:', session) + + const workerId = session != null && session.user != null ? session.user.getString('id') : '' + console.warn('[CHECKIN RPC] workerId:', workerId) + + if (workerId == '' || workerId == null) { + console.warn('[CHECKIN RPC] 未登录,返回 NOT_LOGGED_IN') + return { distanceMeters: null, allowedRadiusMeters: 0, canCheckin: false, reasonCode: 'NOT_LOGGED_IN' } + } + + // 构建 RPC 参数 + console.warn('[CHECKIN RPC] 步骤 2: 构建 RPC 参数') + const rpcParams = { + p_work_order_id: workOrderId, + p_worker_id: workerId, + p_latitude: latitude, + p_longitude: longitude, + p_coordinate_type: 'gcj02', + p_accuracy: accuracy, + p_location_scene: 'CHECKIN_PRECHECK' + } as UTSJSONObject + console.warn('[CHECKIN RPC] RPC 参数:', JSON.stringify(rpcParams)) + + // 直接调用 Supabase RPC,不需要本地后端 + console.warn('[CHECKIN RPC] 步骤 3: 调用 supa.rpc') + const rpcResponse = await supa.rpc('rpc_homecare_checkin_precheck', rpcParams) + console.warn('[CHECKIN RPC] RPC 原始返回类型:', rpcResponse.constructor.name) + console.warn('[CHECKIN RPC] RPC 原始返回:', rpcResponse) + + // supa.rpc() 返回 AkReqResponse,需要从 data 字段提取 UTSJSONObject + let result: UTSJSONObject | null = null + if (rpcResponse != null) { + // 检查是否是 AkReqResponse 包装 + if ('data' in rpcResponse) { + const respData = (rpcResponse as any).data + console.warn('[CHECKIN RPC] 检测到 AkReqResponse,提取 data 字段') + if (respData != null && respData.constructor.name == 'UTSJSONObject') { + result = respData as UTSJSONObject + console.warn('[CHECKIN RPC] 成功提取 UTSJSONObject') + } else { + console.warn('[CHECKIN RPC] data 字段不是 UTSJSONObject:', respData) + // 尝试直接当作 UTSJSONObject + if (respData != null) { + result = respData as UTSJSONObject + } + } + } else { + // 直接就是 UTSJSONObject + console.warn('[CHECKIN RPC] 直接当作 UTSJSONObject') + result = rpcResponse as UTSJSONObject + } + } + console.warn('[CHECKIN RPC] 最终 result:', result) + + if (result != null) { + console.warn('[CHECKIN RPC] 解析结果字段') + const distanceMetersRaw = result.getNumber('distanceMeters') + const distanceMeters = distanceMetersRaw != null ? distanceMetersRaw : null + const allowedRadiusMetersRaw = result.getNumber('allowedRadiusMeters') + const allowedRadiusMeters = allowedRadiusMetersRaw != null ? allowedRadiusMetersRaw : 0 + const canCheckinRaw = result.get('canCheckin') + const canCheckin = canCheckinRaw === true + const reasonCode = result.getString('reasonCode') ?? '' + const workerLocationAcceptedRaw = result.get('workerLocationAccepted') + const workerLocationAccepted = workerLocationAcceptedRaw === true + const serviceLocationReadyRaw = result.get('serviceLocationReady') + const serviceLocationReady = serviceLocationReadyRaw === true + + console.warn('[CHECKIN RPC] 解析结果: distance=', distanceMeters, ' radius=', allowedRadiusMeters, ' canCheckin=', canCheckin, ' reason=', reasonCode) + + return { + success: canCheckin, + message: getReasonText(reasonCode), + distanceMeters: distanceMeters, + allowedRadiusMeters: allowedRadiusMeters, + canCheckin: canCheckin, + reasonCode: reasonCode, + workerLocationAccepted: workerLocationAccepted, + serviceLocationReady: serviceLocationReady + } + } + + console.warn('[CHECKIN RPC] RPC 返回 null,返回默认错误') + return { + success: false, + message: '预校验返回空结果', + distanceMeters: null, + allowedRadiusMeters: 0, + canCheckin: false, + reasonCode: 'UNKNOWN', + workerLocationAccepted: false, + serviceLocationReady: false + } + } catch (error) { + console.warn('[CHECKIN RPC] ========== 捕获异常 ==========') + console.warn('[CHECKIN RPC] 异常类型:', error instanceof Error ? 'Error' : typeof error) + console.warn('[CHECKIN RPC] 异常信息:', error) + console.warn('[CHECKIN RPC] ========== checkinPrecheck END (ERROR) ==========') + + // RPC 失败时返回详细错误 + const errMsg = error instanceof Error ? error.message : String(error) + return { + success: false, + message: '预校验失败: ' + errMsg, + distanceMeters: null, + allowedRadiusMeters: 0, + canCheckin: false, + reasonCode: 'RPC_ERROR', + workerLocationAccepted: false, + serviceLocationReady: false + } + } +} diff --git a/utils/pagesMode.uts b/utils/pagesMode.uts index 6bd5cc62..ef2e3a8a 100644 --- a/utils/pagesMode.uts +++ b/utils/pagesMode.uts @@ -1 +1 @@ -export const CURRENT_PAGES_MODE = 'consumer' +export const CURRENT_PAGES_MODE = 'delivery' diff --git a/utils/utils.uts b/utils/utils.uts index 6ddb265c..0ec17db9 100644 --- a/utils/utils.uts +++ b/utils/utils.uts @@ -196,3 +196,28 @@ export function formatTime(dateStr: string): string { } } +/** + * 格式化日期时间为可读字符串 + * @param value ISO 格式的日期字符串 + * @returns 格式化后的日期时间字符串(当年:MM-DD HH:mm,跨年:YYYY-MM-DD HH:mm) + */ +export function formatDateTime(value: string): string { + if (value == '') return '' + const parsed = Date.parse(value) + if (!isNaN(parsed)) { + const date = new Date(parsed) + const currentYear = new Date().getFullYear() + const year = date.getFullYear() + const month = String(date.getMonth() + 1).padStart(2, '0') + const day = String(date.getDate()).padStart(2, '0') + const hour = String(date.getHours()).padStart(2, '0') + const minute = String(date.getMinutes()).padStart(2, '0') + if (year == currentYear) { + return month + '-' + day + ' ' + hour + ':' + minute + } else { + return year + '-' + month + '-' + day + ' ' + hour + ':' + minute + } + } + return value +} + diff --git a/报错信息.txt b/报错信息.txt deleted file mode 100644 index 79bdf7d9..00000000 --- a/报错信息.txt +++ /dev/null @@ -1,640 +0,0 @@ -[自动热重载] 已开启代码文件保存后自动热重载 -mp.esm.js:529 [getUserNotifications] 开始获取通知 -mp.esm.js:529 [ak-req] request GET auth-mode: pre-set prefer: (none) -mp.esm.js:529 [getUserNotifications] 获取到通知数量: 3 -mp.esm.js:529 [getChatRooms] 开始获取聊天会话 -mp.esm.js:529 [ak-req] request GET auth-mode: pre-set prefer: (none) -mp.esm.js:529 [getChatRooms] 获取到会话数量: 2 -mp.esm.js:529 [Vue warn]: Property "manageMode" was accessed during render but is not defined on instance. - at -(anonymous) @ mp.esm.js:529 -warn$1 @ vue.runtime.esm.js:1251 -get @ vue.runtime.esm.js:2629 -(anonymous) @ messages.uvue:760 -vFor @ vue.runtime.esm.js:6364 -f @ vue.runtime.esm.js:6718 -(anonymous) @ messages.uvue:760 -renderComponentRoot @ vue.runtime.esm.js:5065 -componentUpdateFn @ vue.runtime.esm.js:5192 -run @ vue.runtime.esm.js:180 -instance.update @ vue.runtime.esm.js:5216 -callWithErrorHandling @ vue.runtime.esm.js:1356 -flushJobs @ vue.runtime.esm.js:1563 -Promise.then (async) -queueFlush @ vue.runtime.esm.js:1472 -queueJob @ vue.runtime.esm.js:1466 -(anonymous) @ vue.runtime.esm.js:5210 -resetScheduling @ vue.runtime.esm.js:263 -triggerEffects @ vue.runtime.esm.js:307 -triggerRefValue @ vue.runtime.esm.js:1067 -set @ vue.runtime.esm.js:1112 -_callee$ @ messages.uvue:639 -s @ regeneratorRuntime.js?forceSync=true:1 -(anonymous) @ regeneratorRuntime.js?forceSync=true:1 -(anonymous) @ regeneratorRuntime.js?forceSync=true:1 -fulfilled @ uni.mp.esm.js:1134 -Promise.then (async) -step @ uni.mp.esm.js:1134 -fulfilled @ uni.mp.esm.js:1134 -Promise.then (async) -step @ uni.mp.esm.js:1134 -(anonymous) @ uni.mp.esm.js:1134 -__awaiter @ uni.mp.esm.js:1134 -loadMessages @ messages.uvue:416 -(anonymous) @ messages.uvue:759 -callWithErrorHandling @ vue.runtime.esm.js:1356 -callWithAsyncErrorHandling @ vue.runtime.esm.js:1363 -hook.__weh.hook.__weh @ vue.runtime.esm.js:2461 -invokeArrayFns @ uni-shared.es.js:1344 -callHook @ uni.mp.esm.js:241 -mpOptions. @ uni.mp.esm.js:281 -mp.esm.js:529 [Vue warn]: Property "manageMode" was accessed during render but is not defined on instance. - at -(anonymous) @ mp.esm.js:529 -warn$1 @ vue.runtime.esm.js:1251 -get @ vue.runtime.esm.js:2629 -(anonymous) @ messages.uvue:760 -vFor @ vue.runtime.esm.js:6364 -f @ vue.runtime.esm.js:6718 -(anonymous) @ messages.uvue:760 -renderComponentRoot @ vue.runtime.esm.js:5065 -componentUpdateFn @ vue.runtime.esm.js:5192 -run @ vue.runtime.esm.js:180 -instance.update @ vue.runtime.esm.js:5216 -callWithErrorHandling @ vue.runtime.esm.js:1356 -flushJobs @ vue.runtime.esm.js:1563 -Promise.then (async) -queueFlush @ vue.runtime.esm.js:1472 -queueJob @ vue.runtime.esm.js:1466 -(anonymous) @ vue.runtime.esm.js:5210 -resetScheduling @ vue.runtime.esm.js:263 -triggerEffects @ vue.runtime.esm.js:307 -triggerRefValue @ vue.runtime.esm.js:1067 -set @ vue.runtime.esm.js:1112 -_callee$ @ messages.uvue:639 -s @ regeneratorRuntime.js?forceSync=true:1 -(anonymous) @ regeneratorRuntime.js?forceSync=true:1 -(anonymous) @ regeneratorRuntime.js?forceSync=true:1 -fulfilled @ uni.mp.esm.js:1134 -Promise.then (async) -step @ uni.mp.esm.js:1134 -fulfilled @ uni.mp.esm.js:1134 -Promise.then (async) -step @ uni.mp.esm.js:1134 -(anonymous) @ uni.mp.esm.js:1134 -__awaiter @ uni.mp.esm.js:1134 -loadMessages @ messages.uvue:416 -(anonymous) @ messages.uvue:759 -callWithErrorHandling @ vue.runtime.esm.js:1356 -callWithAsyncErrorHandling @ vue.runtime.esm.js:1363 -hook.__weh.hook.__weh @ vue.runtime.esm.js:2461 -invokeArrayFns @ uni-shared.es.js:1344 -callHook @ uni.mp.esm.js:241 -mpOptions. @ uni.mp.esm.js:281 -mp.esm.js:529 [Vue warn]: Property "manageMode" was accessed during render but is not defined on instance. - at -(anonymous) @ mp.esm.js:529 -warn$1 @ vue.runtime.esm.js:1251 -get @ vue.runtime.esm.js:2629 -(anonymous) @ messages.uvue:760 -vFor @ vue.runtime.esm.js:6364 -f @ vue.runtime.esm.js:6718 -(anonymous) @ messages.uvue:760 -renderComponentRoot @ vue.runtime.esm.js:5065 -componentUpdateFn @ vue.runtime.esm.js:5192 -run @ vue.runtime.esm.js:180 -instance.update @ vue.runtime.esm.js:5216 -callWithErrorHandling @ vue.runtime.esm.js:1356 -flushJobs @ vue.runtime.esm.js:1563 -Promise.then (async) -queueFlush @ vue.runtime.esm.js:1472 -queueJob @ vue.runtime.esm.js:1466 -(anonymous) @ vue.runtime.esm.js:5210 -resetScheduling @ vue.runtime.esm.js:263 -triggerEffects @ vue.runtime.esm.js:307 -triggerRefValue @ vue.runtime.esm.js:1067 -set @ vue.runtime.esm.js:1112 -_callee$ @ messages.uvue:639 -s @ regeneratorRuntime.js?forceSync=true:1 -(anonymous) @ regeneratorRuntime.js?forceSync=true:1 -(anonymous) @ regeneratorRuntime.js?forceSync=true:1 -fulfilled @ uni.mp.esm.js:1134 -Promise.then (async) -step @ uni.mp.esm.js:1134 -fulfilled @ uni.mp.esm.js:1134 -Promise.then (async) -step @ uni.mp.esm.js:1134 -(anonymous) @ uni.mp.esm.js:1134 -__awaiter @ uni.mp.esm.js:1134 -loadMessages @ messages.uvue:416 -(anonymous) @ messages.uvue:759 -callWithErrorHandling @ vue.runtime.esm.js:1356 -callWithAsyncErrorHandling @ vue.runtime.esm.js:1363 -hook.__weh.hook.__weh @ vue.runtime.esm.js:2461 -invokeArrayFns @ uni-shared.es.js:1344 -callHook @ uni.mp.esm.js:241 -mpOptions. @ uni.mp.esm.js:281 -mp.esm.js:529 [Vue warn]: Property "manageMode" was accessed during render but is not defined on instance. - at -(anonymous) @ mp.esm.js:529 -warn$1 @ vue.runtime.esm.js:1251 -get @ vue.runtime.esm.js:2629 -(anonymous) @ messages.uvue:760 -vFor @ vue.runtime.esm.js:6364 -f @ vue.runtime.esm.js:6718 -(anonymous) @ messages.uvue:760 -renderComponentRoot @ vue.runtime.esm.js:5065 -componentUpdateFn @ vue.runtime.esm.js:5192 -run @ vue.runtime.esm.js:180 -instance.update @ vue.runtime.esm.js:5216 -callWithErrorHandling @ vue.runtime.esm.js:1356 -flushJobs @ vue.runtime.esm.js:1563 -Promise.then (async) -queueFlush @ vue.runtime.esm.js:1472 -queueJob @ vue.runtime.esm.js:1466 -(anonymous) @ vue.runtime.esm.js:5210 -resetScheduling @ vue.runtime.esm.js:263 -triggerEffects @ vue.runtime.esm.js:307 -triggerRefValue @ vue.runtime.esm.js:1067 -set @ vue.runtime.esm.js:1112 -_callee$ @ messages.uvue:639 -s @ regeneratorRuntime.js?forceSync=true:1 -(anonymous) @ regeneratorRuntime.js?forceSync=true:1 -(anonymous) @ regeneratorRuntime.js?forceSync=true:1 -fulfilled @ uni.mp.esm.js:1134 -Promise.then (async) -step @ uni.mp.esm.js:1134 -fulfilled @ uni.mp.esm.js:1134 -Promise.then (async) -step @ uni.mp.esm.js:1134 -(anonymous) @ uni.mp.esm.js:1134 -__awaiter @ uni.mp.esm.js:1134 -loadMessages @ messages.uvue:416 -(anonymous) @ messages.uvue:759 -callWithErrorHandling @ vue.runtime.esm.js:1356 -callWithAsyncErrorHandling @ vue.runtime.esm.js:1363 -hook.__weh.hook.__weh @ vue.runtime.esm.js:2461 -invokeArrayFns @ uni-shared.es.js:1344 -callHook @ uni.mp.esm.js:241 -mpOptions. @ uni.mp.esm.js:281 -mp.esm.js:529 [Vue warn]: Property "manageMode" was accessed during render but is not defined on instance. - at -(anonymous) @ mp.esm.js:529 -warn$1 @ vue.runtime.esm.js:1251 -get @ vue.runtime.esm.js:2629 -(anonymous) @ messages.uvue:760 -vFor @ vue.runtime.esm.js:6364 -f @ vue.runtime.esm.js:6718 -(anonymous) @ messages.uvue:760 -renderComponentRoot @ vue.runtime.esm.js:5065 -componentUpdateFn @ vue.runtime.esm.js:5192 -run @ vue.runtime.esm.js:180 -instance.update @ vue.runtime.esm.js:5216 -callWithErrorHandling @ vue.runtime.esm.js:1356 -flushJobs @ vue.runtime.esm.js:1563 -Promise.then (async) -queueFlush @ vue.runtime.esm.js:1472 -queueJob @ vue.runtime.esm.js:1466 -(anonymous) @ vue.runtime.esm.js:5210 -resetScheduling @ vue.runtime.esm.js:263 -triggerEffects @ vue.runtime.esm.js:307 -triggerRefValue @ vue.runtime.esm.js:1067 -set @ vue.runtime.esm.js:1112 -_callee$ @ messages.uvue:639 -s @ regeneratorRuntime.js?forceSync=true:1 -(anonymous) @ regeneratorRuntime.js?forceSync=true:1 -(anonymous) @ regeneratorRuntime.js?forceSync=true:1 -fulfilled @ uni.mp.esm.js:1134 -Promise.then (async) -step @ uni.mp.esm.js:1134 -fulfilled @ uni.mp.esm.js:1134 -Promise.then (async) -step @ uni.mp.esm.js:1134 -(anonymous) @ uni.mp.esm.js:1134 -__awaiter @ uni.mp.esm.js:1134 -loadMessages @ messages.uvue:416 -(anonymous) @ messages.uvue:759 -callWithErrorHandling @ vue.runtime.esm.js:1356 -callWithAsyncErrorHandling @ vue.runtime.esm.js:1363 -hook.__weh.hook.__weh @ vue.runtime.esm.js:2461 -invokeArrayFns @ uni-shared.es.js:1344 -callHook @ uni.mp.esm.js:241 -mpOptions. @ uni.mp.esm.js:281 -mp.esm.js:529 [Vue warn]: Property "manageMode" was accessed during render but is not defined on instance. - at -(anonymous) @ mp.esm.js:529 -warn$1 @ vue.runtime.esm.js:1251 -get @ vue.runtime.esm.js:2629 -(anonymous) @ messages.uvue:760 -vFor @ vue.runtime.esm.js:6364 -f @ vue.runtime.esm.js:6718 -(anonymous) @ messages.uvue:760 -renderComponentRoot @ vue.runtime.esm.js:5065 -componentUpdateFn @ vue.runtime.esm.js:5192 -run @ vue.runtime.esm.js:180 -instance.update @ vue.runtime.esm.js:5216 -callWithErrorHandling @ vue.runtime.esm.js:1356 -flushJobs @ vue.runtime.esm.js:1563 -Promise.then (async) -queueFlush @ vue.runtime.esm.js:1472 -queueJob @ vue.runtime.esm.js:1466 -(anonymous) @ vue.runtime.esm.js:5210 -resetScheduling @ vue.runtime.esm.js:263 -triggerEffects @ vue.runtime.esm.js:307 -triggerRefValue @ vue.runtime.esm.js:1067 -set @ vue.runtime.esm.js:1112 -_callee$ @ messages.uvue:639 -s @ regeneratorRuntime.js?forceSync=true:1 -(anonymous) @ regeneratorRuntime.js?forceSync=true:1 -(anonymous) @ regeneratorRuntime.js?forceSync=true:1 -fulfilled @ uni.mp.esm.js:1134 -Promise.then (async) -step @ uni.mp.esm.js:1134 -fulfilled @ uni.mp.esm.js:1134 -Promise.then (async) -step @ uni.mp.esm.js:1134 -(anonymous) @ uni.mp.esm.js:1134 -__awaiter @ uni.mp.esm.js:1134 -loadMessages @ messages.uvue:416 -(anonymous) @ messages.uvue:759 -callWithErrorHandling @ vue.runtime.esm.js:1356 -callWithAsyncErrorHandling @ vue.runtime.esm.js:1363 -hook.__weh.hook.__weh @ vue.runtime.esm.js:2461 -invokeArrayFns @ uni-shared.es.js:1344 -callHook @ uni.mp.esm.js:241 -mpOptions. @ uni.mp.esm.js:281 -mp.esm.js:529 [Vue warn]: Property "manageMode" was accessed during render but is not defined on instance. - at -(anonymous) @ mp.esm.js:529 -warn$1 @ vue.runtime.esm.js:1251 -get @ vue.runtime.esm.js:2629 -(anonymous) @ messages.uvue:760 -vFor @ vue.runtime.esm.js:6364 -f @ vue.runtime.esm.js:6718 -(anonymous) @ messages.uvue:760 -renderComponentRoot @ vue.runtime.esm.js:5065 -componentUpdateFn @ vue.runtime.esm.js:5192 -run @ vue.runtime.esm.js:180 -instance.update @ vue.runtime.esm.js:5216 -callWithErrorHandling @ vue.runtime.esm.js:1356 -flushJobs @ vue.runtime.esm.js:1563 -Promise.then (async) -queueFlush @ vue.runtime.esm.js:1472 -queueJob @ vue.runtime.esm.js:1466 -(anonymous) @ vue.runtime.esm.js:5210 -resetScheduling @ vue.runtime.esm.js:263 -triggerEffects @ vue.runtime.esm.js:307 -triggerRefValue @ vue.runtime.esm.js:1067 -set @ vue.runtime.esm.js:1112 -_callee$ @ messages.uvue:639 -s @ regeneratorRuntime.js?forceSync=true:1 -(anonymous) @ regeneratorRuntime.js?forceSync=true:1 -(anonymous) @ regeneratorRuntime.js?forceSync=true:1 -fulfilled @ uni.mp.esm.js:1134 -Promise.then (async) -step @ uni.mp.esm.js:1134 -fulfilled @ uni.mp.esm.js:1134 -Promise.then (async) -step @ uni.mp.esm.js:1134 -(anonymous) @ uni.mp.esm.js:1134 -__awaiter @ uni.mp.esm.js:1134 -loadMessages @ messages.uvue:416 -(anonymous) @ messages.uvue:759 -callWithErrorHandling @ vue.runtime.esm.js:1356 -callWithAsyncErrorHandling @ vue.runtime.esm.js:1363 -hook.__weh.hook.__weh @ vue.runtime.esm.js:2461 -invokeArrayFns @ uni-shared.es.js:1344 -callHook @ uni.mp.esm.js:241 -mpOptions. @ uni.mp.esm.js:281 -mp.esm.js:529 [Vue warn]: Property "manageMode" was accessed during render but is not defined on instance. - at -(anonymous) @ mp.esm.js:529 -warn$1 @ vue.runtime.esm.js:1251 -get @ vue.runtime.esm.js:2629 -(anonymous) @ messages.uvue:760 -vFor @ vue.runtime.esm.js:6364 -f @ vue.runtime.esm.js:6718 -(anonymous) @ messages.uvue:760 -renderComponentRoot @ vue.runtime.esm.js:5065 -componentUpdateFn @ vue.runtime.esm.js:5192 -run @ vue.runtime.esm.js:180 -instance.update @ vue.runtime.esm.js:5216 -callWithErrorHandling @ vue.runtime.esm.js:1356 -flushJobs @ vue.runtime.esm.js:1563 -Promise.then (async) -queueFlush @ vue.runtime.esm.js:1472 -queueJob @ vue.runtime.esm.js:1466 -(anonymous) @ vue.runtime.esm.js:5210 -resetScheduling @ vue.runtime.esm.js:263 -triggerEffects @ vue.runtime.esm.js:307 -triggerRefValue @ vue.runtime.esm.js:1067 -set @ vue.runtime.esm.js:1112 -_callee$ @ messages.uvue:639 -s @ regeneratorRuntime.js?forceSync=true:1 -(anonymous) @ regeneratorRuntime.js?forceSync=true:1 -(anonymous) @ regeneratorRuntime.js?forceSync=true:1 -fulfilled @ uni.mp.esm.js:1134 -Promise.then (async) -step @ uni.mp.esm.js:1134 -fulfilled @ uni.mp.esm.js:1134 -Promise.then (async) -step @ uni.mp.esm.js:1134 -(anonymous) @ uni.mp.esm.js:1134 -__awaiter @ uni.mp.esm.js:1134 -loadMessages @ messages.uvue:416 -(anonymous) @ messages.uvue:759 -callWithErrorHandling @ vue.runtime.esm.js:1356 -callWithAsyncErrorHandling @ vue.runtime.esm.js:1363 -hook.__weh.hook.__weh @ vue.runtime.esm.js:2461 -invokeArrayFns @ uni-shared.es.js:1344 -callHook @ uni.mp.esm.js:241 -mpOptions. @ uni.mp.esm.js:281 -mp.esm.js:529 [Vue warn]: Property "manageMode" was accessed during render but is not defined on instance. - at -(anonymous) @ mp.esm.js:529 -warn$1 @ vue.runtime.esm.js:1251 -get @ vue.runtime.esm.js:2629 -(anonymous) @ messages.uvue:760 -vFor @ vue.runtime.esm.js:6364 -f @ vue.runtime.esm.js:6718 -(anonymous) @ messages.uvue:760 -renderComponentRoot @ vue.runtime.esm.js:5065 -componentUpdateFn @ vue.runtime.esm.js:5192 -run @ vue.runtime.esm.js:180 -instance.update @ vue.runtime.esm.js:5216 -callWithErrorHandling @ vue.runtime.esm.js:1356 -flushJobs @ vue.runtime.esm.js:1563 -Promise.then (async) -queueFlush @ vue.runtime.esm.js:1472 -queueJob @ vue.runtime.esm.js:1466 -(anonymous) @ vue.runtime.esm.js:5210 -resetScheduling @ vue.runtime.esm.js:263 -triggerEffects @ vue.runtime.esm.js:307 -triggerRefValue @ vue.runtime.esm.js:1067 -set @ vue.runtime.esm.js:1112 -_callee$ @ messages.uvue:639 -s @ regeneratorRuntime.js?forceSync=true:1 -(anonymous) @ regeneratorRuntime.js?forceSync=true:1 -(anonymous) @ regeneratorRuntime.js?forceSync=true:1 -fulfilled @ uni.mp.esm.js:1134 -Promise.then (async) -step @ uni.mp.esm.js:1134 -fulfilled @ uni.mp.esm.js:1134 -Promise.then (async) -step @ uni.mp.esm.js:1134 -(anonymous) @ uni.mp.esm.js:1134 -__awaiter @ uni.mp.esm.js:1134 -loadMessages @ messages.uvue:416 -(anonymous) @ messages.uvue:759 -callWithErrorHandling @ vue.runtime.esm.js:1356 -callWithAsyncErrorHandling @ vue.runtime.esm.js:1363 -hook.__weh.hook.__weh @ vue.runtime.esm.js:2461 -invokeArrayFns @ uni-shared.es.js:1344 -callHook @ uni.mp.esm.js:241 -mpOptions. @ uni.mp.esm.js:281 -[渲染层网络层错误] Failed to load local image resource /static/icons/system-notice.png - the server responded with a status of 500 (HTTP/1.1 500 Internal Server Error) -(env: Windows,mp,1.06.2504030; lib: 3.15.2) -mp.esm.js:529 [Vue warn]: Property "manageMode" was accessed during render but is not defined on instance. - at -(anonymous) @ mp.esm.js:529 -warn$1 @ vue.runtime.esm.js:1251 -get @ vue.runtime.esm.js:2629 -(anonymous) @ messages.uvue:760 -vFor @ vue.runtime.esm.js:6364 -f @ vue.runtime.esm.js:6718 -(anonymous) @ messages.uvue:760 -renderComponentRoot @ vue.runtime.esm.js:5065 -componentUpdateFn @ vue.runtime.esm.js:5192 -run @ vue.runtime.esm.js:180 -instance.update @ vue.runtime.esm.js:5216 -callWithErrorHandling @ vue.runtime.esm.js:1356 -flushJobs @ vue.runtime.esm.js:1563 -Promise.then (async) -queueFlush @ vue.runtime.esm.js:1472 -queueJob @ vue.runtime.esm.js:1466 -(anonymous) @ vue.runtime.esm.js:5210 -resetScheduling @ vue.runtime.esm.js:263 -trigger @ vue.runtime.esm.js:403 -set @ vue.runtime.esm.js:524 -(anonymous) @ messages.uvue:760 -callWithErrorHandling @ vue.runtime.esm.js:1356 -callWithAsyncErrorHandling @ vue.runtime.esm.js:1363 -invoke @ vue.runtime.esm.js:6223 -invoker @ vue.runtime.esm.js:6235 -mp.esm.js:529 [Vue warn]: Property "manageMode" was accessed during render but is not defined on instance. - at -(anonymous) @ mp.esm.js:529 -warn$1 @ vue.runtime.esm.js:1251 -get @ vue.runtime.esm.js:2629 -(anonymous) @ messages.uvue:760 -vFor @ vue.runtime.esm.js:6364 -f @ vue.runtime.esm.js:6718 -(anonymous) @ messages.uvue:760 -renderComponentRoot @ vue.runtime.esm.js:5065 -componentUpdateFn @ vue.runtime.esm.js:5192 -run @ vue.runtime.esm.js:180 -instance.update @ vue.runtime.esm.js:5216 -callWithErrorHandling @ vue.runtime.esm.js:1356 -flushJobs @ vue.runtime.esm.js:1563 -Promise.then (async) -queueFlush @ vue.runtime.esm.js:1472 -queueJob @ vue.runtime.esm.js:1466 -(anonymous) @ vue.runtime.esm.js:5210 -resetScheduling @ vue.runtime.esm.js:263 -trigger @ vue.runtime.esm.js:403 -set @ vue.runtime.esm.js:524 -(anonymous) @ messages.uvue:760 -callWithErrorHandling @ vue.runtime.esm.js:1356 -callWithAsyncErrorHandling @ vue.runtime.esm.js:1363 -invoke @ vue.runtime.esm.js:6223 -invoker @ vue.runtime.esm.js:6235 -mp.esm.js:529 [Vue warn]: Property "manageMode" was accessed during render but is not defined on instance. - at -(anonymous) @ mp.esm.js:529 -warn$1 @ vue.runtime.esm.js:1251 -get @ vue.runtime.esm.js:2629 -(anonymous) @ messages.uvue:760 -vFor @ vue.runtime.esm.js:6364 -f @ vue.runtime.esm.js:6718 -(anonymous) @ messages.uvue:760 -renderComponentRoot @ vue.runtime.esm.js:5065 -componentUpdateFn @ vue.runtime.esm.js:5192 -run @ vue.runtime.esm.js:180 -instance.update @ vue.runtime.esm.js:5216 -callWithErrorHandling @ vue.runtime.esm.js:1356 -flushJobs @ vue.runtime.esm.js:1563 -Promise.then (async) -queueFlush @ vue.runtime.esm.js:1472 -queueJob @ vue.runtime.esm.js:1466 -(anonymous) @ vue.runtime.esm.js:5210 -resetScheduling @ vue.runtime.esm.js:263 -trigger @ vue.runtime.esm.js:403 -set @ vue.runtime.esm.js:524 -(anonymous) @ messages.uvue:760 -callWithErrorHandling @ vue.runtime.esm.js:1356 -callWithAsyncErrorHandling @ vue.runtime.esm.js:1363 -invoke @ vue.runtime.esm.js:6223 -invoker @ vue.runtime.esm.js:6235 -mp.esm.js:529 [Vue warn]: Property "manageMode" was accessed during render but is not defined on instance. - at -(anonymous) @ mp.esm.js:529 -warn$1 @ vue.runtime.esm.js:1251 -get @ vue.runtime.esm.js:2629 -(anonymous) @ messages.uvue:760 -vFor @ vue.runtime.esm.js:6364 -f @ vue.runtime.esm.js:6718 -(anonymous) @ messages.uvue:760 -renderComponentRoot @ vue.runtime.esm.js:5065 -componentUpdateFn @ vue.runtime.esm.js:5192 -run @ vue.runtime.esm.js:180 -instance.update @ vue.runtime.esm.js:5216 -callWithErrorHandling @ vue.runtime.esm.js:1356 -flushJobs @ vue.runtime.esm.js:1563 -Promise.then (async) -queueFlush @ vue.runtime.esm.js:1472 -queueJob @ vue.runtime.esm.js:1466 -(anonymous) @ vue.runtime.esm.js:5210 -resetScheduling @ vue.runtime.esm.js:263 -trigger @ vue.runtime.esm.js:403 -set @ vue.runtime.esm.js:524 -(anonymous) @ messages.uvue:760 -callWithErrorHandling @ vue.runtime.esm.js:1356 -callWithAsyncErrorHandling @ vue.runtime.esm.js:1363 -invoke @ vue.runtime.esm.js:6223 -invoker @ vue.runtime.esm.js:6235 -mp.esm.js:529 [Vue warn]: Property "manageMode" was accessed during render but is not defined on instance. - at -(anonymous) @ mp.esm.js:529 -warn$1 @ vue.runtime.esm.js:1251 -get @ vue.runtime.esm.js:2629 -(anonymous) @ messages.uvue:760 -vFor @ vue.runtime.esm.js:6364 -f @ vue.runtime.esm.js:6718 -(anonymous) @ messages.uvue:760 -renderComponentRoot @ vue.runtime.esm.js:5065 -componentUpdateFn @ vue.runtime.esm.js:5192 -run @ vue.runtime.esm.js:180 -instance.update @ vue.runtime.esm.js:5216 -callWithErrorHandling @ vue.runtime.esm.js:1356 -flushJobs @ vue.runtime.esm.js:1563 -Promise.then (async) -queueFlush @ vue.runtime.esm.js:1472 -queueJob @ vue.runtime.esm.js:1466 -(anonymous) @ vue.runtime.esm.js:5210 -resetScheduling @ vue.runtime.esm.js:263 -trigger @ vue.runtime.esm.js:403 -set @ vue.runtime.esm.js:524 -(anonymous) @ messages.uvue:760 -callWithErrorHandling @ vue.runtime.esm.js:1356 -callWithAsyncErrorHandling @ vue.runtime.esm.js:1363 -invoke @ vue.runtime.esm.js:6223 -invoker @ vue.runtime.esm.js:6235 -mp.esm.js:529 [Vue warn]: Property "manageMode" was accessed during render but is not defined on instance. - at -(anonymous) @ mp.esm.js:529 -warn$1 @ vue.runtime.esm.js:1251 -get @ vue.runtime.esm.js:2629 -(anonymous) @ messages.uvue:760 -vFor @ vue.runtime.esm.js:6364 -f @ vue.runtime.esm.js:6718 -(anonymous) @ messages.uvue:760 -renderComponentRoot @ vue.runtime.esm.js:5065 -componentUpdateFn @ vue.runtime.esm.js:5192 -run @ vue.runtime.esm.js:180 -instance.update @ vue.runtime.esm.js:5216 -callWithErrorHandling @ vue.runtime.esm.js:1356 -flushJobs @ vue.runtime.esm.js:1563 -Promise.then (async) -queueFlush @ vue.runtime.esm.js:1472 -queueJob @ vue.runtime.esm.js:1466 -(anonymous) @ vue.runtime.esm.js:5210 -resetScheduling @ vue.runtime.esm.js:263 -trigger @ vue.runtime.esm.js:403 -set @ vue.runtime.esm.js:524 -(anonymous) @ messages.uvue:760 -callWithErrorHandling @ vue.runtime.esm.js:1356 -callWithAsyncErrorHandling @ vue.runtime.esm.js:1363 -invoke @ vue.runtime.esm.js:6223 -invoker @ vue.runtime.esm.js:6235 -mp.esm.js:529 [Vue warn]: Property "manageMode" was accessed during render but is not defined on instance. - at -(anonymous) @ mp.esm.js:529 -warn$1 @ vue.runtime.esm.js:1251 -get @ vue.runtime.esm.js:2629 -(anonymous) @ messages.uvue:760 -vFor @ vue.runtime.esm.js:6364 -f @ vue.runtime.esm.js:6718 -(anonymous) @ messages.uvue:760 -renderComponentRoot @ vue.runtime.esm.js:5065 -componentUpdateFn @ vue.runtime.esm.js:5192 -run @ vue.runtime.esm.js:180 -instance.update @ vue.runtime.esm.js:5216 -callWithErrorHandling @ vue.runtime.esm.js:1356 -flushJobs @ vue.runtime.esm.js:1563 -Promise.then (async) -queueFlush @ vue.runtime.esm.js:1472 -queueJob @ vue.runtime.esm.js:1466 -(anonymous) @ vue.runtime.esm.js:5210 -resetScheduling @ vue.runtime.esm.js:263 -trigger @ vue.runtime.esm.js:403 -set @ vue.runtime.esm.js:524 -(anonymous) @ messages.uvue:760 -callWithErrorHandling @ vue.runtime.esm.js:1356 -callWithAsyncErrorHandling @ vue.runtime.esm.js:1363 -invoke @ vue.runtime.esm.js:6223 -invoker @ vue.runtime.esm.js:6235 -mp.esm.js:529 [Vue warn]: Property "manageMode" was accessed during render but is not defined on instance. - at -(anonymous) @ mp.esm.js:529 -warn$1 @ vue.runtime.esm.js:1251 -get @ vue.runtime.esm.js:2629 -(anonymous) @ messages.uvue:760 -vFor @ vue.runtime.esm.js:6364 -f @ vue.runtime.esm.js:6718 -(anonymous) @ messages.uvue:760 -renderComponentRoot @ vue.runtime.esm.js:5065 -componentUpdateFn @ vue.runtime.esm.js:5192 -run @ vue.runtime.esm.js:180 -instance.update @ vue.runtime.esm.js:5216 -callWithErrorHandling @ vue.runtime.esm.js:1356 -flushJobs @ vue.runtime.esm.js:1563 -Promise.then (async) -queueFlush @ vue.runtime.esm.js:1472 -queueJob @ vue.runtime.esm.js:1466 -(anonymous) @ vue.runtime.esm.js:5210 -resetScheduling @ vue.runtime.esm.js:263 -trigger @ vue.runtime.esm.js:403 -set @ vue.runtime.esm.js:524 -(anonymous) @ messages.uvue:760 -callWithErrorHandling @ vue.runtime.esm.js:1356 -callWithAsyncErrorHandling @ vue.runtime.esm.js:1363 -invoke @ vue.runtime.esm.js:6223 -invoker @ vue.runtime.esm.js:6235 -mp.esm.js:529 [Vue warn]: Property "manageMode" was accessed during render but is not defined on instance. - at -(anonymous) @ mp.esm.js:529 -warn$1 @ vue.runtime.esm.js:1251 -get @ vue.runtime.esm.js:2629 -(anonymous) @ messages.uvue:760 -vFor @ vue.runtime.esm.js:6364 -f @ vue.runtime.esm.js:6718 -(anonymous) @ messages.uvue:760 -renderComponentRoot @ vue.runtime.esm.js:5065 -componentUpdateFn @ vue.runtime.esm.js:5192 -run @ vue.runtime.esm.js:180 -instance.update @ vue.runtime.esm.js:5216 -callWithErrorHandling @ vue.runtime.esm.js:1356 -flushJobs @ vue.runtime.esm.js:1563 -Promise.then (async) -queueFlush @ vue.runtime.esm.js:1472 -queueJob @ vue.runtime.esm.js:1466 -(anonymous) @ vue.runtime.esm.js:5210 -resetScheduling @ vue.runtime.esm.js:263 -trigger @ vue.runtime.esm.js:403 -set @ vue.runtime.esm.js:524 -(anonymous) @ messages.uvue:760 -callWithErrorHandling @ vue.runtime.esm.js:1356 -callWithAsyncErrorHandling @ vue.runtime.esm.js:1363 -invoke @ vue.runtime.esm.js:6223 -invoker @ vue.runtime.esm.js:6235 \ No newline at end of file diff --git a/验收清单.md b/验收清单.md new file mode 100644 index 00000000..dfcf5628 --- /dev/null +++ b/验收清单.md @@ -0,0 +1,238 @@ +# 配送端"待接单"Tab 修复验收清单 + +## 修复内容总结 + +### 1. 后端 SQL 修复 (20260610_fix_delivery_pending_blank.sql) +- ✅ 为 `hss_service_orders` 表添加缺失列:`request_id`, `service_category`, `price`, `staff_income`, `elder_name`, `elder_phone`, `duration_minutes` 等 +- ✅ 扩展 CHECK 约束,允许 `pending_assignment` 和 `pending_accept` 状态 +- ✅ 重写 `delivery_build_order_json()` 函数,所有展示字段都有兜底值 +- ✅ 重写 `delivery_get_legacy_order_json()` 函数 +- ✅ 重写 `rpc_delivery_order_list()` 函数 + +### 2. 历史数据回填 (20260610_backfill_pending_orders_core_fields.sql) +- ✅ 安全回填 `pending_assignment` 和 `pending_accept` 订单的核心字段 +- ✅ 使用 COALESCE/NULLIF 防止覆盖已有非空字段 +- ✅ 包含预检查和后检查 SQL 查询 + +### 3. 未来订单链路修复 (医疗-consumer/services/serviceOrderService.uts) +- ✅ `createServiceOrder()` 插入 `hss_service_orders` 时添加以下字段: + - `service_category`: 使用 `params.service.category`,默认为 '居家服务' + - `elder_name`: 使用 `params.recipientName` + - `elder_phone`: 使用 `params.recipientPhone` + - `price`: 使用 `payableAmount` + - `staff_income`: 使用 `payableAmount` + - `duration_minutes`: 固定为 90 + - `scheduled_at`: 使用 `appointmentTime` + - `request_id`: 空字符串 + +### 4. 前端展示兜底 (医疗-delivery/api/delivery.uts) +- ✅ `mapRpcOrderItemCompat()` 函数添加安全兜底: + - `serviceName` 为空 → 显示 '居家服务订单' + - `elderName` 为空 → 显示 `contactName` 或 '服务对象待补充' + - `address` 为空 → 显示 '地址待补充' + - `appointmentTime` 为空 → 使用 `createdAt` 或 '时间待补充' + - `staffIncome` 为空 → 显示 0 +- ✅ `enrichOrderWithRequestFallback()` 限制日志输出:只输出前3个缺少 `request_id` 的订单警告日志 + +--- + +## 验收步骤 + +### 步骤 1:执行 SQL 迁移文件 +在 Supabase SQL Editor 中按顺序执行以下文件: + +```sql +-- 1. 修复表结构和 RPC 函数 +-- 文件:mall_sql/migrations/20260610_fix_delivery_pending_blank.sql +-- 说明:添加缺失列、扩展 CHECK 约束、重写 RPC 函数 + +-- 2. 回填历史数据 +-- 文件:mall_sql/migrations/20260610_backfill_pending_orders_core_fields.sql +-- 说明:安全回填 pending_assignment 和 pending_accept 订单的核心字段 +``` + +**验证查询:** +```sql +-- 检查表结构 +SELECT column_name, data_type +FROM information_schema.columns +WHERE table_name = 'hss_service_orders' +AND column_name IN ('request_id', 'service_category', 'price', 'staff_income', 'elder_name', 'elder_phone', 'duration_minutes') +ORDER BY column_name; + +-- 检查 CHECK 约束 +SELECT conname, pg_get_constraintdef(oid) +FROM pg_constraint +WHERE conrelid = 'hss_service_orders'::regclass +AND contype = 'c'; + +-- 检查 RPC 函数是否存在 +SELECT routine_name +FROM information_schema.routines +WHERE routine_schema = 'public' +AND routine_name LIKE 'delivery_%' +ORDER BY routine_name; + +-- 检查待接单订单数量 +SELECT COUNT(*) as pending_count +FROM hss_service_orders +WHERE status IN ('pending_assignment', 'pending_accept') +AND deleted_at IS NULL; +``` + +### 步骤 2:测试待接单 Tab 显示 +1. 打开配送端 App +2. 登录服务人员账号 +3. 进入"订单"页面 +4. 切换到"待接单"Tab +5. **预期结果:** + - 订单卡片显示完整信息: + - 服务名称:显示具体服务名称或 '居家服务订单' + - 服务对象:显示姓名或 '服务对象待补充' + - 预约时间:显示具体时间或 '时间待补充' + - 地址:显示详细地址或 '地址待补充' + - 预计收入:显示具体金额(不再显示 ¥0) + - 联系电话:显示联系人电话 + - 控制台不再大量输出 "request fallback skipped: missing request_id" 日志 + - 最多只输出前3条此类警告日志 + +### 步骤 3:测试其他 Tab 功能 +1. **今天订单 Tab**:确保仍然正常工作 +2. **历史订单 Tab**:确保仍然正常工作 +3. **全部订单 Tab**:确保仍然正常工作 + +### 步骤 4:测试订单操作功能 +1. **接单**:点击"接单"按钮,验证功能正常 +2. **拒单**:点击"拒单"按钮,验证功能正常 +3. **签到**:到达服务地点后签到,验证功能正常 +4. **开始服务**:点击"开始服务",验证功能正常 +5. **完成服务**:点击"完成服务",验证功能正常 + +### 步骤 5:测试新订单创建链路 +1. 在 consumer 端创建一个新的居家服务订单 +2. 等待自动派单(或手动派单) +3. 在配送端查看该订单 +4. **预期结果:** + - 订单信息显示完整,不再缺失核心字段 + - 控制台无异常日志 + +### 步骤 6:验证数据一致性 +```sql +-- 检查 pending_assignment 订单的核心字段填充情况 +SELECT + id, + service_name, + service_category, + elder_name, + contact_name, + price, + staff_income, + duration_minutes, + appointment_time, + status +FROM hss_service_orders +WHERE status IN ('pending_assignment', 'pending_accept') +AND deleted_at IS NULL +ORDER BY created_at DESC +LIMIT 10; + +-- 检查是否有订单仍然缺少核心字段 +SELECT COUNT(*) as missing_core_info +FROM hss_service_orders +WHERE status IN ('pending_assignment', 'pending_accept') +AND deleted_at IS NULL +AND ( + service_name = '' OR + service_name IS NULL OR + elder_name = '' OR + elder_name IS NULL OR + price IS NULL OR + price = 0 +); +``` + +--- + +## 注意事项 + +1. **不要修改页面样式**:本次修复只涉及数据层面,不改变任何 UI 样式 +2. **不要删除现有功能**:确保"今天订单"、"历史订单"、"接单"、"拒单"、"签到"、"开始服务"等功能正常工作 +3. **RLS 策略**:不要修改已有的 RLS 策略,除非发现明确的安全问题 +4. **向后兼容**:确保修复不影响已有的订单数据 +5. **日志控制**:控制台日志输出受到限制,避免刷屏 + +--- + +## 回滚方案 + +如果修复后出现问题,可以按以下步骤回滚: + +1. **回滚 SQL 变更:** +```sql +-- 删除添加的列(如果列存在) +ALTER TABLE public.hss_service_orders +DROP COLUMN IF EXISTS request_id, +DROP COLUMN IF EXISTS service_category, +DROP COLUMN IF EXISTS price, +DROP COLUMN IF EXISTS staff_income, +DROP COLUMN IF EXISTS elder_name, +DROP COLUMN IF EXISTS elder_phone, +DROP COLUMN IF EXISTS duration_minutes; + +-- 恢复原始 CHECK 约束 +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_original CHECK ( + status IN ( + 'created', 'paid', 'assigned', 'accepted', 'rejected', 'departed', 'arrived', + 'in_service', 'completed', 'pending_acceptance', 'accepted_by_user', + 'reviewed', 'settled', 'cancelled', 'exception' + ) +); +``` + +2. **回滚前端代码:** + - 使用 Git 回滚 `医疗-delivery/api/delivery.uts` 的修改 + - 使用 Git 回滚 `医疗-consumer/services/serviceOrderService.uts` 的修改 + +3. **重启服务:** + - 重启配送端 App + - 清除缓存并重新登录 + +--- + +## 常见问题 + +### Q1: 为什么待接单 Tab 显示空白? +**A:** 根因是 `hss_service_orders` 表缺少 `service_category`、`price`、`staff_income`、`elder_name` 等列,导致 RPC 函数返回的订单数据核心字段为空。 + +### Q2: 为什么控制台大量输出 "request fallback skipped: missing request_id"? +**A:** 因为 `hss_service_orders` 表没有 `request_id` 列,前端尝试从 `ec_service_requests` 表回填数据时失败。修复后已限制日志输出,最多只显示前3条警告。 + +### Q3: 为什么价格显示为 ¥0? +**A:** 因为 `price` 和 `staff_income` 字段不存在,RPC 函数返回默认值 0。修复后会在创建订单时写入正确的价格。 + +### Q4: 回填 SQL 会覆盖已有数据吗? +**A:** 不会。回填 SQL 使用 `COALESCE` 和 `NULLIF` 函数,只填充 NULL 或空字符串的字段,不会覆盖已有的非空数据。 + +### Q5: 修复后旧订单会显示正常吗? +**A:** 会。通过执行回填 SQL,历史订单的核心字段会被安全填充。即使不执行回填,前端展示兜底也会显示友好的提示信息(如'地址待补充')。 + +--- + +## 完成标志 + +✅ 所有验收步骤通过 +✅ 待接单 Tab 显示完整订单信息 +✅ 控制台无大量重复警告日志 +✅ 其他 Tab 和功能正常工作 +✅ 新创建的订单信息完整 +✅ 数据一致性验证通过 + +--- + +**最后更新**: 2026-06-10 +**负责人**: AI 编码助手 +**状态**: 待验收