1. 为什么个体店铺要自建一套轻量交易闭环先说个挺现实的问题个体店铺做线上生意最常见的选择是直接上美团、饿了么、抖音团购这种大平台。但很多店主跑一段时间就会算明白一笔账——平台抽佣、流量竞价、活动强制参与加起来足够吃掉本来就不厚的利润。还有一类店主已经有了自己的私域客群比如顾客加了微信、进了群他们需要的其实不是公域导流而是一个能把线下熟客转化到线上的工具。这个项目做的就是这样一件事用Spring Boot搭后端用微信小程序做前端给个体店铺一套商品展示 交易管理的自有系统。我参与这个项目时的定位挺清楚——不搞复杂的分布式、不做高并发设计、不追微服务而是围绕个体店铺的真实经营场景把商品管理、下单支付、订单处理、库存核算这几条核心链路打磨顺。技术上为什么选Spring Boot 微信小程序我的判断是这样的先看小程序。对个体店主来说小程序的门槛比独立App低太多了。客户在微信里搜一下、扫个码就能用不用下载安装、不用做应用商店审核。更关键的是微信自带支付能力用户从浏览到下单再到付款整个路径都在微信生态内完成转化链路非常短。对于没有技术团队的个体店主小程序几乎是触达C端客户的唯一低成本选择。再看Spring Boot。后端选它很大程度是因为省心。Spring Boot封装好了配置、内置了Tomcat、有一整套starter生态一个人开发时不需要花大量时间在环境搭建上。单体应用对店铺这种体量来说完全够用一台普通云服务器就能跑得很稳。更重要的是Java体系在支付对接、订单状态机、事务管理这些钱相关的环节上成熟度很高出了问题社区里能查到的经验也多。第四方支付、对接第三方物流、多店铺连锁管理这些需求这个项目都没有做。原因很简单——项目服务的是个体单店不是连锁品牌。功能多做一步开发成本、维护成本、出错概率都会翻倍做了反而过度设计。我经常看到有人做类似系统时习惯性先画一张大架构图把Redis缓存、消息队列、微服务注册中心都摆上去。对个体店铺管理系统来说这不是加分项是负担。真正应该花心思的是店主拿手机上传一张商品图要几步顾客能不能在3秒内找到想买的东西订单状态变了之后顾客会不会被及时通知这篇文章就把整个系统的核心设计思路、数据库表结构、支付对接逻辑、实际开发中的坑一条条拆开讲清楚。如果你也在给某个小店或者小团队做类似的交易系统或者自己准备接一个Spring Boot 小程序的全栈项目这算是一份可以直接参考的实战笔记。2. 先想清楚个体店铺和大电商系统的差异再动手设计很多开发者接手这类项目时容易犯一个方向性错误把淘宝那套架构思维直接搬到个体店铺系统里。结果做出来一个看起来很专业但店主根本用不动的东西。2.1 核心需求拆解店主到底要什么跟店主聊天后会发现他们的真实诉求非常朴素。我整理下来大概就是下面几条能上传商品配图、定价、写描述最好手机上就能搞定顾客能在小程序里看到商品分类和详情下单后能付款订单来了店主能看得到能改状态比如备货中、已发货库存别超卖哪种商品快卖完了最好能提醒一声顾客能查自己的订单记录省得天天打电话问注意没有一条提到营销裂变会员积分大数据分析这些概念。不是说这些没用而是对个体店铺来说先把交易闭环跑通才是第一优先级。项目第一期我的建议是只做商品展示、购物车、下单支付、订单管理、库存管理这五个模块。其他的一率往后放。还有一个需求容易被忽略就是操作门槛要足够低。我在需求访谈时遇到一位店主习惯用手机所有操作都在微信里完成你让她去开电脑登录网页后台她嫌麻烦。所以管理端我直接做了两个入口小程序端里给店主做一个简易的管理页面同时提供PC端后台。小程序端以上架商品、改库存、看订单为主PC端负责相对复杂的操作比如批量导入商品、查看统计报表。2.2 技术选型的取舍逻辑技术选型这块我直接说结论再解释为什么模块选择理由后端框架Spring Boot 2.7.x稳定版本资料多个体项目不需要追新数据库MySQL 8.0 MyBatis-Plus关系型数据用起来直观事务有保障鉴权方案JWT 微信登录openid小程序端最标准的登录方式支付微信支付V3JSAPI下单用户在小程序内直接拉起支付部署单台云服务器 Docker省成本运维简单前端uni-app用Vue语法开发可编译到微信小程序有几个点值得展开说说。鉴权为什么要用openid而不是手机号登录。小程序的天然优势是微信已经帮我们做了用户识别。前端调用wx.login()拿到一个临时code后端拿这个code去微信服务器换openid这个openid就是用户在这个小程序里的唯一身份标识。用户不用注册、不用输密码体验非常顺滑。对个体店铺来说让顾客输入手机号注册账号每多一步操作就劝退一批不熟悉手机操作的中老年用户。手机号可以作为下单后的补充信息填写那属于收货地址范畴跟登录鉴权分开。为什么要用uni-app而不是原生小程序。说实话如果确定只在微信小程序上做原生开发完全可行。但用uni-app的好处是以后如果想同步出一份支付宝小程序或者H5版本同一套代码可以直接编译过去。个体老板的生意可能会扩展比如以后想在抖音上做个店铺至少前端代码还有复用的余地。当然这也看个人习惯我认识有开发者坚持原生开发也没问题这个选择不绝对。JWT过期时间怎么设。我设的是30天而不是常见的2小时。原因很简单顾客打开小店小程序不需要频繁重新登录30天的过期时间基本可以做到一次登录长期使用。安全性方面因为openid本身不是敏感隐私信息泄露后的风险主要在接口被刷。配合后端接口合理的频率校验问题不大。2.3 数据库表设计别把简单事情复杂化表结构设计是这个项目里最该花时间的部分。我见过有人给个体店铺系统设计了三四张订单表、独立的SKU表、优惠券表、多级分类表结果开发三个月还没上线。个体店铺的商品经营特点是品种多但每个品种的规格选项很少。举个例子一家咖啡店卖拿铁可能有热/冷两个规格顶多加一个大杯/小杯这就够了。没必要为了通用的SKU模型搞出一套规格组合生成逻辑。所以我这里做了一个简化设计商品表product的字段核心就是名称、主图、描述、价格、库存量、分类ID、上下架状态。价格直接用Decimal存单个数值如果店铺确有规格需求再加一张product_spec表存规格名和价格加价不做笛卡尔积式的规格组合生成。订单相关的表这样设计orders订单主表订单号、用户ID、店铺ID、总金额、支付状态、订单状态、收货信息、创建时间order_item订单明细表一次下单可能包含多个商品每条记录对应一个商品的购买数量、单价、快照名称和图片这个快照字段很关键。商品信息店家是可以随时修改的但顾客下单那一刻看到的价格、名称、图片必须被原样保存下来不然以后对账、处理售后时会出现当时明明买的25元现在怎么显示28元的麻烦。order_item里冗余存一份商品名称和图片就是典型的用空间换确定性。用户表更简单就是 openid、昵称、头像、手机号这几个字段。但注意手机号和收货地址要跟用户主表分开或者至少设为可空。因为很多用户下单时微信里已经有了收货地址我们直接在小程序端用wx.chooseAddress把地址带回后端保存没必要强制用户去填一份个人资料。库存这张表我一开始设计的是商品表内一个库存字段后来发现不够用原因后面在超卖问题那里会详细说。总之如果商品有规格库存就要落在规格级别否则会出现商品显示有货但顾客选完规格后不能下单的尴尬情况。3. Spring Boot后端从接口列表到事务边界逐层搭建后端这部分是整个系统的地基。接口设计得好不好直接决定小程序端开发的顺畅程度。我按业务模块把后端拆成了几条主线一条条说。3.1 微信登录与全局鉴权拦截后端的登录接口只有一个POST /api/auth/login。前端把wx.login()拿到的code传过来后端通过HttpClient调用微信接口jscode2session拿到openid和session_key。然后拿openid去user表查查不到就自动注册一个新用户最后签发一个JWT返回给前端。有个细节要注意解析微信返回结果时先检查errcode是否为0。微信的code的有效期只有5分钟而且是一次性的前端如果频繁调用wx.login()可能导致code失效。实际开发中我遇到过前端因为页面onLoad和onShow都调用了登录第二次code拿到的就是无效code调试时还以为是后端接口问题。拦截器这边我用Spring MVC的HandlerInterceptor实现了一个AuthInterceptor从请求头里取Authorization: Bearer token解析JWT后把用户ID塞进ThreadLocal或RequestContextHolder。这样每个Controller里直接拿用户ID不用每次写解析逻辑。放行哪些接口要分清楚商品列表、商品详情、首页轮播图这类浏览型接口放行。下单、支付、订单查询、管理端所有接口必须登录。这里的判断其实隐含一个产品决策——游客可以看商品但不能下单逼着顾客走到下单那一步才去登录能最大限度减少围观造成的流失。微信小程序里用户点击授权登录的心理门槛比注册账号低得多但依然不能放在浏览阶段。3.2 商品管理上传、轮播图和状态切换商品相关的核心接口围着一张product表转。店主上传商品图片时前端uni-app通过wx.chooseMedia选图然后以multipart/form-data方式传到POST /api/product/upload后端用cos对象存储或者本地磁盘存下来返回图片URL。个体店铺的图片量不大本地磁盘够用但我建议存对象存储而不是本地。一个原因小程序端对图片域名有白名单限制你必须把图片域名配置到小程序后台的downloadFile合法域名里用对象存储的话CDN加速后前端加载更快。另一个原因是以后店铺扩容、加服务器时图片不用迁移。商品状态我做了三种下架0、上架1、售罄2。售罄不是纯粹的手动状态而是当库存数为0时系统自动把状态置为2小程序端显示已售罄而不是隐藏商品。对店铺来说售罄商品保留展示是一种很好的经营技巧比如某些手作商品售罄本身就说明这家店的东西很抢手。轮播图我单独建了一张banner表因为轮播图的运营频率和商品是不一样的——店主可能会在节日、促销节点频繁换首页轮播图但商品图换了就要重新加载缓存尽量不频繁动。这两者的生命周期不同表拆开更合理。3.3 下单与库存扣减这部分的坑最多下单接口POST /api/order/create是后端最核心、也最容易出问题的地方。我的实现逻辑是前端把购物车里的商品ID、规格ID、数量列表传给后端后端重新查询商品当前价格以数据库里的价格为准计算总金额不能让前端传金额进来判断库存是否充足扣减库存生成订单主表和订单明细表返回订单号前端拿订单号去发起微信支付看到没前半段我一直在强调后端重新计算价格。这不仅是安全要求防止用户篡改请求参数也是业务要求——店家改价了顾客看到的是最新价而不是购物车里的旧价格。关于先扣库存还是先创建订单我踩过不少坑。最开始实现时是生成订单 → 扣库存 → 支付后来发现订单如果长时间不支付库存就被无效占用容易造成超卖。改成预扣库存 → 生成订单 → 支付超过30分钟未支付的订单由定时任务扫描并回滚库存。这两个方案都不是完美的但预扣 超时回滚在实践中更接近真实店铺的锁货概念。就好比你去店里买东西店员说这个我先给你留着你半小时内来付钱超过时间就放回货架逻辑很自然。扣库存的SQL一定用原子操作UPDATE product_spec SET stock stock - #{count} WHERE id #{specId} AND stock #{count}这条SQL在数据库层面同时完成判断库存够不够和扣减库存两个动作返回受影响行数如果为0说明库存不足直接抛业务异常。如果先在Java代码里SELECT stock再UPDATE stock并发场景下两个请求同时读到库存为1就会双双扣减成功最后库存变成-1超卖就发生了。个体店铺的并发量不大但秒杀一个爆款商品的时候十几个人同时下单就可能触发这个问题。3.4 支付回调异步通知与订单状态流转微信支付V3的流程是后端调用JSAPI下单接口拿到prepay_id前端用wx.requestPayment拉起支付面板用户输入密码支付支付结果由微信服务器异步通知到后端配置的回调地址。回调地址在微信商户平台配置对应后端接口POST /api/pay/callback。微信支付的回调通知有一个非常重要的特性——它会重试直到你的服务端返回一个特定的成功响应V3要求返回HTTP 200且body为{code: SUCCESS}。如果服务端处理失败返回500微信会在接下来一段时间反复通知频率是15秒/15秒/30秒/3分钟/10分钟/20分钟/30分钟/30分钟/30分钟/60分钟/3小时/3小时/3小时/6小时/6小时。所以你处理回调的逻辑必须是幂等的。也就是说同一笔订单的回调来了10次处理10次的结果跟处理1次是一样的。实现方式是先查订单当前状态如果已经是已支付直接返回成功不再重复修改数据库。回调处理的关键步骤验签用微信支付平台证书验证回调报文签名确认这确实是微信发来的通知解密回调内容用APIv3密钥加密了需要解密拿到订单号和支付结果查询订单状态从数据库查这笔订单当前状态如果是待支付则更新为已支付、更新支付时间同时把预扣的库存转成实扣返回成功应答整个逻辑我建议放进一个事务方法里并发问题是这套系统里最不该出现的。万一极端情况下出现重复回调且两个线程都查到待支付一个事务更新了状态另一个事务会因为乐观锁比如订单状态字段带上版本号更新失败整体回滚不会出现重复加库存之类的严重问题。4. 小程序端首页、商品详情、购物车与支付链路的落地细节小程序端是顾客直接面对的部分体验好不好就看这一层。我用了uni-app开发编译成微信小程序。这里不打算粘贴大段代码重点讲几个从设计到实现必须谨慎对待的细节。4.1 首页信息架构让顾客三步内到达下单按钮首页的模块划分是我跟店家反复推敲出来的顶部搜索框、轮播图、分类导航、推荐商品列表。没有什么花哨的东西。分类导航这里有一个分情况考虑的点。如果店铺的商品种类超过10个分类导航必须做得显眼比如饮品/甜品/轻食这种大分类。如果商品就二三十个且都是同类产品分类反而是干扰这时做成一个简单的全部/新品/热销筛选就够了。开发的时候我做成配置化的店家在后台可以设置启用哪种模式前端根据配置动态展示。商品列表用流式布局每个卡片包含主图、名称、价格、销量。注意一个细节小程序的像素单位是rpxiPhone的物理像素和Android有差异图片的尺寸比例要固定我统一用750x750即正方形图片。店铺拍照大多用手机竖拍如果让店主传任意比例图片列表页的排版就会乱成一片。我在后台上传接口做了图片等比压缩和居中裁剪处理从源头解决排版问题。商品详情页商品信息从后端接口获取接口返回主图、详情图、规格列表、库存、销量。详情图我用了rich-text组件渲染后端存的是富文本内容HTML标签渲染前要做防XSS处理防止店家粘贴的商品详情里带上了恶意的script标签。虽然是小程序网络请求由微信管控XSS风险比网页端低但做好过滤不会有坏处。4.2 购物车本地缓存还是后端存储小程序的购物车有两种实现方式uni.setStorageSync存在本地以及后端建购物车表存服务端。我选了本地缓存。理由很直接个体店铺的顾客不需要跨设备同步购物车。本地缓存的好处是加购零延迟、不依赖网络用户在浏览商品时随手加入购物车非常流畅。坏处是如果顾客清缓存或换设备购物车就丢了但对这个场景来说完全可接受。购物车的数据结构我用的是一个数组每项包含productId、specId、count、checked几个字段。在购物车页面修改数量、勾选商品时实时更新本地缓存。点击去结算时从本地缓存里把勾选的商品传给后端创建订单。这里有个安全注意事项购物车里展示的价格只能当作参考价格真正下单时以后端数据库的价格为准。所以我每次加载购物车列表时都会调用一个接口POST /api/cart/preview把购物车商品ID列表传过去后端返回最新的商品价格、库存状态、上下架状态。如果商品已经下架或者价格变了前端同步更新显示并提示顾客。4.3 结算页与支付流程体验顺滑但逻辑严谨结算页的字段有三块收货地址、商品清单、金额明细。金额明细包括商品总额、配送费、优惠目前只做了满减更多营销后面再说、应付总额。用户第一次进入结算页如果还没填过收货地址我用wx.chooseAddress引导用户选择微信里已有的收货地址。这个体验比手动填地址好很多微信的地址数据里包含姓名、手机号、省市区、详细地址后端校验一下手机号格式直接入库。点击提交订单后前端拿到后端创建的订单信息再用uni.requestPayment发起支付。注意一个体验要点用户支付完成之后前端不能立刻跳转我的订单列表。因为钱真正到账依赖后端回调回调通常有1到3秒延迟。如果前端跳得早了订单列表里还是待支付状态顾客会觉得钱付了订单没生成容易引发投诉。我的处理办法是小程序端在支付成功后轮询订单状态接口每1秒一次最多查6次查到订单状态变为已支付再跳转订单详情页。要是6秒还没确认到就弹提示支付结果确认中请稍后查看订单列表但订单列表里也要做兜底——顾客自己下拉刷新时后端根据微信支付单号再查一次支付结果。4.4 订单列表与售后顾客只看状态不关注内部逻辑订单列表页我按状态分了五个标签全部、待支付、待发货、待收货、已完成含售后。每个状态卡片展示订单的商品缩略图、商品名称、数量、实付金额和状态操作按钮。这里最重要的状态流转设计我从业务角度梳理了一遍待支付顾客可以取消订单后端取消时回滚库存待发货店主在管理端点击发货后变为待收货待收货顾客点击确认收货或者系统在发货后15天自动确认收货已完成订单闭环不计入售后流程退款我放在订单详情页里处理顾客在待发货状态下可以申请退款店主在管理端一键退款调用微信支付退款接口退款原路返回。这个流程不复杂但围绕退款一定得记得做幂等——微信支付退款接口要是同一个out_refund_no重复调用是会报错的所以后端生成唯一的退款单号并先查库判断是否已经退过款。5. 超卖、掉单、状态丢失联调时踩过的三个典型坑有了完整系统之后真正的测试阶段才开始。开发环境和真实环境之间隔着一道鸿沟下面这几个问题是我在这个项目联调过程中真实遇到并解决的写出来给后面要接类似项目的同学做个参考。5.1 并发超卖联合索引 原子更新缺一不可某次测试时我用JMeter模拟了20个用户同时下单抢购同一件库存只有5件的商品结果订单生成了7个。查日志发现库存扣到了-2但校验层显然没拦住。问题根源在于我第一版的扣库存逻辑是这样的ProductSpec spec specMapper.selectById(specId); if (spec.getStock() count) { throw new BusinessException(库存不足); } spec.setStock(spec.getStock() - count); specMapper.updateById(spec);这个先查后更新的操作在并发情况下就是典型的竞态条件——线程A查到库存5线程B也查到库存5两个线程都通过校验各自把库存减了3最后更新时A把库存覆盖为2B也把它覆盖为2实际卖出6件但库存显示2。修复后的写法前面已经给过UPDATE product_spec SET stock stock - #{count} WHERE id #{specId} AND stock #{count}同时我在orders表加了一个order_no唯一索引防止同一个订单号的重复插入。这属于纵深防御——即便业务层代码有bug数据库层面也会兜住最后一道防线。5.2 支付回调掉了怎么办主动查单兜底有一次测试顾客付了钱订单一直停在待支付状态。排查发现微信服务器确实发了回调但那时我的服务刚好在重启回调请求打过来接收失败微信重试了几轮后放弃了。这个问题的困难点在于支付成功与否小程序端知道、微信服务端知道但我们的后端睡过去了。解决方案是加了一个定时任务每5分钟扫描一次已创建超过30分钟但状态仍是待支付的订单调用微信支付的查单接口主动查询这些订单的真实支付状态。如果查到已支付就执行跟支付回调一样的后续逻辑。这个兜底方案让我养成了一个习惯所有涉及支付状态更新的方法都抽成一个公共方法回调逻辑和主动查单逻辑走同一个入口。避免两处逻辑各写各的后面一处改了另一处没跟上就会出大事。5.3 订单状态被跳变状态机才是根本解法第三个坑是管理端的发货操作偶尔会把已退货的订单状态改成已发货。听起来很魔幻但确实发生了店主在管理端看到订单列表有人同时操作了发货和退款两个请求同时到达后端。这个问题的本质是状态更新没有约束任何状态都可以直接跳到任何状态。订单这块的业务规则其实非常清晰待支付只能变成已取消或已支付已支付才能发货已发货才能收货退款只能发生在待发货之前。这些规则应该写死在代码里而不是靠开发人员每次写if (order.status 1) order.status 2时自己记得检查。我最后用枚举实现了一个简单的状态机public enum OrderStatus { UNPAID(0), PAID(1), SHIPPED(2), COMPLETED(3), CANCELLED(4), REFUNDED(5); private static final MapOrderStatus, SetOrderStatus allowedTransitions new HashMap(); static { allowedTransitions.put(UNPAID, EnumSet.of(PAID, CANCELLED)); allowedTransitions.put(PAID, EnumSet.of(SHIPPED, REFUNDED)); allowedTransitions.put(SHIPPED, EnumSet.of(COMPLETED)); allowedTransitions.put(COMPLETED, EnumSet.of(CANCELLED)); // 仅售后异常单可触发 } public boolean canTransitionTo(OrderStatus target) { SetOrderStatus allowed allowedTransitions.get(this); return allowed ! null allowed.contains(target); } }这个枚举类编译期就能保证状态的合法流转运行时不满足canTransitionTo的更新直接抛异常。建议所有跟订单状态相关的操作都加一个这样的校验成本低、收益高。6. 店铺运营侧的隐藏功能库存预警、销量统计与小店效率很多开发者做完商品、订单、支付三个模块就觉得系统完成了。但真正让店铺愿意持续用下去的往往是运营感强的功能。我在这里补充两个个体店铺特别实用、又不会被大型系统重视的模块如果你的项目也需要可以按这个思路加。6.1 库存预警手机端推送比后台报表管用我之前把库存管理做成一张数据表格放在PC后台以为店主自己会去看。结果几乎没有店主打开过。后来我直接在登录小程序管理端时检查一下如果有商品的库存低于阈值店主可以自己设定默认10件就在管理首页显示一个红色警示卡片比如拿铁大杯库存仅剩4件点击补货。具体实现并不复杂定时任务每天扫描一次product_spec表把库存低于阈值的商品写入一张stock_alert表管理端接口只查这张表。店主点击补货时跳转到对应商品的规格管理页直接修改库存数。从实际使用反馈来看这个功能的使用率比预期高得多。店主不需要任何数据分析能力扫一眼红色数字就知道今天该准备什么货了。对小店来说快卖完了这个信号比销售额趋势重要得多。6.2 简易销量统计用SQL聚合就够别碰大数据有需求就会有店主问能不能看这个月哪天卖得最好。我一开始想引一个报表引擎后来想想真的没必要。个体店铺的订单量一天几十单顶天了用SQL做简单的聚合查询性能完全没压力。我实现了一个接口GET /api/stats/overview返回今日营业额、今日订单数、本周趋势按天分组、热销商品TOP10。数据源就是orders表和order_item表几条GROUP BY就能查出来。这里有一个订单金额口径的问题要提前定义清楚营业额统计的是已支付订单的实付金额合计而不是订单总金额。如果统计口径不统一店主会质疑数据不准。我干脆把订单的应付金额和实付金额两个字段都存了下来实付金额在支付回调成功时写入。报表页面我放在了小程序管理端用的是uni-app的ucharts组件画折线图和柱状图。店主手机上直接看不用打开电脑。实践证明手机上能看的报表才是店主真正会看的报表——这个认知多便宜但很多开发者就是理解不了。7. 项目上线前后的一些流程建议代码写完只是第一步上线前后的流程安排决定了项目能不能平稳落地。这里分享几条我在实际交付过程中的经验供参考。7.1 微信侧的资质申请要提前动手小程序上架需要注册小程序账号、完成微信认证个体户可以认证需要营业执照、申请微信支付商户号。这三个事情的时间线要提前规划比如微信支付商户号的审核通常需要1到3个工作日如果等到代码写完了再申请就会干等好几天。我记得遇到过一个问题个体户的营业执照名称和微信支付商户号的名称必须一致开店名称不能用别人的招牌名。如果店铺使用的是品牌授权或加盟性质还需要额外上传授权资料。这些细节在对接微信支付时很容易卡住。7.2 数据安全合规小项目也要有底线两个底线建议一是初始化管理员账号时不要用admin/admin123这种默认口令至少改成强密码并建议定期更换二是顾客的手机号和收货地址属于个人信息接口传输必须用HTTPS数据库里手机号建议脱敏存储或加密。我还在后端做了简单的接口限流用拦截器对单个IP每分钟的请求次数做个计数器超过阈值直接拒绝。原因很简单——店铺系统一旦暴露在公网就会被各种扫描器盯上防不住合法的攻击但起码能挡住最傻的批量抓取。7.3 Docker部署笔记部署我用Docker编排了一个最小可用的容器组mysql、springboot-app、nginx做HTTPS反向代理和静态资源服务用docker-compose管起来。服务器的系统盘只放Docker数据图片上传到挂载的独立数据卷里备份时把MySQL的SQL文件和数据卷目录一起拷走就行。支付回调地址的配置一定要用https且公网可访问微信服务器不会理会内网地址。这个我联调时吃过亏第一次回调地址填了http://localhost微信返回了回调地址不合法。还有一个小细节服务器时间要和标准时间同步。JWT签发、支付签名、订单过期时间都要依赖系统时间如果有几秒钟的偏差可能导致签名验证失败。在Dockerfile里我加了时区设置ENV TZAsia/Shanghai同时用NTP保证宿主机时间准确。项目收尾时我的感受是这套系统没有用到任何高级技术但每一处都踩在个体店铺的真实痛点上。对开发者来说把Spring Boot、微信小程序、微信支付这条链路完整走一遍学到的远比新技术名词多得多。技术从来都是为场景服务的想清楚店铺要什么比想清楚自己要写什么代码重要得多。