微信小程序购物商城源码:Java后端+MySQL全栈实战与避坑指南
简介这是一套面向高校学生与Java初学者的微信小程序购物商城完整项目适用于毕业设计、期末大作业与课程设计场景帮助读者快速获得可运行、可答辩的高分方案。资源包共1247个文件约36.23MB涵盖小程序端wxml、wxss、js页面代码Java后端java源码与class编译文件以及jar依赖、sql数据库脚本、json配置、png与gif界面素材和html、jsp页面等前后端与数据库一应俱全。项目含详细代码注释新手也能看懂并附文档说明下载后简单部署即可使用。内容预览可见商品、订单、购物车、登录等Action类与BaseDaoImpl、DateUtils等基础工具类模块划分清晰便于理解商城业务逻辑与分层架构。目前已有499人学习下载作者自述为手打98分项目导师认可度高适合需要完整赛题方案、排错思路与目录结构参考的读者借鉴。1. 从一份能跑通的微信小程序购物商城源码说起很多人搜「基于微信小程序购物商城app设计带Java后端源代码文档说明数据库高分」真正想要的不是一份论文而是一套能跑起来、能改、能讲清楚数据怎么流动的完整工程。我带过几届毕业设计也帮朋友改过不少商城类小程序最常见的翻车场景是前端页面点得动但一登录就报 500或者商品列表永远空白——问题几乎都出在前后端分离的接口约定和数据库字段对不上。这套方案的核心就是微信小程序做展示层、JavaSpring Boot MyBatis-Plus做后端、MySQL 存数据三者通过 HTTP 接口串起来。它适合正在做课程设计、毕业设计或者想拿一个真实项目练手前后端分离项目实战的开发者。下面我按「先跑通、再讲透、最后避坑」的顺序把这条链路拆开讲。2. 微信小程序 Java 后端 MySQL 的工程骨架怎么搭2.1 三层结构各自负责什么先把职责划清楚后面写代码才不会乱。微信小程序层只做三件事渲染页面、收集用户输入、调用后端接口。它不直接碰数据库也不做业务计算。Java 后端层负责接收请求、校验参数、执行业务逻辑、返回 JSON。MySQL 层只负责存数据表结构一旦定下来后端代码要围着它转而不是反过来。我一般会按下面的目录组织后端工程这是最常见也最稳的做法mall-backend/ ├── src/main/java/com/mall/ │ ├── controller/ # 接口入口只做参数接收和返回 │ ├── service/ # 业务逻辑比如下单扣库存 │ ├── mapper/ # MyBatis-Plus 的 Mapper 接口 │ ├── entity/ # 和数据库表一一对应的实体类 │ └── config/ # 跨域、拦截器、全局异常 ├── src/main/resources/ │ ├── application.yml # 数据库连接、端口配置 │ └── mapper/ # 复杂 SQL 的 XML 文件 └── pom.xml # Maven 依赖小程序端则按页面拆mall-miniprogram/ ├── pages/ │ ├── index/ # 首页商品列表 │ ├── detail/ # 商品详情 │ ├── cart/ # 购物车 │ └── order/ # 订单确认 ├── utils/ │ └── request.js # 统一封装 wx.request └── app.js # 全局登录态管理这样拆的好处是后端接口路径可以按模块命名比如/api/goods/list、/api/cart/add前端一看就知道该调哪个。数据库表也按模块建user、goods、cart、orders、order_item。表名和实体类名保持一致MyBatis-Plus 能自动映射省掉大量手写 SQL。2.2 数据库建表五个核心表的最小字段集数据库增删改查是这套系统的地基。我见过太多人把表建得花里胡哨结果字段类型和 Java 实体对不上查出来全是 null。下面这五个表是我验证过能支撑一个完整购物流程的最小集合。-- 用户表 CREATE TABLE user ( id INT PRIMARY KEY AUTO_INCREMENT, openid VARCHAR(64) NOT NULL UNIQUE COMMENT 微信唯一标识, nickname VARCHAR(64), phone VARCHAR(20), create_time DATETIME DEFAULT CURRENT_TIMESTAMP ); -- 商品表 CREATE TABLE goods ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(128) NOT NULL, price DECIMAL(10,2) NOT NULL, stock INT NOT NULL DEFAULT 0, cover_url VARCHAR(255), detail TEXT ); -- 购物车表 CREATE TABLE cart ( id INT PRIMARY KEY AUTO_INCREMENT, user_id INT NOT NULL, goods_id INT NOT NULL, count INT NOT NULL DEFAULT 1, UNIQUE KEY uk_user_goods (user_id,goods_id) ); -- 订单表 CREATE TABLE orders ( id INT PRIMARY KEY AUTO_INCREMENT, user_id INT NOT NULL, total_price DECIMAL(10,2) NOT NULL, status TINYINT DEFAULT 0 COMMENT 0待付款 1已付款 2已发货, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ); -- 订单明细表 CREATE TABLE order_item ( id INT PRIMARY KEY AUTO_INCREMENT, order_id INT NOT NULL, goods_id INT NOT NULL, count INT NOT NULL, price DECIMAL(10,2) NOT NULL );这里有几个参数要特别注意。price用DECIMAL(10,2)而不是FLOAT因为浮点数算钱会出现0.10.20.30000000000000004这种玄学问题。cart表加了user_id goods_id的唯一索引防止同一商品重复插入后端用INSERT ... ON DUPLICATE KEY UPDATE count count 1就能实现「加购」逻辑。orders和order_item拆开是因为一个订单可能包含多个商品拆开后查询和统计都方便。2.3 后端接口用 MyBatis-Plus 写商品列表和加购后端我一般用 Spring Boot 2.7 MyBatis-Plus 3.5这是目前最稳的组合。先看application.yml里数据库和端口的配置server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/mall?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: your_password driver-class-name: com.mysql.cj.jdbc.Driver mybatis-plus: configuration: map-underscore-to-camel-case: true global-config: db-config: id-type: automap-underscore-to-camel-case: true这个配置很关键它让数据库的cover_url自动映射到 Java 的coverUrl不用每个字段都写TableField。serverTimezone必须设成Asia/Shanghai否则插入的时间会差 8 小时这个坑我踩过不止一次。商品列表接口的 Controller 长这样RestController RequestMapping(/api/goods) CrossOrigin // 开发阶段允许跨域上线后改成具体域名 public class GoodsController { Autowired private GoodsService goodsService; GetMapping(/list) public Result list(RequestParam(defaultValue 1) Integer page, RequestParam(defaultValue 10) Integer size) { PageGoods p new Page(page, size); return Result.ok(goodsService.page(p)); } GetMapping(/detail/{id}) public Result detail(PathVariable Integer id) { return Result.ok(goodsService.getById(id)); } }Result是我自己封装的统一返回体包含code、msg、data三个字段。前端request.js里统一判断code 200才取data否则弹提示。这样后端改返回结构时前端只改一个地方。加购接口要处理「已存在则数量加一」的逻辑PostMapping(/cart/add) public Result addCart(RequestBody CartDTO dto) { // dto 包含 userId 和 goodsId Cart exist cartService.getOne(new LambdaQueryWrapperCart() .eq(Cart::getUserId, dto.getUserId()) .eq(Cart::getGoodsId, dto.getGoodsId())); if (exist ! null) { exist.setCount(exist.getCount() 1); cartService.updateById(exist); } else { Cart cart new Cart(); cart.setUserId(dto.getUserId()); cart.setGoodsId(dto.getGoodsId()); cart.setCount(1); cartService.save(cart); } return Result.ok(); }这段代码先查再判断逻辑清晰但并发下可能重复插入。更稳的做法是直接依赖数据库唯一索引用saveOrUpdate配合自定义 SQL。不过对于毕业设计级别的并发量先查再写完全够用重点是让读者理解「唯一索引 业务判断」的双保险思路。2.4 小程序端封装 request 和登录态小程序端最容易乱的地方是每个页面都写一遍wx.request。我一般会在utils/request.js里统一封装const BASE_URL http://localhost:8080; 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 }, success(res) { if (res.data.code 200) { resolve(res.data.data); } else { wx.showToast({ title: res.data.msg || 请求失败, icon: none }); reject(res.data); } }, fail(err) { wx.showToast({ title: 网络异常, icon: none }); reject(err); } }); }); } module.exports { request };页面里调用就变成request({ url: /api/goods/list })清爽很多。登录态我放在app.js的globalData里用户授权后拿到openid存进去后续请求带上userId。这里要注意微信小程序登录获取手机号需要用户主动点击按钮触发不能自动获取这是平台限制不是代码问题。3. 前后端联调接口约定、跨域和登录态怎么打通3.1 接口约定先定文档再写代码前后端分离项目实战里最大的时间浪费不是写代码而是「前端以为后端返回的是 A后端实际返回的是 B」。我的习惯是先用一张表把接口定死前后端各留一份。接口路径方法入参返回字段说明/api/goods/listGETpage, sizeid, name, price, coverUrl分页商品列表/api/goods/detail/{id}GETid商品全字段商品详情/api/cart/addPOSTuserId, goodsId无加购/api/cart/listGETuserIdgoodsId, name, price, count购物车列表/api/order/createPOSTuserId, cartIdsorderId创建订单/api/user/loginPOSTcodeopenid, userId登录换 openid这张表定下来后前端可以先用 mock 数据把页面跑通后端按表实现接口最后联调时基本一次过。字段名统一用驼峰数据库用下划线靠 MyBatis-Plus 的自动映射衔接不要手动在 SQL 里写AS别名容易漏。3.2 跨域问题后端跨域配置的两种写法本地开发时小程序开发者工具请求localhost:8080会遇到跨域。常见做法有两种。第一种是在 Controller 上加CrossOrigin简单但只对单个类生效。第二种是写一个全局配置类Configuration public class CorsConfig implements WebMvcConfigurer { Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping(/**) .allowedOriginPatterns(*) .allowedMethods(GET, POST, PUT, DELETE) .allowCredentials(true) .maxAge(3600); } }allowedOriginPatterns(*)比allowedOrigins(*)更安全因为后者在allowCredentials(true)时会报错。上线后要把*换成小程序的合法域名否则会有安全风险。另外微信小程序正式环境要求所有请求走 HTTPS本地开发可以在开发者工具里勾选「不校验合法域名」但上线前必须配好证书。3.3 登录态openid 换 userId 的完整链路微信小程序的登录流程是前端调wx.login()拿到code把code发给后端后端用codeappidsecret调微信接口换openid再根据openid查或建用户返回userId给前端。后端代码大致这样PostMapping(/api/user/login) public Result login(RequestBody LoginDTO dto) { String url https://api.weixin.qq.com/sns/jscode2session ?appid appid secret secret js_code dto.getCode() grant_typeauthorization_code; String resp restTemplate.getForObject(url, String.class); JSONObject json JSON.parseObject(resp); String openid json.getString(openid); User user userService.getOne(new LambdaQueryWrapperUser() .eq(User::getOpenid, openid)); if (user null) { user new User(); user.setOpenid(openid); userService.save(user); } return Result.ok(user.getId()); }appid和secret放在application.yml里不要硬编码在 Java 文件里。restTemplate需要先在配置类里注册成 Bean。这段代码的坑在于jscode2session接口返回的openid可能为空如果code过期或appid不对会返回errcode。所以拿到响应后要先判断json.containsKey(openid)否则后面会空指针。4. 避坑与排查商城小程序最容易翻车的五个地方4.1 商品列表空白控制台报 401现象小程序首页请求/api/goods/list返回 401页面一直转圈。原因后端加了登录拦截器但商品列表接口本应放行拦截器配置里漏了/api/goods/**。解决在拦截器注册时用excludePathPatterns把公开接口排除掉比如/api/goods/**、/api/user/login。改完后重启后端小程序清缓存再试。4.2 加购后购物车数量不对总是 1现象同一商品点两次加购购物车显示数量还是 1。原因cart表的唯一索引生效了但后端用的是save而不是saveOrUpdate第二次插入直接抛异常被全局异常捕获前端只看到失败提示。解决把加购逻辑改成先查再更新或者用INSERT ... ON DUPLICATE KEY UPDATE的自定义 SQL。我一般用前者逻辑直观方便调试。4.3 订单金额算出来有小数误差现象购物车总价19.9 29.9算出来是49.79999999999999。原因Java 里用double做金额运算。解决所有金额字段用BigDecimal并且用new BigDecimal(19.9)而不是BigDecimal.valueOf(19.9)。数据库用DECIMAL(10,2)前端展示时用toFixed(2)。这个坑血泪经验改起来不难但一开始不注意后面到处补。4.4 小程序真机调试请求失败开发者工具正常现象开发者工具里一切正常真机预览时所有接口都超时。原因真机不能访问localhost必须用局域网 IP 或者公网域名。解决把BASE_URL改成电脑的局域网 IP比如http://192.168.1.100:8080确保手机和电脑在同一 WiFi 下。如果还不行检查电脑防火墙是否拦了 8080 端口。上线时换成 HTTPS 域名。4.5 数据库中文乱码商品名显示问号现象插入中文商品名后数据库里显示???。原因建表时没指定字符集或者连接 URL 没加characterEncodingutf8。解决建表语句末尾加DEFAULT CHARSETutf8mb4连接 URL 加useUnicodetruecharacterEncodingutf8。utf8mb4比utf8更全能存 emoji商品名里带表情也不会挂。5. 让这套商城源码真正拿得出手的两个进阶技巧第一个技巧是给接口加统一日志和耗时统计。很多人交上去的代码功能能跑但一出问题就抓瞎因为没有日志。我一般会写一个AOP切面拦截所有controller包下的方法打印请求路径、入参、返回值和耗时。这样联调时前端说「接口报错了」你直接看后端控制台就知道是哪个参数传错了不用来回猜。切面代码不长但能省下大量排查时间。Aspect Component public class LogAspect { private static final Logger log LoggerFactory.getLogger(LogAspect.class); Around(execution(* com.mall.controller..*(..))) public Object logAround(ProceedingJoinPoint pjp) throws Throwable { long start System.currentTimeMillis(); String method pjp.getSignature().toShortString(); Object[] args pjp.getArgs(); log.info(请求: {} 入参: {}, method, Arrays.toString(args)); Object result pjp.proceed(); log.info(返回: {} 耗时: {}ms, result, System.currentTimeMillis() - start); return result; } }第二个技巧是给数据库加一个「软删除」字段而不是直接DELETE。商品下架、订单取消都用UPDATE status -1代替物理删除。这样数据可追溯答辩时老师问「用户误删了怎么办」你可以说「有后悔药改状态就能恢复」。实体类上加TableLogic注解MyBatis-Plus 会自动在查询时过滤掉已删除数据不用每个 SQL 都写WHERE status ! -1。TableLogic private Integer deleted;对应的数据库字段是deleted TINYINT DEFAULT 0。这个改动很小但能让整个系统的数据管理上一个台阶。我自己的习惯是任何带「删除」功能的表建表时先把deleted字段加上后面就不用改表结构了。希望帮到你。本文还有配套的精品资源点击获取