易到家 Product Audit
全项目审计 · 与仓库同步

System, Business Logic & Security Review

产品功能、业务逻辑与整改审计

基于当前代码库(Expo Router + Supabase Postgres / RPC / Edge)的全量盘点,覆盖 src/、supabase/migrations(98 个迁移,以每个函数的最新定义为准)与 supabase/functions。金额单位统一为分(fen);状态跃迁与账务以 SQL RPC 为准。功能卡片可点击展开查看具体规则与公式。

订单状态 13 新增 refund_pending(退款审核中)
Definer 函数 111 扫描脚本 npm run audit:definer 无可疑项
未清零 P0 / P1 – 含部分整改项
已整改 – tsc 0 错误;自动化测试仍为 0

架构与现状

  • 客户端:Expo SDK 58 / React Native 0.88 / Expo Router / NativeWind / Zustand / i18next(8 语种,顾客端键完全对齐;技师/商家/平台后台仍大量硬编码中文)
  • 后端:Supabase Auth + Postgres(PostGIS)+ RLS + SECURITY DEFINER RPC;前端路由守卫只管显示, 真正边界在 RLS 与 RPC 内的 is_platform_admin() / my_worker_id() / my_merchant_id()
  • Edge Functions:phone-auth / oauth-auth / oss-signature / check-text / enforce-timeouts
  • 10-30 之后新落地:多券叠加与受众/有效期、技师拒券、邀请奖励单(审核 + 有效消费 + 现金)、工单「联系客服」、 评价回复与昵称/展示名审核、余额真实扣款、收银台多支付方式 + 演示模式、待接单超时自动退款、 退款审批(refund_requests)、服务超时自动结束 + 默认好评、后台名录订单统计、 财务账本(托管资金 vs 平台收入)、目录多语言
  • 支付现状:余额为真实扣款/退回;微信、支付宝、Apple、云闪付仅在 customer_pay_demo_mode=true 时为模拟支付(默认与兜底已改为 false,但库里已存的 true 需在后台手动关闭),真实网关尚未接入
  • 11-01 整改:迁移 20261101190000–20261101230000 与 Edge / 客户端改动已提交, 尚未部署(需执行 supabase db push 并重新部署 Edge Functions)。下方问题清单逐条标注了整改状态。
Customer

下单 · 多券叠加 · 余额/模拟支付 · 退款申请 · 邀请 · 工单 · 评价

Worker

接单 · 履约 · 拒券 · 拉黑 · 回复评价 · 钱包提现

Merchant

团队 · 改派 · 拉黑筛选 · 保证金 · 钱包提现

Platform

退款/提现审批 · 财务账本 · 邀请审核 · 发券 · 工单 · 内容审核 · 审计

02 · Must Fix Now

紧急结论:4 个 P0

这四项都可以被普通用户(部分甚至未登录)直接利用,影响资金或密钥。代码已在 20261101190000_security_lockdown.sql 中修复,部署后生效;演示支付还需在后台把已存的开关关掉。详见 整改路线图。

03 · Feature Inventory

功能清单(按角色)

可按角色筛选;点击卡片展开具体逻辑、规则与公式。路径为应用内路由 / RPC。标「新」的为 10-30 之后上线。

04 · Order State Machine

订单生命周期与权限

状态跃迁由行守卫 guard_order_protected_columns 与 RPC create_order / pay_order_mock / cancel_my_order / advance_my_worker_order / admin_close_order / admin_review_refund_request / enforce_order_timeouts 共同约束。

create_order → pending_payment → pending_accept → accepted → en_route → arrived
serving → awaiting_review → completed (accepted / en_route / arrived 顾客取消 → refund_pending → 批准 refunded | 驳回 回到原状态)
rejected cancelled payment_expired refunded
跃迁 触发方 机制 / 备注
— → pending_payment 顾客 create_order(9 参,含 p_user_coupon_ids[]);快照价格、券、分佣;地址 FOR UPDATE
pending_payment → pending_accept 顾客 pay_order_mock:方式须在 customer_pay_methods 内;余额真实 wallet_debit;其余方式仅演示模式开启时可用
pending_payment → cancelled | payment_expired 顾客 / 系统 顾客取消;或超过 unpaid_order_auto_cancel_minutes(15)自动过期;触发器退券
pending_accept → refunded 顾客 / 技师 / 系统 顾客取消、技师拒单、或付款后 pending_accept_auto_refund_minutes(30)无人接单 → 全额自动退,写 refund_requests
accepted | en_route | arrived → refund_pending 顾客 cancel_my_order → submit_order_refund_for_review;技师此时不可推进订单
refund_pending → refunded | 原状态 平台 admin_review_refund_request:批准按「总额 − 路费 − 误工费」退;驳回须填原因并恢复原状态
pending_accept → accepted 技师 advance_my_worker_order;须已签《技师合作协议》且带有效定位
accepted → en_route → arrived → serving → awaiting_review 技师 每步写入位置证据;顾客可提前结束服务(客户端直接 UPDATE,见问题清单)
serving → awaiting_review 系统 新:开始时间 + 项目时长 + serving_auto_finish_grace_minutes(30)仍未结束则自动结束
awaiting_review → completed 顾客评价 / 系统 新:超过 order_auto_complete_days(3 天)未评价 → 写入五星「系统默认好评」并完成;随后结算入账并冻结
任意已支付 → refunded(平台) 平台 admin_close_order:refund_pending 时视为批准申请;其余走自动退款并写审计。先过 order_refund_block_reason(已评价 / 已解冻则拒绝)

05 · Refunds & Timeouts

退款审批与超时治理

所有退款都落一条 refund_requests(自动退款记为 auto_approved=true、状态 refunded),资金页可查全量。 迁移:20261031240000_refund_request_review.sql。

场景 是否人工审核 实退金额 来源 source
待接单时顾客取消 否,自动 全额 customer_cancel
技师拒单 否,自动 全额 worker_reject
付款后 30 分钟无人接单 否,巡检自动 全额 accept_timeout
已接单 / 已出发 / 已到达时顾客取消 是,进 refund_pending;48 小时未处理自动批准 max(总额 − 往返路费 − 误工费, 0);扣留部分分给技师 / 商家 / 平台 customer_cancel
平台关单(含服务中 / 待评价 / 冻结期内已完成) 平台已决定 前三阶段按规则;之后全额且不退券 admin_close
已评价,或已完成且提成已解冻 硬拦,不可退(顾客与后台统一)

超时巡检 enforce_order_timeouts()(pg_cron 每 5 分钟 + Edge 双通道)

  • 未支付过期:pending_payment 超过 15 分钟 → payment_expired,触发器退券。
  • 无人接单退款:已付 pending_accept 超过 30 分钟 → 自动全额退款。
  • 退款审核超时:refund_pending 超过 refund_review_auto_approve_hours(默认 48,0 关闭)→ 自动批准并按规则分账。
  • 服务超时结束:serving 超过项目时长 + 30 分钟 → awaiting_review;时长缺失时取服务目录,再缺按 120 分钟。
  • 默认好评:awaiting_review 超过 3 天 → 五星「系统默认好评」(is_system,不计平均分)+ completed。
  • 解冻:commission_holds.release_at ≤ now() → 解冻到可用余额,之后订单不可再退。
  • 每一步失败写入 job_errors(订单 id、步骤、错误信息),仅管理员可读;后台页面待做。

06 · Worker ↔ Customer Block

技师拉黑顾客

关系表 worker_customer_blocks;维度是技师–顾客对,不是订单状态字段。迁移:20261030170000_worker_customer_blocks.sql。

环节 规则 入口 / RPC
发起条件 本人订单 · status ∈ awaiting_review | completed · 未有生效拉黑 技师订单卡「拉黑客户」· block_customer_on_my_order
原因码 harassment · rude · safety · fraud · unreasonable · privacy · other(须备注 ≥2 字) reason_code + 可选 reason_note
生效拦截 新下单拒绝(顾客见「该技师暂不可预约」);商家改派到已拉黑技师拒绝;进行中旧单继续履约 触发器 guard_order_worker_customer_block
解除 技师本人 / 旗下商家 / 平台管理员 unblock_customer_for_my_worker · merchant_lift_… · admin_lift_…
审计 target_type=user · ban/unban · 快照 kind=worker_customer_block log_admin_action

07 · Pricing & Commission

计费与分佣公式

公式与 SQL 最新实现对齐;客户端只做展示预估,最终金额以 create_order 计算为准。

A. 订单应付总额(多券叠加)

service_fee = COALESCE(worker_services.price_fen, services.price_fen)
travel_fee  = travel_fee_fen_for_meters(one_way_m, is_night)
remaining   = service_fee
for coupon in DISTINCT(p_user_coupon_ids):   // 顺序不保证
  require (service_fee + tip) ≥ min_spend    // 每张都按原价判断门槛
  d = coupon_deduction(coupon, remaining)    // ≤ remaining
  remaining −= d
coupon      = service_fee − remaining        // 路费不可抵
total_fee   = service_fee + travel_fee + tip_fee − coupon

任一张 is_stackable=false 的券必须单独使用;技师可在 worker_disabled_coupons 拒收某模板。

B. 服务价浮动边界

delta = mode=='percent'
      ? round(price_fen × value / 100)   // value: 1..100
      : value                            // fen, ≤ price_fen
minFen = max(0, price_fen − delta)
maxFen = price_fen + delta

源:priceFloat.ts ≡ service_price_float_delta_fen

C. 往返路费

if one_way ≤ 1000m → 0
units = 100000                       // 起步 10 元(0.01 分单位)
if m > 3000:  units += (min(m,10000)−3000) × 25   // 2.5 元/km
if m > 10000: units += (m−10000) × 30             // 3 元/km
if night:     units = units × 6 / 5               // +20%
one_way_fen = ⌊(units+9999)/10000⌋ × 100          // 按元进位
travel_fee  = one_way_fen × 2

夜间:本地时区 23:00–05:00(默认 Asia/Shanghai)。src/utils/travelFee.ts 与 SQL 一致但无人引用。

D. 抽成比例解析 resolve_order_take_rates

platform_ratio =
  商家技师 ? COALESCE(merchant.commission_ratio, default_m)
           : default_m
worker_ratio =
  商家技师 ? COALESCE(个人, merchant.worker_commission_ratio, default_w)
           : COALESCE(个人, default_w)

default_m = merchant_commission_rate / 100     // 默认 20%
default_w = worker_commission_rate / 100       // 默认 70%
travel_platform_ratio = travel_fee_platform_rate / 100 // 默认 0%

E. 分佣拆分 split_order_commission(券成本按比例共担)

base = (service_fee − coupon) + tip_fee   // 券后服务费 + 小费;不含路费
platform_from_base = min(round(base × platform_ratio), base)
remainder          = base − platform_from_base
worker_from_base   = min(round(remainder × worker_ratio), remainder)
merchant_from_base = remainder − worker_from_base   // 吃掉舍入尾差
若无商家: platform 吞并 merchant 份额

platform_from_travel = min(round(travel × travel_platform_ratio), travel)
worker_from_travel   = travel − platform_from_travel   // 商家不参与路费

每 ¥1 券成本:平台 p · 技师 (1−p)·w · 商家 (1−p)(1−w)
默认(有商家)≈ 0.20 / 0.56 / 0.24;无商家 ≈ 0.44 / 0.56
结算: platform → ledger(order_commission);退款 → order_commission_reversal

F. 退款实退 compute_order_refund_fen

pending_accept:
  refund = total_fee                                  // 自动

accepted | en_route | arrived:
  refund = max(total_fee − travel_fee − idle_fee, 0)  // 进人工审核,48h 未处理自动批准
  idle_fee = orders.cancel_idle_fee_fen               // 下单时快照 order_cancel_idle_fee_fen(默认 5000)

扣留分账 settle_order_cancel_compensation:
  retained    = total_fee − refund
  travel_part = min(travel_fee, retained)             // 先算路费
  idle_part   = retained − travel_part                // 误工费按服务费处理
  路费:平台 = round(travel_part × travel_fee_platform_rate%),其余给技师(默认 0% → 全给技师)
  误工费:平台 = round(idle_part × platform_ratio)
          技师 = round((idle_part − 平台) × worker_ratio),余下给商家(无商家则归平台)
  技师 / 商家 → wallet_credit('income') + 冻结;平台 → platform_ledger_entries('order_commission')

serving | awaiting_review | completed(冻结期内、未评价):
  顾客不可退;admin_close 可强制全额,不退券

硬拦 order_refund_block_reason:
  已有 reviews → 不可退;completed 且无未释放 commission_holds → 不可退

余额支付 → wallet_credit(user, refund);其他方式 → 仅改状态(模拟)
退款 → release_user_coupon_for_order(券退回 available;后台强制退服务中及之后的单除外)

顾客实付 = 实退 + 技师所得 + 商家所得 + 平台所得,分配结果存于订单 cancel_comp_*_fen,后台订单详情可查。 本次迁移同时为历史上已批准的此类退款补记了分账。

G. 提现手续费

fee_fen    = round(amount_fen × min(rate%, 100) / 100)
payout_fen = amount_fen − fee_fen
约束: fee < amount;可用余额 ≥ amount
服务端触发器 assert_withdrawal_policy 校验最低额 / 开放日 / 时间窗 / 收款方式
仅技师、商家钱包可提现

H. 保证金应缴

required = COALESCE(
  deposit_overrides(owner, city),
  city_deposit_defaults[city],
  sys.{merchant|worker}_deposit_fen
)
shortfall = max(required − paid, 0)

商家按城市分户;技师共享一笔余额覆盖各城门槛;不进可提现钱包。

系统配置键 单位 默认 用途

08 · Money Flow & Books

资金流与财务账本

  1. 下单快照

    锁定服务费、路费、小费、券抵扣与三方分佣金额;占用排班;券标记已用。

  2. 支付

    余额:wallet_debit(user, total_fee),交易号 bal_…。微信 / 支付宝 / Apple / 云闪付:演示模式下只写 payment_method / paid_at,交易号由服务端生成。

  3. 退款

    先过硬拦;已结算则 reverse_order_settlement 冲回技师/商家并记 order_commission_reversal;余额支付按实退 wallet_credit(user);券退回(后台强制退服务中及之后的单除外);写 refund_requests。接单后取消扣下的路费 + 误工费由 settle_order_cancel_compensation 分给技师 / 商家(入钱包并冻结)与平台(记 order_commission)。

  4. 履约完成结算

    settle_order_amounts:技师 / 商家 wallet_credit + commission_holds 冻结(默认 24h);平台 order_commission 入账本。

  5. 解冻与提现

    解冻后不可再退款。提现时插入 withdrawals,触发器冻结全额 → 审核 → 打款扣 payout + fee,fee 记 withdrawal_fee。顾客钱包(邀请现金、余额退款)只能用于余额支付,不能提现。

  6. 财务报表(新)

    admin_finance_statement:托管资金 = 各钱包可用 + 冻结,按期初 / 收入 / 支出 / 期末与「轧差」校验;平台确认收入 = 抽成 + 提现手续费 − 冲正。冻结 / 解冻只是搬家,不计收支。admin_list_finance_entries 合并钱包流水与平台账本。

  7. 保证金旁路

    deposit 独立账本,不进入可提现 wallets;pay_my_deposit 仍为模拟缴纳。

Customer ──balance(debit) / demo(mock)──► Order(snapshot − coupons)
   ▲          │ cancel / reject / timeout / admin_close            │ completed (review or 3d default)
   │          ▼                                                    ├──────────────┬────────────────┐
   │   refund_requests ──► refund to user wallet (balance only)    platform_ledger  worker wallet    merchant wallet
   │   + commission_reversal (if settled) + coupon released        (commission)     + freeze 24h     + freeze 24h
   │   retained travel + idle fee ──► split: platform_ledger / worker / merchant (+ freeze)
   │                                                                                 └── withdrawal ──┘
   └── invite cash reward (wallet_credit) ──► user wallet (balance pay only)                 │ fee → ledger

09 · Findings

问题清单

按严重度、领域与整改状态筛选;每条附位置、整改方式与本轮处理结果。P0 = 可被直接利用造成资金 / 密钥损失;P1 = 上线真实收款前必须修;P2 = 体验、口径与可维护性。

10 · Remediation Plan

整改路线图

按「立即 → 上线真实收款前 → 迭代优化」三个阶段推进;每一阶段完成后用冒烟脚本回归退款、提现与邀请链路。

本轮已提交的整改迁移(待 supabase db push)

20261101190000_security_lockdown.sql
  收回退款 / 发券 / 邀请 / 探测类内部函数的客户端执行权;合同与评分辅助函数不对外
  sys_settings 按键名屏蔽密钥(secret / private_key / api_v3_key / pnvs_h5_)
  pay_order_mock 交易号服务端生成,演示模式兜底 false;my_contract_status 只查本人

20261101200000_cancel_compensation_and_jobs.sql
  误工费下单快照;接单后取消扣留按规则分给技师 / 商家 / 平台(并补记历史)
  强制退款服务中及之后不退券;refund_pending 超时自动批准;job_errors 落表
  默认好评 is_system 不计平均分;服务时长缺失兜底

20261101210000_growth_content_hardening.sql
  邀请字段受保护;兑换码独立表;补发券校验有效消费;评价改动复审、驳回回复隔离
  券按选择顺序抵扣;工单号加锁、终态不可回退;系统设置与券模板写审计

20261101220000_customer_review_rpc_and_order_guard.sql
  submit_order_review 一次写评价并完成订单;订单守卫补充受保护列

20261101230000_sms_rate_limit.sql
  短信按号码 / IP 限流,校验失败 5 次锁定

没有采用「默认收回所有函数执行权」:现有大量 RPC 依赖默认授权,改为用 npm run audit:definer 扫描新增的 SECURITY DEFINER 函数。 支付私钥仍建议迁到 Supabase Secrets;若后台曾填写过密钥,部署后建议在渠道侧轮换。

11 · Engineering Health

工程质量指标

指标 现状 建议

12 · Interactive Sandbox

公式沙盘

按当前仓库规则试算路费、退款与分佣(本地计算,不连后端)。

路费试算

退款试算

分佣试算(含券)