# 闪送功能 App 接口接入文档

本文档供用户端 App 和骑手端 App 接入闪送功能使用，以当前后端实现为准。文档只描述接口契约和业务流程，不包含 App 前端实现代码。

> 更新时间：2026-09-07。当前口径已对齐下单设计稿：帮送/帮取与普通/加急正交、统一分时段运价、完整物品信息、费用明细及创建订单报价校验；闪送仍不接入支付。

本次调整后的下单契约要点：

- `serviceType` 只区分 `HELP_SEND`（帮送）和 `HELP_PICKUP`（帮取），不再使用 `URGENT` 作为服务类型。
- 是否加急由独立字段 `deliveryType=NORMAL/URGENT` 表达；帮送和帮取使用同一套分时段基础运价。
- 报价和创建订单都必须提交取件方式、物品类别、数量、总重量、规格说明和骑手小费；立即取件使用 `deliveryMode=NOW`，预约取件使用 `deliveryMode=SCHEDULED` 并提交取件时间段。
- App 必须先报价，再把报价 ID、版本及四个费用字段原样回传创建订单接口。后端重新计算不一致时不会创建订单，而会返回最新报价。

## 1. 功能范围

当前闪送支持：

- 帮送、帮取两种业务场景，以及普通配送、1 对 1 加急两种配送等级。
- 物品类别、数量、总重量、体积/规格，以及服务端生成的重量档。
- 基础配送费、距离费、加急费、骑手小费和订单总金额明细。
- 共享地址簿、路线报价、立即配送和预约配送。
- 用户发布订单，并按“我发的/我收的”查询订单；发件人可取消，发件人或创建时绑定的收件人可确认收货。
- 骑手查看待抢订单、抢单、确认取件和确认送达。
- 可选的四位交付 PIN。
- 寄件、取件和送达图片凭证。
- 送达 24 小时后仍未由用户确认的订单自动完成。

当前不包含支付、退款、骑手收入、结算、代购垫付、自动派单和骑手放弃订单。

设计原型中的支付方式选择、已付款状态、附近骑手上线数量、预计接单时间和骑手收益金额均为静态示意，接口不提供这些数据，App 不应展示或请求它们。发布成功页统一使用静态文案“发布后等待附近骑手接单”。

## 2. 通用约定

### 2.1 基础地址和请求头

接口路径均为相对路径，实际请求地址为：

```text
{baseUrl}{接口路径}
```

除特别说明外，用户端和骑手端接口都必须携带以下请求头：

| 请求头 | 必填 | 说明 |
|---|---:|---|
| `token` | 是 | App 登录后取得的 JWT。不要放在 `Authorization` 中 |
| `Content-Type` | POST JSON 接口必填 | `application/json` |

用户身份和骑手身份均由 `token` 解析，请求体中不提交 `userId` 或 `riderId`。

### 2.2 统一响应

成功响应：

```json
{
  "code": 200,
  "msg": "操作成功",
  "data": {}
}
```

无返回数据的成功响应通常不含 `data`：

```json
{
  "code": 200,
  "msg": "操作成功"
}
```

业务失败响应：

```json
{
  "code": 500,
  "msg": "当前订单状态不允许此操作"
}
```

登录失效响应：

```json
{
  "code": 401,
  "msg": "token已过期，请重新登录！"
}
```

App 必须以响应体 `code` 判断业务是否成功，不能只依赖 HTTP 状态码。`msg` 已由后端国际化，可直接用于错误提示。

### 2.3 分页响应

列表接口的分页数据位于 `data` 中，不使用若依传统的顶层 `rows/total`：

```json
{
  "code": 200,
  "msg": "操作成功",
  "data": {
    "records": [],
    "total": 0,
    "current": 1,
    "size": 10,
    "pages": 0
  }
}
```

- `page` 默认 `1`，小于 `1` 时按 `1` 处理。
- `size` 默认 `10`，范围为 `1` 至 `100`。
- App 至少读取 `records`、`total`、`current` 和 `size`。

### 2.4 时间、金额和距离

- 请求中的预约时间使用 ISO 日期时间字符串，例如 `2026-09-02T10:00:00+08:00`。
- 返回时间以服务端实际 JSON 时间格式为准，App 应按日期时间解析，不应依赖固定展示格式。
- 金额单位为新台币，`currency` 固定为 `TWD`；`startingFare`、`freight`、`distanceFee`、`baseDeliveryFee`、`minimumUrgentFee`、`urgentFee`、`tipAmount` 和 `amount` 都是整数，不带小数。
- `distanceMeters` 单位为米。
- 运价配置中的 `startingDistance`、`distance` 以及报价中的 `billableDistance` 单位为公里，可保留两位小数。
- `estimatedDurationSeconds` 单位为秒，路线服务降级时可能为 `null`。
- `distanceSource` 为 `ROUTE` 时表示地图驾车路线，为 `STRAIGHT_LINE` 时表示地图服务不可用后使用直线距离降级。

## 3. 枚举和状态

### 3.1 服务类型 `serviceType`

| 值 | 含义 |
|---|---|
| `HELP_SEND` | 帮送 |
| `HELP_PICKUP` | 帮取 |

`serviceType` 只表达业务场景，不再表达是否加急。帮送和帮取共享同一套运价时段。

### 3.2 配送等级 `deliveryType`

| 值 | 含义 |
|---|---|
| `NORMAL` | 普通配送，加急费固定为 0 |
| `URGENT` | 1 对 1 加急配送，收取加急费并启用骑手独占规则 |

### 3.3 包裹类型 `packageType`

| 值 | 含义 |
|---|---|
| `DOCUMENT` | 文件 |
| `GIFT` | 礼品 |
| `CLOTHING` | 服饰 |
| `BEAUTY` | 美妆 |
| `DAILY_NECESSITIES` | 日用品 |
| `FOOD_INGREDIENTS` | 食材 |
| `ELECTRONICS` | 数码产品 |
| `SMALL_APPLIANCE` | 小家电 |
| `OTHER` | 其他 |

### 3.4 包裹重量档 `packageSize`

| 值 | 含义 |
|---|---|
| `SMALL` | 不超过 5kg |
| `MEDIUM` | 不超过 12kg |
| `LARGE` | 不超过 20kg |

App 必须提交 `totalWeightKg`，不得提交 `packageSize`。后端按总重量生成重量档：不超过 5kg 为 `SMALL`，超过 5kg 且不超过 12kg 为 `MEDIUM`，超过 12kg 且不超过 20kg 为 `LARGE`。

### 3.5 配送方式 `deliveryMode`

| 值 | 含义 |
|---|---|
| `NOW` | 立即配送，也是未传值时的默认值 |
| `SCHEDULED` | 预约配送 |

预约配送要求：

- `scheduledPickupStartAt` 和 `scheduledPickupEndAt` 都必填。
- 开始时间不得早于当前时间，且不得晚于创建订单时刻后三天。
- 结束时间必须比开始时间晚 30 分钟。
- 预约订单创建后仍为 `WAITING_ACCEPTANCE`，但在预约开始时间到达前不会出现在骑手待抢列表中。

立即配送时不得传 `scheduledPickupStartAt` 和 `scheduledPickupEndAt`。

### 3.6 订单状态 `status`

```text
WAITING_ACCEPTANCE -> ACCEPTED -> PICKED_UP -> DELIVERED -> COMPLETED
          |               |
          +--- 发件人可取消 +--- 发件人可取消
```

| 值 | 含义 | 下一步 |
|---|---|---|
| `WAITING_ACCEPTANCE` | 待骑手接单 | 骑手抢单，或发件人取消 |
| `ACCEPTED` | 骑手已接单 | 骑手确认取件，或发件人取消 |
| `PICKED_UP` | 骑手已取件 | 骑手确认送达 |
| `DELIVERED` | 骑手已送达 | 发件人或创建时绑定的收件人确认收货；超过 24 小时可由系统自动完成 |
| `COMPLETED` | 已完成 | 终态 |
| `CANCELLED` | 已取消 | 终态 |

## 4. 推荐调用流程

### 4.1 用户端

1. 调用首页接口取得当前可用服务及价格摘要。
2. 从共享地址簿选择地址，或填写取件和收件信息。
3. 调用报价接口，展示服务端返回的路线和费用。
4. 用户确认后调用创建订单接口。创建时服务端会重新计算路线和金额，最终以创建响应为准。
5. 使用 `role=sender` 查看“我发的”，使用 `role=receiver` 查看“我收的”，并通过列表或详情刷新状态。当前闪送模块没有 App 实时推送接口。
6. 仅发件人可在 `WAITING_ACCEPTANCE` 或 `ACCEPTED` 状态取消；发件人和创建时绑定的收件人都可在 `DELIVERED` 状态确认收货。

### 4.2 骑手端

1. 调用待抢列表查看当前可接订单。
2. 可先调用详情接口查看同一份脱敏摘要。
3. 调用抢单接口。只有抢单成功后才能取得完整联系人和精确地址。
4. 到达取件点后先上传图片，再调用确认取件接口。
5. 到达收件点后先上传图片；如订单启用 PIN，向收件人取得四位 PIN，再调用确认送达接口。

抢单为并发原子操作。即使列表中仍显示订单，也可能已被其他骑手抢走；收到失败响应后应刷新待抢列表。

## 5. 图片上传

闪送订单接口不接收文件，只接收上传完成后的 HTTP(S) 图片 URL。

App 可复用现有上传接口：

| 方法 | 路径 | Content-Type | 参数 |
|---|---|---|---|
| POST | `/utils/Upload` | `multipart/form-data` | 文件字段名 `file` |

成功响应示例：

```json
{
  "code": 200,
  "msg": "上传成功",
  "data": "/profile/upload/2026/09/01/example.jpg"
}
```

该接口返回的 `data` 可能是相对资源路径。提交给闪送接口前，必须补全为外部可访问的绝对 URL，例如：

```text
https://api.example.com/profile/upload/2026/09/01/example.jpg
```

图片 URL 规则：

- 只接受 `http://` 或 `https://` URL，必须包含主机名。
- 单个 URL 最长 1000 个字符。
- 寄件图片 `senderImageUrls` 可不传，最多 9 张。
- 取件图片和送达图片各至少 1 张、最多 9 张。

## 6. 公共数据对象

### 6.1 下单地址对象

报价和创建订单中的 `pickup`、`delivery` 使用以下结构：

```json
{
  "name": "王小明",
  "phone": "0912345678",
  "address": "台北市信义区市府路1号",
  "addressDetail": "3楼A室",
  "city": "台北市",
  "area": "信义区",
  "handoffMethod": "请电话联系",
  "longitude": 121.5645,
  "latitude": 25.033
}
```

| 字段 | 类型 | 必填 | 规则 |
|---|---|---:|---|
| `name` | string | 是 | 最长 64 字符 |
| `phone` | string | 是 | 最长 32 字符 |
| `address` | string | 是 | 完整主地址，最长 255 字符 |
| `addressDetail` | string | 否 | 楼层、门牌等，最长 255 字符 |
| `city` | string | 否 | 最长 64 字符；用于骑手抢单前的区域展示 |
| `area` | string | 否 | 最长 64 字符；用于骑手抢单前的区域展示 |
| `handoffMethod` | string | 否 | 交接说明，最长 40 字符 |
| `longitude` | number | 是 | `-180` 至 `180` |
| `latitude` | number | 是 | `-90` 至 `90` |

取件和收件地址不能是相同地址文本，也不能使用完全相同的经纬度。

### 6.2 订单字段

App 完整详情使用以下订单字段，响应的 `data` 直接返回这些字段，不再套用 `order`、`images`、`logs` 包装。

| 字段 | 类型 | 说明 |
|---|---|---|
| `id` | number | 订单主键，后续接口路径使用此值 |
| `orderNo` | string | 闪送订单号 |
| `serviceType` | string | 业务场景：`HELP_SEND` 或 `HELP_PICKUP` |
| `deliveryType` | string | 配送等级：`NORMAL` 或 `URGENT` |
| `status` | string | 当前状态 |
| `packageType` | string | 包裹类型 |
| `quantity` | integer | 物品数量，正整数 |
| `totalWeightKg` | number | 物品总重量，大于 0 且不超过 20kg，最多两位小数 |
| `specification` | string/null | 体积或规格说明，最长 255 字符 |
| `packageSize` | string | 后端按总重量生成的重量档 |
| `deliveryMode` | string | `NOW` 或 `SCHEDULED` |
| `scheduledPickupStartAt` | datetime/null | 预约开始时间 |
| `scheduledPickupEndAt` | datetime/null | 预约结束时间 |
| `pinRequired` | boolean | 是否需要交付 PIN |
| `pickup` | object | 取件地址快照，结构见 6.1 |
| `delivery` | object | 收件地址快照，结构见 6.1 |
| `distanceMeters` | number | 配送距离，单位米 |
| `distanceSource` | string | `ROUTE` 或 `STRAIGHT_LINE` |
| `estimatedDurationSeconds` | number/null | 预计时长，单位秒 |
| `baseDeliveryFee` | integer | 基础配送费，即起送价加距离费 |
| `distanceFee` | integer | 超出起送距离后的距离费 |
| `urgentFee` | integer | 加急费；普通配送为 0 |
| `tipAmount` | integer | 用户填写的骑手小费 |
| `amount` | integer | 订单金额，整数 TWD |
| `currency` | string | `TWD` |
| `userNote` | string/null | 用户备注 |
| `acceptedAt` | datetime/null | 接单时间 |
| `pickedUpAt` | datetime/null | 取件时间 |
| `deliveredAt` | datetime/null | 送达时间 |
| `completedAt` | datetime/null | 完成时间 |
| `cancelledAt` | datetime/null | 取消时间 |
| `cancelReason` | string/null | 取消原因 |
| `createTime` | datetime | 创建时间 |
| `updateTime` | datetime | 更新时间 |

用户列表摘要固定字段为：`id,orderNo,serviceType,deliveryType,status,packageType,quantity,totalWeightKg,deliveryMode,scheduledPickupStartAt,scheduledPickupEndAt,pickupAddress,pickupDetailAddress,deliveryAddress,deliveryDetailAddress,estimatedDurationSeconds,baseDeliveryFee,urgentFee,tipAmount,amount,currency,deliveredAt,createTime`。用户列表不返回联系人、电话、精确坐标、照片、日志或内部计价审计字段。

骑手列表摘要固定字段为：`id,orderNo,serviceType,deliveryType,status,packageType,quantity,totalWeightKg,specification,packageSize,deliveryMode,scheduledPickupStartAt,scheduledPickupEndAt,pinRequired,pickupAddress,pickupDetailAddress,deliveryAddress,deliveryDetailAddress,pickupDistanceMeters,distanceMeters,estimatedDurationSeconds,baseDeliveryFee,distanceFee,urgentFee,tipAmount,amount,currency,createTime`。`newTask` 分页响应额外返回 `nearbyTaskCount` 和 `highestOrderAmount`，不返回内部加急比例、最低加急费或骑手收入字段。

### 6.3 App 完整详情对象

用户创建订单、用户订单详情、中单骑手详情及抢单成功响应的 `data` 结构为：

```json
{
  "id": 101,
  "orderNo": "FD1234567890",
  "serviceType": "HELP_SEND",
  "deliveryType": "URGENT",
  "status": "ACCEPTED",
  "packageType": "DOCUMENT",
  "quantity": 1,
  "totalWeightKg": 3.50,
  "specification": "30 x 20 x 10 cm",
  "packageSize": "SMALL",
  "deliveryMode": "NOW",
  "scheduledPickupStartAt": null,
  "scheduledPickupEndAt": null,
  "pinRequired": true,
  "pickup": {},
  "delivery": {},
  "distanceMeters": 4200,
  "distanceSource": "ROUTE",
  "estimatedDurationSeconds": 900,
  "baseDeliveryFee": 72,
  "distanceFee": 12,
  "urgentFee": 15,
  "tipAmount": 5,
  "amount": 92,
  "currency": "TWD",
  "userNote": null,
  "acceptedAt": "2026-09-01T10:20:00+08:00",
  "pickedUpAt": null,
  "deliveredAt": null,
  "completedAt": null,
  "cancelledAt": null,
  "cancelReason": null,
  "createTime": "2026-09-01T10:00:00+08:00",
  "updateTime": "2026-09-01T10:20:00+08:00",
  "senderImageUrls": [
    "https://api.example.com/profile/upload/sender-1.jpg"
  ],
  "pickupImageUrls": [],
  "deliveryImageUrls": [],
  "rider": {
    "name": "陈骑手",
    "avatar": "https://api.example.com/avatar.jpg",
    "rating": 4.9,
    "imUserId": "10086",
    "longitude": 121.56,
    "latitude": 25.03
  },
  "deliveryPinCode": "4821"
}
```

- `senderImageUrls`、`pickupImageUrls` 和 `deliveryImageUrls` 分别表示寄件、取件和送达图片数组。
- App 详情不返回原始状态日志；页面使用 `status`、`acceptedAt`、`pickedUpAt`、`deliveredAt`、`completedAt` 和 `cancelledAt` 展示进度。
- `deliveryPinCode` 只向订单发件人和创建时绑定的收件人返回，并且只在启用 PIN 时出现；骑手响应永不返回此字段。
- `rider` 只向用户返回。订单未接单时为 `null`。骑手对象不包含真实电话号码，联系入口使用 `imUserId` 打开 IM 会话，原型中的通话按钮没有接口支撑。
- 骑手位置只在订单状态为 `ACCEPTED` 或 `PICKED_UP` 时向用户返回，其他状态下经纬度为 `null`。

平台订单详情是审计专用契约，仍可返回 `{order,images,logs}`；App 不应依赖或解析该结构。

## 7. 用户端接口

所有接口均要求用户登录 `token`。

### 7.1 获取闪送首页配置

```http
GET /system/flashDelivery/home
```

响应 `data`：

```json
{
  "serviceTypes": ["HELP_SEND", "HELP_PICKUP"],
  "deliveryTypes": ["NORMAL", "URGENT"],
  "services": [
    {
      "serviceType": "HELP_SEND",
      "pricingId": 1,
      "startTime": "00:00",
      "endTime": "08:00",
      "startingDistance": 3.00,
      "startingFare": 60,
      "distance": 1.00,
      "freight": 10,
      "urgentRate": 20.00,
      "minimumUrgentFee": 15,
      "pricingVersion": 1,
      "currency": "TWD"
    },
    {
      "serviceType": "HELP_PICKUP",
      "pricingId": 1,
      "startTime": "00:00",
      "endTime": "08:00",
      "startingDistance": 3.00,
      "startingFare": 60,
      "distance": 1.00,
      "freight": 10,
      "urgentRate": 20.00,
      "minimumUrgentFee": 15,
      "pricingVersion": 1,
      "currency": "TWD"
    }
  ]
}
```

帮送和帮取会返回两条业务场景数据，但两条数据引用同一个 `pricingId`、`pricingVersion` 和费用配置。`startTime` 包含、`endTime` 不包含，均为台北时区的 `HH:mm`；`24:00` 只会作为结束时间。`services` 为空表示当前时刻没有匹配的统一运价。

### 7.2 获取实时报价

```http
POST /system/flashDelivery/quote
```

请求体：

```json
{
  "serviceType": "HELP_SEND",
  "deliveryType": "URGENT",
  "deliveryMode": "SCHEDULED",
  "scheduledPickupStartAt": "2026-09-08T16:30:00+08:00",
  "scheduledPickupEndAt": "2026-09-08T17:00:00+08:00",
  "packageType": "DOCUMENT",
  "quantity": 1,
  "totalWeightKg": 3.50,
  "specification": "30 x 20 x 10 cm",
  "tipAmount": 5,
  "pickup": {
    "name": "王小明",
    "phone": "0912345678",
    "address": "台北市信义区市府路1号",
    "addressDetail": "3楼A室",
    "city": "台北市",
    "area": "信义区",
    "handoffMethod": "请电话联系",
    "longitude": 121.5645,
    "latitude": 25.033
  },
  "delivery": {
    "name": "李小华",
    "phone": "0987654321",
    "address": "台北市大安区信义路三段56号",
    "addressDetail": "1楼",
    "city": "台北市",
    "area": "大安区",
    "handoffMethod": "交给本人",
    "longitude": 121.538,
    "latitude": 25.0335
  }
}
```

报价请求字段：

| 字段 | 类型 | 必填 | 规则 |
|---|---|---:|---|
| `serviceType` | string | 是 | `HELP_SEND` 或 `HELP_PICKUP` |
| `deliveryType` | string | 是 | `NORMAL` 或 `URGENT` |
| `deliveryMode` | string | 否 | `NOW` 或 `SCHEDULED`，不传按 `NOW` 处理 |
| `scheduledPickupStartAt` | datetime | 预约时是 | `SCHEDULED` 时为未来三天内的预约开始时间；`NOW` 时不得传 |
| `scheduledPickupEndAt` | datetime | 预约时是 | 必须比开始时间晚 30 分钟；`NOW` 时不得传 |
| `packageType` | string | 是 | 取值见 3.3 |
| `quantity` | integer | 是 | 大于 0 的整数 |
| `totalWeightKg` | number | 是 | 大于 0 且不超过 20，最多两位小数 |
| `specification` | string | 否 | 体积或规格说明，去除首尾空格后最长 255 字符 |
| `tipAmount` | integer | 是 | 骑手小费，非负整数 TWD；没有小费时传 `0` |
| `pickup` | object | 是 | 完整取件信息，结构和规则见 6.1 |
| `delivery` | object | 是 | 完整收件信息，结构和规则见 6.1 |

只要业务场景、配送等级、取件时间、物品信息、小费或取送地址发生变化，App 都必须重新调用报价接口，不能继续使用变化前的报价。

响应 `data`：

```json
{
  "serviceType": "HELP_SEND",
  "deliveryType": "URGENT",
  "deliveryMode": "SCHEDULED",
  "scheduledPickupStartAt": "2026-09-08T16:30:00+08:00",
  "scheduledPickupEndAt": "2026-09-08T17:00:00+08:00",
  "pricingId": 1,
  "startTime": "16:00",
  "endTime": "18:00",
  "distanceMeters": 4200,
  "distanceSource": "ROUTE",
  "estimatedDurationSeconds": 900,
  "startingDistance": 3.00,
  "startingFare": 60,
  "distance": 1.00,
  "freight": 10,
  "urgentRate": 20.00,
  "minimumUrgentFee": 15,
  "billableDistance": 1.20,
  "distanceFee": 12,
  "baseDeliveryFee": 72,
  "urgentFee": 15,
  "tipAmount": 5,
  "amount": 92,
  "currency": "TWD",
  "pricingVersion": 1
}
```

距离规则与现有外卖订单一致：先用路线公里数减去 `startingDistance`；超出不足 `0.5` 公里时 `billableDistance` 为 `0`，达到 `0.5` 但不足 `1` 公里时按 `1` 公里，达到 `1` 公里后按实际超出距离。`distanceFee = roundHalfUp(billableDistance / distance × freight)`，`baseDeliveryFee = startingFare + distanceFee`。普通配送的 `urgentFee=0`；加急配送的 `urgentFee=max(minimumUrgentFee, roundHalfUp(baseDeliveryFee × urgentRate / 100))`；`amount=baseDeliveryFee+urgentFee+tipAmount`。小费不参与加急费计算，预计时长也不参与计价。

注意：`baseDeliveryFee` 是已经包含 `distanceFee` 的基础配送费小计。费用合计时不要再次把 `distanceFee` 加入总金额；`distanceFee` 仅用于向用户解释基础配送费中有多少是距离附加费。

立即配送按报价时刻匹配运价，预约配送按 `scheduledPickupStartAt` 的台北时间匹配运价。创建订单必须回传本次报价；后端会重新计算并逐项精确校验，任何变化都不会创建订单，而是返回最新报价供用户重新确认。

### 7.3 创建闪送订单

```http
POST /system/flashDelivery/orders
```

创建请求必须完整包含 7.2 的所有报价请求字段，并在同一个 JSON 对象中增加以下字段。下例只展示新增部分，实际请求不能省略 7.2 中的业务场景、配送等级、取件方式、适用的预约时间段、物品、小费和完整取送地址：

```json
{
  "clientRequestId": "FD-20260908-USER1001-0001",
  "pricingId": 1,
  "pricingVersion": 1,
  "quotedBaseDeliveryFee": 72,
  "quotedDistanceFee": 12,
  "quotedUrgentFee": 15,
  "quotedAmount": 92,
  "pinRequired": true,
  "senderImageUrls": [
    "https://api.example.com/profile/upload/sender-1.jpg"
  ],
  "userNote": "文件请勿折叠"
}
```

| 新增字段 | 类型 | 必填 | 说明 |
|---|---|---:|---|
| `clientRequestId` | string | 是 | 客户端幂等请求号，去除首尾空格后最长 64 字符 |
| `pricingId` | integer | 是 | 报价响应的运价配置 ID |
| `pricingVersion` | integer | 是 | 报价响应的配置版本 |
| `quotedBaseDeliveryFee` | integer | 是 | 报价响应的基础配送费 |
| `quotedDistanceFee` | integer | 是 | 报价响应的距离费 |
| `quotedUrgentFee` | integer | 是 | 报价响应的加急费 |
| `quotedAmount` | integer | 是 | 报价响应的总金额 |
| `pinRequired` | boolean | 否 | 不传时默认为 `true` |
| `senderImageUrls` | string[] | 否 | 寄件图片，最多 9 张 |
| `userNote` | string | 否 | 最长 500 字符 |

同一用户使用相同 `clientRequestId` 重试时，后端返回第一次创建的订单，不会重复创建。一次下单动作生成一个请求号；网络超时重试必须复用原请求号，新下单必须生成新请求号。使用旧请求号但修改请求体，仍会返回旧订单。

成功响应 `data` 为完整详情对象，初始状态为 `WAITING_ACCEPTANCE`。创建响应中的 `amount` 是最终整数订单金额。

若运价版本、路线或费用发生变化，响应 `code=500`、`msg=报价已变化，请重新确认最新费用`，并在 `data` 返回与报价接口相同的完整最新报价。App 必须更新费用明细并要求用户再次确认，不能自动用旧报价重试。

报价变化响应示例：

```json
{
  "code": 500,
  "msg": "报价已变化，请重新确认最新费用",
  "data": {
    "serviceType": "HELP_SEND",
    "deliveryType": "URGENT",
    "deliveryMode": "SCHEDULED",
    "scheduledPickupStartAt": "2026-09-08T16:30:00+08:00",
    "scheduledPickupEndAt": "2026-09-08T17:00:00+08:00",
    "pricingId": 2,
    "startTime": "16:00",
    "endTime": "18:00",
    "distanceMeters": 4200,
    "distanceSource": "ROUTE",
    "estimatedDurationSeconds": 900,
    "startingDistance": 3.00,
    "startingFare": 60,
    "distance": 1.00,
    "freight": 10,
    "urgentRate": 25.00,
    "minimumUrgentFee": 15,
    "billableDistance": 1.20,
    "distanceFee": 12,
    "baseDeliveryFee": 72,
    "urgentFee": 18,
    "tipAmount": 5,
    "amount": 95,
    "currency": "TWD",
    "pricingVersion": 2
  }
}
```

创建时，后端会移除收件手机号中的空白、`+`、`-`、`(`、`)` 后，匹配有效且未删除的普通用户账号。仅在恰好匹配一个账号时把该账号固化为收件人；无匹配或重复匹配均不绑定。此绑定只执行一次，历史订单不会因收件人之后注册或修改手机号而自动认领。

### 7.4 查询用户订单列表

```http
GET /system/flashDelivery/orders?page=1&size=10&role=sender
```

查询参数：

| 参数 | 必填 | 说明 |
|---|---:|---|
| `page` | 否 | 默认 `1`，小于 `1` 时按 `1` 处理 |
| `size` | 否 | 默认 `10`，范围为 `1` 至 `100` |
| `role` | 否 | `sender` 查询“我发的”，`receiver` 查询“我收的”；默认 `sender` |

仅查询当前用户作为指定角色的订单，按创建时间倒序返回全部状态的订单摘要。接口不接受 `status`、`scene` 或 `serviceType`；响应 `data.records` 中每项为用户列表摘要对象。

`role` 只接受 `sender` 或 `receiver`，不区分大小写；传空值按 `sender` 处理，传其他值返回业务错误。“我收的”完全依据订单创建时固化的收件账号，不会在查询时重新按手机号匹配。

设计原型中的“待接单、配送中、已完成、已取消”筛选页签当前没有对应查询参数。第一版建议只展示“全部”列表；如需页签，只能基于当前页数据本地过滤，且不应依赖它得到准确的分页总数。

### 7.5 查询用户订单详情

```http
GET /system/flashDelivery/orders/{id}
```

订单发件人和创建时固化的收件人均可访问。响应 `data` 直接为 App 完整订单字段，包括订单快照、三类图片数组、骑手摘要和可选的交付 PIN，不返回原始状态日志。响应不包含支付状态或支付方式字段，详情页不要展示“已付款”等支付信息。

### 7.6 用户取消订单

```http
POST /system/flashDelivery/orders/{id}/cancel
```

请求体：

```json
{
  "reason": "行程有变，不需要配送"
}
```

- `reason` 必填，去除首尾空格后不能为空，最长 500 字符。
- 仅订单发件人可以取消；收件人不能取消。
- 仅 `WAITING_ACCEPTANCE` 和 `ACCEPTED` 状态允许用户取消。
- `PICKED_UP` 之后需要平台介入，用户端不能直接取消。
- 设计原型中“骑士接单前可免费取消”的文案比接口口径更严格：接口允许骑手已接单但尚未取件时取消，且当前没有支付，取消不产生任何费用；接入支付后取消规则需重新对齐。

### 7.7 用户确认收货

```http
POST /system/flashDelivery/orders/{id}/confirmReceipt
```

无请求体。订单发件人或创建时固化的收件人都可操作；仅 `DELIVERED` 状态可确认，成功后状态变为 `COMPLETED`。

## 8. 骑手端接口

所有接口均要求骑手登录 `token`，且账号 `userType` 必须为 `2`。

接单使用共享 Redis 锁 `lock:delivery:rider:{riderId}` 串行执行跨外卖、闪送的资格检查和订单条件更新，并在事务完成后释放。该锁只覆盖接单事务，不会一直持有到配送完成；持续独占由每次接新单时查询骑手进行中的订单保证，Redis 锁用于防止两个并发接单请求同时绕过查询。

`deliveryType=URGENT` 的急送要求骑手没有任何进行中的外卖或闪送；骑手已有进行中的急送时，也不能再接普通闪送或外卖。普通闪送不会阻止骑手承接外卖，外卖也不会阻止骑手承接普通闪送。Redis 不可用或 3 秒内未取得锁时采用失败关闭策略，本次接单直接失败，不更新订单。

### 8.1 查询骑手订单列表

```http
GET /system/flashDelivery/rider/orders?page=1&size=10&tab=newTask&longitude=121.5&latitude=25.0
```

查询参数：

| 参数 | 必填 | 说明 |
|---|---:|---|
| `page` | 否 | 默认 `1`，小于 `1` 时按 `1` 处理 |
| `size` | 否 | 默认 `10`，范围为 `1` 至 `100` |
| `tab` | 否 | 默认 `newTask`；可选 `newTask`、`toPickup`、`delivering`、`completed` 或 `cancelled` |
| `longitude` | 否 | 骑手当前位置经度；`newTask` 传入时用于附近范围、取件点距离和排序，范围 `-180` 至 `180` |
| `latitude` | 否 | 骑手当前位置纬度；`newTask` 传入时用于附近范围、取件点距离和排序，范围 `-90` 至 `90` |

页签映射：

| `tab` | 对应状态 | 查询范围 |
|---|---|---|
| `newTask` | `WAITING_ACCEPTANCE` | 当前可抢任务 |
| `toPickup` | `ACCEPTED` | 当前骑手本人任务 |
| `delivering` | `PICKED_UP` | 当前骑手本人任务 |
| `completed` | `DELIVERED`、`COMPLETED` | 当前骑手本人任务 |
| `cancelled` | `CANCELLED` | 当前骑手本人任务 |

除 `newTask` 外只查询当前骑手本人任务。闪送没有退款流程，不提供外卖列表中的 `refund` 页签。

`newTask` 的 `longitude` 和 `latitude` 仅用于附近范围筛选、计算取件点距离和排序；其他页签忽略坐标。列表按页签和骑手范围完成服务端过滤后再分页，不由 App 拉取完整集合后过滤。

`newTask` 分页响应的 `data` 除 `records`、`total`、`current`、`size` 外，还返回 `nearbyTaskCount` 和 `highestOrderAmount`。不返回尖峰倍率或骑手收入字段。`amount` 是服务端计算的订单报价金额，当前不会发起支付或扣款，页面文案不得表述为“预估收益”或“收益”；`nearbyTaskCount` 是附近待抢订单数，不是上线骑手数。

所有页签的 `data.records` 使用骑手列表摘要字段：

```json
{
  "id": 101,
  "orderNo": "FD1234567890",
  "serviceType": "HELP_SEND",
  "deliveryType": "URGENT",
  "status": "WAITING_ACCEPTANCE",
  "packageType": "DOCUMENT",
  "quantity": 1,
  "totalWeightKg": 3.50,
  "specification": "30 x 20 x 10 cm",
  "packageSize": "SMALL",
  "deliveryMode": "NOW",
  "scheduledPickupStartAt": null,
  "scheduledPickupEndAt": null,
  "pinRequired": true,
  "pickupAddress": "台北市信义区市府路1号",
  "pickupDetailAddress": "3楼A室",
  "deliveryAddress": "台北市大安区信义路三段56号",
  "deliveryDetailAddress": "1楼",
  "pickupDistanceMeters": 420,
  "distanceMeters": 4200,
  "estimatedDurationSeconds": 900,
  "baseDeliveryFee": 72,
  "distanceFee": 12,
  "urgentFee": 15,
  "tipAmount": 5,
  "amount": 92,
  "currency": "TWD",
  "createTime": "2026-09-01T09:00:00+08:00"
}
```

接单前列表可展示完整的取件、送达文字地址（包括详细地址），但不得返回联系人、电话、实际 PIN、精确坐标、用户备注、图片凭证、状态日志、用户 ID、幂等号或计价审计字段。列表中的文字地址不能作为精确坐标使用。

### 8.2 查询骑手订单详情

```http
GET /system/flashDelivery/rider/orders/{id}
```

响应 `data` 始终直接返回订单字段，顶层结构保持稳定，不使用 `{order,images,logs}` 包装，也不要求 App 判断 `data.order`。接单前隐藏联系人、电话、实际 PIN、精确坐标和履约图片；联系人姓名与电话一样都不返回，列表和详情不要按设计原型渲染“·联系人姓名”。当前骑手接单后，详情接口才补齐其有权限查看的完整履约信息。

### 8.3 抢单

```http
POST /system/flashDelivery/rider/orders/{id}/accept
```

无请求体。骑手账号必须已开通闪送配送。抢单成功后状态变为 `ACCEPTED`，响应 `data` 直接返回完整订单字段；订单已被其他骑手抢走时返回“订单已被其他骑手接走”。违反急送独占规则时返回统一的独占冲突提示，App 应保留在列表页并刷新可接任务。

### 8.4 确认取件

```http
POST /system/flashDelivery/rider/orders/{id}/pickup
```

请求体：

```json
{
  "imageUrls": [
    "https://api.example.com/profile/upload/pickup-1.jpg"
  ]
}
```

仅订单骑手可在 `ACCEPTED` 状态调用。图片至少 1 张、最多 9 张；成功后状态变为 `PICKED_UP`。

### 8.5 确认送达

```http
POST /system/flashDelivery/rider/orders/{id}/deliver
```

启用 PIN 的订单：

```json
{
  "imageUrls": [
    "https://api.example.com/profile/upload/delivery-1.jpg"
  ],
  "pinCode": "4821"
}
```

未启用 PIN 的订单可不传 `pinCode`：

```json
{
  "imageUrls": [
    "https://api.example.com/profile/upload/delivery-1.jpg"
  ]
}
```

仅订单骑手可在 `PICKED_UP` 状态调用。图片至少 1 张、最多 9 张。PIN 校验失败时不会保存送达图片，也不会改变订单状态；成功后状态变为 `DELIVERED`。

## 9. 共享地址簿接口

地址簿由闪送和现有收货业务共用。以下接口都要求用户登录 `token`，并且只能操作当前用户自己的地址。

### 9.1 查询地址列表

```http
GET /system/address/getaddress?keyword=王小明
```

`keyword` 可选，同时搜索姓名、电话、主地址和详细地址。置顶地址优先，响应 `data` 为地址数组。

### 9.2 查询地址详情

```http
GET /system/address/getaddressxq?id={addressId}
```

响应 `data` 为地址对象。

### 9.3 新增或修改地址

```http
POST /system/address/address
```

新增地址不传 `id`；修改地址传本人地址的 `id`：

```json
{
  "id": null,
  "name": "王小明",
  "phone": "0912345678",
  "address": "台北市信义区市府路1号",
  "addressDetail": "3楼A室",
  "longitude": 121.5645,
  "latitude": 25.033,
  "country": "TW",
  "province": "台北市",
  "city": "台北市",
  "area": "信义区",
  "annexes": null
}
```

| 字段 | 必填 | 规则 |
|---|---:|---|
| `name` | 是 | 最长 64 字符 |
| `phone` | 是 | 最长 32 字符 |
| `address` | 是 | 最长 255 字符 |
| `addressDetail` | 否 | 最长 255 字符 |
| `longitude` | 是 | `-180` 至 `180` |
| `latitude` | 是 | `-90` 至 `90` |
| `country`、`province`、`city`、`area` | 否 | 各最长 64 字符 |
| `annexes` | 否 | 最长 1000 字符 |

后端忽略客户端身份字段，地址所有者始终取自 `token`。保存成功后 `data` 返回保存后的地址对象。

地址簿对象不含 `handoffMethod`。把地址用于闪送报价或下单时，需要映射联系人、地址和坐标，并按本次订单另行填写交接方式。

### 9.4 删除地址

```http
DELETE /system/address/{id}
```

无请求体，仅可删除本人地址。

### 9.5 置顶地址

```http
POST /system/address/{id}/top
```

无请求体。置顶只影响当前用户地址簿的排序。

## 10. 常见业务错误与处理建议

| `msg` 含义 | 常见原因 | App 处理建议 |
|---|---|---|
| token 已过期 | 登录态失效 | 清理登录态并重新登录 |
| 对应取件时段暂无可用运价 | 立即时刻或预约开始时刻没有匹配统一运价 | 重新选择取件时间或稍后重试 |
| 报价已变化，请重新确认最新费用 | 运价版本、路线或费用与创建请求回传值不一致 | 用响应 `data` 刷新费用并让用户再次确认 |
| 物品数量、总重量或规格信息不正确 | 数量非正整数、重量不大于 0、超过 20kg、小数超过两位或规格过长 | 返回物品信息页修正 |
| 取件地址和收件地址不能相同 | 地址文本相同或经纬度完全相同 | 要求用户修改地址 |
| 配送距离不能超过 40 公里 | 服务端路线距离超限 | 提示当前路线不可下单 |
| 预约取件时段无效 | 时间已过、超过三天或不是 30 分钟 | 重新选择预约时段 |
| 订单已被其他骑手接走 | 并发抢单失败 | 刷新待抢列表 |
| 骑手存在进行中的独占配送任务 | 当前订单或骑手已有急送冲突 | 保留当前页并刷新可接任务 |
| 系统繁忙，请稍后重试 | Redis 不可用或 3 秒内未取得骑手锁 | 不重复提交，稍后刷新并重试 |
| 当前订单状态不允许此操作 | 页面状态已过期或重复操作 | 重新拉取订单详情 |
| 交付 PIN 不正确 | 骑手提交 PIN 错误 | 保留页面并重新输入 PIN |
| 闪送订单不存在或无权访问 | ID 不存在或不属于当前账号 | 返回列表并刷新数据 |
| 凭证图片地址无效 | URL 不是完整 HTTP(S) 地址 | 检查上传结果并补全资源域名 |

## 11. 接入验收清单

- 首页只展示 `/home` 返回的当前时段可用服务，不在 App 中维护启停状态或默认价格。
- 报价和创建订单都提交 `serviceType`、`deliveryType`、配送方式、完整物品信息、骑手小费及取送坐标。
- 创建订单原样回传报价的配置 ID、版本、基础配送费、距离费、加急费和总金额；报价变化时展示最新明细并重新确认。
- 金额按整数 TWD 展示；`baseDeliveryFee` 已包含 `distanceFee`，距离费只作为基础配送费的明细展示，总金额只按 `baseDeliveryFee + urgentFee + tipAmount` 计算；不显示支付成功、骑手收入、时长费或尖峰费用。
- 创建订单具备稳定的 `clientRequestId` 重试策略。
- 预约时间满足未来三天内、固定 30 分钟的约束。
- App 使用响应体 `code` 判断成功或失败。
- 分页列表从 `data.records` 和 `data.total` 取值。
- 图片先上传，再向闪送接口提交完整 HTTP(S) URL。
- “我发的”只在待接单或已接单状态显示取消入口；“我发的”和“我收的”都只在已送达状态显示确认收货入口。
- 骑手抢单前可展示完整文字地址但不使用坐标导航；抢单成功后再读取联系人、电话、精确坐标和履约图片。
- 骑手严格按 `ACCEPTED -> PICKED_UP -> DELIVERED` 顺序操作。
- 启用 PIN 时，骑手送达必须提交用户端详情显示的四位 PIN。
- 对抢单冲突、状态变化和重复点击，均以刷新后的服务端订单状态为准。
- 用户订单列表使用 `role=sender/receiver` 切换“我发的”和“我收的”；状态页签如需展示，仅基于当前页数据本地过滤。
- “我收的”只展示创建时唯一匹配并固化到订单的账号；不在列表或详情查询时动态认领历史订单。
- 发布成功页显示“发布后等待附近骑手接单”，不显示虚构的骑手数量或预计接单时长。
- 骑手端金额文案统一为“订单金额”，不使用“收益”；联系骑手/用户通过 IM，不提供通话。
