System, Business Logic & Security Review
产品功能、业务逻辑与整改审计
基于当前代码库(Expo Router + Supabase Postgres / RPC / Edge)的全量盘点,覆盖
src/、supabase/migrations(98 个迁移,以每个函数的最新定义为准)与
supabase/functions。金额单位统一为分(fen);状态跃迁与账务以
SQL RPC 为准。功能卡片可点击展开查看具体规则与公式。
架构与现状
- 客户端: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)。下方问题清单逐条标注了整改状态。
下单 · 多券叠加 · 余额/模拟支付 · 退款申请 · 邀请 · 工单 · 评价
接单 · 履约 · 拒券 · 拉黑 · 回复评价 · 钱包提现
团队 · 改派 · 拉黑筛选 · 保证金 · 钱包提现
退款/提现审批 · 财务账本 · 邀请审核 · 发券 · 工单 · 内容审核 · 审计
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 共同约束。
| 跃迁 | 触发方 | 机制 / 备注 |
|---|---|---|
| — → 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
资金流与财务账本
-
下单快照
锁定服务费、路费、小费、券抵扣与三方分佣金额;占用排班;券标记已用。
-
支付
余额:
wallet_debit(user, total_fee),交易号bal_…。微信 / 支付宝 / Apple / 云闪付:演示模式下只写payment_method/paid_at,交易号由服务端生成。 -
退款
先过硬拦;已结算则
reverse_order_settlement冲回技师/商家并记order_commission_reversal;余额支付按实退wallet_credit(user);券退回(后台强制退服务中及之后的单除外);写refund_requests。接单后取消扣下的路费 + 误工费由settle_order_cancel_compensation分给技师 / 商家(入钱包并冻结)与平台(记order_commission)。 -
履约完成结算
settle_order_amounts:技师 / 商家wallet_credit+commission_holds冻结(默认 24h);平台order_commission入账本。 -
解冻与提现
解冻后不可再退款。提现时插入
withdrawals,触发器冻结全额 → 审核 → 打款扣 payout + fee,fee 记withdrawal_fee。顾客钱包(邀请现金、余额退款)只能用于余额支付,不能提现。 -
财务报表(新)
admin_finance_statement:托管资金 = 各钱包可用 + 冻结,按期初 / 收入 / 支出 / 期末与「轧差」校验;平台确认收入 = 抽成 + 提现手续费 − 冲正。冻结 / 解冻只是搬家,不计收支。admin_list_finance_entries合并钱包流水与平台账本。 -
保证金旁路
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
公式沙盘
按当前仓库规则试算路费、退款与分佣(本地计算,不连后端)。