From e70211f1d2c6b536c92133c70d0e6f908fdf21e7 Mon Sep 17 00:00:00 2001 From: not-like-juvenile <16056107+not-like-juvenile@user.noreply.gitee.com> Date: Thu, 12 Mar 2026 18:05:32 +0800 Subject: [PATCH] =?UTF-8?q?=E5=90=8C=E6=AD=A5=E4=BF=AE=E6=94=B9=E9=A1=B5?= =?UTF-8?q?=E9=9D=A2=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- BACKEND_MIGRATION_PLAN.md | 152 ++++++++++++++++++ pages/mall/delivery/test/api-simulator.uvue | 3 +- .../delivery/test/consumer-order-list.uvue | 8 + .../delivery/test/merchant-order-list.uvue | 18 ++- pages/mall/delivery/test/mock-service.uts | 102 ++++++++++-- .../webhook-server/PARTNER_ONBOARDING.md | 84 ++++++++++ .../webhook-server/webhook-receiver.js | 92 +++++++++-- server/push-server.js | 57 ++++++- utils/store.uts | 50 +++++- 9 files changed, 522 insertions(+), 44 deletions(-) create mode 100644 BACKEND_MIGRATION_PLAN.md create mode 100644 pages/mall/delivery/webhook-server/PARTNER_ONBOARDING.md diff --git a/BACKEND_MIGRATION_PLAN.md b/BACKEND_MIGRATION_PLAN.md new file mode 100644 index 00000000..1d8ab626 --- /dev/null +++ b/BACKEND_MIGRATION_PLAN.md @@ -0,0 +1,152 @@ +# 后端收敛与防护设计(最小可落地方案) + +说明:本文档针对当前仓库中前端直接写 Supabase(存在风险:前端使用 service_role key)问题,提供可执行的最小后台设计、接口与 DB 改造清单,供开发/运维快速落地。 + +--- + +## 目标(Why) +- 阻断前端持有 `service_role` 导致的越权写库风险。 +- 将关键业务写入(接单/订单状态/资金/库存等)收敛到可信后端,保证原子性、幂等和审计。 +- 在数据库侧加入最后一道防线(约束 + RLS + RPC)。 + +--- + +## 概要执行步骤(What / High level) +1. 立刻移除前端的 `service_role`,前端只用 `anon`(阻断最大风险)。 +2. 实现最小后台 API(先做配送接单/状态流转 rpc + HTTP 接口)。 +3. 在数据库添加约束、RLS 策略与 RPC(把关键状态流转做成原子函数)。 +4. 前端分阶段切换到新 API 并回归测试。 +5. 补充审计、幂等与监控,逐步迁移其它敏感写入。 + +--- + +## 详细步骤(可直接执行) + +### A. 紧急措施(立即) +- 编辑 `ak/config.uts`:注释或删除明文 `SUPA_KEY`(service_role),替为 anon 或从构建环境注入。 +- 确认前端不再把 service_role 打包发布(CI/构建流水线更新)。 + +### B. 最小后端 API(优先交付) +在 `server/` 下新增 `server/routes/delivery.js`(或 Fastify 插件)。 + +推荐接口(统一返回 `{ ok, code?, message?, data? }`): +- POST /api/v1/delivery/accept-task + - body: { task_id, action_id?: string } + - auth: Bearer user_jwt + - 后端流程:验证 token → 验证司机权限 → 调用 RPC `rpc_accept_task(uid, task_id, action_id)` → 返回 task 新状态 +- POST /api/v1/delivery/update-status + - body: { task_id, new_status, action_id, metadata? } + - 后端流程:验证 → 调用 `rpc_update_delivery_status(...)`(RPC 内部做多表事务) +- POST /api/v1/notifications/express/create + - body: notification payload + message_id/action_id(幂等) + - 后端流程:幂等检查 -> 插入 -> 触发 push(内部队列) + +后端实现要点: +- 鉴权:解析并验证前端 Supabase JWT,取 `uid` 作为操作人。 +- 内部调用:后端使用环境变量 `SERVICE_ROLE_KEY` 或直接调用 RPC(推荐)执行受权写入。 +- 返回格式统一并带错误码,便于前端处理与监控。 + +### C. Postgres RPC(示例 SQL) +下面为 `rpc_accept_task` 草稿,供 DB 管理员在 Dev 环境调整与测试: + +```sql +-- rpc_accept_task: driver 领取任务(幂等 + 原子) +CREATE OR REPLACE FUNCTION public.rpc_accept_task(p_driver_uuid uuid, p_task_uuid uuid, p_action_id text) +RETURNS jsonb +LANGUAGE plpgsql +SECURITY DEFINER +AS $$ +DECLARE + v_task RECORD; +BEGIN + -- 幂等:插入到去重表,若已存在则返回已处理结果 + INSERT INTO action_dedupe(action_id, created_at) + VALUES (p_action_id, now()) + ON CONFLICT (action_id) DO NOTHING; + + -- 执行原子更新:仅当 task 可被领取(status=1 且 driver_id IS NULL) + WITH u AS ( + UPDATE ml_delivery_tasks + SET driver_id = p_driver_uuid, status = 2, updated_at = now() + WHERE id = p_task_uuid AND status = 1 AND driver_id IS NULL + RETURNING * + ) + SELECT * INTO v_task FROM u LIMIT 1; + + IF NOT FOUND THEN + RETURN jsonb_build_object('ok', false, 'message', 'task not available'); + END IF; + + -- 写审计 + INSERT INTO audit_logs(actor_id, action, target_table, target_id, payload, created_at) + VALUES (p_driver_uuid, 'accept_task', 'ml_delivery_tasks', p_task_uuid, row_to_json(v_task), now()); + + RETURN jsonb_build_object('ok', true, 'task', to_jsonb(v_task)); +END; +$$; +``` + +注意: +- 使用 `SECURITY DEFINER` 并确保函数拥有适当权限(仅 server/service_role 可调用)。 +- `action_dedupe` 表需创建并设 unique(action_id)。 + +### D. RLS 与 约束(示例) +- 开启 RLS,例如对 `user_addresses`: +```sql +ALTER TABLE user_addresses ENABLE ROW LEVEL SECURITY; +CREATE POLICY user_owns_address ON user_addresses + FOR ALL + USING (auth.uid() = user_id) + WITH CHECK (auth.uid() = user_id); +``` +- 对关键表(`ml_delivery_tasks`、`ml_orders`): + - 禁止直接由匿名或普通前端更新关键列(例如 driver_id、status);只允许 `rpc` / service 调用通过函数更新。 +- 索引/约束: + - message_id 唯一:`CREATE UNIQUE INDEX ux_express_notifications_message_id ON express_notifications(message_id);` + - 补全 NOT NULL / FK / CHECK(枚举字段限制等)。 + +### E. 鉴权实现要点(后端) +- 验证前端 JWT:解析/校验 Supabase JWT(可使用 Supabase 的用户 API 或直接 JWT 验证)。从 token 获取 `uid` 作为操作人。 +- 后端自身使用环境变量 `SERVICE_ROLE_KEY` 调用 Supabase Admin API 或直接调用 Postgres RPC(建议后者更原子)。 +- 不要在后端把 `service_role` 返回或写进前端文件。 + +### F. 幂等与去重 +- 对所有会被重复调用的外部动作(webhook、client retry)要求 `action_id`/`message_id`,并在 DB 层做 `ON CONFLICT DO NOTHING` 或在 RPC 先检查 `action_dedupe`。 + +### G. 审计与监控 +- 新建 `audit_logs(actor_id, action, target_table, target_id, payload, created_at)` 并在 RPC 中写入。 +- 错误报警(Sentry 或日志轮询),并在关键接口记录 metrics(错误率、latency、冲突数)。 + +--- + +## 迁移与上线路线(分阶段) +1. Dev:实现 RPC + API,更新 dev 配置把前端用 anon,完成单元测试与集成测试。 +2. Stage:灰度发布后端 API,部分司机/少量流量切换到 API。监控 48h。 +3. Prod:全面切换,删除前端中的直写调用(或通过 feature-flag 关闭)。 +4. 回滚:保留旧直写代码并用 feature flag 随时回退;若回退需同时短期恢复前端原 key(仅极端应急,注意风险并在短时间内移除)。 + +--- + +## 最小交付清单(可追踪任务) +- [ ] 注释/移除前端 `service_role`:`ak/config.uts`(紧急) +- [ ] 在 `server/` 新增 `POST /api/v1/delivery/accept-task`(鉴权 + 调用 RPC) +- [ ] 在 DB 创建 `rpc_accept_task`、`action_dedupe`、`audit_logs` 表 +- [ ] 对 `user_addresses`、`ml_delivery_tasks`、`ml_orders` 等启用 RLS 策略(逐表) +- [ ] 前端将接单/确认调用迁移到新 API(feature-flag)并回归测试 +- [ ] 部署后监控并逐步扩大灰度 + +--- + +## 风险与注意事项 +- 风险:短期内若不移除 `service_role`,即便实现后台 API,也无法阻止攻击者通过已有 key 直接写库。优先级最高。 +- 注意隐私/合规:审计日志要避免写入敏感明文(例如完整支付凭证)或加密存储。 +- DB 权限:确保 RPC 使用受限角色执行并限制 RPC 所能做的动作。 + +--- + +## 推荐下一步(我可以马上帮你做) +- 选项 1:生成并提交 `ak/config.uts` 的 patch,将 `SUPA_KEY` 替换为示例 anon(阻断风险)。 +- 选项 2:生成 `rpc_accept_task` 的完整 SQL + `server/routes/delivery.js` 的 Node 实现样例(包含鉴权中间件与调用示例)。 +- 选项 3:列出仓库内所有前端写入点并按“必须走后端 / 可保留直写”分级清单(便于逐步迁移)。 + +请回复 `1`、`2` 或 `3` 选项,或告诉我需要调整的文档内容/格式。 diff --git a/pages/mall/delivery/test/api-simulator.uvue b/pages/mall/delivery/test/api-simulator.uvue index 408a30f5..2f9495a5 100644 --- a/pages/mall/delivery/test/api-simulator.uvue +++ b/pages/mall/delivery/test/api-simulator.uvue @@ -65,7 +65,8 @@ }) async function loadOrders() { - const data = await mockService.getMockOrders() + // 使用不带用户过滤的接口,加载所有运单,便于模拟推送测试 + const data = await mockService.getAllOrders() orders.value = data } diff --git a/pages/mall/delivery/test/consumer-order-list.uvue b/pages/mall/delivery/test/consumer-order-list.uvue index 4caa8904..f837830c 100644 --- a/pages/mall/delivery/test/consumer-order-list.uvue +++ b/pages/mall/delivery/test/consumer-order-list.uvue @@ -45,6 +45,7 @@