ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

商超小程序后台实战:自提核销与同城配送订单系统设计

商超小程序后台实战:自提核销与同城配送订单系统设计 接这单的时候客户只跟我说要做一个商超门店小程序我当时以为又是一个标准电商模板商品列表、购物车、下单支付、后台发货就完事了。结果需求聊到第三轮真正难啃的骨头才浮出来——顾客线上下单后到店自提怎么核销同城配送的配送费按什么算门店每天几百个订单库存怎么防超卖后台不是给程序员看的是给店里四十多岁的店长看的这个度怎么拿捏这篇文章就是这单商超门店小程序自提 同城配送后台开发的完整复盘。我把整个项目的需求拆解、技术选型、订单状态机设计、后台管理端实现逻辑以及过程中踩过的坑包括配送费精度、并发扣库存、支付回调幂等、小程序端动态标题适配这些细节全部整理出来。如果你是独立开发者或者正准备接类似的本地生活商单这篇文章能帮你少走不少弯路。1. 商单需求拆解商超小程序不只是“做个商城”1.1 客户真正想要的是什么客户是一家开在大型社区底商的连锁超市面积不大不小三百多平生鲜、日用、零食都有。他们之前用过第三方外卖平台但平台抽点加营销费用算下来利润越来越薄所以想自己搞一个小程序把流量攥在自己手里。表面需求很清晰小程序商城 自提 同城配送。但深入聊下去真正的需求痛点在于自提场景周边很多老顾客习惯线上看了货、下了单下班顺路来拿。门店需要一个核销机制不能靠店员人工翻聊天记录。核销完还要通知顾客“可以来取了”。配送场景门店三公里内的订单店家自己配送不依赖第三方骑手平台。配送费怎么算满多少免配送费超出配送范围怎么处理这些都必须可配置。后台管理店长不是程序员商品上下架、库存修改、订单处理、配送状态更新必须简单到培训十分钟就能上手。很多独立开发者接这类单子容易犯一个毛病拿着通用电商模板就往上套结果自提和配送逻辑打架后台做得跟ERP一样复杂店长根本不想用。我接下这个项目后做的第一件事不是写代码而是把业务场景画清楚。1.2 核心角色与业务流程图梳理这个项目里有三类核心角色顾客、门店运营人员店长/店员、配送员多数情况下也是店员。订单从创建到完成会经历一条完整的状态链路顾客在小程序端浏览商品、加购、下单选择“到店自提”或“同城配送”支付成功后生成订单自提订单进入“待备货”状态门店备完货点击“备货完成”顾客收到提货码到店核销配送订单进入“待配送”状态门店接单后安排配送配送中、已送达依次流转。这些流程看起来不复杂但每个节点都涉及后台功能订单列表筛选、状态变更操作、通知触达、异常处理。如果一开始不把状态机定义清楚后面开发到一半会发现自己被业务逻辑绕晕。我在需求阶段就把关键的业务规则列成了一张表贴在项目文档首页后面所有开发都围绕这张表来业务场景关键规则后台要求到店自提支付完成后生成核销码备货完成后才能核销核销码有效性校验、一键核销同城配送三公里范围内配送超出需门店确认配送范围可视化配置、配送费计算商品库存线下门店库存与线上库存同步下单锁库存、支付成功扣库存、关单回滚订单状态每个节点可回溯异常订单可人工干预状态机清晰、操作留痕1.3 为什么自提和配送要分开设计这是整个项目里最关键的架构决策。很多电商系统把订单统一做成物流发货模型到店自提只是“配送方式自提”的一个选项但实际运营中这两种场景的后台操作逻辑完全不一样。自提订单的核心是核销。顾客到了门店出示提货码店员用后台扫一下或者输入码确认核销订单完成。这个过程中不涉及配送员、不涉及距离、不涉及配送费后台需要的是核销工具不是物流管理。配送订单的核心则是履约。谁去送什么时候送送到哪顾客电话打不通怎么办后台需要的是配送任务分配和状态更新甚至要考虑多点配送的顺序优化。把这两种模式塞进同一条流程里后台界面会变得异常复杂。我的方案是订单模型里用order_type字段区分类型自提订单和配送订单在列表、详情、操作按钮上完全分流店长看到的是什么类型就只看到对应的操作。后台哪怕复杂也要复杂得“看不见”用户才觉得简单。2. 技术选型与整体架构小程序端和后台端的搭配方案2.1 小程序端为什么选了 uni-app小程序端的选型我在原生微信小程序和 uni-app 之间犹豫了一段时间。最终选了 uni-app核心原因就一个客户明确说以后可能要上支付宝小程序和抖音小程序我不想同一个项目写三套。uni-app 基于 Vue 语法开发体验接近传统前端生态成熟遇到问题基本都能搜到答案。HarmonyOS 那套先不考虑目前微信小程序还是绝对大头。实际开发中uni-app 的编译到微信小程序会有一些细节差异比如部分 CSS 样式在 H5 正常但小程序端渲染异常这类问题我在后面的踩坑部分会专门提到。前端涉及的关键能力包括微信登录、商品浏览、购物车、下单支付、订单列表、自提核销码展示、配送进度查看。其中定位权限的申请和使用是小程序审核的重点必须在 manifest 里声明清楚用途否则容易被驳回。2.2 后台端NestJS MySQL Redis 的组合后台我用的是 Node.js 生态框架选了 NestJS。选择 NestJS 不是因为它最流行而是因为这个项目需要清晰的模块化结构——用户模块、商品模块、订单模块、配送模块、核销模块、消息通知模块每个模块之间边界清楚后面维护成本低。TypeScript 的强类型约束在多人协作和后期接盘时优势极其明显。数据存储用了 MySQL 8.0核心业务表包括用户表、商品表、SKU 表、购物车表、订单主表、订单明细表、核销记录表、配送记录表、门店配置表。缓存层用了 Redis主要干三件事并发扣库存、订单号生成、高频读取的门店配置缓存。有人可能会问为什么不直接用 MongoDB 或者云开发坦白说商超订单系统对事务一致性要求很高MySQL 的关系型事务在这里比文档型数据库更稳。云开发虽然省事但客户后续想把数据导出来做分析或者接自己的会员系统自建后端更灵活。这个项目客户对数据主权有要求所以我从一开始就决定自建服务。2.3 整体部署架构与接口设计规范部署上我用了一台 4核8G 的云服务器Nginx 托管前端静态资源和反向代理后端接口HTTPS 证书直接配好。MySQL 和 Redis 也在这台机器上初期访问量不大完全够用。后面如果订单量上来MySQL 和 Redis 拆出来单独部署就行代码层面不用大改。接口设计遵循几个原则统一返回格式{ code, message, data }前端只要判断 code 是否为 0所有接口带签名或登录态校验微信登录换取 openid 后签发 JWT分页列表统一传page和pageSize返回total和list时间字段统一用时间戳前端自行格式化避免时区问题。这里最容易被忽略的是接口错误码的设计。我维护了一张统一的错误码表比如10001表示参数错误20001表示库存不足30001表示订单状态不允许当前操作。错误码表一是方便前端统一处理弹窗二是在排查线上问题时看日志能一眼定位问题类型不用猜。3. 核心业务流程设计与实现订单状态机、库存扣减与支付回调3.1 订单状态机的关键定义订单状态是后台系统的骨架状态定义不清楚后面的所有逻辑都会乱。这个项目我把订单状态定义成五个主状态状态码状态名说明可操作动作0待支付已下单未支付取消订单、去支付1待备货/待接单已支付等待门店处理门店接单/备货2待取货/配送中备货完成或配送员已出发核销/确认送达3已完成顾客已取货或确认收货无4已取消/已退款订单取消且退款完成无光有主状态还不够同一主状态下自提和配送的操作按钮完全不同。所以我在代码里用sub_status做了细分比如配送订单在“待接单”下还能再分“待分配配送员”和“配送员已接单”这样运营人员能清楚知道卡在哪个环节。状态机实现上我用了一个allowedTransitions映射表明确每个状态能跳转到哪些状态任何非法跳转直接拒绝const ORDER_STATUS_TRANSITIONS { 0: [1, 4], // 待支付 - 待备货/待接单、已取消 1: [2, 4], // 待备货/待接单 - 待取货/配送中、已取消 2: [3], // 待取货/配送中 - 已完成 3: [], 4: [] };这个方案带来的好处是后台任何操作都先过状态校验完全避免“订单已经完成了还能被取消”这种低级事故。在管理端前端根据当前状态动态渲染可操作按钮同样的逻辑在后端再校验一遍双重保险。3.2 库存扣减Redis Lua 脚本防超卖商超项目的超卖问题是必须解决的。线下门店一天可能卖掉几百单每单包含多个商品如果直接在 MySQL 里做UPDATE stock SET count count - 1 WHERE id ? AND count 0在高并发下会产生大量的行锁等待数据库会被拖垮。我的做法是把库存扣减放到 Redis 里利用 Redis 单线程的特性配合 Lua 脚本保证原子性。商品上架时先把库存同步到 Redis下单支付完成后执行扣减脚本local stock redis.call(GET, KEYS[1]) if stock false then return -1 end if tonumber(stock) tonumber(ARGV[1]) then return 0 end redis.call(DECRBY, KEYS[1], ARGV[1]) return 1这个脚本检查库存是否充足充足则扣减不足返回失败。整个操作是原子的不会出现两个请求同时读到库存为 1 然后都扣减成功的情况。但 Redis 扣库存有一个绕不开的问题Redis 的数据和 MySQL 的数据怎么保持一致。我的方案是每个订单创建时先在 Redis 扣减库存订单支付成功后异步把扣减结果写入 MySQL如果订单超时取消或者用户主动取消Redis 加回库存同时更新 MySQL。为了避免 Redis 崩溃导致库存数据丢失每天晚上会执行一次全量对账脚本比对 Redis 和 MySQL 的库存差异发现不一致就报警并自动纠正。3.3 支付回调的幂等设计微信支付的回调通知是异步的同一个支付结果可能会被微信服务器通知多次。如果在回调里不做幂等处理就会出现订单状态被重复更新、库存被重复扣减、甚至重复发放积分之类的严重问题。我的处理方式是在回调处理逻辑开头先做幂等校验const exists await orderLogRepo.findOne({ where: { transactionId: params.transaction_id } }); if (exists) { return { code: 0 }; } await orderRepo.manager.transaction(async (manager) { await this.markOrderPaid(manager, orderNo); await this.deductInventory(manager, orderNo); await orderLogRepo.manager.save({ transactionId: params.transaction_id, orderNo, rawData: JSON.stringify(params) }); });核心逻辑是支付流水表里如果有这个transaction_id说明已经处理过了直接返回成功不再重复处理。这里必须把“写支付流水”和“更新订单状态”放在同一个数据库事务里要么都成功要么都失败不能出现订单已支付但流水没记上的情况。我在实际开发中就遇到过回调重复通知导致库存被扣两次的情况当时就是没做幂等排查了半天才定位到问题。后来加了流水表唯一索引transaction_id数据库层面的唯一约束兜底问题彻底解决。3.4 定时关单与库存回滚的配合用户下单后如果一直不支付需要定时关闭订单。这个功能用了 Redis 的过期键监听 定时任务双保险。用户下单时把订单号写入 Redis设置过期时间为 30 分钟过期后触发回调判断订单是否还未支付未支付则自动关闭并释放已锁定的库存。但 Redis 过期监听有延迟而且如果 Redis 重启期间丢失了过期事件订单就会漏关。所以我另外起了一个定时任务每 5 分钟扫一次 MySQL 中超过 30 分钟未支付的订单批量关闭并回滚库存。双保险机制下即使 Redis 挂了定时任务也能兜底。这里有个细节值得注意关闭订单和回滚库存必须在同一个事务里执行而且要先关闭订单再回滚库存避免用户刚好在关单的同时支付成功导致订单已关闭但用户的钱被扣了。这种边界情况在测试时很难发现但线上运营中真的会遇到处理不好就是客诉。4. 后台管理端的设计与实现让店长十分钟上手4.1 管理端功能模块划分后台管理端是这个项目里最见功夫的部分。我接触过不少独立开发者代码能力没问题但设计出来的后台界面自己看着舒服客户用起来一头雾水。这个项目的管理端我从一开始就把目标定在“店长不看说明书也能操作”的级别。管理端分了五个核心模块商品管理商品上下架、价格修改、库存调整、分类管理、批量导入。这里的核心操作是“一键下架”和“批量改价”促销活动时效率极高。订单管理订单列表、订单详情、状态筛选、订单操作。自提订单和配送订单分标签展示操作按钮按状态动态渲染。核销管理输入核销码或扫顾客的二维码完成核销核销记录可查。配送管理待配送订单列表、分配配送员、配送状态更新。数据统计今日销售额、订单量、商品销售排行、自提/配送占比。每个模块都配了导出功能店长每天下班前导出当天的订单数据做对账这是运营刚需千万别漏。4.2 核销工具的实现细节自提核销是整个商超小程序和普通电商后台最大的区别所在。顾客支付完成后小程序端会生成一个动态二维码和 8 位核销码后台核销时输入 8 位核销码或扫码即可完成核销。核销码的设计有几个原则不能太短以免被猜出来不能有容易混淆的字符比如 0 和 O、1 和 I过期后自动失效。我在生成核销码时用了固定的字符集去掉易混淆字符再用 Redis 存储核销码和订单号的映射关系设置过期时间。核销操作是高频操作店长一天可能要核销几百单所以我把核销入口做成了管理端首页的快捷按钮点击后直接进入扫码/输码界面。核销成功后页面会有明显反馈核销失败必须给出具体原因码不存在、订单未支付、订单已核销、订单已过期。还有一个细节很多人会忽略核销操作一定要记录操作人。万一出现店员核销错了订单能追溯到具体是谁操作的这是门店管理的底线。4.3 配送范围与配送费的可视化配置同城配送这块客户需要一个能自己调整配送范围的方案不能每次改范围都找开发商。我在后台做成地图选点模式门店位置为中心拖动圆点调整配送半径保存后用经纬度计算实际距离来判断订单是否在配送范围内。配送费规则设计成阶梯模式后台可以配置多个档位配送距离配送费触发条件0 - 1公里2元订单金额小于50元1 - 2公里3元订单金额小于80元2 - 3公里5元订单金额小于100元任意距离免配送费订单金额满100元实际计算的时候配送距离用的是 Haversine 公式根据用户收货地址经纬度和门店经纬度计算。这里有一个现金精度问题JavaScript 的浮点数计算 0.1 0.2 这种会出问题所以所有金额计算我都用了整数分来存储配送费 2 元在数据库里是200分计算完再除以 100 展示避免精度损失。4.4 消息通知的触达策略消息通知在商超项目里特别重要顾客下单后要通知门店门店备货完成要通知顾客配送员出发要通知顾客每个环节漏了都容易产生客诉。我的方案是微信订阅消息为主一次性订阅消息配合模板。顾客下单时引导用户订阅“订单状态变更通知”这样门店更新订单状态后就能通过微信服务通知触达顾客。一次订阅只发一条消息所以我在用户下单、备货完成等多个节点分别引导订阅保证全流程都能通知到。门店侧的通知用的是公众号模板消息 管理端轮询。考虑到店员的手机不一定支持微信订阅消息的接收管理端页面的轮询反而是更可靠的方案。一开始我完全依赖微信订阅消息结果发现店员根本收不到通知后来改成管理端实时轮询新订单接单时效明显提升。5. 踩坑复盘这些坑我替你踩过了5.1 配送费“少一分钱”引发的客诉第一个大坑是配送费计算精度问题。上线第一天就有一个顾客投诉订单金额刚好 99.9 元按规则不满 100 元要收配送费但前端展示的金额和后端计算的金额差了 1 分钱。排查后发现前端用了 JavaScript 的浮点数直接计算订单金额99.9 元在浮点数里表示有误差导致展示给用户的金额是 99.89 元而后端用整数分计算是 9990 分按满 100 元免配送费的规则前端以为不满 100后端以为满 100两边出现了分歧。从那以后我定了两条规矩第一所有金额字段后端统一用整数分传输前端展示时再除以 100第二免配送费门槛的判断以后端计算为准前端只负责展示结果不做任何金额计算。这个小问题给了我一个很大的教训订单相关的金额逻辑前端永远只做展示运算全部交给后端。5.2 高并发下单时库存超卖Redis 和 MySQL 不一致第二个坑出现在一次促销活动期间。门店搞了一个“鸡蛋 0.1 元限量 200 份”的活动结果活动开始后一分钟内有超过 500 个用户同时下单虽然 Redis 的 Lua 脚本成功挡住了超卖但 Redis 扣减库存和 MySQL 同步库存之间出现了短暂的不一致导致用户看到下单成功但实际没有扣减 MySQL 的库存。问题出在异步同步的时机上。原方案是支付回调成功后异步更新 MySQL 库存但活动期间支付回调有延迟异步任务堆积MySQL 库存没有及时更新。后来我加了库存对账定时任务每 5 分钟执行一次 Redis 和 MySQL 的差异比对发现差异立即告警并修正。同时把库存同步从纯异步改成了“先写 MySQL 订单明细再同步减库存”的顺序确保数据最终一致。5.3 自提订单顾客没到店订单一直挂着没人处理自提订单有一个特殊的业务场景顾客下单支付后一直不来取货。开始的时候这类订单会一直挂在“待取货”状态店里也没有任何提醒时间久了后台堆积了很多死单。后来我在管理端加了一个“超时未核销提醒”自提订单备货完成后超过 24 小时未核销系统自动标记为异常订单提醒店员联系顾客确认是否还需要。如果顾客明确不要了店员可以操作取消订单并退款整个过程留痕。5.4 小程序端动态标题和页面加载的适配问题项目里有一个需求顾客在订单列表页看到的标题要根据订单状态动态变化比如“待取货订单”“配送中订单”。开发时发现 uni-app 编译到微信小程序后动态设置导航栏标题的接口在某些版本上有兼容性问题在 H5 端正常在微信小程序端标题就是不变。排查了很久发现是uni.setNavigationBarTitle在页面栈里的调用时机问题。后来改成在onShow生命周期里统一调用并且标题跟着页面 onShow 刷新问题解决。这类小程序端的细节问题看似不大但处理不好直接拉低客户对项目的整体评价。我建议开发的时候尽量多用真机调试特别是涉及微信小程序端特有的接口能力不能只在开发者工具里点两下就当完成了。5.5 支付回调偶尔丢失订单状态卡死微信支付回调虽然设计上是可靠的但实际运行中偶尔会出现回调没有被正确接收到的情况导致订单已经扣款成功但订单状态一直停留在“待支付”。这种问题在商超场景中很致命顾客在店里等着取货店员却查不到有效订单。我的兜底方案是主动拉取微信支付结果。后端有一个定时任务每隔 5 分钟扫描一次超过 3 分钟仍未支付成功但未关闭的订单调用微信支付查询接口主动确认支付状态。如果查询结果是已支付就手动触发支付成功流程补上状态更新。这套“被动回调 主动查询”的双保险机制上线后再也没出现过用户付了钱订单却没激活的情况。开发的时候多花半天写这个定时任务能让上线后少接几十个客诉电话这个投入太值了。5.6 后台列表加载慢原来是索引没建好项目上线两周后订单表数据量到了两万多条后台订单列表查询开始明显变慢有时候要两三秒才出结果。排查后发现订单表只有主键索引查询时用了WHERE order_status ? AND order_time BETWEEN ? AND ?这样的条件相当于全表扫描。加了联合索引(order_status, order_time, store_id)之后查询从 2 秒多降到了 100 毫秒以内。这个坑给我的教训是开发阶段数据量小看不出问题一定要在数据表设计阶段就想清楚常用的查询组合提前建好索引。5.7 常见问题速查表问题现象根本原因解决方案前端展示金额与后端计算不一致JavaScript 浮点数精度误差金额统一用整数分前端不计算金额高并发下单库存超卖库存扣减非原子性Redis Lua 脚本原子扣减对账任务兜底支付成功但订单未激活微信支付回调丢失定时任务主动查询支付结果双保险门店收不到新订单提醒微信订阅消息不可达管理端实时轮询 订阅消息并行通知后台列表查询缓慢缺乏联合索引配合查询场景按常用查询场景设计联合索引6. 后台开发的工程规范与调试技巧6.1 接口文档先行前后端并行开发独立开发者很多时候是前后端一把梭但即便如此我也强烈建议先把接口文档写清楚再动手开发。这个项目我用 Apifox 定义接口文档所有的请求参数、响应格式、错误码都沉淀在文档里前端开发的时候不再追着我问“这个接口返回什么字段”。文档的价值不只是开发阶段更是验收阶段的依据。客户提出需求变更时我第一件事就是打开接口文档把改动的范围标出来评估影响。有了一份清晰的接口文档商务沟通会顺畅很多。6.2 环境隔离本地、测试、生产三套环境项目从开发到上线我坚持用三套环境本地环境、测试环境、生产环境。测试环境用来给客户演示功能生产环境正式运营。三套环境的配置通过环境变量区分切换环境只需要改应用的配置文件不用动代码。这套方案的好处是很明显的开发中改代码不影响客户演示客户在测试环境随便点不用担心搞坏数据。另外微信支付回调这类外部接口测试环境配置一个专用的回调地址避免回调打到生产环境上。6.3 日志与告警排查线上问题的救命稻草后台服务上线后最重要的事情不是写新功能而是确保线上问题能及时发现、快速定位。我用 NestJS 的日志模块把所有关键操作的日志按级别输出同时记录操作人、操作时间、请求参数、返回结果。告警这边接入了企业微信机器人重点监控三类事件支付回调失败、库存不足、订单状态异常。一旦触发告警机器人直接把消息推到群里我手机第一时间就能看到。这套监控体系上线后很多问题在客户发现之前我就已经处理掉了客户体验完全不一样。7. 商单经验总结独立开发者接本地生活项目的几点体会这单做下来和客户从需求对接到上线验收前后花了两个半月其中有三分之二的时间是需求确认和联调。独立开发者接这类本地生活商单技术能力只是基础真正考验的是业务理解能力。第一需求一定要聊透。商超小程序不是普通电商一定要搞清楚自提核销、配送范围、库存管理、对账报表这些场景的具体玩法。客户说“做个商城”你得知道商城背后要支撑哪些运营动作。第二价格评估要留出足够的余量。这类项目最大的变数在需求变更。我第一次报价时留了 20% 的缓冲空间最后需求变更的工时仍然超过了预期。建议把基础版和扩展版分开报价新增功能单独计费避免被“顺手帮我加个小功能”拖垮。第三验收标准要白纸黑字写清楚。上线前我列了一份功能验收清单从顾客下单到门店核销每一个流程都在测试环境走一遍客户确认签字后才部署生产。这样既保证交付质量也避免后续扯皮。第四交付不是结束而是服务的开始。门店小程序上线后运营数据每天都在变化商品要调价、活动要配置、配送规则要调整这些后续的维护需求会成为稳定的收入来源。我在交付时给客户提供了一年的运维服务包每月固定费用包含日常维护和功能优化。运营三个月后客户主动续费了这单的长期价值远超过最初的开发费用。做独立开发者这几年我最大的感受是接本地生活类商单比的不是谁的技术栈更酷炫而是谁更能站在门店经营者的角度去解决问题。后台系统做得再复杂店长用不顺手项目就是失败的。每次看到店长熟练地用我做的后台核销、接单、改价那种成就感远超过写代码本身。
返回列表