小程序开发日记①:快递与外卖下单流程

小程序最核心的动作是下单。项目支持快递和外卖两类配送,它们共享收货人、船舶和位置数据,但订单来源、货品字段、预计时间与图片凭证不同。
1. 两个页面,一套订单状态仓库
页面分别位于:
pages/order/courier-form.vue:快递订单;pages/order/takeaway-form.vue:外卖订单;store/modules/order.js:两类订单的表单状态与 payload 构造;store/modules/recipient.js:收货人与船舶地址。
没有强行把两张表单做成几十个条件分支的“万能页面”,而是共享数据模型和组件。页面保留各自业务表达,仓库负责复用。
2. 快递订单模型
快递侧包含快递公司、快递单号、重量、体积和备注:
function buildCourierPayload(recipient) { return normalizeOrderPayload({ ...(editingOrderId.value ? { orderId: editingOrderId.value } : {}), serviceType: 'EXPRESS', expressCompanyName: courierCompany.value, expressNo: courierNo.value, cargoWeightKg: packageWeight.value, cargoVolume: includeVolume.value ? Number(packageVolume.value) : null, cargoRemark: courierRemark.value, vesselName: recipient.shipName, mmsi: recipient.shipMmsi, receiverName: recipient.recipientName, receiverPhone: recipient.recipientPhone, regionId: recipient.regionId, shipLongitude: recipient.longitude, shipLatitude: recipient.latitude })}重量必须大于 0,长宽高只有填写时才成组校验。可选字段不要用空字符串冒充有效数字,否则后端可能得到 0、NaN 或错误体积。
3. 外卖订单模型
外卖侧包含平台、平台订单号、骑手电话、预计到达时间和备注:
function buildTakeawayPayload(recipient) { return normalizeOrderPayload({ serviceType: 'TAKEOUT', sourcePlatformName: takeawayPlatform.value, sourceOrderNo: takeawayOrderNo.value, riderPhone: riderPhone.value, expectedArriveTime: buildTodayDateTime(arriveTime.value), cargoRemark: takeawayRemark.value, ...buildRecipientFields(recipient) })}预计时间选择器会限制最早可选时间,并把页面的时、分组合成后端需要的完整日期时间。不能只提交 13:40,否则服务端无法判断是哪一天。
4. 收货地址与服务范围
下单页面优先读取默认地址,并把地址簿记录填入 recipientStore。提交前除了校验姓名、手机号、MMSI 和经纬度,还会检查坐标是否在服务区域内。
选择/读取默认地址 ↓校验收货信息完整性 ↓检查经纬度是否落在服务范围 ↓构造订单 payload服务范围校验应该使用经纬度,而不是地址文本。两个名字相似的港区可能相距很远,文字无法替代空间关系。
5. 图片凭证上传
订单截图和货品照片使用不同凭证类型:
await DeliveryApi.uploadProof(filePath, { orderId, proofType: DELIVERY_PROOF_TYPES.ORDER_PHOTO, sortNo: index + 1})提交顺序是先创建订单,拿到 orderId 后再上传图片。上传任务可以并行,但只有全部完成才提示成功:
const response = await OrderApi.createOrder(payload)const orderId = response.data?.orderId || response.dataawait uploadProofFiles(orderId)6. 新增、修改与重新提交
同一表单支持三种模式:
| 模式 | 判断条件 | 接口 |
|---|---|---|
| 新增 | 无 editingOrderId | create |
| 修改 | 有订单 ID | update |
| 驳回重提 | resubmit=1 | resubmit |
编辑时通过 fillFromOrder 回显数据,并下载已有凭证为临时地址。仓库额外记录 existingOrderPhotoFiles 和 existingGoodsPhotoFiles,避免未变更的远程图片被重复上传。
const shouldUpload = (filePath, existingFiles) => { return isResubmitting.value || !existingFiles.includes(filePath)}删除已有图片时,则调用删除凭证接口并重新加载当前凭证集合,保持服务端和页面一致。
7. 防重复提交
按钮的 loading 与 submitting 同时作为视觉反馈和逻辑锁:
if (submitting.value) returnsubmitting.value = truetry { // 创建或更新订单,再上传凭证} finally { submitting.value = false}单靠按钮变灰不够,网络慢时用户可能连续触发事件,逻辑层仍需兜底。
8. 本篇小结
下单失败如何保留现场
接口失败后不能调用 reset(),用户选择的地址、订单号和图片都应保留。只有创建与凭证上传全部成功,才清理 Store 并跳转订单页。如果订单创建成功但图片上传失败,页面需要提示“订单已创建,凭证上传失败”,并允许进入编辑模式补传,不能再次创建一张重复订单。
页面返回时的状态边界
从地址簿或地图选点返回属于下单流程的一部分,表单状态必须保留;从提交成功页重新发起下单,则需要干净的新表单。为此,Store 的清理动作由明确业务事件触发,而不是粗暴地写在每次 onUnload 中。
联调时我按“必填字段—服务范围—订单创建—凭证上传—成功跳转”逐段制造失败,确认每一步的按钮状态和用户输入都合理。这比只测一次顺利下单更能暴露流程问题。
下单流程表面是填表,实际串起了地址、空间校验、订单状态和图片凭证。两个页面既不能复制粘贴到失控,也不能抽象成谁都看不懂的超级表单。最后我找到的平衡是:业务页面各说各话,共用仓库在背后默契配合。
支持与分享
如果这篇文章对你有帮助,欢迎分享给更多人或打赏支持!





























