☰
JAVA微信小程序商城源码+完整后台:从部署到支付链路闭环
2026/10/5 7:27:05 网站建设 项目流程

简介:面向需要快速搭建微信小程序商城或学习Java全栈开发的开发者,这套源码提供基于SpringMVC+MyBatis+Spring+Maven+MySQL架构的后端服务与H5/CSS3前端,并配有基于Bootstrap-Ace框架的完整后台管理界面。功能覆盖商品发布、物流管理、评价系统、优惠券、运费系统、在线客服、在线支付与退款、微信管理等多个电商核心模块,基本可满足中小型商城项目的二次开发与功能扩展需求。同时,完整前后端代码有助于梳理微信支付/退款、客服会话、运费计算等业务流程,理解小程序与后台服务之间的接口设计。该项目目录结构清晰,前后端分层明确,适合边读源码边对照梳理业务流程。资源采用7z格式压缩,约20.5MB,下载部署较为方便。目前已有5047人学习/下载,适合具备一定Java基础、希望结合微信生态实践商城开发的中高级开发者直接参考。

1. JAVA微信小程序商城源码:一套能真正上线的代码长什么样

做商城类小程序,最怕拿到一个看着齐全、改起来处处受限的半成品:前端能逛能加购物车,后台却只有一个空壳,订单、库存、支付链路全是缺口。JAVA微信小程序商城源码+完整后台指的正是同一个项目里把微信小程序原生前端、JAVA后端服务和管理后台三件套一起交付:前端有商城页面,后端有商品、订单、会员、营销、权限等模块的接口与维护界面。它适合需要快速起盘的小团队、接私活的外包工程师,以及已经在用Spring Boot+MyBatis做业务、不想重复造轮子的开发者。这套方案的价值关键不在代码量,而在链路有没有闭环——从登录、下单、支付回调到库存扣减,每一环都要在同一套代码里走通。

2. 跑通前先拆结构:小程序端和JAVA后台的代码边界

2.1 小程序端优先看这四个目录

拿到源码先别急着导入工具,先看目录结构。原生微信小程序项目里,pages目录放页面,每个页面是.js+.json+.wxml+.wxss四件套;components目录放公共组件,比如商品卡片、价格标签、数量选择器;utils目录放工具函数,重点看request.js和auth.js;config目录里则是baseUrl、AppID、支付参数这些运行配置。我一般先打开config里的配置文件,确认服务端地址是写死还是按环境区分。

很多源码会在utils/request.js里封装一层统一请求:自动带token、统一处理401、把后端返回的{code, msg, data}解包。这一层决定了后面所有页面的调用方式,也是二次开发最常动的地方。有的项目还会在app.js里做全局登录态初始化,这个文件也优先看,因为它决定用户进入小程序后是先跳登录页还是静默登录。

// utils/request.js —— 小程序端统一请求封装 const { baseUrl } = require('../config/index.js') function request(url, method = 'GET', data = {}) { const token = wx.getStorageSync('token') return new Promise((resolve, reject) => { wx.request({ url: baseUrl + url, method, data, header: { 'Content-Type': 'application/json', // 后台靠这个请求头识别当前用户 'Authorization': token ? 'Bearer ' + token : '' }, success(res) { if (res.data.code === 401) { // token过期:清掉本地登录态,触发重新登录 wx.removeStorageSync('token') wx.removeStorageSync('userInfo') reject({ code: 401, msg: '登录已过期' }) } else { resolve(res.data) } }, fail(err) { reject({ code: -1, msg: '网络异常' }) } }) }) } module.exports = { request }

这段封装的逻辑要点:所有请求统一带Authorization头,后台拦截器靠它判断用户是否登录;401统一在请求层处理,业务代码里不用到处写“判断登录过期”。baseUrl如果写localhost,手机预览时会直接失败——我通常会在config里区分dev和prod两套值,编译时切换,而不是每次手动改代码。

2.2 后台为什么用Spring Boot+MyBatis

市面上流通的JAVA商城源码,绝大多数落在Spring Boot+MyBatis这个组合上。选它不单是资料多、招聘匹配度高,而是这套组合在“复杂查询”和“快速CRUD”之间平衡得最好。MyBatis的XML里写联表查询很直观,分页插件PageHelper一行就能接上,和微信小程序端“列表加载更多”这种场景天然匹配。服务分层通常是Controller→Service→Mapper三层:Controller接HTTP参数,Service写业务规则,Mapper只做SQL。

这也意味着,后续改动最频繁的是Service层。下单时要扣库存、生成订单、记录流水,Controller里只是一段调用,业务全在Service里。如果二次开发时把SQL直接堆在Controller里,后续维护成本会指数级上升。看一个典型的商品列表Controller:

@RestController @RequestMapping("/api/goods") public class GoodsController { @Autowired private GoodsService goodsService; /** * 商品分页列表,小程序端滚动加载 */ @GetMapping("/list") public JsonResult list(@RequestParam(defaultValue = "1") Integer page, @RequestParam(defaultValue = "10") Integer size, @RequestParam(required = false) Integer categoryId, @RequestParam(required = false) String sortField) { // Service层内部用PageHelper做物理分页 PageResult<GoodsVO> result = goodsService.pageQuery(page, size, categoryId, sortField); return JsonResult.ok(result); } }

sortField参数用于按价格、销量、新品排序。后台不能直接把前端传的字符串拼进ORDER BY,常见做法是维护一张白名单映射表,把price、sales、new翻译成SQL列名,匹配不到就返回默认排序。这个位置是SQL注入的高发区,也是我审查源码时必看的一行。

2.3 数据模型三张主表说清商城业务

看懂表结构比看懂代码更快。商城项目围绕三张主表转:用户表、商品表、订单表。用户表除微信相关字段外,通常会放openid、unionid、手机号、余额、积分;商品表一般是spu+sku两层,spu代表“一款商品”,sku代表“一个规格”,比如红色加大码是一个sku,库存挂在sku上而不是spu上。订单表则是主表+子表,订单主表存总金额、状态、收货地址快照,订单子表存每个商品的sku、数量、单价、优惠明细。

订单状态字段的设计直接决定后续代码好不好改。常见做法是用整数status表示状态机:0待支付、1已支付待发货、2已发货、3已完成、4已关闭、5退款中、6已退款。用字符串枚举也可以,但后期新增状态时,老数据迁移永远比新代码难。

主表核心字段关联关系
useropenid, unionid, nickname, balance1对多:订单表
goods_sputitle, cover, status1对多:goods_sku
goods_skuspu_id, spec, stock, price1对多:订单子表
orderorder_no, user_id, total_amount, status1对多:order_item

如果源码自带多商户或分销模块,表会更多,但核心链路不变。看清这三张主表,菜单栏里那些看着复杂的营销功能,本质都是在给订单表加字段或加关联表。

3. 本地部署到能下单:两端的启动方法与联调姿势

3.1 后台启动:初始化SQL、改配置、跑起来

在IDEA里以Maven项目方式导入源码根目录,等依赖下载完成。源码根目录下一般有doc或db文件夹,放着xxx.sql初始脚本。这个脚本最先执行,它建库建表,同时写入一批演示商品和后台管理账号。SQL文件里如果有CREATE DATABASE语句,直接用Navicat或命令行整体执行;没有的话手动建库并指定utf8mb4字符集,再导入表结构。

注意:导入SQL前先确认字符集。商城商品名称、收货地址里会出现生僻字和emoji,数据库字符集最好用utf8mb4而不是utf8,否则后续写库直接报错。

然后改application配置:

# application.yml —— 本地联调常用配置 server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/mall?useUnicode=true&characterEncoding=utf8mb4&useSSL=false&serverTimezone=Asia/Shanghai username: root password: 123456 redis: host: localhost port: 6379 mall: wx: appid: 你的小程序AppID secret: 你的小程序Secret

数据库、Redis、微信AppID三项是启动前必改的。AppID在本地联调时可以先填测试号,但支付功能必须用已认证的小程序账号。Redis也没得省——商城源码里登录态、验证码、购物车基本都会用Redis,本机没装Redis服务就直接启动失败。第一次启动建议先确认JDK版本和项目pom里Spring Boot版本匹配,JDK 8还是11、Tomcat内嵌端口是否被占用,都能从启动日志里一眼看出来。

跑mvn spring-boot:run看到Tomcat started,后台才算起来。启动报错分两类:一是端口占用、Redis连不上这类环境问题,二是数据库脚本没导全导致的表不存在。环境问题看报错前几行就能判断,表不存在则要回头重导SQL,不要手动建表去补,否则字段对不上后面全是坑。

3.2 小程序端导入与调试模式

小程序端在微信开发者工具里以“小程序项目”方式导入,填上自己的AppID。导入后先别急着改代码,确认两件事:一是工具右上角“详情-本地设置”里勾上“不校验合法域名”,否则wx.request请求本地后台会被拦截;二是把config里的baseUrl改成电脑的局域网IP,而不是localhost——手机预览时,localhost指向的是手机自己。

// config/index.js —— 联调时切换环境 module.exports = { // 开发环境:电脑局域网IP + 后台端口 baseUrl: 'http://192.168.31.42:8080/api', // 生产环境:备案过的HTTPS域名,微信要求不能带端口 prodUrl: 'https://api.yourdomain.com/api' }

改完baseUrl,小程序端所有列表页就能拉到数据了。注意真机预览时手机和电脑要连同一个局域网,防火墙要放行后台端口。请求还是红的话,先到后台日志看有没有收到请求——没收到是网络不通,收到了再往下查接口报错。

3.3 登录态是怎么打通的:wx.login到JWT

小程序端首次进入页面,调用wx.login拿到一个临时code,把code发给后台;后台拿code去微信的code2Session接口换openid和session_key。openid是用户在这个小程序里的唯一标识,session_key用于解密手机号等敏感数据,两者都不能暴露给前端。

常见做法是后台不自己存session,而是签发一个自定义token返回给小程序端,前端把token存Storage,之后每个请求在请求头带上。token方案要么是UUID存Redis,要么是JWT。UUID方案token失效要查Redis,JWT方案签发后过期前没法主动踢人,各有利弊,取决于业务需要。

看前端侧的核心登录代码:

// utils/auth.js —— 静默登录流程 const { request } = require('./request.js') function silentLogin() { return new Promise((resolve, reject) => { // 已有token且没过期,直接放行 const token = wx.getStorageSync('token') if (token) { resolve(token) return } // 没有token,走wx.login换临时code wx.login({ success: res => { if (res.code) { // code发给后台,后台换成我们自己的token request('/auth/login', 'POST', { code: res.code }) .then(data => { wx.setStorageSync('token', data.token) wx.setStorageSync('userInfo', data.userInfo) resolve(data.token) }) .catch(reject) } else { reject(new Error('wx.login获取code失败')) } }, fail: reject }) }) }

这段逻辑有两个要点:wx.login的code只能用一次,服务端换完openid后立即作废;后端返回的token要设过期时间,比如7天,前端配合401拦截自动静默登录,用户感知不到登录过程。很多源码出现死循环登录,就是401处理里又触发wx.login,旧token没清干净造成来回跳。

4. 商城的钱都在代码细节里:订单支付要处理的4个边界

4.1 下单与库存扣减:一句带条件的UPDATE

下单业务最怕超卖——库存只剩1件,两个用户同时下单却都成功了。常见做法不是先select库存再判断,而是把扣减条件直接写进UPDATE语句:

UPDATE goods_sku SET stock = stock - 1 WHERE id = #{skuId} AND stock > 0

这段SQL执行后,影响行数为1说明扣减成功;影响行数为0说明库存不足或商品已下架,直接返回“库存不足”。这种方式天然带乐观锁语义,不需要select for update,也不需要分布式锁。在MyBatis里,更新影响行数用int接收,判断rows == 1即可。

扣减成功后再插入订单,如果订单插入失败要回滚事务并把库存加回去。事务边界要包住“查地址、扣库存、插订单、发消息通知”的全流程,任意一步失败全部回滚。线上看到库存变负数,基本都是直接把这段SQL改成了“先查后改”或删掉了stock > 0条件。

4.2 支付回调验签:先验签再改状态

用户在小程序端调起微信支付,支付结果不是小程序端通知后台的,而是微信服务器直接请求统一下单时填写的notify_url。这个回调接口是整套源码里最容易被改坏的地方。

/** * 微信支付回调 */ @PostMapping("/notify") public String wxPayNotify(@RequestBody String xmlData) { // 第一步:验签,确认请求确实来自微信 WxPayService wxPayService = ...; if (!wxPayService.verifyNotify(xmlData)) { return "<xml><return_code>FAIL</return_code><return_msg>签名错误</return_msg></xml>"; } // 第二步:解析回调,拿到商户订单号、支付金额 Map<String, String> result = wxPayService.parseNotify(xmlData); String outTradeNo = result.get("out_trade_no"); String totalFee = result.get("total_fee"); // 第三步:查订单,比对金额一致后更新状态 Order order = orderMapper.selectByOrderNo(outTradeNo); if (order != null && order.getStatus() == 0 && totalFee.equals(order.getPayAmount().toString())) { orderMapper.updateStatus(outTradeNo, 1); } // 第四步:通知微信处理成功,避免微信持续重试 return "<xml><return_code>SUCCESS</return_code><return_msg>OK</return_msg></xml>"; }

给微信返回的报文格式必须固定,return_code不带SUCCESS微信就会一直重试。代码里已经限定订单当前状态为待支付才更新,这是一道天然的重复回调防线。金额比对也不能省,回调里的total_fee必须和订单里的应付金额完全一致再改状态,防止金额被篡改。

4.3 幂等处理:同一个回调收到两次怎么办

支付成功通知、退款结果通知这类回调,微信官方策略是不保证只通知一次。网络抖动、后台重启都可能导致同一个out_trade_no被通知两次。如果代码不做幂等,第二次回调时订单可能已经进入发货环节,状态被倒回去,或者账户余额被二次加总。

我的做法是在订单表设计上留一道约束:给out_trade_no建唯一索引,同时用一张order_pay_log表记录每一笔支付通知。插入前先查流水号是否已存在,重复回调直接返回SUCCESS给微信,不进入后续业务逻辑。订单状态字段配合使用,每个状态流转都判断前置状态,比如只有待支付才能变成已支付,已支付才能变成已发货。并发场景下配合乐观锁version字段,谁先提交成功谁生效。

4.4 订单超时关闭:定时扫描与库存回补

待支付订单超过15分钟或30分钟未支付,要自动关闭并回补库存。这套逻辑一般落在后台定时任务里,Spring Boot里就是@Scheduled注解加cron表达式:

@Scheduled(cron = "0 */5 * * * ?") // 每5分钟扫一次 public void closeTimeoutOrders() { // 查所有超过30分钟仍为待支付的订单 List<Order> list = orderMapper.selectTimeoutOrders(30); for (Order order : list) { // 带条件的UPDATE关单,避免多节点重复处理 int rows = orderMapper.closeOrder(order.getOrderNo()); if (rows == 1) { // 关单成功后再回补库存 skuMapper.restoreStock(order.getOrderNo()); } } }

cron表达式从左到右是秒、分、时、日、月、周,0 */5 * * * ?表示每5分钟的第0秒执行。扫描间隔不是越短越好,30分钟超时配5分钟扫描完全够用,太频繁只会增加数据库压力。多实例部署时还要注意同一个定时任务只能有一个节点真正执行,常见做法是加Redis分布式锁,否则关单逻辑会被同时跑好几遍。

5. 避坑清单:从登录报错到列表加载更多

5.1 登录接口报错:code被用了两次

现象:新用户首次进入页面,登录接口返回token,紧接着请求商品列表却提示登录失效。

原因:wx.login返回的code只能用一次,前端在页面生命周期里把同一个code发了两遍。有些源码在app.js的onLaunch里触发登录,同时某个页面的onShow里又触发一次,两个请求并发发出,后到的那次必然失败。

解决:登录方法里加状态锁,登录进行中直接丢弃后续触发;请求封装里对401做统一处理,不要在业务代码里到处写wx.login。后台日志要打印调用微信接口的原始返回,看到invalid code就能直接定位是重复使用。

5.2 微信小程序报10002这类编号错误,先查后台原始日志

现象:真机调试时弹窗提示请求失败,错误码类似10002,小程序端看不到具体描述。

原因:这类编号错误多数来自微信服务端,和登录态、接口调用频率、参数校验相关,不同场景含义不同,前端只能拿到一个统一笼统的提示。

解决:在登录、支付、订阅消息这几个环节的catch里,把微信返回的原始errMsg打到服务端日志,然后拿着报错时的code、订单号去对照微信侧的返回内容。本地调试用抓包工具看小程序发出的真实请求和响应有帮助,但服务端日志才是最终判断依据。

5.3 列表加载更多:分页参数看着对,数据就是不出

现象:首页滚动到底部,onReachBottom触发了好几次,新数据没加载出来,或者第二页内容和第一页重复。

原因:page起始值约定不一致。后端按LIMIT (page-1)*size解析,前端却传0,两边查的是同一页;还有的源码把hasMore写成list.length < total,少了乘page,导致永远加载不完。

解决:统一page从1开始,hasMore = page * size < total。

// 页面中:下拉加载更多 onReachBottom() { if (!this.data.hasMore || this.data.loading) return const page = this.data.page + 1 request('/goods/list', 'GET', { page, size: 10 }) .then(res => { this.setData({ list: this.data.list.concat(res.data.list), page, hasMore: page * 10 < res.data.total }) }) }

loading标志位必须在请求完成后复位,否则快速滚动时多次触发回调,会重复请求同一页数据。

5.4 顶部导航栏高度:机型一换就错位

现象:开发者工具里页面正常,真机上标题栏压住了胶囊按钮,或自定义导航栏和右上角胶囊重叠。

原因:自定义导航栏时把navigationBarHeight写死成了固定值,但不同机型状态栏高度差异很大,刘海屏和普通屏能差几十像素。

解决:用wx.getWindowInfo()获取状态栏高度,用wx.getMenuButtonBoundingClientRect()获取胶囊按钮位置,动态计算导航栏高度。旧版本基础库没有getWindowInfo时,回退到wx.getSystemInfoSync(),调用前做能力判断。这个坑在商城源码里特别常见,因为很多页面都用自定义导航栏来腾出商品展示空间。

5.5 手机真机全挂,模拟器却正常

现象:开发者工具里一切正常,手机预览打开白屏,所有请求失败。

原因:最常见是baseUrl写成了localhost,手机访问localhost指向的是手机自己;另外后台监听的是127.0.0.1,电脑外的设备根本访问不到。

解决:baseUrl改成电脑局域网IP,后台启动参数加上--server.address=0.0.0.0,防火墙放行对应端口。排查顺序有讲究:先看后台日志有没有请求进来,没收到是网络不通,收到了就继续查接口报错。按这个顺序排查,能省下大半天时间。

6. 上线前的心跳检查:压测、慢SQL和一套能落地的验证顺序

真正决定这套源码能不能撑住线上运营的,是上线前那几个小时的验证顺序。我习惯按“压测→慢查→回归”三步走。

第一步是压测。挑用户量最大的3个接口:首页商品列表、商品详情、提交订单。用ApacheBench跑一轮,命令是ab -n 1000 -c 50 -k https://api.example.com/api/goods/list,1000次请求、50并发,看失败率和平均响应时间。失败率超过5%就要回头查代码。订单接口还要额外模拟并发下单,验证库存扣减逻辑在压力下是否真的挡住了超卖。

第二步是慢SQL排查。上线前开启MySQL慢查询日志,跑一轮冒烟测试,之后把慢查询记录捞出来看有没有全表扫描。商城最常见的慢查询是订单列表按用户维度查询时没建复合索引,在order表上加(user_id, status)联合索引,效果立竿见影。

第三步是回归验证清单。把开发者工具的“不校验合法域名”关掉,换成正式HTTPS域名,走一遍注册、登录、加购、下单、支付、退款、售后全流程。支付不能只在模拟器里测,模拟器和真机触发的回调路径有差异,至少要在安卓真机上走一单1分钱下单到支付成功再退款。

我自己踩过最大的坑,是上线当天把Tomcat工作线程数调得过大,反而把数据库连接池打满,整个后台响应成了黑匣子。现在我的原则是线程数宁小勿大,配合连接池上限一起配置,宁可排队也不要让数据库被压垮。这套JAVA微信小程序商城源码的方向,花一周时间吃透订单和支付两条链路,后续所有二次开发都会顺很多。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询