# 闪送配送服务设计记录

本文件记录 2026-08-31 已经逐项确认的设计决策，作为后续 `plan.md`、`tasks.md` 和接口契约的输入。

## 1. 设计来源

蓝湖“闪送”分组共 7 个页面，均已通过蓝湖 MCP 读取图片、设计标注、颜色与图层：

1. 地址薄
2. 闪送 取
3. 首页202608
4. 填写收件信息
5. 闪送 加急
6. 闪送
7. 闪送 送

最初的蓝湖 7 页明确展示三种入口、取件与收件地址、联系人和电话、地址簿、起步价格及下单入口；后续用户端/骑手端原型明确包裹类别、数量、总重量、体积/规格、预约配送、交付 PIN、寄件图片以及基础配送、距离加价、加急和骑手小费明细。此前把精确重量和件数排除在范围外是错误判断，2026-09-07 起按设计稿补齐。

## 2. 架构决策

采用独立闪送订单模块，不复用 `pos_order` 或 `taxi_order`。

```text
用户端 / 骑手端 / 平台端
          |
          v
ruoyi-admin
  - 用户、骑手、平台 Controller
  - 报价编排与地图路线适配器
  - 闪送状态流转与自动完成任务
  - 通知编排
          |
          v
ruoyi-system
  - 闪送领域实体、Mapper、Service
  - 计价配置、配送凭证、状态日志
  - 共享地址所有权数据访问
```

外部地图 HTTP 集成位于 `ruoyi-admin`，保持 `ruoyi-admin -> ruoyi-system` 的模块依赖方向。SQL 只写入 `updatesql/sql.md`，由开发者手动执行。

## 3. 接口边界

### 3.1 用户端

- `GET /system/flashDelivery/home`
- `POST /system/flashDelivery/quote`
- `POST /system/flashDelivery/orders`
- `GET /system/flashDelivery/orders`
- `GET /system/flashDelivery/orders/{id}`
- `POST /system/flashDelivery/orders/{id}/cancel`
- `POST /system/flashDelivery/orders/{id}/confirmReceipt`

### 3.2 骑手端

- `GET /system/flashDelivery/rider/orders`
- `GET /system/flashDelivery/rider/orders/{id}`
- `POST /system/flashDelivery/rider/orders/{id}/accept`
- `POST /system/flashDelivery/rider/orders/{id}/pickup`
- `POST /system/flashDelivery/rider/orders/{id}/deliver`

### 3.3 平台端

- `GET /system/flashDelivery/admin/pricing`
- `POST /system/flashDelivery/admin/pricing`
- `PUT /system/flashDelivery/admin/pricing/{id}`
- `DELETE /system/flashDelivery/admin/pricing/{id}`
- `GET /system/flashDelivery/admin/orders`
- `GET /system/flashDelivery/admin/orders/{id}`
- `POST /system/flashDelivery/admin/orders/{id}/cancel`
- `POST /system/flashDelivery/admin/orders/{id}/complete`

平台管理前端沿用 `foodie-admin-vue` 的 Vue 2、Element UI、动态菜单和权限指令，不额外引入 UI 或状态管理依赖：

- “闪送管理”作为父菜单，“价格配置”和“闪送订单”作为两个子页面。
- `src/api/flashDelivery/index.js` 集中封装平台接口，页面不直接拼接请求。
- 价格配置页展示帮送和帮取共享的统一运价时段，新增与编辑弹窗承载时间、距离、整数金额、加急比例和最低加急费校验，支持删除不再需要的时段；配置保存即生效，不提供启停开关。
- 订单页以服务端分页表格为主体，详情弹窗分区展示概况、地址、费用、凭证和日志；取消原因使用独立输入弹窗，完成操作使用二次确认。
- 操作按钮同时使用状态条件和 `v-hasPermi` 控制可见性，后端权限与状态机仍是最终安全边界。
- 所有新增文本集中在四个前端语言文件的 `flashDelivery` 对象中，key 集合保持一致。

### 3.4 共享地址簿

- `GET /system/address/getaddress`
- `GET /system/address/getaddressxq`
- `POST /system/address/address`
- `DELETE /system/address/{id}`
- `POST /system/address/{id}/top`

地址接口继续由现有收货与闪送场景共用，但所有用户接口都必须携带 token；后端按当前登录用户限制详情、保存、删除和置顶范围。

## 4. 数据结构方向

### 4.1 `flash_delivery_order`

保存订单号、客户端请求号、用户、骑手、帮送/帮取业务场景、普通/加急配送等级、包裹类别、数量、总重量、规格、服务端生成的重量档、配送方式、预约时段、交付 PIN、取件与收件信息快照、路线距离、距离来源、预计时长、基础配送费、距离费、加急费、小费、总金额、匹配时段完整计价快照、币种、计价配置版本、备注、状态、并发版本号、关键操作时间与取消信息。

### 4.2 `flash_delivery_pricing`

保存一套供帮送和帮取共享、全局互不重叠的运价时段；每个时段保存开始时间、结束时间、起送距离、起送价格、计价距离、计价金额、加急比例、最低加急费、配置版本、修改人和修改时间。保存即生效，删除即失效，不设置启停字段。

### 4.3 `flash_delivery_order_image`

保存订单、凭证类型、图片 URL、排序、操作人类型、操作人和创建时间。凭证类型包括 `SENDER`、`PICKUP` 与 `DELIVERY`。

### 4.4 `flash_delivery_order_log`

保存变更前状态、变更后状态、操作人类型、操作人 ID、原因和时间。

### 4.5 `info_address`

增加置顶标记和置顶时间；列表按置顶时间与既有排序规则返回。

## 5. 计价与距离

```text
目标计价时刻 = NOW ? 当前报价时刻 : scheduledPickupStartAt
当前配置 = 按目标计价时刻匹配唯一统一运价时段
超出公里 = max(0, 路线距离公里 - 起送距离)
计费公里 = 超出公里 < 0.5 ? 0 : 超出公里 < 1 ? 1 : 超出公里
里程费用 = 计费公里 * (计价金额 / 计价距离)
普通配送费 = 起送价格 + 四舍五入到整数元的里程费用
加急费 = NORMAL ? 0 : max(最低加急费, 四舍五入(普通配送费 * 加急比例 / 100))
订单金额 = 普通配送费 + 加急费 + 骑手小费
```

普通计价规则与现有外卖订单一致，但闪送使用自己的统一时段配置表。起送价格和计价金额为正整数新台币；距离、比例与除法过程使用十进制定点数，里程费用和比例加急费采用 `HALF_UP` 四舍五入到整数元，最终价格不显示小数，也不执行外卖旧实现中金额达到 1000 后的千位特殊取整。立即订单按当前时刻、预约订单按预约开始时刻匹配时段；目标时刻没有匹配时段时拒绝报价和创建。路线预计时长继续返回和保存，仅用于履约展示，不参与计价。报价优先使用地图路线距离，失败时降级为直线距离；创建订单时服务端必须重新匹配时段、计算价格并核对客户端回传报价，完全一致后才能固化配置快照。

## 6. 状态机

```text
WAITING_ACCEPTANCE
        |
        | 骑手原子抢单
        v
ACCEPTED
        |
        | 订单骑手 + 取件图片
        v
PICKED_UP
        |
        | 订单骑手 + 送达图片
        v
DELIVERED
        |
        | 用户确认 / 24小时自动完成 / 平台完成
        v
COMPLETED
```

`WAITING_ACCEPTANCE` 和 `ACCEPTED` 允许订单用户取消。`PICKED_UP` 和 `DELIVERED` 只有平台能够介入取消。任一允许取消状态进入 `CANCELLED`。`COMPLETED` 与 `CANCELLED` 为终态。

## 7. 安全与隐私

- 用户、骑手身份只从请求头 token 解析，不从请求体读取。
- 用户订单查询和操作按 `order_id + user_id` 校验。
- 骑手履约操作按 `order_id + rider_id + expected_status` 校验。
- 骑手抢单使用数据库条件更新，避免双抢。
- 待抢订单按原型显示完整取送文字地址，但隐藏联系人、电话、实际 PIN、精确坐标和内部字段；抢单后才向订单骑手展示完整履约信息。
- 地址操作按 `address_id + user_id` 校验，客户端 `userId` 不参与归属判断。
- Controller 使用明确 DTO、`@RequestBody`、`@RequestParam`、`@PathVariable` 和 `@RequestHeader`，不接收 Map。
- 地图密钥不返回客户端、不写日志；业务错误使用国际化消息。

## 8. 测试方向

- 统一时段匹配、预约取件时段匹配、重叠拒绝、0.5 公里边界、整数金额取整、比例加急费、最低加急费、小费和配置版本测试。
- 地图路线成功、失败及直线降级测试。
- 地址和订单跨用户访问测试。
- 多骑手并发抢单测试。
- 取件与送达图片必填测试。
- 全部允许和禁止状态流转测试。
- 24 小时自动完成与并发幂等测试。
- 平台权限和配置校验测试。
- MyBatis XML 字段、条件更新与索引契约测试。
- 平台前端 API 路径、方法、参数和权限字符串契约测试。
- 平台前端价格校验、状态操作可见性和四语言 key 一致性测试。
- 平台前端 ESLint、生产构建和常用后台宽度的页面检查。

## 9. 明确延期内容

支付、退款、分账、骑手收入、代购、垫付、商品金额、自动派单、动态附近骑手数、预计接单时间以及用户端、骑手端和商家端页面均不在本期范围。物品数量、总重量、体积/规格和骑手小费属于本期接口范围。

## 10. App 订单列表精简增量（2026-09-01）

### 10.1 设计选择

采用单一骑手列表接口，查询参数与现有骑手外卖订单列表保持一致。该方案让 App 的两类配送列表共享 `page、size、tab、longitude、latitude` 的调用方式，同时由服务端完成页签到状态的映射。原 `/available` 与 `/mine` 拆分接口、`scene` 与 `serviceType` 组合筛选不再保留。

没有采用“保留两个接口只改参数名”，因为 App 仍需维护两套请求和响应；也没有采用“列表继续返回完整详情对象”，因为列表页面只需要卡片字段，完整订单信息已有独立详情接口。

### 10.2 用户列表

- `GET /system/flashDelivery/orders?page=1&size=10`
- 只查询当前用户，按创建时间倒序混合返回各状态订单。
- 列表不接受 `status`、`scene` 或 `serviceType`，页面无需为了简单卡片组装筛选条件。
- 使用用户列表摘要 DTO，只包含卡片展示所需的订单标识、服务/包裹、状态、取送文字地址、配送时段、预计时长、金额和关键展示时间；完整联系方式、精确坐标和照片由详情接口按明确字段返回，原始状态日志不向 App 返回。

### 10.3 骑手列表

- `GET /system/flashDelivery/rider/orders?page=1&size=10&tab=newTask&longitude=121.5&latitude=25.0`
- 参数名、默认分页和坐标用途与骑手外卖订单列表保持一致；经纬度用于 `newTask` 的附近范围、取件点距离和排序，其他页签忽略坐标。
- `newTask -> WAITING_ACCEPTANCE`；`toPickup -> ACCEPTED`；`delivering -> PICKED_UP`；`completed -> DELIVERED + COMPLETED`；`cancelled -> CANCELLED`。除 `newTask` 外均只查询当前骑手本人。
- 闪送没有退款流程，因此不接受外卖列表的 `refund` 页签。
- 列表使用统一卡片摘要 DTO，包含订单标识、业务场景、配送等级、状态、包裹类别/数量/总重量/重量档、配送方式/预约时段、是否需要 PIN、完整取送文字地址、取件点距离、路线距离、预计时长、费用明细、订单金额和创建时间。
- `newTask` 分页响应额外返回附近任务总数与最高订单金额，用于原型顶部摘要；不返回已移除的尖峰倍率，也不将订单金额表述为尚未实现的骑手收入。
- 接单前不返回联系人、电话、实际 PIN、精确坐标、用户备注、图片凭证、状态日志、用户 ID、幂等号或计价审计字段。接单成功后，详情接口仅向中单骑手返回完整履约信息。

### 10.4 App 详情响应

- 用户创建订单、用户订单详情、骑手订单详情和骑手接单成功响应的 `data` 直接返回订单详情字段，不再使用 `{order,images,logs}` 包装。
- 寄件、取件和送达照片分别使用 `senderImageUrls`、`pickupImageUrls`、`deliveryImageUrls`，避免 App 再按通用图片记录的类型分组。
- App 详情不返回原始状态日志；页面使用 `status`、`acceptedAt`、`pickedUpAt`、`deliveredAt`、`completedAt` 和 `cancelledAt` 展示进度。平台详情继续保留 `{order,images,logs}` 供审计。
- 骑手接单前后使用同一个详情 DTO：接单前隐藏联系人、电话、实际 PIN、精确坐标和履约凭证，接单后只向中单骑手补齐可见字段，`data` 顶层形状保持不变。

### 10.5 兼容和错误处理

- App 与后端同步切换到新接口，不保留测试阶段旧 `/available`、`/mine` 路由或旧参数兼容层。
- `page`、`size` 或 `tab` 非法时返回国际化业务错误；坐标必须成对出现并通过经纬度范围校验。
- 列表必须先按页签和角色过滤，再由数据库分页；禁止查出完整集合后在 Java 或客户端过滤。

## 11. 2026-09-03 蓝湖稿对齐增量

### 11.1 已确认边界

- 闪送不接入支付；订单创建后直接进入 `WAITING_ACCEPTANCE`，蓝湖中的待支付、去支付和支付倒计时不进入接口。
- 金额继续使用整数 TWD，蓝湖小数价格作为视觉占位处理。
- 不实现动态附近骑手数或预计接单分钟数，用户端文案统一为“发布后等待附近骑手接单”。
- `deliveryType=URGENT` 按 1 对 1 专送实现骑手独占；`serviceType` 只表示帮送或帮取业务场景。

### 11.2 “我收的”身份绑定

采用创建时快照方案。创建订单时将收件手机号去除首尾/内部空白、`+`、`-` 和圆括号但保留国家码数字，并以同样规则匹配当时已注册的普通用户：恰好匹配一名时保存其用户 ID 到 `receiver_user_id`；没有匹配或出现重复匹配时保存空值。该字段创建后不可因注册、手机号换绑或地址簿变化而自动改写。

没有采用每次查询时按当前手机号动态匹配，因为手机号回收或换绑会让新持有人读取历史订单；也不实现注册后追溯认领，因为这需要独立的手机号归属证明、认领审计和撤销流程，超出本次范围。

用户列表增加 `role=sender/receiver`，省略时保持原有 `sender` 行为。寄件人与绑定收件人都能查看详情和交付 PIN、确认签收；取消仍只允许订单创建人。列表和详情权限直接使用固化用户 ID，不使用请求中的手机号判断。

### 11.3 急送独占与并发

所有骑手接单入口先获取同一 Redisson 骑手锁 `lock:delivery:rider:{riderId}`，再查询该骑手的有效外卖和闪送任务。抢 `deliveryType=URGENT` 订单时要求不存在未送达外卖及 `ACCEPTED/PICKED_UP` 闪送；抢普通外卖或 `deliveryType=NORMAL` 闪送时，如果已有 `ACCEPTED/PICKED_UP` 急送则拒绝。急送进入 `DELIVERED`、`COMPLETED` 或 `CANCELLED` 后解除独占。

同一骑手的外卖和闪送接单必须共享这把分布式锁，确保并发请求不能分别在两张订单表中同时通过“无冲突”检查。锁使用 Redisson 看门狗续期，不指定固定租期，并通过事务同步回调在提交或回滚完成后释放；获取超时、线程中断或 Redis 异常均拒绝本次接单。数据库订单条件更新继续负责防止不同骑手抢中同一订单。

## 12. 2026-09-07 费用与物品信息对齐增量

### 12.1 正交业务维度

`serviceType` 只区分 `HELP_SEND` 和 `HELP_PICKUP`；`deliveryType` 独立区分 `NORMAL` 和 `URGENT`；`deliveryMode` 继续区分 `NOW` 和 `SCHEDULED`。帮送和帮取共享同一套运价，只有 `deliveryType=URGENT` 启用加急计价和骑手独占。

### 12.2 统一分时段运价

平台每个时间段配置 `startTime`、`endTime`、`startingDistance`、`startingFare`、`distance`、`freight`、`urgentRate` 和 `minimumUrgentFee`。所有时间段全局互斥，不再按 `serviceType` 分组。立即订单按报价时刻匹配，预约订单按 `scheduledPickupStartAt` 匹配；日期只用于确定预约有效性，时间段按业务当地时间的时分命中。

普通配送费先沿用现有起送与超距规则计算：`baseDeliveryFee = startingFare + distanceFee`。普通配送 `urgentFee=0`；加急配送使用 `urgentFee = max(minimumUrgentFee, roundHalfUp(baseDeliveryFee × urgentRate / 100))`。骑手小费 `tipAmount` 是用户输入的独立非负整数 TWD，不参与加急费计算；最终 `amount = baseDeliveryFee + urgentFee + tipAmount`。

### 12.3 报价回传与创建校验

报价响应返回配置 ID/版本、距离及来源、普通运价明细、`baseDeliveryFee`、`urgentRate`、`minimumUrgentFee`、`urgentFee`、`tipAmount` 和 `amount`。创建订单时客户端回传配置 ID/版本以及 `baseDeliveryFee`、`distanceFee`、`urgentFee`、`amount`；服务端重新取路线、匹配时间段并计算费用，逐项精确一致才创建。任何配置或费用变化都不得静默创建订单，响应必须携带最新报价供 App 重新确认。

### 12.4 物品信息

请求保存 `packageType`、`quantity`、`totalWeightKg` 和可选 `specification`。数量必须是正整数；总重量精确到两位小数且大于 0、不超过 20kg；规格说明去除首尾空格后最长 255 字符。`packageSize` 不再由客户端提交，而由服务端按总重量生成并作为订单快照保留给骑手与平台使用。

### 12.5 非支付边界

费用字段是报价和订单应付金额快照，不代表已经完成资金扣款。订单创建后仍直接进入 `WAITING_ACCEPTANCE`，不增加待支付状态、支付倒计时、支付回调或退款流程；骑手小费也只作为订单费用明细和接单参考展示。
