每到毕业季,“家用电器商城”这种基于Spring Boot加微信小程序的毕设选题都会迎来一波高峰。这不难理解——后端用Spring Boot,前端用微信小程序,业务上又有完整的“用户浏览商品、加购物车、下单、支付、后台发货”电商闭环,规模不大不小,刚好卡在毕业设计最舒服的复杂度区间。不少同学手上已经拿到了包含源码、LW(论文文档)、部署说明、演示视频的完整交付包,但真正打开工程后反而更慌:几十个后端类、十几个小程序页面、一大堆配置,完全不知道从哪看起,更怕答辩时被老师问住。
这篇文章就从一个“拿到全套资料后该怎么消化”的角度,把这种家用电器商城系统从头到尾拆一遍。我不打算只贴代码,而是把技术选型、数据库设计、核心流程、部署坑点、源码阅读顺序和答辩准备全部串起来讲。适合两类人看:一类是刚拿到完整项目包,需要真正弄懂系统才能顺利答辩的同学;另一类是准备自己从零开发同款毕设,想先知道整体架构和容易翻车位置的同学。
1. 家用电器商城这个选题,实际上考了哪些能力
1.1 表面是一套商城,内里是软件工程的基本功
先说选题本身。很多同学看到“商城系统”就觉得是烂大街的CRUD,但家用电器这个品类其实给了你很多可以讲深的空间。电器商品不是一本书、一件T恤,它有型号、能效等级、功率、尺寸、安装方式这些参数,用户在详情页看的不是简单一张图,而是一组规格参数表。所以商品模块不能只做简单的“标题+图片+价格”,商品详情字段、图片列表、参数信息这些都需要在数据库里体现。
再从系统角色看,整套系统天然分成两端:
- 用户端:小程序里浏览首页轮播图、按分类找商品、搜索、查看商品详情、加入购物车、填写收货地址、提交订单、查看订单状态、确认收货。
- 管理端:通常是一个后台管理页面,管理员维护商品分类、商品上下架、编辑商品信息、处理用户订单(发货/取消)、管理轮播图片、查看用户列表。
这两个端合在一起,锻炼的是需求分析能力。你要能在论文的“需求分析”章节里把用户端和管理端的功能模块说清楚,画用例图,不是只写“增删改查”四个字。
家用电器还有一个业务特点值得注意:高单价、低频购买、可能涉及后续的安装和售后。这导致系统里订单流程比一个卖零食的小商城更需要“状态”的概念——用户先提交订单但不一定马上付款,付完款后管理员要能看见已支付订单,接着发货,用户收到货之后确认收货,整个流程是强状态的。这个特性恰好适合做一张订单状态流转表,也是答辩时一个很自然的提问点。
1.2 别一上来就堆功能,先想清楚边界
我见过不少同学拿到这套题后脑子一热,想把“优惠券、积分签到、秒杀、团购、直播带货”全塞进系统里,结果前端页面写了一堆,后端接口对不上,最后连基本流程都跑不通。毕设最核心的评分标准是“系统完整、逻辑自洽、能演示、能讲清楚”,不是功能越花哨越好。
我建议把功能边界控制在下面的范围内:
- 核心必做:用户登录注册(微信授权登录)、商品分类、商品列表与详情、购物车、收货地址管理、订单创建与取消、后台商品管理、后台订单管理。
- 加分可做:搜索、商品收藏、订单模拟支付流程、发货/收货状态更新、商品销量统计。
- 不建议碰:秒杀、优惠券叠加、多级分销、高并发抢购。
这样做有个好处:每一块功能都可以在数据库里落到明确的表上,在前端有对应页面,在后端有对应接口。老师问起来,你能形成一条完整的“业务链路记忆”,而不是散落的功能碎片。
2. 技术栈不是越新越好,稳定可复现才是王道
2.1 为什么普遍是“Spring Boot + 微信小程序原生”的组合
先说后端。前些年毕设里经常看到SSM(Spring + SpringMVC + MyBatis)项目,配置文件的繁琐程度谁用谁知道。Spring Boot把大部分配置做成自动装配,内置Tomcat,打一个jar包就能跑,出问题也容易定位。更重要的是,Spring Boot本身是现在企业里真正在用的主流框架,以后面试聊到自动配置原理、Starter机制、约定优于配置,都有话可接。
前端为什么选微信小程序原生,而不是uni-app或Vue做H5套壳?核心原因是可控性和可解释性。微信小程序用的就是“WXML + WXSS + JS”这套微信自己定义的语法,在微信开发者工具里打开就是完整项目。你用uni-app确实也能发布到小程序平台,但中间隔了一层编译,一旦出现样式错乱、组件兼容性问题,排查成本比原生高出一截。毕设答辩现场,老师很可能会打开你工程目录直接看页面文件,原生小程序的结构一眼就能看懂。
2.2 版本搭配:Spring Boot 2.7.x 是毕设最稳的选择
这里要展开一个经验:Spring Boot版本不是越高越好。很多人新建项目时直接选最新版,比如Spring Boot 3.x,结果代码里引用的教程全是2.x的写法,javax换成jakarta包名,很多依赖也变了,最常见的报错是“程序包javax.servlet不存在”,这就是版本不匹配导致的。
个人建议毕设后端固定用这套组合:
| 组件 | 建议版本/方案 | 原因 |
|---|---|---|
| JDK | 1.8 | 兼容性最广,几乎所有教程都能直接用 |
| Spring Boot | 2.7.18 | 2.x最后一个稳定版本,资料多,坑基本被踩完 |
| 数据库 | MySQL 5.7 或 8.0 | 5.7足够,8.0默认字符集处理更友好 |
| ORM | MyBatis-Plus 3.5.x | 单表CRUD不用写SQL,分页器好用 |
| 小程序端 | 微信原生框架 | 不依赖额外编译链,演示直观 |
如果你拿到的源码是Spring Boot 2.x,就不要新建3.x工程把代码拷贝过去,除非你已经对包路径迁移很有把握。说白了,毕设的目标是稳定跑完整个流程,不是替Spring团队测试新版本。同样道理,如果工程里用到了Redis、RabbitMQ这些中间件,要先确认部署环境里已经装好,不然Spring Boot启动时就会因为连不上中间件直接起不来。
2.3 工程结构要能被一眼读懂
拿到任何一套源码,先看它的包结构,包结构混乱的项目后续一定很难维护。一个规范的后端工程应该长这样:
com.example.mall ├── controller // 接收前端请求 ├── service // 业务逻辑层 ├── mapper // 数据访问层 ├── entity // 数据库表对应的实体类 ├── dto // 接收参数的载体 ├── vo // 返回给前端的展示对象 ├── config // 跨域、拦截器、WebMvc配置 ├── common // 统一返回值、异常处理、工具类 └── utils // JWT、加密等工具小程序端则通常按页面和通用模块分:
miniprogram ├── app.js // 全局启动逻辑 ├── app.json // 页面路由和窗口配置 ├── app.wxss // 全局样式 ├── utils/request.js // 封装wx.request ├── pages/ │ ├── index/ // 首页 │ ├── category/ // 分类 │ ├── cart/ // 购物车 │ ├── user/ // 个人中心 │ ├── goodsDetail/ // 商品详情 │ ├── order/ // 订单列表 │ ├── checkout/ // 确认订单/结算 │ └── address/ // 地址管理 └── components/ // 公共组件这种结构的好处是:当你需要改某个功能时,能直接从“页面文件 → request调用 → Controller → Service → Mapper”一路追踪下去,不需要全工程搜索。
3. 数据模型和接口约定:先想清楚表关系再写业务
3.1 核心表结构不是越多越好,但要保证关系完整
后端代码可以慢慢写,但数据库表结构往往决定了业务的扩展空间。家用电器商城系统通常围绕下面几张核心表来设计:
| 表名 | 关键字段 | 作用 |
|---|---|---|
| user | id, openid, nickname, avatar, phone, create_time | 保存小程序用户信息 |
| address | id, user_id, receiver_name, receiver_phone, province, city, detail | 收货地址 |
| category | id, name, sort, icon | 商品分类 |
| goods | id, category_id, name, subtitle, main_image, price, stock, sales, status, detail | 商品基本信息 |
| goods_spec | id, goods_id, spec_name, spec_value, price, stock | 电器规格(可选) |
| cart_item | id, user_id, goods_id, goods_name, goods_image, price, count, checked | 购物车条目 |
| order_master | id, order_no, user_id, total_amount, pay_amount, status, address_snapshot, pay_time, ship_time | 订单主表 |
| order_item | id, order_id, goods_id, goods_name, goods_image, price, count | 订单快照明细 |
| admin_user | id, username, password, role | 后台登录账号 |
这里要特别提醒三个设计细节,它们经常成为答辩时的加分点。
第一个是金额字段。价格和金额一律用DECIMAL(10,2)而不能用double。浮点数在二进制里无法精确表达,一旦涉及累加或优惠计算,可能出现0.1 + 0.2不等于0.3这种尴尬问题。
第二个是订单明细要保存“商品快照”。用户下单那一刻的商品名称、图片、单价要原封不动存进order_item表,之后就算后台修改了商品价格,用户的订单历史也不受影响。这个细节体现的是对业务的真实理解。
第三个是软删除。商品下架不是从数据库里物理删除,而是通过status字段控制,比如0表示下架、1表示在售。这样做既能保留历史订单中的商品关联,也符合电商后台“上架/下架”的真实操作。
3.2 统一返回体和分页接口怎么设计
前后端分离的项目,接口返回值一定要统一格式,不然前端每请求一个接口都要写一套错误处理。我习惯的返回体是这样:
{ "code": 200, "msg": "success", "data": {} }前端封装request时只看code,等于200就正常返回data,不等于200就弹msg并做对应处理,逻辑会非常清爽。
商品列表接口尤其常用分页查询,用MyBatis-Plus的写法大致是:
@RestController @RequestMapping("/api/goods") public class GoodsController { @Autowired private GoodsService goodsService; @GetMapping("/list") public Result list(@RequestParam(defaultValue = "1") Integer page, @RequestParam(defaultValue = "10") Integer size, @RequestParam(required = false) Integer categoryId, @RequestParam(required = false) String keyword) { Page<Goods> pageParam = new Page<>(page, size); LambdaQueryWrapper<Goods> wrapper = new LambdaQueryWrapper<>(); wrapper.eq(Goods::getStatus, 1); if (categoryId != null) { wrapper.eq(Goods::getCategoryId, categoryId); } wrapper.like(StringUtils.hasText(keyword), Goods::getName, keyword); wrapper.orderByDesc(Goods::getSales); Page<Goods> result = goodsService.page(pageParam, wrapper); return Result.success(result); } }这段代码包含了一个很实用的设计:默认只查status = 1的商品,也就是在前台永远看不到下架商品,但商品数据本身并没有删除。分类过滤用categoryId,搜索用like模糊匹配,排序用sales字段让销量高的排前面。小程序端拿到数据后,通过data.records渲染列表,通过data.total判断还有没有下一页。
小程序端请求封装同样很关键。原生小程序里直接调用wx.request的话,每个页面都要重复写回调逻辑,我建议统一封装到utils/request.js里:
const BASE_URL = 'http://localhost:8080' function request(url, method = 'GET', data = {}) { return new Promise((resolve, reject) => { wx.request({ url: BASE_URL + url, method: method, data: data, header: { 'Content-Type': 'application/json', 'token': 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) } }) }) } module.exports = { request, BASE_URL }这样封装之后,页面里调用网络接口就非常简洁。把token统一放入请求头,也是对登录状态的全局管理。
4. 核心业务链路:从微信登录到订单完成的前因后果
4.1 用户登录:为什么只要拿code换openid,而不是让用户填手机号
很多第一次做小程序的人会潜意识地做一个账号密码注册页,这在真实场景里是可笑的。微信小程序天然就是“每个微信号对应一个身份”,登录的正确链路是:
- 小程序端调用
wx.login()拿到一个临时凭证code。 - 把
code通过自己的后端接口发送到服务器。 - Spring Boot后端拿着
code去请求微信的jscode2session接口,换取该用户在小程序下的唯一标识openid。 - 后端查询数据库是否存在该
openid,不存在就自动注册一个新用户,存在就直接登录。 - 后端生成一个token(可以用UUID,也可以用JWT)返回给小程序端。
- 小程序端把token存入
wx.setStorageSync,后续请求带上。
Controller里的核心逻辑大概长这样:
@PostMapping("/login") public Result login(@RequestBody Map<String, String> params) { String code = params.get("code"); // 调用微信接口,把code换成openid String openid = wxService.code2Session(code); if (openid == null) { return Result.error("微信登录失败"); } // 查用户,不存在则创建 User user = userService.findByOpenid(openid); if (user == null) { user = new User(); user.setOpenid(openid); user.setNickname("微信用户" + openid.substring(openid.length() - 6)); userService.save(user); } // 生成token String token = JwtUtil.createToken(user.getId(), user.getNickname()); return Result.success(token); }这个接口有一个很容易被忽略的细节:openid在数据库里必须加唯一索引。否则用户连续点两次登录,可能因为并发插入产生两条相同openid的记录,后面查数据就全乱了。加了唯一索引后,即使并发请求,数据库层面也能保证重复的openid只有一个用户。
你还需要一个拦截器,把需要登录的接口保护起来。拦截器里从请求头取出token,解析出用户ID,存入ThreadLocal,后面Service层随时可以拿到当前操作人。但要注意:像首页商品列表、商品详情这类接口不必强制登录,拦截器应该设计成“白名单放行”,不然用户还没进首页就被挡在登录外面。
4.2 购物车:放在数据库里的理由,以及下单时的库存校验
购物车表面很简单,但有些实现细节值得较真。有些同学图省事,把购物车数据直接存在小程序本地Storage里,不用请求后端。这样做的缺点是明显的:用户换一台手机登录,购物车就没有了;后台想看用户加购情况也看不到数据。既然已经把用户体系做起来了,我更建议购物车数据跟着用户走,放在数据库cart_item表里。
“加入购物车”接口设计上不是无脑插入一条新记录。用户把同一个商品加入两次购物车,前端合理的体验是数量累加,而不是出现两条一模一样的记录。所以Service里要先查一次:当前用户、当前商品、当前规格是否已在购物车中,存在就把count加1,不存在才新增。
下单时的库存校验是另一个高频答辩点。注意,购物车里的库存数据只是一个“参考值”,真正严格的扣减必须发生在“提交订单”这个动作里。下单接口要做的事,远不是把购物车数据读出来然后生成订单那么简单,而是一个必须加@Transactional的事务性操作:
- 校验用户登录态,取出用户id。
- 查出购物车中被勾选的条目,逐条核对商品当前库存是否充足。
- 扣减库存:用带条件的UPDATE语句,而不是先查后改。
- 创建订单主表和订单明细表,保存商品快照、收货地址快照、总金额。
- 清空购物车中已下单的条目。
- 返回订单号,小程序端跳转到支付页。
这六步中任何一步失败,整个事务都要回滚。尤其第三步的扣库存,最好的做法是写一条带条件的SQL:
UPDATE goods SET stock = stock - 1 WHERE id = ? AND stock >= 1然后用UPDATE的影响行数判断是否扣减成功。这比“先SELECT查库存,判断大于0后再UPDATE”要安全得多,因为多线程并发时,两条线程可能同时读到库存为1,然后同时执行UPDATE,最后库存变成负数。用带条件UPDATE,数据库层面就挡住了并发超卖。
4.3 订单状态机:让订单在不同角色手里正确地流转
订单模块是最容易在答辩时被问细节的部分。一个“订单”从用户提交到交易完成,通常会经历这些状态:
| 状态值 | 含义 | 触发动作 |
|---|---|---|
| 0 | 待支付 | 用户提交订单 |
| 1 | 待发货 | 用户完成支付 |
| 2 | 待收货 | 后台管理员发货 |
| 3 | 已完成 | 用户确认收货 |
| 4 | 已取消 | 用户取消或后台取消未付款订单 |
| 5 | 退款/售后 | 售后流程进入 |
状态机设计的核心意义是:任何角色对订单的操作都必须有明确的前置状态。比如用户不能对“已发货”状态的订单点击取消;管理员不能把“待退货”状态的订单标记为已完成。在下单接口里一鼓作气把状态处理清楚,后面按状态查订单列表就很简单。
如果是不接真实微信支付的情况(很多学生没有企业商户号),实现上可以在用户点击“立即支付”后,不真正调用微信支付,而是走一个“模拟支付成功”的流程,直接把订单状态从0改为1,同时记录支付时间和支付方式。这个方法在毕设里几乎已经是约定俗成的做法,关键是你在LW文档中把“真实支付需要申请微信支付商户号,当前采用模拟支付便于演示”这句话写清楚,答辩时就不会被认为是功能缺陷。
5. 部署与联调:最容易翻车的三个环节
5.1 本地联调的第一个拦路虎:域名校验和localhost地址
如果你直接在微信开发者工具里运行小程序,然后让小程序请求http://localhost:8080,默认会被微信拦下来,提示“不在以下request合法域名列表中”。这不是代码写错,而是微信开发者工具的安全机制。开发阶段最简单的处理方式是在微信开发者工具的“详情-本地设置”里勾选“不校验合法域名、web-view(业务域名)、TLS版本以及HTTPS证书”,这样本地请求http接口就不会被拦截。
这个勾选只对开发模拟器和预览模式有效,正式上线时必须用备案过的HTTPS域名,但毕设阶段完全够用。
5.2 真机预览时找不到后端:分清电脑地址和手机地址
很多同学在模拟器里跑得好好的,一用手机扫码预览就全部请求失败,控制台报网络错误。原因很常见:代码里写的是http://localhost:8080,在模拟器里 localhost 指向的是你电脑,手机上也用 localhost 时指向的是手机自己,手机当然连不上你电脑上的后端。
解决方法是把BASE_URL改成你电脑在局域网里的IP地址,比如http://192.168.1.5:8080,并且确保手机和电脑连同一个WiFi。真机预览时还要检查电脑防火墙,Spring Boot的8080端口如果被防火墙拦住,手机上同样是请求超时。用ipconfig(Windows)或ifconfig(macOS/Linux)查到本机IP,填到前端配置文件里,再用手机访问一次http://192.168.1.5:8080/api/goods/list看是否通,是最直接的排查方式。
5.3 后端服务启动失败:配置文件里藏着一半的坑
Spring Boot项目启动失败,多数问题都出在配置上。最常见的几种情况:
- MySQL没装或没启动,
application.yml里连接的数据库名和实际不一致。 serverTimezone没配置导致连接数据库报时区错误。- Redis等中间件地址不对,Spring Boot启动时尝试连接Redis失败。
- 端口被占用,
8080被占用时换个端口或杀掉占用进程。
还有一个非常典型的坑:MySQL驱动坐标版本不匹配。Spring Boot 2.7里的mysql-connector-java版本可能是8.x,如果本地装的是MySQL 5.7,需要在pom.xml里明确指定兼容的驱动版本。遇到这类报错时不要慌,先看堆栈第一行,然后把application.yml里的连接地址、用户名、密码、数据库名逐项核对一遍,80%的问题都能解决。
Nginx不是必须的,但如果部署到云服务器,我建议还是加一层。Nginx可以把/api/开头的请求反代到本机的8080端口,同时解决跨域,配置很简洁:
server { listen 80; server_name yourdomain.com; location /api/ { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } }这样小程序端请求/api/goods/list时,Nginx自动转发到Spring Boot的8080端口,跨域问题在后端用@CrossOrigin或统一配置跨域过滤器也能解决,但Nginx代理会让整个架构更接近实际项目。
6. 拿到完整源码包之后,怎么把别人的项目变成自己的
6.1 不要从第一个文件读到最后一个文件,先跑通再通读
完整交付包里通常包括源码、LW文档、部署说明和演示视频。很多人拿到源码后的第一反应是从pom.xml开始逐行读代码,这是效率最低的方法。正确做法是先按部署说明把项目跑起来,数据导入,前端连上后端,然后在微信开发者工具里完整操作一遍:登录、浏览商品、加购、下单、模拟支付、后台发货、用户确认收货。
只有把整个流程跑通了,你对系统的理解才有一个“骨架”。接下来再带着问题去读代码,效率会高很多。
6.2 阅读顺序建议:按一次请求的路径走
拿到代码后按这个顺序走一遍,基本就掌握了一整套项目的主干:
- 先看小程序端
pages/index/index.js,找到首页调用了哪个接口。 - 顺着
utils/request.js找到BASE_URL,知道请求发给了谁。 - 到后端工程里找到对应的
GoodsController,看@GetMapping里的路径。 - 进入
GoodsService看具体业务代码。 - 进入
GoodsMapper或查看XML里的SQL,理解数据是怎么查出来的。 - 最后对照数据库表,把字段确认一遍。
走完一次“首页商品列表”的链路,你就理解了前后端是如何协作的。再自己走一遍“下单”链路,从下单页面到Service,仔细看看事务、库存、订单快照这些细节怎么处理,你对整个系统的理解就已经超越了“能运行”的层次。
6.3 低成本高辨识度的改造点
很多人担心的一个问题是:同一个课题,别人也用了相似的完整源码,怎么避免答辩时“撞车”?我的建议不是大改功能,而是做几个低成本的差异化改造:
第一,把项目的品牌包装改成你自己的。小程序首页轮播图、商品图换成你自定义的图片;全局主题色在app.wxss里修改,比如把默认的蓝色换成更符合你设定品牌气质的颜色。视觉上先做出区分度。
第二,在后台或者数据库里增加一个有业务含义的字段。比如给商品加一个“品牌”字段,在商品列表页增加品牌筛选;或者记录商品浏览量,在首页增加一个“浏览最多”的排序区块。这种改动不需要重构数据库,只需要加一个字段、改一个查询接口、前端加一个选项,但老师看到的是你思考过“这个系统如何更好用”。
第三,给订单模块加一个简单的搜索或筛选条件。比如管理员按订单号搜索订单,用户订单列表按下单时间倒序排列。这个小功能很实用,在演示时也能顺手展示。
答辩时千万不要说自己“参考了某个现成项目”,要讲的应该是“我负责了哪些模块,为什么这样设计,遇到什么问题,最后怎么解决的”。你对系统细节理解得越深,被问住的风险就越低。比如老师问“你的订单金额怎么算的”“购物车数据存哪里”“下订单的时候库存会不会超卖”,如果你能答出来“金额用Decimal,购物车是跟着用户存数据库的,扣库存用的是条件UPDATE”,那这个答辩已经稳了一大半。
按我自己的经验,还有一个特别容易忽略但很影响演示效果的点:时间问题。微信登录的code有效期很短,真机演示前如果代码改动了导致需要重新编译,可能之前的登录态已经失效,重新登录一次就好,这本身没什么。但演示前一定要提前测试一遍,不要在答辩现场现点。演示视频录制时,也建议按“项目介绍→小程序用户操作→后台管理操作→代码核心讲解”的顺序录,视频里最好能看到关键代码。这些细节不复杂,但做好了明显能提升整套交付物的完成度。
家用电器商城听起来是满大街的选题,但它把电商系统最主干的部分都包含了:用户、商品、购物车、订单、后台管理。把这个项目从头到尾吃透,哪怕将来不做电商方向,你对Spring Boot的分层架构、小程序的前后端交互、数据库的事务与状态管理也会有非常扎实的认识。这才是这套毕设源码真正值钱的地方。