简介:这是一套面向Java后端与微信小程序开发学习者的网上商城系统源码,采用Spring Boot框架搭建后端服务,配合微信小程序前端,适合作为课程设计、毕业设计或全栈练手项目。系统核心围绕购物车与在线咨询两大模块展开,提供列表展示、分页查询、详情获取、保存、更新、删除等完整接口,并设有提醒接口用于统计相关数据数量,数据持久化依托MyBatis Plus完成,前后端通过HTTP请求交互。压缩包共1160个文件,约14.57MB,包含119个Java源文件、129个Vue组件、160个JavaScript脚本、78个WXSS与76个WXML小程序页面文件,以及大量PNG、SVG图标资源和SQL脚本、配置文件,覆盖后端逻辑、管理端页面与小程序端界面。已有84人学习下载。读者可借此理解Spring Boot与MyBatis Plus的接口分层写法、分页与CRUD实现思路,并参考小程序页面组织方式,快速搭建可运行的商城原型。
1. 从一份 Spring Boot + 微信小程序的商城源码说起:它能帮你省掉哪三周
拿到「基于 Spring Boot 和微信小程序的网上商城系统」这类源码包,多数人的第一反应是解压、找 SQL、改数据库密码、启动,然后卡在登录接口 401 上。我见过太多团队在这个环节耗掉两三周:小程序端wx.login拿到的 code 换不回 token,后端application.yml里的 appid 和前端project.config.json对不上,商品图片路径写死在本地磁盘导致换台机器全挂。这套技术组合之所以长期占据「Spring Boot 设计题目商城」和「微信小程序项目实例」的搜索前排,不是因为它新,而是因为它把 Java 后端最典型的 CRUD 分层、鉴权、文件上传,和小程序端最典型的登录态、请求封装、页面栈管理全串了一遍。它适合三类人:想拿一个完整业务练手的 Java 后端新手、需要快速搭出可演示商城原型的独立开发者、以及要给学生讲清「前后端如何真正对接」的授课者。读完你能自己判断这份源码值不值得改、哪些模块必须重写、上线前哪几个参数不调一定翻车。
2. 先看清这套商城的技术骨架:Spring Boot 分层与小程序端如何对上
2.1 后端为什么是 Controller-Service-Mapper 三层,而不是一把梭
这类商城源码的后端几乎都是 Spring Boot + MyBatis-Plus 的组合,包结构长这样:controller收请求、service写业务、mapper碰数据库、entity映射表、vo和dto做进出参转换。很多人觉得这是模板化冗余,但商城业务里订单创建要同时扣库存、写订单、写订单明细、清购物车,这四步必须在一个事务里,如果全塞进 Controller,事务注解会加得满屏都是,回滚边界根本说不清。分层真正的价值是让「事务只出现在 Service」这条规则可执行。
我一般会先看pom.xml确认三件事:Spring Boot 版本、MyBatis-Plus 版本、有没有引入spring-boot-starter-validation。版本不匹配是新手第一个坑,比如 Spring Boot 3.x 必须配 MyBatis-Plus 3.5.3 以上,否则启动直接报Invalid value type for attribute 'factoryBeanObjectType'。下面是一段典型的商品查询接口,我把它拆开讲参数:
@RestController @RequestMapping("/api/goods") public class GoodsController { @Autowired private GoodsService goodsService; // page 从 1 开始,size 默认 10,categoryId 为空表示全部 @GetMapping("/list") public Result<IPage<GoodsVO>> list( @RequestParam(defaultValue = "1") Integer page, @RequestParam(defaultValue = "10") Integer size, @RequestParam(required = false) Long categoryId) { // 分页对象交给 MyBatis-Plus 拦截器处理,不要自己写 limit Page<Goods> p = new Page<>(page, size); return Result.ok(goodsService.pageGoods(p, categoryId)); } }page和size用@RequestParam而不是路径变量,是因为小程序端wx.request拼 query 更顺手;categoryId设成required = false,前端不传就查全部,避免小程序首页因为漏传参数直接 400。Result是统一返回体,code、msg、data三个字段,小程序端拦截器只认code === 200。这里有个容易忽略的点:分页必须配 MyBatis-Plus 的PaginationInnerInterceptor,否则Page对象不会真正 limit,会把整表查出来再内存分页,数据量一上来接口直接超时。
2.2 小程序端的请求封装:为什么不能每个页面直接 wx.request
小程序原生wx.request是回调式的,一个页面里写三四个请求就会嵌套得没法看,而且 token 过期、loading 状态、错误提示这些逻辑会散落在每个页面。常见做法是封一个request.js,统一拼 baseUrl、注入 token、处理 401 跳登录。下面是我一般会用的封装骨架:
// utils/request.js const BASE_URL = 'http://localhost:8080/api'; function request(options) { return new Promise((resolve, reject) => { wx.request({ url: BASE_URL + options.url, method: options.method || 'GET', data: options.data || {}, header: { 'content-type': 'application/json', // token 存在 storage,登录后写入 'Authorization': wx.getStorageSync('token') || '' }, success(res) { if (res.data.code === 200) { resolve(res.data.data); } else if (res.data.code === 401) { // 登录态失效,清缓存并跳登录页 wx.removeStorageSync('token'); wx.navigateTo({ url: '/pages/login/login' }); reject(res.data); } else { wx.showToast({ title: res.data.msg, icon: 'none' }); reject(res.data); } }, fail(err) { wx.showToast({ title: '网络异常', icon: 'none' }); reject(err); } }); }); } export default request;BASE_URL在开发阶段指向本机,真机调试时不能写localhost,要换成局域网 IP,否则手机访问不到。Authorization头是后端拦截器读取 token 的地方,字段名必须和后端JwtInterceptor里request.getHeader("Authorization")完全一致,大小写敏感。401 分支里先清 storage 再跳转,顺序反了会出现跳转后页面还在读旧 token 的死循环。这套封装跑通后,页面里就只剩const data = await request({ url: '/goods/list' })一行,可读性差距非常大。
2.3 数据库表设计里最容易被改坏的三张表
商城源码的 SQL 文件通常有十几张表,但真正决定系统能不能跑的是user、goods、orders这三张。user表里openid字段必须有唯一索引,因为微信登录是靠 openid 认人的,重复 openid 会导致同一用户每次登录都新建账号。goods表的price用decimal(10,2)而不是float,浮点存价格在订单金额累加时会出现0.1 + 0.2 = 0.30000000000000004这种经典问题。orders表的order_no建议用时间戳加随机数生成,不要用自增 id 直接暴露给前端,否则用户能通过订单号推测出平台总单量。
改表时最常见的翻车是字符集。源码 SQL 里如果写的是utf8,存 emoji 或部分生僻字会报Incorrect string value,统一改成utf8mb4和utf8mb4_general_ci。另外orders表的外键约束在演示环境可以保留,但生产环境高并发下外键检查会拖慢插入,我一般会去掉物理外键,改在 Service 层做逻辑校验。
3. 把源码跑起来的最小闭环:登录、商品列表、下单三步走通
3.1 微信登录换 token:code2Session 的四个必填参数
小程序登录的完整链路是:小程序调wx.login拿 code,把 code 发给后端,后端拿 code + appid + secret 去微信接口换 openid 和 session_key,再生成自己的 token 返回。后端这段代码是整套系统的入口,写错了后面全白搭:
@Service public class WxLoginService { @Value("${wx.appid}") private String appid; @Value("${wx.secret}") private String secret; public String login(String code) { // 微信官方接口,GET 请求,参数必须 URL 编码 String url = "https://api.weixin.qq.com/sns/jscode2session" + "?appid=" + appid + "&secret=" + secret + "&js_code=" + code + "&grant_type=authorization_code"; String resp = restTemplate.getForObject(url, String.class); JSONObject json = JSON.parseObject(resp); // 40029 表示 code 无效,45011 表示频率限制 if (json.getInteger("errcode") != null && json.getInteger("errcode") != 0) { throw new BizException("微信登录失败:" + json.getString("errmsg")); } String openid = json.getString("openid"); // 查库,没有就注册,有就更新登录时间 User user = userService.findOrCreateByOpenid(openid); return JwtUtil.createToken(user.getId()); } }appid和secret放在application.yml里,不要硬编码进 Java 文件,否则换个小程序就得重新编译。grant_type固定是authorization_code,写错会返回 40013。code 只能用一次,且五分钟内有效,前端如果重复提交同一个 code,第二次一定失败,所以登录按钮要做防抖。errcode判断不能只看openid是否为空,因为微信在出错时也会返回 200 HTTP 状态码,必须显式检查errcode。
3.2 商品列表分页与图片路径:本地存储换成可访问 URL
商品列表接口跑通后,下一个必炸的是图片。源码里图片路径经常写成D:/upload/xxx.jpg或者/static/xxx.jpg,小程序端<image src="{{item.img}}">根本加载不出来,因为小程序只能访问 https 的合法域名,本地路径和 http 都不行。开发阶段可以在微信开发者工具里勾选「不校验合法域名」,但真机预览必须解决。
常见做法是把图片存到后端能对外访问的目录,通过 Spring Boot 静态资源映射暴露:
@Configuration public class WebConfig implements WebMvcConfigurer { @Override public void addResourceHandlers(ResourceHandlerRegistry registry) { // 访问 /img/xxx.jpg 实际读取本地 upload 目录 registry.addResourceHandler("/img/**") .addResourceLocations("file:" + System.getProperty("user.dir") + "/upload/"); } }addResourceLocations里的路径必须以file:开头,且结尾要有斜杠,少了斜杠会 404。System.getProperty("user.dir")拿到的是项目启动目录,比写死绝对路径更可移植。上传接口保存文件时,文件名用 UUID 重命名,避免中文名和重名覆盖。数据库里存的应该是/img/xxx.jpg这种相对路径,前端拼上 baseUrl 再渲染,这样换域名不用改库。
3.3 下单接口的事务边界:库存扣减和订单写入必须同生共死
下单是商城最核心也最容易出并发问题的地方。正确顺序是:校验库存、扣库存、写订单、写明细、清购物车,全部在一个@Transactional方法里。扣库存的 SQL 必须带条件,不能先查再改:
@Transactional(rollbackFor = Exception.class) public Long createOrder(Long userId, Long goodsId, Integer num) { // 原子扣减,stock >= num 才更新,返回影响行数 int rows = goodsMapper.reduceStock(goodsId, num); if (rows == 0) { throw new BizException("库存不足"); } Orders order = new Orders(); order.setOrderNo(OrderNoUtil.gen()); order.setUserId(userId); order.setStatus(0); // 0 待支付 ordersMapper.insert(order); // 写订单明细、清购物车略 return order.getId(); }对应的 Mapper SQL 是UPDATE goods SET stock = stock - #{num} WHERE id = #{goodsId} AND stock >= #{num}。这条语句在数据库层面是原子的,并发下不会超卖。如果写成先select stock再update stock = 查到的值 - num,两个请求同时进来就会把库存扣成负数。rollbackFor = Exception.class不能省,默认只回滚运行时异常,业务里抛的受检异常不会触发回滚。订单号生成用「yyyyMMddHHmmss + 6 位随机数」,长度可控且不易撞。
4. 避坑与排查:这套源码最容易翻车的五个地方
4.1 小程序请求报「不在以下 request 合法域名列表中」
现象是开发者工具正常,真机预览所有接口失败,控制台提示域名不合法。原因是微信小程序正式环境只允许 https 且已备案的域名,localhost、局域网 IP、http 全部被拦。解决办法分两步:开发阶段在开发者工具「详情-本地设置」勾选「不校验合法域名」,真机调试时用手机连同一 WiFi 并通过「预览」生成二维码;上线前必须把后端部署到有 https 证书的域名,并在小程序后台「开发-开发设置-服务器域名」里配置 request 合法域名。注意域名不能带端口,必须是 443。
4.2 后端启动报数据库连接失败但密码明明是对的
现象是Access denied for user 'root'@'localhost',反复核对密码无误。原因通常是application.yml里 url 少了时区参数,MySQL 8 的驱动要求显式指定serverTimezone,否则连接建立阶段就失败,报错信息却指向密码。解决是把 url 改成jdbc:mysql://localhost:3306/mall?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false。另外 MySQL 8 的驱动类名是com.mysql.cj.jdbc.Driver,写成老的com.mysql.jdbc.Driver会有警告甚至连不上。
4.3 token 过期后小程序一直白屏不跳登录
现象是登录态失效后,页面请求全部失败但没有任何提示,用户卡在当前页。原因是请求封装里只处理了code === 200,其他情况直接 reject,页面没有 catch。解决是在封装的 401 分支里主动跳登录页,并且加一个标志位防止多个并发请求同时触发多次跳转。更稳妥的做法是在app.js的onLaunch里检查 token 是否存在,不存在直接wx.reLaunch到登录页,避免用户看到首页再被踢走。
4.4 商品图片上传成功但列表里显示裂图
现象是上传接口返回成功,数据库里也有路径,但小程序端图片是破图。原因通常是三个:路径存的是本地绝对路径、静态资源映射没配、或者小程序端拼接时多了或少了一个斜杠。排查顺序是先在后端直接浏览器访问http://localhost:8080/img/xxx.jpg,能打开说明后端没问题,再检查小程序端拼接逻辑。我一般会在数据库里统一存/img/xxx.jpg,前端用BASE_URL.replace('/api', '') + item.img拼完整地址,避免路径混乱。
4.5 下单后库存没变但订单已生成
现象是订单表有记录,商品库存纹丝不动。原因是扣库存和写订单不在同一个事务里,或者扣库存方法没有加@Transactional,又或者异常被 catch 后没有重新抛出导致事务没回滚。排查时先看日志里有没有「库存不足」但订单仍插入的情况,再检查 Service 方法上的事务注解是否生效——同类内部方法调用不会走代理,事务会失效,必须通过注入自身或拆到另一个 Service 来调用。
5. 让这套商城源码真正可用的两个进阶动作
把最小闭环跑通只是及格线,要让这套源码能拿去演示甚至小范围上线,还得做两件事。第一件是接口鉴权从「拦截器 + 手写 token 校验」升级到 Spring Security 或 Sa-Token。源码里常见的JwtInterceptor只做了 token 解析,没有做角色区分,普通用户能直接调管理端的商品删除接口。我一般会加一个@RequireRole("admin")注解,在拦截器里读注解判断角色,管理端接口全部标注,这样不用改业务代码就能把权限收口。
第二件是给关键接口加缓存和限流。商品详情是读多写少的典型场景,用 Spring Cache + Redis 把goods:detail:{id}缓存 5 分钟,能挡掉大部分重复查询。限流用 Guava RateLimiter 或 Redis 计数器,对下单接口按用户维度限制每秒 1 次,防止脚本刷单。下面是一个基于注解的限流切面骨架:
@Aspect @Component public class RateLimitAspect { private final Map<String, RateLimiter> limiters = new ConcurrentHashMap<>(); @Around("@annotation(rateLimit)") public Object around(ProceedingJoinPoint pjp, RateLimit rateLimit) throws Throwable { String key = pjp.getSignature().toLongString(); RateLimiter limiter = limiters.computeIfAbsent(key, k -> RateLimiter.create(rateLimit.qps())); if (!limiter.tryAcquire()) { throw new BizException("操作过于频繁"); } return pjp.proceed(); } }RateLimiter.create(qps)里的 qps 按接口重要性设,下单接口设 1 到 2,商品查询设 20。ConcurrentHashMap存 limiter 是为了每个接口独立计数,key 用方法签名保证唯一。注意这个方案是单机限流,多实例部署时要换成 Redis 的INCR + EXPIRE方案,否则每个实例各限各的,总量会翻倍。
验证这套改造是否生效,我习惯用两个动作:一是用 Postman 并发 50 个下单请求打同一个商品,看库存是否精确扣到 0 且不出现负数;二是把 token 手动改错一位,确认所有需要登录的接口都返回 401 而不是 500。这两个测试过了,这套源码才算从「能跑」变成「敢给人看」。
我自己踩过最深的一个坑,是早期图省事把微信 secret 直接写在了小程序端的 js 里,以为前端调微信接口更方便,结果被人抓包后拿去刷接口,账号被限流了整整一天。从那以后我的习惯是:凡是带 secret、密钥、管理权限的东西,一律只留在后端,前端只拿临时凭证。希望帮到你。
本文还有配套的精品资源,点击获取