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) | 小程序端最标准的登录方式 |
| 支付 | 微信支付V3(JSAPI下单) | 用户在小程序内直接拉起支付 |
| 部署 | 单台云服务器 + 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个,分类导航必须做得显眼,比如"饮品/甜品/轻食"这种大分类。如果商品就二三十个且都是同类产品,分类反而是干扰,这时做成一个简单的"全部/新品/热销"筛选就够了。开发的时候我做成配置化的,店家在后台可以设置启用哪种模式,前端根据配置动态展示。
商品列表用流式布局,每个卡片包含主图、名称、价格、销量。注意一个细节:小程序的像素单位是rpx,iPhone的物理像素和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把库存覆盖为2,B也把它覆盖为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 Map<OrderStatus, Set<OrderStatus>> 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) { Set<OrderStatus> 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 TZ=Asia/Shanghai,同时用NTP保证宿主机时间准确。
项目收尾时我的感受是:这套系统没有用到任何"高级技术",但每一处都踩在个体店铺的真实痛点上。对开发者来说,把Spring Boot、微信小程序、微信支付这条链路完整走一遍,学到的远比"新技术名词"多得多。技术从来都是为场景服务的,想清楚店铺要什么,比想清楚自己要写什么代码,重要得多。