diff --git a/ak/config.uts b/ak/config.uts index b9ac3206..f7f43091 100644 --- a/ak/config.uts +++ b/ak/config.uts @@ -1,11 +1,14 @@ // Supabase 配置 // 内网环境 - 本地部署的 Supabase -// IP: 192.168.1.63 +// IP: 192.168.1.62 // Kong HTTP Port: 8000 +//export const SUPA_URL: string = 'http://192.168.1.62:18000' +//export const SUPA_KEY: string = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiYW5vbiIsImlzcyI6InN1cGFiYXNlLTEiLCJpYXQiOjE3Njk2NzY0OTgsImV4cCI6MTkyNzM1NjQ5OH0.ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890' export const SUPA_URL: string = 'http://192.168.1.63:18000' export const SUPA_KEY: string = 'eyJhbGciOiAiSFMyNTYiLCAidHlwIjogIkpXVCJ9.eyJyb2xlIjogImFub24iLCAiaXNzIjogInN1cGFiYXNlIiwgImlhdCI6IDE3Njk4NDczMzQsICJleHAiOiAyMDg1MjA3MzM0fQ.js-2CS5_cUmf4iVv8aCmmx9iyFsQvLNDbt8YYOngeLU' // WebSocket 实时连接(内网使用 ws:// 而非 wss://) +//export const WS_URL: string = 'ws://192.168.1.62:18000/realtime/v1/websocket' export const WS_URL: string = 'ws://192.168.1.63:18000/realtime/v1/websocket' // 备用配置(已注释,如需切换可取消注释) diff --git a/components/supadb/SESSION_RECOVERY.md b/components/supadb/SESSION_RECOVERY.md new file mode 100644 index 00000000..3dd9a88d --- /dev/null +++ b/components/supadb/SESSION_RECOVERY.md @@ -0,0 +1,41 @@ +**Supabase 会话恢复说明(supa session recovery)** + +- **目的**: 说明为什么刷新后 `session` / `user` 变为 `null`,以及我在项目中做了哪些修改来改善会话恢复。 + +- **文件变更**: + - **`components/supadb/aksupainstance.uts`**: 统一 Supabase 实例导出,新增 `supaReady` 初始化流程。在初始化时尝试从持久化 token(`AkReq` 存储)读取 `access_token` / `refresh_token`,将 refresh token 注入到 `supa.session` 并调用 `supa.refreshSession()` 以恢复会话;保留 `ensureSupabaseReady()` 作为兼容接口。 + - **`utils/store.uts`**: 将 `import` 改为 `import supa, { supaReady }`,并在获取 session 前 `await supaReady`,确保恢复逻辑已经运行完毕。 + - **`utils/sapi.uts`**、**`pages/sense/senseDataService.uts`** 及若干页面文件: 替换对旧 `ensureSupabaseReady` 的调用,改为 `await supaReady`,并使用默认导出的 `supa` 实例。 + +- **问题根本原因**: + 1. 系统原先将会话信息保持在 `AkSupa.session` / `AkSupa.user`(内存)中,页面刷新或应用重启会清空内存,导致 `supa.getSession()` 返回 `null`。 + 2. 虽然 `AkReq.setToken` 将 `access_token` / `refresh_token` 持久化到 `uni` storage,但 `AkSupa` 在启动时没有读取这些持久化 token 并执行恢复/刷新流程(或未将 refresh token 注入到 `AkSupa`),因此无法重建会话。 + 3. 若 refresh token 过期或没有被正确持久化,也会导致恢复失败。 + +- **我做的修改说明(要点)**: + - 在 `aksupainstance.uts` 添加 `supaReady`:这是一个 Promise,模块初始化时会尝试读取 `AkReq.getRefreshToken()` / `AkReq.getToken()`;若找到 refresh token,则把其临时赋到 `supa.session`,再调用 `supa.refreshSession()` 以更新内存中的 session 和 user。 + - 在所有依赖会话的模块中,在执行数据库请求前 `await supaReady`,确保恢复尝试已完成,避免 race condition(页面刷新后立即调用 supa API 导致 401/空 session)。 + - 保留向后兼容接口 `ensureSupabaseReady()`(内部直接返回 `supaReady`)。 + +- **如何验证(开发环境)**: + 1. 登录并确认本地存储中有 token(通过控制台或在 uni 环境运行): + - `uni.getStorageSync('akreq_access_token')` + - `uni.getStorageSync('akreq_refresh_token')` + 2. 刷新页面或重启应用,打开控制台查找初始化日志或错误(`Supabase instance init failed` 等)。 + 3. 在页面中(例如 `pages/mall/delivery/index.uvue`)查看已有的调试输出:search `supa session=` 的 console 日志,应显示非 null session(若 refresh 成功)。 + 4. 若恢复失败,请检查后端 refresh 接口是否返回 200,以及 refresh token 是否已过期。 + +- **后续建议(可选)**: + - 在 `AkSupa.signIn` / `refreshSession` 成功时,将完整 session(或至少 `refresh_token`)写入 `uni.setStorageSync`(持久化),并在 `signOut` 时清除,这样可以进一步减少恢复失败的情况。 + - 将 `supa.session` 的 JSON 快照也写入 storage(作为额外冗余),并在 init 时尝试直接恢复(注意安全和敏感信息保护)。 + - 增加更详细的日志(成功/失败原因),并在 UI 层对“登录过期”做更友好的提示或自动跳转到登录页。 + +- **注意事项**: + - 切勿在代码中硬编码匿名 key 或生产密钥;请在 `ak/config.uts` 中正确配置 `SUPA_URL` 和 `SUPA_KEY`。 + - refresh token 本身也是敏感凭证,应妥善保管;如需长期保持登录,建议使用 refresh 流程并合理设置过期与刷新策略。 + +如果你需要,我可以继续: +- 在 `AkSupa` 中实现“登录后持久化 session JSON”的补丁,并在 `signOut` 时清理; +- 或添加更详细的调试输出帮助定位某次具体恢复失败的 HTTP 请求和响应。 + +文件路径:components/supadb/SESSION_RECOVERY.md diff --git a/components/supadb/aksupainstance.uts b/components/supadb/aksupainstance.uts index 77c0b19a..faa33c5a 100644 --- a/components/supadb/aksupainstance.uts +++ b/components/supadb/aksupainstance.uts @@ -1,17 +1,52 @@ // /components/supadb/aksupainstance.uts import AkSupa from './aksupa.uts' import { SUPA_URL, SUPA_KEY } from '@/ak/config.uts' +import { AkReq } from '@/uni_modules/ak-req/index.uts' +// 创建 AkSupa 实例(在全局复用) const supa = new AkSupa(SUPA_URL, SUPA_KEY) -// Do not perform hard-coded auto sign-in during page preload (development mode may preload pages). -// Instead, mark supa as ready if an existing session is present; otherwise defer sign-in to explicit user action. +/** + * supaReady: 初始化时尝试从持久化 token 恢复会话。 + * - 若内存中已有 session 则直接返回 true + * - 否则尝试读取 AkReq 中持久化的 refresh token 并调用 `refreshSession()` 恢复 + */ const supaReady: Promise = (async () => { try { - const sess = supa.getSession() - if (sess != null && sess.session != null) { + const cur = supa.getSession() + if (cur && cur.session != null && cur.session.access_token) { return true } + + // 从持久化 storage 读取 token + const access = AkReq.getToken() + const refresh = AkReq.getRefreshToken() + const expiresAt = AkReq.getExpiresAt() ?? 0 + + if (refresh && refresh !== '') { + // 临时注入 session(以便 refreshSession 使用 refresh_token) + try { + supa.session = { + access_token: access ?? '', + refresh_token: refresh, + expires_at: expiresAt, + user: null, + token_type: '', + expires_in: 0, + raw: new UTSJSONObject({}) + } + } catch (e) { + // 在某些环境 UTSJSONObject 构造可能不可用,忽略并继续 + try { (supa as any).session = { access_token: access ?? '', refresh_token: refresh, expires_at: expiresAt } } catch (_) {} + } + + const ok = await supa.refreshSession() + if (ok) return true + + // 刷新失败,清空内存 session + try { supa.session = null; supa.user = null } catch (_) {} + } + return true } catch (err) { console.error('Supabase instance init failed', err) @@ -19,10 +54,8 @@ const supaReady: Promise = (async () => { } })() -// 兼容具名导出(部分代码使用 import { supabase, ensureSupabaseReady }) -export const supabase = supa - -export async function ensureSupabaseReady(): Promise { +// 向后兼容:保留 ensureSupabaseReady 接口 +export async function ensureSupabaseReady() { return await supaReady } diff --git a/doc_mall/database/create_supabase_auth_users.js b/doc_mall/database/create_supabase_auth_users.js index a3038387..24d6569c 100644 --- a/doc_mall/database/create_supabase_auth_users.js +++ b/doc_mall/database/create_supabase_auth_users.js @@ -92,6 +92,43 @@ const config = { nickname: '快递小哥2' } } + , + { + email: 'merchant.real@example.com', + password: 'Test123456!', + user_metadata: { + name: '测试商家', + role: 'merchant', + nickname: '测试商家' + } + }, + { + email: 'zhang.san@example.com', + password: 'Test123456!', + user_metadata: { + name: '张三', + role: 'customer', + nickname: '张三' + } + }, + { + email: 'li.si@example.com', + password: 'Test123456!', + user_metadata: { + name: '李四', + role: 'customer', + nickname: '李四' + } + }, + { + email: 'wang.wu@example.com', + password: 'Test123456!', + user_metadata: { + name: '王五', + role: 'customer', + nickname: '王五' + } + } ] }; diff --git a/package-lock.json b/package-lock.json index 4a86479c..507bf6c8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4,6 +4,7 @@ "requires": true, "packages": { "": { + "name": "mall", "dependencies": { "echarts": "^6.0.0" }, diff --git a/pages.json b/pages.json index 4a62865b..32bc1e4a 100644 --- a/pages.json +++ b/pages.json @@ -324,6 +324,34 @@ "navigationStyle": "custom" } }, + { + "path": "help-center", + "style": { + "navigationBarTitleText": "帮助中心", + "navigationStyle": "custom" + } + }, + { + "path": "about", + "style": { + "navigationBarTitleText": "关于我们", + "navigationStyle": "custom" + } + }, + { + "path": "feedback", + "style": { + "navigationBarTitleText": "意见反馈", + "navigationStyle": "custom" + } + }, + { + "path": "test", + "style": { + "navigationBarTitleText": "test", + "navigationStyle": "custom" + } + }, { "path": "settings", "style": { diff --git a/pages/mall/delivery/about.uvue b/pages/mall/delivery/about.uvue new file mode 100644 index 00000000..8cf90f4c --- /dev/null +++ b/pages/mall/delivery/about.uvue @@ -0,0 +1,293 @@ + + + + + \ No newline at end of file diff --git a/pages/mall/delivery/doc/db-data-generation.md b/pages/mall/delivery/doc/db-data-generation.md new file mode 100644 index 00000000..abbfabf2 --- /dev/null +++ b/pages/mall/delivery/doc/db-data-generation.md @@ -0,0 +1,205 @@ +## 测试数据生成与验证记录 + +本文档记录了在本项目中为配送端页面生成并验证测试数据库数据的全过程,包含关键表结构要点、执行的幂等 SQL、遇到的问题及解决办法,以及前端验证步骤,便于回溯和复现。 + +--- + +**概述** + +- 目标:让配送端页面 `pages/mall/delivery/index.uvue` 能读取真实 DB 中的配送任务(`ml_delivery_tasks`)并在附近订单 / 当前任务中展示;为此需要在 DB 中创建或复用 `ak_users`、`ml_delivery_drivers`、`ml_orders`、`ml_delivery_tasks` 的测试数据。 +- 环境:Supabase(Postgres)。建议在 Supabase SQL Editor 中以 `Role = postgres` 执行 SQL 以避免 RLS/触发器引起的权限问题。 + +**关键表与约束摘要** + +- `public.ak_users`:项目用户表,`id` 为主键,`auth_id` 对应 `auth.users.id`。 +- `public.ml_delivery_drivers`:配送员表,重要列:`id`、`user_id` (引用 `ak_users.id`,UNIQUE NOT NULL)、`real_name`、`id_card`。注意:此表没有 `phone` 列(以前曾误以为存在)。 +- `public.ml_orders`:订单表,重要列:`id`、`order_no`(UNIQUE NOT NULL)、`user_id`、`merchant_id`、`shipping_address` (JSONB NOT NULL)、`order_status`。 +- `public.ml_delivery_tasks`:配送任务表,重要列:`id`、`order_id` (UNIQUE NOT NULL,引用 `ml_orders.id`)、`driver_id` (引用 `ml_delivery_drivers.id`)、`pickup_address`/`delivery_address` (JSONB)、`status` (CHECK in (1..6))。注意:联系人信息应内嵌到地址 JSON 中(没有单独 `pickup_contact` 字段)。 + +--- + +**我执行的主要步骤(已实现的幂等脚本)** + +1. 查找或创建 `ak_users`(按 `auth_id` 唯一):避免重复插入,若已存在则复用其 `id`。 +2. 查找或创建 `ml_delivery_drivers`(以 `ak_users.id` 为 `user_id`):注意去掉对不存在列的引用(例如 `phone`)。 +3. 插入两个测试订单(使用唯一 `order_no` 标识,若已存在则跳过),`shipping_address` 使用 JSONB 格式且包含 `contact`/`phone`/`province`/`city`/`detail` 等字段。 +4. 为每个订单创建对应的 `ml_delivery_tasks`:一个保持 `status = 1`(可接单,`driver_id = NULL`),另一个设置 `status = 4` 并分配上步骤创建或找到的 `driver_id`。 + +下面是我实际使用的幂等 SQL(已去除不存在列并兼容 schema)。在 Supabase SQL Editor 中以 `Role = postgres` 运行整段脚本即可。 + +```sql +-- 替换 auth_id 为你的 auth.users.id(示例值可替换) +WITH +found_user AS ( + SELECT id FROM public.ak_users WHERE auth_id = 'dae9f45b-3955-43ae-992f-a3e24beaa520' +), +ins_user AS ( + INSERT INTO public.ak_users (id, auth_id, email, username, created_at) + SELECT uuid_generate_v4(), 'dae9f45b-3955-43ae-992f-a3e24beaa520', 'test+delivery@example.com', 'test_delivery_user', NOW() + WHERE NOT EXISTS (SELECT 1 FROM found_user) + RETURNING id +), +user_id AS ( + SELECT id FROM ins_user + UNION ALL + SELECT id FROM found_user + LIMIT 1 +), + +found_driver AS ( + SELECT id, user_id FROM public.ml_delivery_drivers WHERE user_id = (SELECT id FROM user_id) LIMIT 1 +), +ins_driver AS ( + INSERT INTO public.ml_delivery_drivers (id, user_id, real_name, id_card, created_at) + SELECT uuid_generate_v4(), (SELECT id FROM user_id), '张师傅', 'ID-TEST-0001', NOW() + WHERE NOT EXISTS (SELECT 1 FROM found_driver) + RETURNING id, user_id +), +driver_row AS ( + SELECT id, user_id FROM ins_driver + UNION ALL + SELECT id, user_id FROM found_driver + LIMIT 1 +), + +ins_order_1 AS ( + INSERT INTO public.ml_orders ( + order_no, user_id, merchant_id, + product_amount, shipping_fee, total_amount, + shipping_address, created_at, updated_at + ) + SELECT + 'TEST-DELIV-20260202-001', + (SELECT id FROM user_id), + (SELECT id FROM user_id), + 88.00, 12.00, 100.00, + ('{"contact":"李小明","phone":"13800000002","province":"上海市","city":"上海市","district":"浦东新区","street":"测试路100号","detail":"5楼502室","lat":31.2000,"lng":121.5000}')::jsonb, + NOW(), NOW() + WHERE NOT EXISTS (SELECT 1 FROM public.ml_orders WHERE order_no = 'TEST-DELIV-20260202-001') + RETURNING id, order_no, order_status +), +sel_order_1 AS ( + SELECT id, order_no FROM ins_order_1 + UNION ALL + SELECT id, order_no FROM public.ml_orders WHERE order_no = 'TEST-DELIV-20260202-001' + LIMIT 1 +), + +ins_task_1 AS ( + INSERT INTO public.ml_delivery_tasks ( + order_id, pickup_address, delivery_address, delivery_fee, status, created_at, updated_at + ) + SELECT + (SELECT id FROM sel_order_1), + ('{"contact":"商家小二","phone":"13900000002","street":"商家路1號","city":"上海市","detail":"商家门店A"}')::jsonb, + ('{"contact":"李小明","phone":"13800000002","street":"测试路100号","city":"上海市","detail":"5楼502室"}')::jsonb, + 12.00, + 1, + NOW(), NOW() + WHERE NOT EXISTS ( + SELECT 1 FROM public.ml_delivery_tasks dt WHERE dt.order_id = (SELECT id FROM sel_order_1) + ) + RETURNING id, order_id, status, driver_id +), + +ins_order_2 AS ( + INSERT INTO public.ml_orders ( + order_no, user_id, merchant_id, + product_amount, shipping_fee, total_amount, + shipping_address, created_at, updated_at + ) + SELECT + 'TEST-DELIV-20260202-002', + (SELECT id FROM user_id), + (SELECT id FROM user_id), + 59.00, 8.00, 67.00, + ('{"contact":"王小红","phone":"13800000003","province":"上海市","city":"上海市","district":"闵行区","street":"示例街20号","detail":"2幢101室","lat":31.1000,"lng":121.4000}')::jsonb, + NOW(), NOW() + WHERE NOT EXISTS (SELECT 1 FROM public.ml_orders WHERE order_no = 'TEST-DELIV-20260202-002') + RETURNING id, order_no, order_status +), +sel_order_2 AS ( + SELECT id, order_no FROM ins_order_2 + UNION ALL + SELECT id, order_no FROM public.ml_orders WHERE order_no = 'TEST-DELIV-20260202-002' + LIMIT 1 +), + +ins_task_2 AS ( + INSERT INTO public.ml_delivery_tasks ( + order_id, driver_id, pickup_address, delivery_address, + delivery_fee, status, assigned_at, picked_at, created_at, updated_at + ) + SELECT + (SELECT id FROM sel_order_2), + (SELECT id FROM driver_row), + ('{"contact":"商家B","phone":"13900000003","street":"商家街2號","city":"上海市","detail":"商家门店B"}')::jsonb, + ('{"contact":"王小红","phone":"13800000003","street":"示例街20号","city":"上海市","detail":"2幢101室"}')::jsonb, + 8.00, + 4, + NOW(), NOW(), NOW(), NOW() + WHERE NOT EXISTS ( + SELECT 1 FROM public.ml_delivery_tasks dt WHERE dt.order_id = (SELECT id FROM sel_order_2) + ) + RETURNING id, order_id, status, driver_id +) + +SELECT + (SELECT id FROM user_id) AS ak_user_id, + (SELECT id FROM driver_row) AS delivery_driver_id, + (SELECT id FROM sel_order_1) AS order1_id, + (SELECT order_no FROM sel_order_1) AS order1_no, + (SELECT id FROM sel_order_2) AS order2_id, + (SELECT order_no FROM sel_order_2) AS order2_no, + (SELECT id FROM ins_task_1) AS task1_id, + (SELECT id FROM ins_task_2) AS task2_id; +``` + +--- + +**常见错误与处理** + +- 错误:列不存在(例如 `address` / `phone` / `pickup_contact`)。处理:核对 `complete_mall_database.sql` 中的表结构,确认应使用 `shipping_address`、并将联系人内嵌到 JSON 中。 +- 错误:插入触发 FK 或 NOT NULL 违规(23503 / 23502)。处理:按顺序插入依赖表或使用已存在的 FK 值(例如先查找 `ak_users` 再插入 driver)。 +- 错误:唯一约束冲突(23505,例如 `ml_delivery_tasks.order_id` 唯一)。处理:脚本使用 `WHERE NOT EXISTS` 与 `ORDER_NO` 检查以避免重复插入。 +- 无行返回(UPDATE/SELECT 返回 “Success. No rows returned”):可能因为复制 id 时包含不可见字符,或 WHERE 条件不匹配。建议先用 `SELECT` 按 `order_no` 查找 task,再用 `UPDATE ... FROM public.ml_orders WHERE order_no = '...' RETURNING ...` 更新并检查返回值。 + +--- + +**前端验证(我在 `pages/mall/delivery/index.uvue` 已加入日志)** + +1. 刷新配送端页面(或重新打开),观察控制台日志: + - `loadDriverInfo` 日志:应输出 `driverInfo`,例如打印 `loadDriverInfo: try user_id=... res=...`。 + - `loadCurrentTask` 日志:输出查询结果 `loadCurrentTask: driverId=... res=...`,若 driver 有未完成任务(status < 5),`currentTask` 会被设置。 + - `loadAvailableOrders` 日志:输出 `loadAvailableOrders: query result=...`,用于验证 `status = 1` 且 `driver_id IS NULL` 的任务是否列出。 + +2. 若需要把某条 `ml_delivery_tasks` 的 `status` 改为 4(分配给司机),可运行: + +```sql +UPDATE public.ml_delivery_tasks dt +SET status = 4, + driver_id = '', + assigned_at = NOW(), + updated_at = NOW() +FROM public.ml_orders o +WHERE dt.order_id = o.id + AND o.order_no = 'TEST-DELIV-20260202-001' +RETURNING dt.id, dt.order_id, dt.status, dt.driver_id, dt.assigned_at, dt.updated_at; +``` + +执行后刷新前端并贴回 `loadCurrentTask` / `loadAvailableOrders` 的日志,以便确认页面行为。 + +--- + +**附:运行建议与注意事项** + +- 优先在 Supabase SQL Editor 中以 `Role = postgres` 执行插入与更新脚本,能避免 RLS 导致的“无行返回”。 +- 若需要创建 `ml_order_items`,需先确保 `ml_products` 存在或用最小 mock 产品插入,否则会触发 FK 约束错误。为减少依赖,我先仅创建 `ml_orders` 与 `ml_delivery_tasks`。 +- 保持 `order_no` 的唯一性(测试用的 `order_no` 推荐加时间戳或前缀 `TEST-`)。 + +如需我把脚本扩展为同时创建 `ml_products` / `ml_order_items`(以便测试完整订单页),或希望我直接在 repo 中加入一个可执行的 SQL 脚本文件,请回复我想要的范围,我来补充。 + +--- + +作者:自动化助手(配合用户执行并记录) +日期:2026-02-02 diff --git a/pages/mall/delivery/doc/delivery.md b/pages/mall/delivery/doc/delivery.md new file mode 100644 index 00000000..afa66568 --- /dev/null +++ b/pages/mall/delivery/doc/delivery.md @@ -0,0 +1,170 @@ +# Delivery 模块 — 总体架构 + +## 概要 +本文件给出配送(delivery)模块的总体架构说明,覆盖前端视图组件、后端数据表与 API 交互、关键业务流、并发/事务策略与建议改进方向,便于开发者与运维快速理解与修改。 + +## 架构概览 +- 前端(Uni-app / .uvue 组件):负责页面渲染、用户交互与轻量校验,主要文件位于 `pages/mall/delivery/`。 +- 后端(Supabase/Postgres + RPC):负责数据持久化、原子操作、权限校验与并发控制。建议将关键并发操作(例如“接单”)放在后端 RPC/函数中执行。 +- 数据层(Postgres 表):主表包括 `ml_orders`、`ml_order_items`、`ml_shops`、`ml_delivery_tasks`、`ml_delivery_drivers` 等。 +- 通信:前端通过 Supabase 客户端或后端 HTTP RPC 调用进行读写;推荐对写操作使用后端受限 RPC,减小竞态风险。 + +## 前端组件映射(关键文件) +- 入口与导航:`pages/mall/delivery/index.uvue`(模块入口) +- 任务列表 / 历史:`pages/mall/delivery/tasks.uvue`、`pages/mall/delivery/order-history.uvue` +- 任务详情 / 订单详情:`pages/mall/delivery/task-detail.uvue`、`pages/mall/delivery/order-detail.uvue` +- 配送员信息:`pages/mall/delivery/profile.uvue` / `profile-edit.uvue` +- 收益 / 统计:`pages/mall/delivery/earnings.uvue` +- 车辆管理:`pages/mall/delivery/vehicle.uvue`、`vehicle-add.uvue`、`vehicle-edit.uvue` +- 帮助/反馈/测试:`help-center.uvue`、`feedback.uvue`、`test.uvue` + +对应文档(开发说明)位于 `pages/mall/delivery/doc/`,每个视图对应一个 `.md` 说明文件(例如 `order-detail.md`、`tasks.md` 等)。 + +## 数据模型(常用字段 / 表) +- `ml_orders`: id, cid, order_no, user_id, merchant_id, order_status / status, total_amount, shipping_fee, paid_amount, shipping_address, remark, created_at +- `ml_order_items`: id, order_id, product_id, product_name, quantity, price, image_url +- `ml_shops`: id, merchant_id, shop_name, contact_name, contact_phone, address +- `ml_delivery_tasks`: id, order_id, driver_id, status, distance, estimated_time, pickup_time, delivered_time, created_at +- `ml_delivery_drivers`: id, user_id, vehicle_id, status + +字段命名在代码中可能存在兼容写法(如 `order_status` vs `status`、`shipping_fee` vs `delivery_fee`),建议在前端统一使用一个转换函数 `normalizeOrderRow(row)` 做映射。 + +## 关键业务流(简要) +- 接单(Accept) + - 不要在前端直接做盲写;推荐后端 RPC 原子化操作: + - SQL 示例(后端执行): + ```sql + UPDATE ml_delivery_tasks + SET driver_id = $1, status = 2 + WHERE order_id = $2 AND (driver_id IS NULL OR driver_id = '') + RETURNING *; + ``` + - 前端调用 RPC:根据返回行数判断是否接单成功;若返回 0 行则提示“已被其他配送员接单”。 + +- 确认取货 / 确认送达 + - 后端更新 `ml_delivery_tasks` 的时间戳字段(`pickup_time` / `delivered_time`)并更新 `ml_orders.order_status`,前端在成功回调后同步 UI 状态并展示时间戳。 + +- 读取详情页(order-detail) + - 并行查询 `ml_orders`, `ml_order_items`, `ml_shops`, `ml_delivery_tasks` 并合并结果,务必对空值做兼容处理(地址可能是字符串或对象)。 + +## 并发与事务策略 +- 将所有会改变接单归属或状态的操作放在数据库事务或后端函数中执行,前端仅调用并根据返回结果更新 UI。 +- 使用行级条件(`WHERE driver_id IS NULL` / `WHERE order_status = expected`)与 `RETURNING` 判断是否成功,避免乐观锁冲突。 +- 在前端增加防重(loading 标志位、按钮禁用)和超时重试策略,并记录幂等请求 ID(如必要)。 + +## 错误处理与监控 +- 前端对所有写操作展示用户友好错误(网络错误 / 已被接单 / 权限不足)。 +- 后端应记录关键操作日志(接单、拒单、确认取货/送达),并导出指标(接单失败率、并发冲突数)。 + +## 权限与安全 +- 接单/确认类操作必须在后端校验当前用户是合法配送员(基于 `ml_delivery_drivers.user_id`),并确保 RPC 仅对认证用户开放。 + +## 开发与测试建议 +- 在 `pages/mall/delivery/test/create_test_orders.sql` 中准备自动化测试数据,用于本地/CI 环境的集成测试。 +- 在前端加入单元测试/集成测试用例,模拟并发接单场景,验证后端 RPC 行为。 + +## 建议改进(优先级) +- 高:把接单逻辑迁移为后端 RPC,前端只调用并根据返回结果更新 UI。 +- 高:在所有变更接口返回受影响行数与最新状态,便于前端做幂等与回退。 +- 中:统一并导出状态常量(例如 `ORDER_STATUS`)供前端与文档引用,避免魔法数字。 +- 中:为详情页加入字段规范示例(在 `order-detail.md` 中补充),并同步到 API 文档。 + +--- + +(如需将 `order-detail.uvue` 的接单逻辑改为调用 RPC 并添加 loading 状态,我可以继续提交补丁。) +# Delivery 模块文档 + +本文档为 `pages/mall/delivery` 模块的开发与运维手册,已按与 `analytics` 模块相近的结构补充:概览、文件清单、技术特性、页面架构、实现要点、部署与测试建议、下一步计划与代码质量指引。 + +## 概览 +配送端(Delivery)面向配送员,覆盖:司机状态管理、任务与订单流转、配送记录/历史、收入统计、个人资料与车辆管理、帮助与反馈。页面既包含前端聚合逻辑,也与后端(Supabase / RPC)交互实现关键业务。 + +## 文件清单(快速索引) + +- [about.uvue](pages/mall/delivery/about.uvue) — 关于我们(静态) +- [index.uvue](pages/mall/delivery/index.uvue) — 配送端首页(司机信息、今日统计、可接订单) +- [tasks.uvue](pages/mall/delivery/tasks.uvue) — 任务列表(筛选、分页) +- [task-detail.uvue](pages/mall/delivery/task-detail.uvue) — 任务详情(联系、取货、完成) +- [order-detail.uvue](pages/mall/delivery/order-detail.uvue) — 订单详情(多表聚合、状态流转) +- [order-history.uvue](pages/mall/delivery/order-history.uvue) — 历史订单(本地合并刚完成订单显示) +- [earnings.uvue](pages/mall/delivery/earnings.uvue) — 收入明细(按 `order_no` 聚合) +- [profile.uvue](pages/mall/delivery/profile.uvue) — 个人中心与入口 +- [profile-edit.uvue](pages/mall/delivery/profile-edit.uvue) — 编辑资料(头像、服务区域) +- [vehicle.uvue](pages/mall/delivery/vehicle.uvue) — 车辆管理(列表/新增/编辑) +- [vehicle-add.uvue](pages/mall/delivery/vehicle-add.uvue) — 添加车辆表单 +- [vehicle-edit.uvue](pages/mall/delivery/vehicle-edit.uvue) — 编辑车辆 +- [feedback.uvue](pages/mall/delivery/feedback.uvue) — 意见反馈(图片、关联订单) +- [help-center.uvue](pages/mall/delivery/help-center.uvue) — 帮助中心/FAQ +- [ratings.uvue](pages/mall/delivery/ratings.uvue) — 评价记录(分页/筛选) +- [settings.uvue](pages/mall/delivery/settings.uvue) — 设置页 +- [test.uvue](pages/mall/delivery/test.uvue) — Supabase 连接/权限诊断 + +## 技术特性 + +- UTS / Uni-app 页面实现(`.uvue`),兼容 UTS Android 规范(类型定义、UTSJSONObject 使用等)。 +- 使用 `supa`(项目内 Supabase 封装)进行数据读取、RPC 调用与认证。示例:`supa.from('ml_delivery_tasks')`、`supa.rpc('rpc_analytics_top_products', params)`(analytics 模式的参考)。 +- 本模块使用本地存储做页面间临时数据传递(`uni.setStorageSync`),并在部分页面(如 `vehicle`、`order-history`)合并本地临时项以支持无后端或原型场景。 +- 对接建议:关键写操作(接单/状态变更)必须依赖后端原子性保护(事务或 WHERE driver_id IS NULL 的更新语句)。 + +## 页面架构与实现要点 + +- 首页(`index.uvue`):聚合 `driver`、`todayStats`、`currentTask` 与 `availableOrders`;包含 `_transformTask` 用于兼容多种地址/联系人字段格式。 +- 订单详情(`order-detail.uvue`):按 id 格式选择查询字段(`id`/`cid`/`order_no`),并联查 `ml_order_items`, `ml_shops`, `ml_delivery_tasks`;提供接单/拒单/确认取货/确认送达等操作。 +- 任务流(`tasks` / `task-detail`):任务筛选(待接单/进行中/已完成)与详情页的状态变更操作;完成时应记录时间戳与凭证以便对账。 +- 收入(`earnings`):前端按 `order_no` 聚合并补齐来源类型,建议后端提供已聚合接口以提升性能与准确性。 +- 个人/车辆页:使用图片上传、表单校验与本地缓存作为临时方案;生产应对接车辆管理 API 并保存到服务端。 + +## 典型 DB / RPC 示例 + +Supabase (supa) 示例: +``` +// 查询司机信息 +await supa.from('ml_delivery_drivers').select('*').eq('user_id', userId).limit(1).execute() + +// 查询可接任务(示例) +await supa.from('ml_delivery_tasks').select('*').is('driver_id', null).eq('status', 1).order('created_at', { ascending: false }).range(offset, offset+size-1).execute() + +// 接单(需后端并发保护) +await supa.from('ml_delivery_tasks').update({ driver_id: driverId, status: 2 }).eq('id', taskId).eq('driver_id', null).execute() +``` + +## 样式与 UI 设计 + +- 推荐采用与 `analytics` 模块相同的卡片式布局与 KPI 卡片样式(便于统一风格);关键组件(任务卡、订单明细卡、统计卡)应抽象成可复用组件以减少重复代码。 + +## 部署与配置 + +- 环境变量:确认 `SUPA_URL`/`SUPA_KEY` 在 `ak/config.uts` 或环境配置中正确配置。 +- 本地测试:使用 `pages/mall/delivery/test.uvue` 验证 Supabase 连接与权限(避免在仓库暴露明文 key)。 + +## 测试建议 + +- 功能测试:接单并发测试、状态流转(取货/送达)、图片上传与表单校验、分页与历史数据展示。 +- 兼容性:Android/不同屏幕尺寸测试(UTS Android 兼容性须验证)。 +- 性能:大量任务/历史记录分页测试,收入聚合在后端做性能验证。 + +## 下一步计划 + +1. 抽取并复用 UI 组件(任务卡、订单详情卡、统计 KPI 卡)。 +2. 将前端聚合逻辑(如 `earnings`)迁移或补充后端聚合接口以提升性能。 +3. 用后端事务或乐观锁增强接单并发保护,并在前端实现失败回退提示。 +4. 添加自动化测试用例覆盖关键流程(接单/确认/拒单/结算)。 + +--- +Generated on: 2026-02-02 +## 快速参考(已合并页摘要) + +- `about.uvue`:关于我们(静态信息,运营可从 CMS 拉取版本/更新日志)。 +- `help-center.uvue`:帮助中心 / FAQ(本地数组或后端搜索,支持分类与全文检索)。 +- `settings.uvue`:设置页(通知、语言、隐私、登出),存储建议使用 `uni.setStorageSync` + 后端同步。 +- `feedback.uvue`:意见反馈(文本 + 图片 + 关联订单),图片需先上传再提交 URL。 +- `ratings.uvue`:评价记录列表(分页筛选,可能需要后端审核/屏蔽)。 +- `profile.uvue` / `profile-edit.uvue`:个人中心与编辑(头像上传、服务区域、车辆信息),建议在保存后同步刷新缓存。 +- `vehicle*.uvue`:车辆管理相关(列表/新增/编辑),当前以本地存储为演示,生产应对接 REST 接口并做权限校验。 + +(已把上述 per-page md 移动到 `pages/mall/delivery/doc/archive/`,保留历史版本以便追溯。) + +--- +Generated on: 2026-02-02 +--- +Generated on: 2026-02-02 diff --git a/pages/mall/delivery/doc/earnings.md b/pages/mall/delivery/doc/earnings.md new file mode 100644 index 00000000..c8b8a825 --- /dev/null +++ b/pages/mall/delivery/doc/earnings.md @@ -0,0 +1,89 @@ +# earnings.uvue — 收入明细 + +## 概要 +按 `order_no` 聚合展示配送员收入明细(配送费、用户打赏、商家打赏),支持分页加载与按时间区间统计。 + +## 数据结构 +- `EarningItem` + - `id: string` + - `order_no: string` + - `date: string` + - `amount: number` + - `source: 'delivery_fee' | 'user_tip' | 'merchant_tip' | string` + - `distance?: number` + +- `OrderEarning` + - `order_no: string` + - `date: string` + - `distance?: number` + - `totalAmount: number` + - `details: EarningItem[]` + +## 关键方法 +- `loadAllEarnings()` + - 从后端或本地数据源取原始 `EarningItem[]`,按 `order_no` 聚合成 `OrderEarning[]`。 + - 对每个 `OrderEarning.details` 补齐三类来源占位项(amount=0),以保证前端渲染顺序稳定。 + +- `calculateTotalStats()` + - 计算 `totalEarnings`, `totalUserTips`, `totalMerchantTips`。 + +- `loadPage()` / `loadMore()` + - 基于 `pageSize` 控制分页渲染,避免一次性载入全部历史记录。 + +## 聚合示例(核心) +``` +const orderMap = new Map() +raw.forEach(item => { + let order = orderMap.get(item.order_no) + if (!order) { + order = { order_no: item.order_no, date: item.date, distance: item.distance, totalAmount: 0, details: [] } + orderMap.set(item.order_no, order) + } + order.totalAmount += Number(item.amount || 0) + order.details.push(item) +}) +// 补齐三类来源 +orderMap.forEach(order => { + const has = type => order.details.some(d => d.source === type) + ;['delivery_fee','merchant_tip','user_tip'].forEach(t => { if (!has(t)) order.details.push({ id: `${order.order_no}_${t}`, order_no: order.order_no, amount: 0, source: t }) }) +}) +const result = Array.from(orderMap.values()) +``` + +## 接口 / DB 建议 +- 推荐后端提供按订单聚合的接口: + - GET `/api/driver/{id}/earnings?start=&end=&page=&size=` + - 返回 `OrderEarning[]` 已聚合的数据以减轻前端压力 + +## 数据源更新:包含已接订单(Accepted) +- 需求说明:为了与“历史订单”页面保持一致,收入统计应包含配送员已接取(accepted/assigned)的订单,不仅限于已完成订单。也就是说,只要 `ml_delivery_tasks` 中 `driver_id` = 当前司机且 `status >= 2`(已接取或进行中),其对应的订单都应计入收入统计范围。 + +- 推荐 Supabase 查询示例(后端或前端按需实现): + +``` +-- 查询分配给当前司机且已接取的任务对应的订单ID +SELECT DISTINCT order_id FROM ml_delivery_tasks WHERE driver_id = :driverId AND status >= 2; + +-- 基于 order_id 查询订单及收入项(示例:ml_orders + ml_delivery_tips) +SELECT o.id,o.order_no,o.created_at,o.distance,o.delivery_fee, t.amount as tip_amount, 'user_tip' as tip_source +FROM ml_orders o +LEFT JOIN ml_delivery_tips t ON t.order_id = o.id +WHERE o.id IN () +ORDER BY o.created_at DESC +LIMIT :size OFFSET :offset; +``` + +- 若在前端直接使用 `supabase-js`: + +```js +const taskRes = await supa.from('ml_delivery_tasks').select('order_id').eq('driver_id', uid).gte('status', 2).execute() +const orderIds = taskRes.data.map(r => r.order_id) +const ordersRes = await supa.from('ml_orders').select('*,ml_delivery_tips(*)').in('id', orderIds).order('created_at',{ascending:false}).limit(size).execute() +``` + +注意:若订单很多,务必在后端做分页/聚合以减轻客户端开销。 + +## 注意事项 +- 在金额计算中使用整数 cents 或 BigInt 或固定小数处理,避免浮点误差。 +- 大量数据应后端分页/聚合并支持过滤(按时间、按来源)。 + diff --git a/pages/mall/delivery/doc/index.md b/pages/mall/delivery/doc/index.md new file mode 100644 index 00000000..dbea39f3 --- /dev/null +++ b/pages/mall/delivery/doc/index.md @@ -0,0 +1,63 @@ +# index.uvue — 配送端首页 + +## 概要 +配送员主界面,包含司机信息、在线/离线切换、今日统计、当前配送任务、附近可接订单与快捷入口。 + +## 数据结构 +- DeliveryDriverType (来自 `ml_delivery_drivers`) + - id, user_id, real_name, avatar_url, work_status, vehicle_type, vehicle_number, rating, service_areas, ... + +- CurrentTaskType / AvailableOrderType + - id, order_no, status, pickup_address, delivery_address, pickup_contact, delivery_contact, delivery_fee, distance, estimated_time, created_at + +## 关键方法 +- `onLoad()` + - 调用 `getCurrentUser()` 确保用户上下文可用 + - 执行:`loadDriverInfo()`, `loadTodayStats()`, `loadCurrentTask()`, `loadAvailableOrders()` + +- `loadDriverInfo()` + - 从 `ml_delivery_drivers` 表按 `user_id` 查询并更新 `driverInfo`。 + +- `loadTodayStats()` + - 查询 `ml_delivery_tasks` 在当天范围内的记录,计算完成订单数、总收入、配送里程等。 + +- `loadCurrentTask()` + - 查询 `ml_delivery_tasks` 中 driver_id 的未完成任务(status < 5),并取最新一条作为当前任务。 + +- `loadAvailableOrders()` + - 若 `isOnline` 且无 `currentTask`,从 `ml_delivery_tasks` 查询 `driver_id IS NULL` 且 status=1 的订单列表(可按地理位置筛选)。 + +- `_transformTask(task)` + - 兼容处理 `pickup_address` / `delivery_address` 与 `pickup_contact` 的多种格式(对象或 JSON 字符串),输出页面期望结构。 + +## 关键 DB 查询 示例 +``` +await supa.from('ml_delivery_drivers').select('*').eq('user_id', userId).limit(1).execute() + +await supa.from('ml_delivery_tasks') + .select('id,delivery_fee,distance,created_at,status') + .eq('driver_id', driverId) + .gte('created_at', start) + .lte('created_at', end) + .execute() +``` + +## 交互与状态流 +- `toggleWorkStatus()`:切换 `isOnline` 并调用 `startWork()` / `stopWork()`。上线时会刷新可接订单列表。 +- 接单/开始取货/确认取货/开始配送/确认送达等均通过对 `ml_delivery_tasks` 的 `update` 操作变更 `status`,并在成功后更新本地 `currentTask`。 + +## 注意事项 +- 高并发接单场景需后端保证原子性(乐观锁或 DB 事务)以防止竞单冲突。 +- `loadAvailableOrders()` 最好按司机服务区域与距离筛选,并使用分页/实时推送代替频繁轮询。 +- 日志(console.log)语句便于调试,但生产环境建议使用集中化日志系统。 +# index.uvue — 配送端首页 + +页面目的:配送员主界面,展示司机资料、工作状态、今日统计、当前任务与可接订单。 + +关键点: +- 数据加载:`onLoad()` 调用 `getCurrentUser()` 后依次调用 `loadDriverInfo()`, `loadTodayStats()`, `loadCurrentTask()`, `loadAvailableOrders()`。 +- Supabase 交互:通过 `supa.from(...).select(...).eq(...).execute()` 查询 `ml_delivery_drivers`、`ml_delivery_tasks` 等表。 +- 兼容解析:`_transformTask(task)` 将 DB 返回的地址/contact 字段兼容解析为页面所需结构(处理字符串或对象)。 +- 操作:接单/开始取货/确认取货/开始配送/确认送达等,会调用 supa 更新 `ml_delivery_tasks` 状态并同步页面状态。 + +注意:对 supa 操作有大量 try/catch,页面对无用户 ID 情况做了保护,适合直接对接真实后端。 diff --git a/pages/mall/delivery/doc/more-orders.md b/pages/mall/delivery/doc/more-orders.md new file mode 100644 index 00000000..6c627aca --- /dev/null +++ b/pages/mall/delivery/doc/more-orders.md @@ -0,0 +1,118 @@ +## 目的 + +当“附近订单”过多时,在主页面展示前 N 条(当前为 20 条),需要提供“更多”入口,跳转到单独页面展示所有可接取的订单并支持翻页/上拉加载。 + +## 路由与文件 + +- 主入口:在 `pages/mall/delivery/index.uvue` 的“附近订单”区块新增“更多”按钮,导航到新页面。 +- 新页面文件:`pages/mall/delivery/all.uvue`(建议路径) +- 文档说明文件:`pages/mall/delivery/doc/more-orders.md`(本文件) + +## 用户流程 + +1. 用户在 `index.uvue` 看到“附近订单”列表(最多 20 条)。 +2. 当可接取订单数量 >= 20 时,显示“更多”按钮。 +3. 点击“更多”跳转到 `all.uvue`,展示可接取订单的完整列表(分页或无限滚动)。 +4. 用户可在新页面接受订单或刷新列表。 + +## 后端 / 查询设计(示例:Supabase 客户端) + +- 建议分页参数:`pageSize = 50`(可配置)。 +- 前端通过 offset/pagination 请求: + +示例(首次加载 page=0, size=50): + +```ts +const page = 0 +const size = 50 +const res = await supa.from('ml_delivery_tasks') + .select('*') + .is('driver_id', 'null') + .eq('status', 1) + .order('created_at', { ascending: false }) + .range(page*size, page*size + size - 1) + .execute() +``` + +- 翻页时将 `page` 增加;上拉加载时继续请求下一段 `range`。 +- 后端/数据库建议对 `ml_delivery_tasks` 建立索引(status, driver_id, created_at)以加速查询。 + +## 前端实现建议 + +- `index.uvue`:在 `availableOrders` 列表下方加入: + + - 条件显示:`availableOrders.length >= 20` 时显示“更多”按钮。 + - 点击处理函数:`uni.navigateTo({ url: '/pages/mall/delivery/all' })`。 + +- `all.uvue` 页面职责: + - 使用与 `index.uvue` 相同的 `supa` 客户端和 `_transformTask` 方法复用数据格式化逻辑。 + - 支持分页(page + pageSize)或无限滚动(onReachBottom 加载下一页)。 + - 显示空状态、加载中状态和错误提示。 + +示例数据加载片段(伪代码): + +```ts +data() { return { page: 0, pageSize: 50, items: [], loading: false, finished: false } } +async loadPage() { + if (this.loading || this.finished) return + this.loading = true + const res = await supa.from('ml_delivery_tasks') + .select('*') + .is('driver_id', 'null') + .eq('status', 1) + .order('created_at', { ascending: false }) + .range(this.page * this.pageSize, (this.page+1)*this.pageSize - 1) + .execute() + const rows = Array.isArray(res.data) ? res.data : [] + if (rows.length < this.pageSize) this.finished = true + this.items.push(...rows.map(r => this._transformTask(r))) + this.page += 1 + this.loading = false +} +``` + +## 并发与安全 + +- 接单时应在服务器端/数据库层做原子性判断,避免竞态: + + - 示例(伪 SQL): + UPDATE ml_delivery_tasks + SET driver_id = :driverId, status = 2 + WHERE id = :taskId AND driver_id IS NULL AND status = 1 + RETURNING id + +- 前端在发起“接受订单”请求后,基于返回结果确认是否成功;若失败需提示用户订单已被接取。 + +## 接受标准/验收条件 + +- 在 `index.uvue` 点击“更多”能跳转到 `all.uvue`。 +- `all.uvue` 能正确加载 >20 条可接订单并支持继续加载直到无更多数据。 +- 接单操作的竞态由后端或事务性查询处理,前端能正确反馈成功/失败。 + +## 测试数据与验证 + +- 使用现有文档 `doc_mall/database/realistic_mock_data.sql` 创建若干测试订单;也可循环插入多条 `ml_delivery_tasks`(status=1, driver_id=NULL)用于分页测试。 + +示例快速生成(Postgres 伪 SQL): + +```sql +INSERT INTO public.ml_delivery_tasks (order_id, pickup_address, delivery_address, delivery_fee, status, created_at, updated_at) +SELECT uuid_generate_v4(), '{"detail":"店铺"}'::jsonb, '{"detail":"用户地址"}'::jsonb, 5.0, 1, NOW() - (i || ' minutes')::interval, NOW() +FROM generate_series(1,200) as s(i); +``` + +## 开发任务清单(简要) + +1. 在 `pages/mall/delivery/index.uvue` 增加“更多”按钮並导航。 +2. 新建 `pages/mall/delivery/all.uvue`,实现分页/上拉加载并复用 `_transformTask`。 +3. 后端或 DB 层确保接单操作的原子性。 +4. 编写 E2E 或手动测试脚本验证加载与接单行为。 + +## 是否需要我实现? + +如果你需要,我可以: +- 直接在 `index.uvue` 添加“更多”按钮示例(小改动)。 +- 新增 `pages/mall/delivery/all.uvue` 的样板实现并提交 PR 样例。 + +--- +文档已创建:`pages/mall/delivery/doc/more-orders.md` diff --git a/pages/mall/delivery/doc/order-detail.md b/pages/mall/delivery/doc/order-detail.md new file mode 100644 index 00000000..31408d88 --- /dev/null +++ b/pages/mall/delivery/doc/order-detail.md @@ -0,0 +1,51 @@ +# order-detail.uvue — 订单详情 + +## 概要 +展示单笔订单全部信息并提供状态流转(接单、确认取货、确认送达、拒单等)。页面会联合查询 `ml_orders`、`ml_order_items`、`ml_shops` 与 `ml_delivery_tasks`。 + +## 数据结构(常用字段) +- `Order` + - `id: string | number` + - `order_no: string` + - `cid?: number` (兼容旧字段) + - `shipping_address | pickup_address`: object/string + - `shipping_fee`, `total_amount`, `status` + +- `OrderItem` + - `id`, `order_id`, `product_name`, `qty`, `price` + +- `DeliveryTask` + - `id`, `order_id`, `driver_id`, `status`, `accepted_at`, `picked_at`, `delivered_at` + +## 关键方法 +- `onLoad(options)` + - 解析 `options.id` 与 `options.status`,调用 `loadOrderDetail(id)`。 + +- `loadOrderDetail(id)` + - 判断 ID 类型(UUID / 数字 / 非数字)以决定查询字段(`id`、`cid`、`order_no`)。 + - 并行查询 `ml_orders`, `ml_order_items`, `ml_shops`, `ml_delivery_tasks` 并合并到页面状态。 + +- `acceptOrder()` / `rejectOrder(reason)` + - accept: 尝试对 `ml_delivery_tasks` 执行 `update driver_id` 操作并设置 `status=2`(处理中),需要后端并发保护。 + - reject: 增加拒单原因到 `ml_delivery_tasks` 或 `order_notes` 并回滚本地 UI 状态。 + +- `confirmPickup()` / `confirmDelivery()` + - 根据 `task.id` 更新相应时间戳字段(`picked_at`/`delivered_at`)并设置状态(例如 `status=3/4`)。 + +## 示例:按 id 类型查询(伪代码) +``` +let q = supa.from('ml_orders').select('*') +if (isUUID(id)) q = q.eq('id', id) +else if (isNumeric(id)) q = q.eq('cid', id) +else q = q.eq('order_no', id) +const { data: order } = await q.limit(1).execute() +``` + +## 事务与并发注意 +- 接单场景应使用后端原子性检查(数据库事务或行级乐观锁)以避免多司机同时接单。 +- 前端接单流程:先尝试 update(带 where driver_id IS NULL),若返回 0 row affected 则提示已被接单。 + +## 错误处理与回退 +- 捕获所有 supa 调用错误并将友好错误展示给用户(例如:'网络错误,请稍后重试')。 +- 对可能缺失的字段(地址为字符串或对象)使用 `_transformAddress()` 做兼容处理。 + diff --git a/pages/mall/delivery/doc/order-history.md b/pages/mall/delivery/doc/order-history.md new file mode 100644 index 00000000..b46ea431 --- /dev/null +++ b/pages/mall/delivery/doc/order-history.md @@ -0,0 +1,31 @@ +# order-history.uvue — 历史订单 + +## 概要 +显示配送员历史订单(已完成/已取消等),并支持按时间范围过滤、分页和插入刚完成订单以做到“即时显示”。 + +## 数据结构 +- `HistoryOrder` + - `order_no`, `id`, `status`, `delivered_at`, `total_amount`, `shop_name` + +## 关键方法 +- `loadOrderHistory({start,end,page,size})` + - 优先按 `ml_delivery_tasks` 中与 `driver_id` 相关的 `order_id` 拉取任务记录,再批量查询 `ml_orders` 获取详情。 + - 若后端支持直接按 `driver_id` 返回已完成订单则调用后端聚合接口更高效。 + +- `checkForNewCompletedOrder()` + - 从 `uni.getStorageSync('completed_order_for_history')` 读取并合并到 `orderHistory` 顶部,随后清除本地缓存键。 + +## DB 查询示例(伪 SQL / supa) +``` +const tasks = await supa.from('ml_delivery_tasks').select('order_id,delivered_at').eq('driver_id', driverId).eq('status', 4).order('delivered_at', { ascending: false }).limit(100).execute() +const orders = await supa.from('ml_orders').select('*').in('id', tasks.map(t=>t.order_id)).execute() +``` + +## 分页与筛选 +- 使用后端分页(`page/size`),前端仅负责渲染和“加载更多”。 +- 支持按日期区间和商家名模糊搜索。 + +## 注意事项 +- 当从本地合并刚完成订单时,去重逻辑必不可少(按 `order_no` 或 `id`)。 +- 对于大量历史数据,应依赖后端支持归档与按需加载。错误/异常应有兜底 UI(空状态/重试按钮)。 + diff --git a/pages/mall/delivery/doc/profile-edit.md b/pages/mall/delivery/doc/profile-edit.md new file mode 100644 index 00000000..9a6ffa14 --- /dev/null +++ b/pages/mall/delivery/doc/profile-edit.md @@ -0,0 +1,25 @@ +# profile-edit.uvue — 编辑资料 + +## 概要 +编辑配送员个人资料(姓名、电话、头像、服务区域、车辆信息等),并将修改提交到后端保存。 + +## 数据结构 +- `ProfileForm` + - `real_name`, `phone`, `avatar_url`, `service_areas[]`, `vehicle`{ + `type`, `model`, `plate_no` + } + +## 关键方法 +- `chooseAvatar()`:`uni.chooseImage` 获取本地图片并调用 `uploadAvatar()` 上传。 +- `uploadAvatar(file)`:上传到文件存储并返回 URL,更新 `form.avatar_url`。 +- `saveProfile()`:表单校验后调用后端 API 更新 `ml_delivery_drivers` 或 `users` 表。 + +## 接口示例 +``` +await api.put('/driver/profile', { ...form }) +``` + +## 注意事项 +- 上传图片应限制大小并在客户端压缩以减少带宽。 +- 服务区域通常是数组或 GeoJSON,保存时与后端约定格式以支持范围匹配。 + diff --git a/pages/mall/delivery/doc/profile.md b/pages/mall/delivery/doc/profile.md new file mode 100644 index 00000000..8886c566 --- /dev/null +++ b/pages/mall/delivery/doc/profile.md @@ -0,0 +1,26 @@ +# profile.uvue — 个人中心 + +## 概要 +配送员个人中心,展示司机基本信息、当前工作状态、统计摘要(今日订单/收入)、常用入口(车辆管理/收入/设置/帮助)以及最近任务预览。 + +## 数据结构 +- `DriverInfo` + - `id`, `user_id`, `real_name`, `avatar_url`, `work_status`, `rating`, `vehicle`(简要信息) + +## 关键方法 +- `loadDriverInfo()`:从 `ml_delivery_drivers` 或 `users` 表加载司机信息并设置 `driverInfo`。 +- `toggleWorkStatus()`:切换上线/下线并触发 `loadAvailableOrders()`(上线时)。 +- `relocate()`:请求定位权限并更新司机服务位置(可调用后端保存)。 + +## 交互示例 +``` +async toggleWorkStatus() { + await supa.from('ml_delivery_drivers').update({ work_status: newStatus }).eq('id', driverInfo.id) + this.driverInfo.work_status = newStatus +} +``` + +## 注意事项 +- 工作状态变更应通知后端并尽量在服务端发出状态变更事件(WebSocket/推送)。 +- 个人信息编辑应跳转到 `profile-edit`,并在保存后同步更新本页缓存。 + diff --git a/pages/mall/delivery/feedback.uvue b/pages/mall/delivery/feedback.uvue new file mode 100644 index 00000000..e11134d7 --- /dev/null +++ b/pages/mall/delivery/feedback.uvue @@ -0,0 +1,451 @@ +