首页 / 资讯中心 / 文章详情

微信小程序发货信息录入功能开发指南:从字段设计到真机调试

微信小程序发货信息录入功能开发指南:从字段设计到真机调试 ★ FEATURED ARTICLE
电商小程序做到订单履约这一步很多团队才发现最麻烦的不是支付不是商品展示而是怎么把货发出去还不发错。用户下单后商家得搞清楚哪个订单该发什么、分几个包裹、用哪家快递、单号是多少。这些信息如果还要靠人肉复制粘贴效率低不说错发漏发的问题一定会找上门。微信小程序发货信息录入这个功能解决的就是这件小事让仓库人员拿着手机扫一下面单选一下物流公司录入就算完成订单状态自动流转买家端也能第一时间看到物流单号。这篇内容我不会只贴一段示例代码而是把实际开发这个功能时踩过的坑、想清楚的逻辑完整梳理一遍字段怎么设计、拆单合并怎么处理、前端怎么扫码、后端 Java 接口怎么保证不重复发货以及真机调试时遇到的基础库版本、顶部导航栏高度、附件保存路径、抓包验证这些问题。适合正在做电商类小程序、需要实现商家或仓库端发货功能的开发者也适合刚入小程序开发、想看看一个完整业务功能是怎么从页面到接口串起来的新手。1. 发货信息录入的业务本质先理清承上启下再动手1.1 发货环节的真实痛点发货信息录入这个功能听起来很简单——不就是填个快递单号吗但放到真实业务里就完全不是这么回事。我接手过好几个电商类项目最典型的问题有三个。第一个是录入分散。订单在商城小程序里发货单在快递系统里操作记录在 Excel 里信息不在一个地方仓库和客服对账全靠吼。第二个是拆单状态混乱。一个订单买了两件商品分两个仓库发或者三件商品合成一个包裹发订单状态到底是已发货还是部分发货很多团队根本没想清楚。第三个是错发漏发追责难。发错了不知道是哪个环节的问题漏发了也不知道是谁漏的。做一个发货信息录入功能如果能顺手把这三个问题都解决掉这个功能的价值就不只是录单了它其实是订单履约流程里最关键的确认动作系统通过一次录入把订单待发货变成订单已发货并附上物流信息同时把操作人、发货时间、包裹明细都记录下来。这个语义很重要后面设计的很多接口和状态流转都是围绕它展开的。1.2 功能边界与角色划分在做需求分析时我习惯先画一条边界发货信息录入功能只管从订单产生到包裹交付给物流公司这一段再往后的物流轨迹查询、买家通知、售后拦截都属于其他模块。功能边界清楚了接口设计才不会什么都往这个模块里塞不然一个简单的录入功能会被各种奇怪的业务需求拖垮。角色上至少要考虑三种商家或店主能看全部订单能录入也能改。仓库操作员能录发货信息但不能改商品价格、不能退款。管理员能查看操作日志能处理异常发货。权限设计不当后面上线了才发现仓库人员能把订单改成已退款那就麻烦了。小程序端的身份识别走wx.login拿 code再到后端换 token这一步很基础但很重要。code 是一次性的后端拿到后调用微信接口换取 openid 和 session_key再自己签发业务 token后续请求都带这个 token 做鉴权。很多新手直接把 openid 放在请求参数里来回传这个习惯不好等于把用户身份证号贴脑门上走路。数据流是这样的小程序录入页发起请求后端校验参数和权限在一个事务里写入发货单、更新订单状态然后返回结果小程序刷新订单列表买家端立刻能看到物流单号。这个链路里发货单是承上启下的核心表接下来就细说这张表怎么设计。2. 字段与页面设计拆单、合并、多包裹时怎么录2.1 数据模型发货单和订单是多对一还是一对多我建议用两张表shipment发货单主表和 shipment_item发货明细表不要试图把所有东西塞进一张大表。主表的字段设计我根据自己的踩坑经验整理如下shipment_no发货单号业务编号仓库那边习惯用这个对账。order_id / order_no关联的原订单。express_company物流公司编码。这里注意存编码不存中文名比如存 ZTO不存中通快递。否则哪天物流公司改个名或者你想接入新的快递渠道就要改一堆历史数据。express_no物流单号。package_count包裹数量。receiver_name / receiver_phone / receiver_address冗余的收货人信息。订单收货地址可能会变但发货这一刻的信息需要快照下来后面打单、对账、客服排查都用得到。status发货单状态1-已创建、2-已发货、3-已签收。operator_id / operator_name操作人。remark备注。shipped_at发货时间。明细表字段相对简单shipment_id、order_item_id、sku_id、product_name、quantity。这里的设计细节是明细表里存的是商品快照而不是直接实时去查订单明细。为什么要快照因为订单可能改价、可能部分退款发货单作为履约凭证必须保留发货那一刻的商品信息。这个道理就像财务为什么要留底单一样不是随便拍拍脑袋定的。2.2 拆单、合并发货的交互设计拆单就是一个订单拆成多个 shipment比如一个订单买了五件衣服分两个仓库发货。合并发货就是多个订单合成一个 shipment比如同一个买家连续拍了两单仓库打包成一个包裹发走。页面交互上我建议这样设计订单列表页展示待发货订单卡片上显示订单号、商品缩略图列表、数量和收货人。点击去发货进入录入页。录入页顶部是待发货商品明细支持勾选和取消勾选。勾选商品后点击添加到当前包裹一个包裹就对应一个 shipment。继续勾选剩余商品点击新建包裹这样就形成了拆单效果。包裹列表下方是物流信息填写区物流公司加物流单号可扫码可手动。合并发货更简单在订单列表页勾选多个订单统一录入同一个物流公司加单号后端按订单逐个生成 shipment 记录。这个方案在真实场景里验证过仓库操作员不需要培训太久看一遍界面就能上手。2.3 录入效率优化不是每个字段都需要手填移动端录入的体验至关重要。仓库人员一天要录几十上百单每次都要手输物流公司再输入一长串单号这个体验会让他们直接放弃小程序回到 Excel 老路上去。我做了三个优化实测下来效率提升非常明显。第一个是记住上次选择。把最近一次使用的物流公司存在本地 storage 里下次进入页面默认选中。这个功能很简单但能省掉百分之八十的点选操作。第二个是扫码直接带出快递公司。快递面单上的条码内容虽然各家不完全一样但很多包含物流公司编码信息扫完可以自动帮你选好公司只需要确认。第三个是批量粘贴。支持一次粘贴多行物流公司 空格 单号的文本自动拆分成多个包裹专门给手里已经有一张表格要批量录入的场景用。这些优化单独看都是小功能但组合起来仓库操作员的操作时间能从一分钟一单降到十几秒一单。千万别小看这几十秒录单员一天几百单下来省下的时间是实打实的人力成本。3. 前端核心实现扫码、物流公司选择与单号校验3.1 用 wx.scanCode 把面单条码扫进去小程序端最重要的交互就是扫描快递面单。wx.scanCode这个接口用起来很简单但有两个细节要注意一是扫出来的内容不一定是纯单号可能是网址或者混合编码需要二次解析二是扫码前要申请相机权限用户拒绝后要有引导不能让用户卡在那里不知道怎么办。下面是我在发货录入页里的扫码实现加了字段清洗和权限处理的逻辑wx.scanCode({ scanType: [barCode, qrCode], success: (res) { const result res.result || ; // 部分面单条码是纯数字部分带字母前缀这里做一次清洗 const expressNo result.replace(/[^0-9A-Za-z]/g, ).slice(-20); this.setData({ form.expressNo: expressNo }); this.autoDetectCompany(expressNo); }, fail: (err) { if (err.errMsg err.errMsg.indexOf(auth deny) -1) { wx.showModal({ title: 提示, content: 需要相机权限才能扫描快递单号请在设置中开启, confirmText: 去设置, success: (r) { if (r.confirm) { wx.openSetting(); } } }); } } });这里有个经验不要拿到扫码结果就直接填进表单。面单上的内容常常混了其他信息我会先做一次清洗去掉杂字符再截取最后一段纯数字和字母组合。如果还要更稳可以加一个智能识别逻辑根据单号前缀自动匹配物流公司这样用户连下拉框都不用点了。3.2 物流公司选择器picker 还是 radio物流公司数量通常有几十家全放 radio 单选框会很难看滚动列表也长。我的建议是默认用 picker 组件展示常用几家点击后弹出完整列表。如果你只有三四家固定物流比如一个校园跑腿平台只和两家快递合作那用 radio 确实更直观点一下就切换不用多一次弹窗确认。热搜词里有人在问微信小程序单选框其实就是这个选择问题。超过五家物流直接上 picker别犹豫。实现逻辑上核心是公司编码和显示名的映射以及按单号前缀自动推断公司// 物流公司数据源建议放后端接口下发前端只保留一份缓存 const companyList [ { code: SF, name: 顺丰速运 }, { code: ZTO, name: 中通快递 }, { code: YTO, name: 圆通速递 }, { code: YUNDA, name: 韵达快递 }, { code: JD, name: 京东物流 } ]; // 根据单号前缀推断物流公司 function autoDetectCompany(expressNo) { const prefixRules [ { code: SF, test: /^SF/i }, { code: JD, test: /^JD/i }, { code: ZTO, test: /^7[0-9]{10,}$/ } ]; const matched prefixRules.find(r r.test.test(expressNo)); if (matched) { this.setData({ form.expressCompany: matched.code }); } }优先用编码存储前端再映射成展示名这是防止后端数据被 UI 绑架的基本原则。后端接口收的是 ZTO而不是中通快递这样即使前端把中通改成中通快运后端代码也不用动。3.3 单号校验规则不同快递公司不一样单号校验不能一刀切。顺丰常见 15 位数字中通、圆通多为 12 位或 13 位京东的单号带 JD 前缀。前端可以做一个快速提示但真正的校验必须放在后端因为绕过前端直接调接口太容易了。不同快递公司的常见单号格式参考物流公司常见单号格式说明顺丰速运15 位纯数字部分冷运单带字母中通快递12-13 位数字以数字开头圆通速递12-13 位数字部分含字母京东物流JD 开头加数字长度不固定韵达快递13 位数字以数字开头前端校验函数示例function validateExpressNo(companyCode, expressNo) { const rules { SF: /^\d{15}$/, ZTO: /^\d{12}$|^\d{13}$/, YTO: /^\d{12}$|^\d{13}$/, JD: /^JD\d{15,20}$/i, YUNDA: /^\d{13}$/ }; const rule rules[companyCode]; if (!rule) return true; // 未知公司不强制拦截 return rule.test(expressNo.trim()); }实际项目里这些规则会随着快递公司调整而变所以我把规则表放到了后端配置里前端通过接口拉取这样改规则不用发版。这个思路在发货信息录入这种低频但准确性要求高的功能上很实用。3.4 发货凭证图片chooseMedia 与本地暂存有些业务要求上传发货凭证比如打包照片、面单照片。微信小程序里用wx.chooseMedia选图片这个接口会返回临时文件路径注意真机上要处理好临时文件路径和持久化存储路径的差异。这里有一个很经典的坑用wx.env.USER_DATA_PATH做本地存储目录时开发工具和真机的文件系统路径完全不一样千万不要在代码里硬编码绝对路径。下面是用 USER_DATA_PATH 暂存附件的写法const filePath ${wx.env.USER_DATA_PATH}/shipment_${Date.now()}.jpg; wx.getFileSystemManager().copyFile({ srcPath: tempFilePath, destPath: filePath, success: () console.log(附件已存到本地目录, filePath), fail: (err) console.error(附件保存失败, err) });这里要重点提醒一点临时文件在退出小程序后可能被清理如果凭证需要留存最稳妥的上传时机是用户点提交发货那一刻直接把图片上传到云存储或后端对象存储服务端只存 URL。本地路径只是暂存不是存档。热搜词里有人问保存附件相关的用法大概率就是在这个边界上踩了坑。3.5 提交状态与防重复提交录入页面点提交后一定要加 loading 状态按钮置灰防止用户连续点两下造成重复发货。前端只能防手滑真正的幂等保障在后端这个放到后面详细说。但前端的 loading 和禁用按钮也绝不能省它是用户体验的第一道防线也是后端幂等设计兜底前的最后一道友好提示。提交成功后的页面反馈也要设计好。发货成功后建议直接清空当前页面表单并显示发货成功的 Toast然后延迟回到订单列表页并刷新。不要让用户手动返回再手动下拉刷新多一步操作就多一分这个系统好不好用的差评。4. 后端接口与状态流转Java 侧怎么承接发货提交4.1 接口设计后端我用 Spring Boot 实现平时直接在 IDEA 里启动服务小程序端用微信开发者工具打开两边同步调试效率很高。核心接口就一个POST /api/shipment。请求体设计如下{ orderNo: SO202501010001, packages: [ { expressCompany: ZTO, expressNo: 773012345678901, itemIds: [1001, 1002], remark: 易碎品请轻放 } ], operatorId: u_10086 }响应体{ code: 0, message: success, data: { shipmentNo: SH202501010003, status: SHIPPED } }为什么请求体里带的是 packages 数组而不是单个 expressNo因为要支持拆单场景一次提交就能处理多个包裹减少网络请求次数也能在同一个事务里同时完成避免第一个包裹提交成功、第二个失败的数据不一致。这个设计是从真实业务教训里得来的最开始版本只支持单个包裹上线没两周就被仓库反馈拆单场景用不了加班改了一版才稳定下来。4.2 参数校验与幂等处理参数校验用Valid加自定义注解不能只靠前端。特别是 expressNo 校验规则后端必须有同样一份否则用户绕过小程序直接调接口脏数据就进库了。一个简化版的参数模型Data public class PackageRequest { NotBlank(message 物流公司不能为空) private String expressCompany; NotBlank(message 物流单号不能为空) Pattern(regexp ^[A-Za-z0-9]{10,32}$, message 物流单号格式不正确) private String expressNo; NotEmpty(message 商品明细不能为空) private ListLong itemIds; private String remark; }幂等处理是发货功能最容易出问题的地方这里必须展开讲。我遇到过一次真实事故仓库网络卡顿操作员点了三次提交结果生成了三张发货单库存和订单状态全乱了客服花了一下午才把多发的货追回来。从那以后我定下两条硬规矩业务幂等键。前端进入录入页时向后端申请一个 shipmentRequestIdUUID提交时带上。后端在 shipment 表上给(order_no, shipment_request_id)建唯一索引重复插入直接报错被拦下。订单状态前置校验。事务内先SELECT ... FOR UPDATE把订单行锁住判断当前状态必须是待发货否则抛异常回滚。核心代码逻辑Transactional public Shipment createShipment(ShipmentCreateRequest req) { // 幂等检查 if (shipmentMapper.existsByRequestId(req.getOrderNo(), req.getShipmentRequestId())) { throw new BusinessException(重复的提交请求); } // 锁订单行防止并发发货 Order order orderMapper.selectByNoForUpdate(req.getOrderNo()); if (order null || order.getStatus() ! OrderStatus.UNSHIPPED) { throw new BusinessException(订单不存在或当前状态不可发货); } // 插入发货单与明细 Shipment shipment buildShipment(req, order); shipmentMapper.insert(shipment); // 更新订单状态 orderMapper.updateStatus(order.getId(), OrderStatus.SHIPPED); // 记录操作日志 operationLogMapper.insert(buildLog(req, shipment)); return shipment; }事务边界很重要插入发货单、更新订单状态、记录操作日志必须放在同一个事务里任何一个失败都要回滚否则就会出现发货单有了但订单还在待发货的状态错乱。这种错乱是最难排查的因为它不报错只是数据对不上往往要等买家投诉没有收到发货通知才会被发现。4.3 状态机与异常处理发货不是终态订单状态机建议明确四个状态待发货、已发货、已签收、已取消。发货接口只允许待发货到已发货这一条路径其他路径全部拒绝。状态控制的权限说明可以用一个表来表达操作允许前状态允许后状态接口备注发货待发货已发货POST /api/shipment事务加幂等修改物流单号已发货已发货POST /api/shipment/{shipmentNo}/modify记录修改人和原因订单取消待发货已取消POST /api/order/cancel需校验未发货有些系统在发货后允许改物流单号比如仓管手误把单号输错了这个操作属于特殊权限不要放在普通发货接口里单独出一个修改接口并且必须记录修改人。字段 audit 日志不能省等真正出问题的时候能靠这个日志定位到人。这里再说一个很多人忽略的点发货不是结束而是物流的开始。如果系统接入了物流查询服务发货成功后应该触发一个异步事件去物流平台订阅轨迹更新再通过模板消息或订阅消息通知买家。这个事件如果和发货事务放在一起同步执行可能会拖慢发货接口的响应所以我用消息队列解耦。前端用户感知不到这个异步过程但查询物流轨迹的体验会差很多属于典型的基础体验优化。5. 真机调试与上线前必须处理的坑功能开发完在开发者工具里看没问题一上真机就翻车这是小程序开发的常态。这一章我把发货信息录入功能最容易在真机上翻车的几个点集中列出来。5.1 顶部导航栏高度自定义导航的适配如果你觉得默认导航栏太丑想做一个自定义顶部导航麻烦就来了。微信小程序的导航栏高度不是固定的带刘海的 iPhone 和安卓机完全不同右上角胶囊按钮的位置也不同。很多人在热搜词里搜微信小程序顶部导航栏高度其实就是被这个适配问题卡住了。正确的做法是用wx.getMenuButtonBoundingClientRect()拿到胶囊按钮的定位再反推状态栏高度和导航栏高度const systemInfo wx.getWindowInfo(); const menuButton wx.getMenuButtonBoundingClientRect(); // 胶囊按钮顶部到屏幕顶部的距离 const statusBarHeight systemInfo.statusBarHeight; const navBarHeight (menuButton.top - statusBarHeight) * 2 menuButton.height;这个值在页面初始化时算一次存到全局变量或 storage 里后面所有自定义导航页面复用。如果录入页顶部要放标题、返回按钮、提交按钮自定义导航栏总高度就是statusBarHeight navBarHeight内容区域要往下避开这个高度。不同机型的差异测试覆盖是必须做的至少安卓和 iOS 各测一台。5.2 基础库版本设置很多人找错地方关于微信小程序基础库版本从哪设置这个问题我看到过不少新手找错地方。开发者工具的右上角详情-本地设置里可以切换调试基础库版本这是开发调试用的。但小程序真正允许用户运行的最低基础库版本是在微信公众平台管理后台配置的路径是设置-服务内容声明-基础库版本。代码层面更重要的不是设置版本而是做兼容判断。比如wx.getWindowInfo()是较新的 API基础库版本低的老手机会调用失败要兼容就得用老的wx.getSystemInfoSync()或者先通过wx.canIUse(getWindowInfo)判断。发货录入页如果用了新 API 又不做兼容老手机上页面直接白屏或功能不可用这是上线后最容易收到投诉的问题。如果你是拿 uniapp 打包成微信小程序基础库版本的设置逻辑稍有不同。uniapp 项目的 manifest.json 里有基础配置编译生成的小程序项目会自动处理一部分兼容但wx.getMenuButtonBoundingClientRect这类原生 API 的调用方式还是一样的条件编译要做好。5.3 附件保存路径wx.env.USER_DATA_PATH 的玄机我在前面已经提到了wx.env.USER_DATA_PATH这里再展开讲一下。开发工具里打印这个变量会得到一个本机绝对路径看起来很正常。到了真机上这个路径是沙盒目录你在电脑上根本找不到这个目录想验证文件是否保存成功必须通过wx.getFileSystemManager().readdir()去读取。而且注意小程序的本地文件存储是有上限的如果发货凭证照片很多保存到本地肯定不够用最终还是得传给后端。我的习惯是本地只做缓存云端才是持久化。录入页把图片先放本地临时目录点提交时再上传到对象存储成功后把 URL 放进接口请求体。这样既保证用户下次打开还能看到待提交的凭证又不会让本地存储吃紧。5.4 抓包验证发货请求发货这个动作涉及资金和订单上线前必须抓包检查一遍确认前端发出的字段名称、格式、签名都与后端一致。我一般用 Charles 做 HTTPS 抓包需要在小程序真机上打开调试模式并安装 Charles 的根证书。有几个坑很常见手机代理设置不正确、手机和电脑不在同一个网段、小程序请求走了微信内部代理导致 Charles 抓不到包。抓包重点看三样东西请求 URL 和 HTTP method 是否正确。请求体里 orderNo 与 packages 是否完整、字段名有没有拼错、中文有没有乱码。响应 code 是否为 0以及重复提交同一份请求时是否真的被幂等逻辑拦下。一次完整的提交发货抓包能帮你拦截掉至少百分之五十的联调问题。不要等到上线了才发现接口字段对不上那时候再排查影响的就是真实订单了。5.5 开发工具里的 handshake failed 报错很多人用开发者工具连接本地后端调试时会看到一行报错handshake failed due to invalid upgrade header: null。这个错误我遇到过好多次原因通常是开发工具在发起 WebSocket 升级请求时本地代理或服务端返回的头信息不规范或者域名校验未通过。解决思路按顺序排查如果用的本地 HTTP 服务确认开发者工具详情-本地设置里勾选了不校验合法域名…。如果项目里接入了 WebSocket 或实时推送检查服务端的升级响应头是否为Connection: Upgrade和Upgrade: websocket。这个错误大概率只在开发工具里出现真机上反而不容易遇到不用过度恐慌。但要确认线上请求用的都是 HTTPS 加已备案域名开发工具和真机的差异在这个点上尤其明显。5.6 一点性能优化lazyCodeLoading如果发货录入页面只在商家端使用而且小程序分包后首包体积偏大可以在 app.json 里开启lazyCodeLoading: requiredComponents让页面按需注入组件代码。这个配置对录入页这种打开频率不高的功能页效果明显能减少商家端首页的加载时间。不过开启时要回归一下其他页面有没有使用未注册的组件避免上线后某个页面白屏。lazyCodeLoading 的坑在于它不是开启就完事组件注册表必须准确否则组件会在某个不起眼的交互中被用到时才报错排查起来比较费劲。如果项目比较老组件管理模式混乱我建议先做一次全量组件引用检查再开这个配置。真机调试这一圈走下来发货录入页基本就稳了。实践经验告诉我小程序功能开发最大的敌人不是逻辑复杂度而是各种环境差异——开发工具和真机不同、安卓和 iOS 不同、老基础库和新基础库不同。这些差异如果不能在上线前充分覆盖光靠测试用例是发现不全的真机真跑一遍才是硬道理。
阅读完成 · 觉得有帮助?
咨询建站