# 闪送配送 API 契约 ## 通用约定 - 用户/骑手端要求请求头 `token`,身份字段不进入请求体。 - 平台权限:`flash:pricing:list/edit`、`flash:order:list/query/cancel/complete`。 - 响应沿用 `AjaxResult`:`{"code":200,"msg":"...","data":...}`;分页 data 为 `records,total,current,size`。 - 金额为整数 TWD;路线距离为米,运价配置距离为公里。 - `serviceType`:`HELP_SEND`、`HELP_PICKUP`。 - `deliveryType`:`NORMAL`、`URGENT`。 - `packageType`:`DOCUMENT`、`GIFT`、`CLOTHING`、`BEAUTY`、`DAILY_NECESSITIES`、`FOOD_INGREDIENTS`、`ELECTRONICS`、`SMALL_APPLIANCE`、`OTHER`。 - 物品信息提交 `packageType`、正整数 `quantity`、可选 `weightRange` 及最长 255 字符的可选 `specification`;传入时 `weightRange` 只接受 `UP_TO_5_KG`、`OVER_5_TO_10_KG`、`OVER_10_TO_15_KG`、`OVER_15_TO_20_KG`。 - `vehicleType`:`1`=机车、`2`=轿车;报价和创建均必填并必须一致。历史订单、骑手资料和运价配置的空值按机车处理。 - 路线距离和 `estimatedDurationSeconds` 按 `vehicleType` 获取:机车使用 Google Routes 的 `TWO_WHEELER`,轿车使用 `DRIVE`。地图路线不可用时,`distanceSource=STRAIGHT_LINE`,`estimatedDurationSeconds` 仍必返:机车以 25 km/h、轿车以 30 km/h 的保守速度按直线距离估算,向上取整且最少 60 秒;该时长不参与金额计算。两轮路线是 Google Beta 能力;本期不修改用户 App 的提示文案。 - `deliveryMode`:`NOW`、`SCHEDULED`。预约起止时间使用 ISO 日期时间,时段固定 30 分钟且开始时间不晚于三天后。 - `status`:`WAITING_ACCEPTANCE`、`ACCEPTED`、`PICKED_UP`、`DELIVERED`、`COMPLETED`、`CANCELLED`。 ## 地址对象 ```json {"name":"王小明","phone":"0912345678","address":"台北市信义区市府路1号","addressDetail":"3楼A室","longitude":121.5645,"latitude":25.0330,"country":"TW","province":"台北市","city":"台北市","area":"信义区","annexes":""} ``` | 方法 | 路径 | 参数/说明 | |---|---|---| | GET | `/system/address/getaddress` | header token;query `keyword?`;仅本人,搜索并置顶优先 | | GET | `/system/address/getaddressxq` | header token;query `id`;仅本人 | | POST | `/system/address/address` | header token;body 地址对象,可含 `id`;忽略 userId | | DELETE | `/system/address/{id}` | header token;仅本人 | | POST | `/system/address/{id}/top` | header token;仅本人 | ## 用户端 | 方法 | 路径 | 请求/说明 | |---|---|---| | GET | `/system/flashDelivery/home` | 返回 `serviceTypes=[HELP_SEND,HELP_PICKUP]`、`deliveryTypes=[NORMAL,URGENT]` 及当前时刻命中的统一运价摘要;不返回内部更新人、时间或地图密钥 | | POST | `/system/flashDelivery/quote` | `serviceType,deliveryType,vehicleType,deliveryMode,scheduledPickupStartAt?,scheduledPickupEndAt?,packageType,quantity,weightRange?,specification?,tipAmount,pickup,delivery`;立即订单按当前时刻、预约订单按预约开始时刻匹配对应车型运价 | | POST | `/system/flashDelivery/orders` | 报价请求字段 + `clientRequestId,pricingId,pricingVersion,quotedBaseDeliveryFee,quotedDistanceFee,quotedUrgentFee,quotedAmount,pinRequired?,senderImageUrls?,userNote?`;服务端按相同车型复算并逐项校验后幂等创建 | | GET | `/system/flashDelivery/orders` | `page,size,role?`;`role=sender/receiver`,省略为 `sender`;按创建时间倒序返回当前用户作为寄件人或已绑定收件人的订单摘要 | | GET | `/system/flashDelivery/orders/{id}` | 订单创建人或创建时已绑定收件人的 App 订单详情;`data` 直接为订单字段,不返回原始状态日志 | | POST | `/system/flashDelivery/orders/{id}/cancel` | `{"reason":"行程变化"}`;仅订单创建人且状态为待接/已接 | | POST | `/system/flashDelivery/orders/{id}/confirmReceipt` | 无 body;订单创建人或已绑定收件人可对 DELIVERED 订单确认签收 | 用户列表摘要字段固定为:`id,orderNo,serviceType,deliveryType,vehicleType,status,packageType,quantity,weightRange,deliveryMode,scheduledPickupStartAt,scheduledPickupEndAt,pickupAddress,pickupDetailAddress,deliveryAddress,deliveryDetailAddress,estimatedDurationSeconds,baseDeliveryFee,urgentFee,tipAmount,amount,currency,deliveredAt,createTime`。列表不返回联系人、电话、精确坐标、照片、日志或内部计价审计字段。`receiver` 不是按当前手机号动态查询:订单只在创建时唯一匹配已注册普通用户并固化 `receiverUserId`,未匹配订单以后不追溯认领。 报价响应 data:`serviceType,deliveryType,vehicleType,deliveryMode,scheduledPickupStartAt?,scheduledPickupEndAt?,pricingId,startTime,endTime,distanceMeters,distanceSource,estimatedDurationSeconds?,startingDistance,startingFare,distance,freight,billableDistance,distanceFee,baseDeliveryFee,urgentRate,minimumUrgentFee,urgentFee,tipAmount,amount,currency,pricingVersion`。`billableDistance` 为按外卖规则处理 0.5 公里边界后的计费公里数;全部费用为整数 TWD,`urgentRate` 为百分比。`baseDeliveryFee=startingFare`,仅表示起步价;普通配送费在服务端按 `baseDeliveryFee+distanceFee` 计算但不单独返回;普通配送 `urgentFee=0`;加急配送 `urgentFee=max(minimumUrgentFee,roundHalfUp((baseDeliveryFee+distanceFee)*urgentRate/100))`;`amount=baseDeliveryFee+distanceFee+urgentFee+tipAmount`。已有订单的金额快照不回写,新口径仅用于此调整发布后的新报价和新订单。 创建时服务端按 `vehicleType` 重新获取对应车型路线、匹配运价并复算费用。`pricingId`、`pricingVersion`、`quotedBaseDeliveryFee`、`quotedDistanceFee`、`quotedUrgentFee`、`quotedAmount` 任一不一致时不创建订单,返回国际化“报价已变化,请重新确认”及完整最新报价。地址快照字段为联系人、电话、市/区、交付方式、地址、详细地址和经纬度。用户创建和详情响应的 `data` 直接为订单详情字段,照片分别为 `senderImageUrls,pickupImageUrls,deliveryImageUrls`,不返回原始状态日志;`deliveryPinCode` 仅在启用时向订单参与用户返回,用户仅在 `ACCEPTED`、`PICKED_UP` 阶段获得骑手位置。 ## 骑手端 所有接口要求 token 用户 `userType=2`。 | 方法 | 路径 | 请求/说明 | |---|---|---| | GET | `/system/flashDelivery/rider/orders` | `page,size,tab,longitude?,latitude?`;参数方式与骑手外卖列表一致,`tab=newTask/toPickup/delivering/completed/cancelled`;坐标用于新任务附近范围、取件点距离和排序 | | GET | `/system/flashDelivery/rider/orders/{id}` | `data` 直接为稳定的骑手订单详情;未接单隐藏敏感字段,接单后仅订单骑手可见完整履约信息 | | POST | `/system/flashDelivery/rider/orders/{id}/accept` | 原子抢单,成功后 `data` 直接返回骑手订单详情 | | POST | `/system/flashDelivery/rider/orders/{id}/pickup` | `{"imageUrls":["https://.../pickup.jpg"]}`;至少一张 | | POST | `/system/flashDelivery/rider/orders/{id}/deliver` | `{"imageUrls":["https://.../delivery.jpg"],"pinCode":"4821"}`;图片至少一张,启用 PIN 时必须正确 | 骑手页签状态映射:`newTask=WAITING_ACCEPTANCE`、`toPickup=ACCEPTED`、`delivering=PICKED_UP`、`completed=DELIVERED+COMPLETED`、`cancelled=CANCELLED`。除 `newTask` 外只返回当前骑手本人任务;闪送没有 `refund` 页签。 骑手列表摘要字段固定为:`id,orderNo,serviceType,deliveryType,vehicleType,status,packageType,quantity,weightRange,specification,deliveryMode,scheduledPickupStartAt,scheduledPickupEndAt,pinRequired,pickupAddress,pickupDetailAddress,deliveryAddress,deliveryDetailAddress,pickupDistanceMeters,distanceMeters,estimatedDurationSeconds,baseDeliveryFee,urgentFee,tipAmount,amount,currency,createTime`。`newTask` 只返回与骑手登记车型一致的待接单任务;骑手查看待接单详情或接单时也会重新校验车型。分页 data 在 `records,total,current,size` 外增加 `nearbyTaskCount,highestOrderAmount`;不返回加急比例、最低加急费或骑手收入字段。 骑手接单前可按原型查看完整取送文字地址,但不返回用户 ID、联系人、电话、实际 PIN、精确经纬度、备注、幂等请求号、履约凭证或日志。抢单成功后才向中单骑手返回完整联系方式、坐标和有权限查看的照片数组。骑手详情在接单前后使用同一个直接 DTO,不返回 `{order,images,logs}` 包装,也不要求 App 判断 `data.order`。 ## 平台端 | 方法 | 路径 | 请求/说明 | |---|---|---| | GET | `/system/flashDelivery/admin/pricing` | 返回全部车型运价时段,按车型、开始时间排序 | | POST | `/system/flashDelivery/admin/pricing` | `vehicleType,startTime,endTime,startingDistance,startingFare,distance,freight,urgentRate,minimumUrgentFee`;保存即生效 | | 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` | `reason` 必填;非终态可取消 | | POST | `/system/flashDelivery/admin/orders/{id}/complete` | 无 body;仅 DELIVERED | 本期没有支付、退款、取消费、退回费、结算、骑手收入、代购/垫付、商品金额、违禁品电子确认、骑手放弃或自动派单字段和接口。所有闪送金额保持整数 TWD;创建订单后直接待接单,不存在待支付状态。`deliveryType=URGENT` 为 1 对 1 独占配送,相关外卖与闪送接单入口必须执行跨订单独占检查。骑手小费仅为订单费用明细,不代表已付款。 ### 骑手抢单资格与容量(2026-09-16) `POST /system/flashDelivery/rider/orders/{id}/accept` 在同一骑手锁内重新校验账号和容量,不信任此前的列表结果。骑手必须为已审核、启用、在线的 `userType=2` 账号并开通 `FLASH`;服务端同时检查进行中闪送数量、外卖与闪送合计数量以及急送独占。外卖接单入口使用相同策略并要求开通 `FOOD`。 容量由 `sys_rider_food_active_order_limit`、`sys_rider_flash_active_order_limit`、`sys_rider_total_active_order_limit` 控制;有效范围 1..20,缺失或非法时回退 1。接口会区分返回离线、配送类型未开通、类型任务已满、总任务已满、急送独占和订单已被抢等原因。当前待抢列表未共用容量过滤,也未增加位置过期或接单距离校验,客户端必须以 accept 响应为最终结果并在失败后刷新列表。 运价时段使用业务当地时间和 `HH:mm` 格式,采用开始时间包含、结束时间不包含的匹配语义;`24:00` 只允许作为结束时间,跨午夜时段必须拆成两个配置。全部时间段不得重叠。起送距离和计价距离必须为正数,起送价格和计价金额必须为正整数,加急比例和最低加急费必须为非负数;立即订单按当前时刻、预约订单按预约开始时刻匹配,目标时刻没有匹配时段时报“对应取件时段暂无可用运价”。 ## 2026-09-09:待接单订单修改取送信息与追加小费 以下接口均要求 header `token`,仅订单创建人可调用。订单必须为 `WAITING_ACCEPTANCE` 且无骑手。用户详情新增 `orderVersion`;这是修改时必须回传的订单版本,与运价版本 `pricingVersion` 不同。 | 方法 | 路径 | 用途 | |---|---|---| | POST | `/system/flashDelivery/orders/{id}/address/quote` | 修改地址预报价,不更新订单 | | POST | `/system/flashDelivery/orders/address` | 用户确认报价后保存地址、路线和费用 | | POST | `/system/flashDelivery/orders/tip` | 追加小费并更新总金额 | ### 地址预报价 请求 DTO:`FlashDeliveryAddressChangeRequest`。必须提交两端完整地址;只改发件或收件一端时,另一端按订单详情原样回传。地址簿数据不随之更新。 ```json { "orderVersion": 2, "pickup": { "name": "寄件人", "phone": "0911111111", "address": "台北市中山區取件地址", "addressDetail": "3樓", "city": "台北市", "area": "中山區", "handoffMethod": "到門口取件", "latitude": 25.0100, "longitude": 121.5100 }, "delivery": { "name": "收件人", "phone": "0922222222", "address": "台北市大安區新收件地址", "addressDetail": "交前台", "city": "台北市", "area": "大安區", "handoffMethod": "交前台", "latitude": 25.0200, "longitude": 121.5200 } } ``` `name/phone/address/latitude/longitude` 必填,其余地址字段可空;取送主地址加详细地址不能完全相同,坐标不能相同,路线不得超过 40 公里。 成功 `data` 为既有 `FlashDeliveryQuoteView` 加 `orderVersion`,包括 `distanceMeters/distanceSource/estimatedDurationSeconds/baseDeliveryFee/distanceFee/urgentFee/tipAmount/amount` 及原订单计价快照。即时单和预约单都沿用原订单运价,不因后台调价或跨时段切换价格;已有小费保留。此接口不更改原订单和状态日志。 ### 确认保存地址 请求 DTO:`FlashDeliveryAddressConfirmRequest`。订单 ID 改为请求体必填的正整数 `orderId`,不放 URL;提交上述完整地址、同一 `orderVersion`,并回传本次报价字段: ```json { "orderId": 101, "orderVersion": 2, "pickup": {"name":"寄件人","phone":"0911111111","address":"取件地址","latitude":25.0100,"longitude":121.5100}, "delivery": {"name":"收件人","phone":"0922222222","address":"新收件地址","latitude":25.0200,"longitude":121.5200}, "quotedDistanceMeters": 4500, "quotedBaseDeliveryFee": 113, "quotedDistanceFee": 23, "quotedUrgentFee": 0, "quotedAmount": 123 } ``` 金额仅为示例,客户端必须使用实际报价;`baseDeliveryFee` 仅为起步价,`distanceFee` 为独立距离附加费,总金额为 `baseDeliveryFee + distanceFee + urgentFee + tipAmount`。 保存前服务端重新计算路线和价格。距离或任一确认金额不同(包括漏传)时,返回非成功 AjaxResult,`msg` 为国际化 `flash.delivery.quote.changed`,`data` 为最新完整报价,订单不变。客户端展示最新报价,经用户再次确认后重新提交,不能自动按新报价扣改金额。 成功返回直接订单详情 DTO(包括新地址、路线、费用和递增后的 `orderVersion`)。修改收件电话后重新匹配收件账号,无法唯一匹配时清空旧账号绑定,原收件人随即失去该订单的查询与签收权限。保持订单 ID、编号、状态、原运价、物品、预约和已有小费。 ### 追加小费 请求 DTO:`FlashDeliveryTipAddRequest`: ```json {"orderId": 101, "orderVersion": 2, "additionalTipAmount": 20} ``` `orderId` 必填,为用户订单详情的正整数 `id`,不放 URL。 `additionalTipAmount` 表示本次增量,必须为正整数 TWD,不是小费最终总额。小费由 10 变为 30 时传 20;总金额同时增加 20,基础配送费、加急费和路线不变。零、负数、空值及金额溢出拒绝。成功返回更新后的直接订单详情和递增版本。此操作仍属于非支付订单金额调整,不产生支付或结算。 ### 状态、并发与重试 - 创建人以外的用户(包括已绑定收件人)请求返回订单不存在;状态不是待接单或已有骑手返回 `flash.delivery.edit.not.allowed`。 - 缺失或负数版本返回 `flash.delivery.order.version.invalid`;旧版本或最终条件 UPDATE 未命中返回 `flash.delivery.state.changed`。 - 最终 UPDATE 同时匹配订单 ID、创建人、待接单、无骑手和版本;接单、取消、另一次地址修改或追加小费先成功时,本次不得覆盖。 - 同一小费请求成功后重发旧版本会被拒绝,不会重复加款。网络超时应刷新详情核对小费和总金额,不应自动换新版本重复追加。 - 地址报价后如追加过小费,旧地址确认失效;刷新详情、重新报价并让用户确认。 - 当前取消规则、支付和退款范围不变;无新增数据库字段或迁移 SQL。 ## 2026-09-15 多取货点增量(覆盖上文单点口径) ### 下单、报价与待接单编辑 - `home` 增加 `pickupStopLimit`、`showMultiPickup`、`showUrgentOption`。上限来自 `flash_pickup_stop_limit`,有效值 1..26,缺失或非法回退 1;`flash_show_multi_pickup` 关闭时只能提交一个取货点。 - 报价、创建新增 `pickups` 数组。每项直接包含地址字段(`name,phone,address,addressDetail,city,area,handoffMethod,latitude,longitude`)及独立物品字段(`packageType,quantity,weightRange,specification`),不再提交订单级物品。 - 原 `pickup` 加订单级物品的单点请求仍可提交;与 `pickups` 同传或多点数组与订单级物品混传均拒绝。新订单一律保存取货及收货站点,不提供缺失站点历史订单回退。 - 地址报价/确认使用完整 `pickups` 和唯一 `delivery`。多点不得仅传旧 `pickup`;旧单点编辑保留原物品。待接单调整站点顺序必须重新报价确认;接单后访问顺序调整不走改址,不重算金额。 - 用户顺序用于整线地图报价,最多 26 个取货点及 1 个收货点,不优化或省略中途点;地图不可用时逐段直线求和,整线限制 40 公里。仅追加小费不调用路线、不重新校验开关及点数上限。 ### 逐站履约 | 方法 | 路径 | 请求体及语义 | |---|---|---| | POST | `/system/flashDelivery/rider/orders/{id}/pickup` | `stopId?,imageUrls`;单点可省略站点 ID,多点必传;实际交接后提交至少 1 张图片 | | POST | `/system/flashDelivery/rider/orders/{id}/stops/{stopId}/pickup` | `imageUrls`,若 body 也传 `stopId` 必须与路径一致;与原取件端点共用实现 | | POST | `/system/flashDelivery/rider/orders/{id}/stops/{stopId}/target` | `orderVersion,clientRequestId,reason,consentDeclared`;切换目标前先沟通取得下单人同意,原因非空、声明为 true | 目标调整使用订单版本及客户端幂等号。切换到不同目标时缺少原因/声明,以及他人订单、收货站、已取站、过期版本及取消订单均拒绝新调整;同一请求重试不能重复写调整记录。选择当前目标可以省略原因和声明,仍以请求号记录一次选择并增加版本;省略声明按 false 处理。原目标未完成前保持所选站点;完成后回到原顺序中最早未取站。目标选择不代表取货,不产生取件凭证;直接上传非当前目标的取件凭证不能绕过调整记录。已确认站点由原骑手重试时幂等返回,不受目标已改变影响。 全部取货点完成才进入 `PICKED_UP`;`pickedUpAt` 是最后一个实际完成站点的时间。仅部分已取时订单仍为 `ACCEPTED`,用户取消必须拒绝。取件、目标调整和用户/平台取消先锁订单行后读取最新站点,事务内统一保存状态、版本、凭证及日志。 ### 返回视图与脱敏 - 用户/骑手列表新增 `pickupStopCount,pickedUpStopCount`,原地址和物品字段表示用户首个取货点摘要。 - 详情新增 `stops,pickupStopCount,pickedUpStopCount,currentTargetStopId,canUserCancel,targetAdjustments`;骑手详情返回 `orderVersion` 用于目标选择。 - `stops` 每项包含 `id,stopType,stopOrder`、平铺地址及物品字段、`status,pickedUpAt,imageUrls`。取货站状态为 `PENDING/PICKED_UP`;唯一收货站 `PENDING/DELIVERED`。 - `targetAdjustments` 含 `fromStopId,toStopId,reason,riderId,createTime,consentDeclared`。骑手声明不等于下单人 App 审批;只能说明骑手声明已经取得同意。 - 接单前逐站隐藏联系人、电话、精确坐标、图片和 PIN;完整文字地址及物品信息用于判断能否履约。各角色视图继续遵循订单参与人权限。 - 平台详情保留 `order,images,logs`,另返回 `stops,targetAdjustments,pickedUpStopCount,currentTargetStopId,handoffImageUrls`。凭证独立关联 `stopId`;聚合图片不能替代逐站证据。 ### 平台异常取消 现有 `/system/flashDelivery/admin/orders/{id}/cancel` 请求增加 `problemStopId,handlingResult,pickedGoodsDisposition,handoffImageUrls`。 - `reason`、本单问题站 `problemStopId`、非空 `handlingResult` 必填。 - 任一取货点已取时,非空 `pickedGoodsDisposition` 及至少一张 `handoffImageUrls` 必填;先记录货物去向和交接,再关闭订单。 - 原因、处理结果及物品去向各最多 500 字符;交接图片最多 9 张,单 URL 最多 1000 字符。问题站可为取货站或拒收涉及的收货站。 - 交接凭证使用 `proofType=HANDOFF`,平台详情单独展示;不代表完成配送,也不触发支付退款。