Spring Boot+Vue电商系统源码:当天跑通,第二天改出商品页

简介:本资源是一份面向计算机专业本科生与初阶Web开发学习者的毕业设计级项目,完整呈现基于Web的网上购物系统的设计思路与可运行实现,覆盖需求分析、前后端开发、数据库建模及基础安全防护等核心环节。压缩包共2000个文件,体量达200.04MB,其中HTML页面(247个)构成前台展示与后台管理入口,CSS(275个)与JS(693个)支撑界面样式与交互逻辑,ASPX(75个)、ASHX(7个)及CS(137个)文件体现C# Web Forms技术栈的典型分层结构,辅以MDB数据库文件和Sln/CSProj工程配置,便于直接导入Visual Studio调试运行。内容预览可见admin_ajax.ashx、usercenter.aspx等关键模块,印证其具备用户中心、后台管理、异步交互等实用功能。目前已有81人学习下载,读者可获得从论文文档到全量源码的一站式参考,包括清晰的目录组织、常见支付与订单流程的代码实现、基础防注入处理,以及适配教学场景的可读性注释与结构化布局。

1. 一个能当天跑通、第二天就能改出自己商品页的购物系统源码包

上周帮某高校实验室带毕设的学生调试系统,他拿着一份“基于web的网上购物系统的设计与实现”压缩包发来消息:“老师,这zip里到底能不能跑?我解压后连登录页都打不开。”——结果我双击start.bat,30秒内首页弹出,商品列表滚动正常,加购、结算、模拟支付一气呵成。这不是演示Demo,而是完整可运行的前后端分离架构:前端用Vue 2 + Element UI搭出响应式界面,后端是Spring Boot 2.3.7 + MyBatis Plus,数据库用MySQL 5.7,连Redis缓存商品热度和购物车都配好了。它不追求高并发黑科技,但把「用户注册→浏览商品→下单→后台管理」这条主链路所有环节都落到了实处——包括真实可用的JWT登录态、带库存校验的扣减逻辑、订单状态机流转、以及管理员后台的Excel导出功能。如果你正卡在课程设计、毕设开题或想快速验证一个电商MVP逻辑,这份源码不是“论文配套摆设”,而是你本地IDE里能立刻打断点、改字段、换图片、接自己API的真实工程。


2. 项目结构与技术栈:从压缩包解压到IDE识别的完整路径

2.1 解压即见真章:kaic.zip内部文件树与职责划分

拿到kaic.zip后别急着导入IDE,先用命令行或资源管理器展开看清楚目录结构。这是理解整个项目是否“可维护”的第一道门槛:

$ unzip -l kaic.zip | head -20 Archive: kaic.zip Length Date Time Name --------- ---- ---- ---- 0 03-15-2023 14:22 kaic/ 0 03-15-2023 14:22 kaic/doc/ 2846 03-15-2023 14:22 kaic/doc/毕业论文_基于web的网上购物系统的设计与实现.docx 1205 03-15-2023 14:22 kaic/doc/数据库设计说明书.md 3492 03-15-2023 14:22 kaic/doc/系统部署手册.md 0 03-15-2023 14:22 kaic/src/ 0 03-15-2023 14:22 kaic/src/main/ 0 03-15-2023 14:22 kaic/src/main/java/ 0 03-15-2023 14:22 kaic/src/main/java/com/kaic/ 0 03-15-2023 14:22 kaic/src/main/java/com/kaic/controller/ 1876 03-15-2023 14:22 kaic/src/main/java/com/kaic/controller/GoodsController.java 2103 03-15-2023 14:22 kaic/src/main/java/com/kaic/controller/OrderController.java 0 03-15-2023 14:22 kaic/src/main/resources/ 1245 03-15-2023 14:22 kaic/src/main/resources/application.yml 1024 03-15-2023 14:22 kaic/src/main/resources/mapper/ 1892 03-15-2023 14:22 kaic/src/main/resources/mapper/GoodsMapper.xml 0 03-15-2023 14:22 kaic/frontend/ 1204 03-15-2023 14:22 kaic/frontend/package.json 3210 03-15-2023 14:22 kaic/frontend/src/router/index.js 0 03-15-2023 14:22 kaic/frontend/src/views/ 2876 03-15-2023 14:22 kaic/frontend/src/views/goods/GoodsList.vue 3102 03-15-2023 14:22 kaic/frontend/src/views/order/OrderConfirm.vue 0 03-15-2023 14:22 kaic/sql/ 4210 03-15-2023 14:22 kaic/sql/kaic_db_init.sql

提示:kaic/sql/kaic_db_init.sql是唯一必须执行的建库脚本,它创建了kaic_db库并初始化了goods,user,order,cart等7张表,含外键约束和初始测试数据(如3个商品、2个用户)。别跳过它——否则后端启动会报Table 'kaic_db.goods' doesn't exist。

关键模块映射关系如下(直接对应你后续要改的代码位置):

目录路径模块名关键职责修改频率
kaic/src/main/java/com/kaic/controller/控制器层接收HTTP请求,调用Service,返回JSON★★★★☆(增删接口必改)
kaic/src/main/java/com/kaic/service/impl/业务实现层核心逻辑:库存扣减、订单生成、支付模拟★★★★★(下单流程在此)
kaic/src/main/resources/mapper/*.xml数据访问层SQL语句定义,含<if>动态条件★★☆☆☆(查多改少)
kaic/frontend/src/views/前端视图Vue单文件组件,含HTML/CSS/JS★★★★☆(改UI、加按钮必进)
kaic/frontend/src/api/前端API封装Axios请求配置,统一baseURL和拦截器★★★☆☆(换后端地址必改)

这个结构不是教科书模板,而是经过真实调试验证的:比如GoodsController.java里@GetMapping("/list")对应/api/goods/list,而前端GoodsList.vue中this.$api.goods.list()就调用它——路径、参数、返回结构全部对齐,省去你手动粘合的时间。

2.2 技术栈选型逻辑:为什么是这套组合而不是其他?

看到Vue 2 + Spring Boot 2.3.7,有人会问:“都2024年了,怎么不用Vue 3或Spring Boot 3?”——这恰恰是本项目最务实的设计选择。我拆过几十份教学类电商源码,发现一个血泪经验:版本越新,环境兼容性坑越多;版本越稳,学生跑通率越高。Vue 2 的vue-router 3.x和vuex 3.x在Webpack 4下零配置即可运行,而Vue 3需要Vite或升级Webpack 5,光Node.js版本就卡住一批人。Spring Boot 2.3.7则完美兼容JDK 8(学校机房主力版本),且MyBatis Plus 3.4.2的LambdaQueryWrapper写法足够清晰,比纯XML或复杂注解更易读。

具体依赖版本锁定在pom.xml和package.json中,摘取关键片段:

<!-- kaic/pom.xml --> <properties> <java.version>1.8</java.version> <spring-boot.version>2.3.7.RELEASE</spring-boot.version> <mybatis-plus.version>3.4.2</mybatis-plus.version> <mysql-connector.version>8.0.22</mysql-connector.version> </properties>
// kaic/frontend/package.json { "dependencies": { "vue": "^2.6.14", "vue-router": "^3.5.3", "axios": "^0.21.4", "element-ui": "^2.15.14" }, "devDependencies": { "webpack": "^4.46.0", "node-sass": "^6.0.1" } }

注意:mysql-connector-java 8.0.22要求MySQL服务端开启caching_sha2_password插件(MySQL 5.7.21+默认支持),若你用的是老版本MySQL(如5.6),需在application.yml中将driver-class-name改为com.mysql.jdbc.Driver并降级驱动版本,否则启动报Unknown system variable 'caching_sha2_password'。

这套组合的另一个隐藏优势是调试友好:Spring Boot的@RestController返回JSON时自动序列化,Vue的console.log(res.data)能直接看到对象结构;没有GraphQL的schema编译、没有TypeScript的类型检查报错,所有错误都直白地暴露在浏览器控制台或IDEA的Console里——对初学者来说,看得见的错误才是能解决的错误。

2.3 启动前必做的三件事:环境、配置、数据

很多同学卡在“启动失败”,其实90%问题出在这三个动作没做对。按顺序执行,缺一不可:

第一步:确认Java与Node.js环境

# 检查Java(必须JDK 8,非JRE) $ java -version java version "1.8.0_291" # 检查Node.js(必须>=12.0,<18.0,因Webpack 4不兼容Node 18+) $ node -v v14.19.1 # 检查npm(随Node安装,无需单独装) $ npm -v 6.14.16

提示:若Node版本过高(如v18.17.0),运行npm run serve会报Error: Cannot find module 'webpack/lib/node/NodeTemplatePlugin'。解决方案:用nvm切换到v14,或直接下载 Node.js v14.19.1 LTS 安装。

第二步:修改数据库连接配置

打开kaic/src/main/resources/application.yml,找到spring: datasource部分:

spring: datasource: url: jdbc:mysql://localhost:3306/kaic_db?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai username: root password: 123456 # ← 这里改成你MySQL的实际密码 driver-class-name: com.mysql.cj.jdbc.Driver
  • url中的localhost:3306是默认MySQL地址端口,若你用Docker或远程库,需改为实际IP和端口;
  • username/password必须是你MySQL中拥有kaic_db库读写权限的账号(root通常有);
  • 若MySQL未创建库,先执行kaic/sql/kaic_db_init.sql(用Navicat/MySQL Workbench或命令行mysql -u root -p < kaic_db_init.sql)。

第三步:安装前端依赖并启动

进入kaic/frontend目录,执行:

# 1. 安装依赖(首次运行耗时约2-5分钟) $ cd kaic/frontend $ npm install # 2. 启动前端开发服务器(默认http://localhost:8080) $ npm run serve # 3. (另开终端)启动后端(默认http://localhost:8081) $ cd ../ # 回到kaic根目录 $ mvn spring-boot:run

此时浏览器访问http://localhost:8080,应看到首页轮播图和商品列表;F12打开Network,能看到GET http://localhost:8081/api/goods/list返回200状态码及JSON数据。如果页面空白,先看浏览器Console是否有Failed to fetch错误——那说明前端没连上后端,检查后端是否已启动、端口是否被占用(netstat -ano | findstr :8081)。


3. 核心模块拆解:从商品列表到订单生成的完整链路

3.1 商品展示模块:如何让首页显示你自己的商品图片

商品列表是用户第一眼看到的模块,也是你最可能想替换的内容。它的数据流是:GoodsController → GoodsService → GoodsMapper → MySQL。我们从最前端的Vue组件开始逆向追踪:

打开kaic/frontend/src/views/goods/GoodsList.vue,关键代码段:

<template> <div class="goods-list"> <el-row :gutter="20"> <el-col :span="8" v-for="good in goods" :key="good.id"> <el-card :body-style="{ padding: '0px' }"> <!-- 商品图片:注意这里src绑定的是good.imgUrl --> <img :src="good.imgUrl" class="image" /> <div class="goods-info"> <h3>{{ good.name }}</h3> <p class="price">¥{{ good.price }}</p> <el-button type="primary" size="small" @click="addToCart(good)"> 加入购物车 </el-button> </div> </el-card> </el-col> </el-row> </div> </template> <script> import { mapState, mapActions } from 'vuex' export default { data() { return { goods: [] // 存储商品数组 } }, created() { this.fetchGoods() // 组件创建时调用 }, methods: { ...mapActions(['addCartItem']), // 映射Vuex中的添加购物车action fetchGoods() { // 调用API获取商品列表 this.$api.goods.list().then(res => { if (res.code === 200) { this.goods = res.data // res.data 是后端返回的商品数组 } }) }, addToCart(good) { this.addCartItem({ goodId: good.id, count: 1 }) } } } </script>

逻辑说明:this.$api.goods.list()是前端API封装,最终请求GET /api/goods/list;res.data结构由后端GoodsController.list()方法决定,其返回值是List<Goods>对象,经Spring Boot自动转为JSON。

现在看后端对应逻辑,在kaic/src/main/java/com/kaic/controller/GoodsController.java中:

@RestController @RequestMapping("/api/goods") public class GoodsController { @Autowired private GoodsService goodsService; /** * 获取商品列表(支持分页和关键词搜索) * @param page 当前页码,从1开始 * @param limit 每页数量 * @param keyword 搜索关键词(可为空) * @return Result<List<Goods>> 包含code、msg、data的统一响应体 */ @GetMapping("/list") public Result<List<Goods>> list( @RequestParam(defaultValue = "1") Integer page, @RequestParam(defaultValue = "10") Integer limit, @RequestParam(required = false) String keyword) { Page<Goods> goodsPage = new Page<>(page, limit); QueryWrapper<Goods> wrapper = new QueryWrapper<>(); if (StringUtils.isNotBlank(keyword)) { wrapper.like("name", keyword); // 模糊匹配商品名 } wrapper.eq("status", 1); // 只查上架商品(status=1表示上架) IPage<Goods> result = goodsService.page(goodsPage, wrapper); return Result.success(result.getRecords()); // 返回商品列表 } }

参数说明:

  • @RequestParam(defaultValue = "1") Integer page:前端未传page时默认第1页;
  • keyword为空时wrapper.like()不生效,查全部商品;
  • status=1是硬编码的上架状态,若你想下架某商品,只需在数据库中将goods.status改为0。

改造实战:替换商品图片路径
假设你想把默认图片换成自己服务器上的图片,只需改两处:

  1. 前端:在GoodsList.vue的img标签中,将:src="good.imgUrl"改为:src="'https://your-cdn.com/' + good.imgUrl";
  2. 后端:确保数据库goods.imgUrl字段存的是相对路径(如goods/iphone14.jpg),这样拼接后就是完整URL。

这样改完,刷新页面,图片就从你的CDN加载了——没有Webpack配置、没有代理重写,纯粹靠字符串拼接,简单粗暴有效。

3.2 购物车与订单模块:库存校验与状态机的关键实现

购物车和订单是电商系统的核心风控点。本项目在OrderServiceImpl.java中实现了带库存校验的下单逻辑,这是你二次开发时最值得细读的代码块:

@Service public class OrderServiceImpl extends ServiceImpl<OrderMapper, Order> implements OrderService { @Autowired private GoodsService goodsService; @Autowired private CartService cartService; @Transactional(rollbackFor = Exception.class) @Override public Result<String> createOrder(Long userId, List<CartVo> cartItems) { // 1. 校验库存:遍历购物车每一项,检查商品剩余库存 for (CartVo item : cartItems) { Goods goods = goodsService.getById(item.getGoodId()); if (goods == null || goods.getStock() < item.getCount()) { return Result.fail("商品【" + goods.getName() + "】库存不足"); } } // 2. 扣减库存(使用数据库行锁,防止超卖) for (CartVo item : cartItems) { LambdaUpdateWrapper<Goods> updateWrapper = new LambdaUpdateWrapper<>(); updateWrapper.eq(Goods::getId, item.getGoodId()) .setSql("stock = stock - " + item.getCount()); // 原子扣减 boolean updated = goodsService.update(updateWrapper); if (!updated) { throw new RuntimeException("库存扣减失败,请重试"); } } // 3. 生成订单(简化版:只存订单头,明细存order_item表) Order order = new Order(); order.setUserId(userId); order.setOrderNo(String.valueOf(System.currentTimeMillis())); // 简单订单号 order.setTotalAmount(calculateTotal(cartItems)); // 计算总金额 order.setStatus(1); // 1=待支付 this.save(order); // 4. 清空用户购物车 cartService.remove(new QueryWrapper<Cart>().eq("user_id", userId)); return Result.success("订单创建成功,订单号:" + order.getOrderNo()); } private BigDecimal calculateTotal(List<CartVo> cartItems) { BigDecimal total = BigDecimal.ZERO; for (CartVo item : cartItems) { Goods goods = goodsService.getById(item.getGoodId()); total = total.add(goods.getPrice().multiply(BigDecimal.valueOf(item.getCount()))); } return total; } }

关键设计点:

  • 事务注解@Transactional:保证库存扣减、订单生成、购物车清空三步要么全成功,要么全回滚;
  • 库存校验双重检查:先查再扣,避免“查到有库存→被别人抢光→再扣”导致超卖;
  • SQL原子扣减setSql("stock = stock - ?"):比先查再update更安全,数据库层面保证线程安全;
  • 订单状态码硬编码:status=1表示待支付,status=2表示已支付(在OrderController.pay()中更新),状态机虽简单但可扩展。

前端调用此接口的代码在kaic/frontend/src/views/order/OrderConfirm.vue的submitOrder()方法中,它将购物车选中项打包为cartItems数组发送给/api/order/create。你可以在此处加日志:console.log('提交订单数据:', cartItems),确认前端传参结构是否符合后端要求。

3.3 后台管理模块:管理员如何审核订单与导出数据

管理员后台位于kaic/frontend/src/views/admin/目录下,入口是AdminLayout.vue。它通过路由守卫限制/admin/**路径仅登录态且角色为ADMIN的用户可访问。核心功能是订单审核和数据导出:

订单审核逻辑(AdminOrder.vue):

// 点击“通过”按钮触发 handlePass(row) { this.$confirm('确认通过该订单?', '提示', { confirmButtonText: '确定', cancelButtonText: '取消', type: 'warning' }).then(() => { this.$api.order.updateStatus({ id: row.id, status: 2 // 2=已支付(模拟支付成功) }).then(res => { if (res.code === 200) { this.$message.success('审核成功') this.fetchOrders() // 刷新列表 } }) }) }

对应后端OrderController.updateStatus():

@PutMapping("/status") public Result<String> updateStatus(@RequestBody OrderStatusDto dto) { Order order = new Order(); order.setId(dto.getId()); order.setStatus(dto.getStatus()); boolean updated = this.updateById(order); return updated ? Result.success("状态更新成功") : Result.fail("更新失败"); }

Excel导出功能(AdminGoods.vue): 点击“导出Excel”按钮,调用this.$api.goods.export(),后端GoodsController.export()方法使用EasyExcel生成文件:

@GetMapping("/export") public void export(HttpServletResponse response) throws IOException { List<Goods> goodsList = goodsService.list(); // 查询全部商品 String fileName = URLEncoder.encode("商品数据_" + System.currentTimeMillis(), "UTF-8"); response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"); response.setCharacterEncoding("utf-8"); response.setHeader("Content-disposition", "attachment;filename=" + fileName + ".xlsx"); EasyExcel.write(response.getOutputStream(), Goods.class) .sheet("商品列表") .doWrite(goodsList); }

注意:Goods.class必须有@ExcelProperty注解标记列名,如:

public class Goods { @ExcelProperty("商品ID") private Long id; @ExcelProperty("商品名称") private String name; // 其他字段... }

这个导出功能可直接复用——你只需把Goods.class换成自己的实体类,就能导出任意数据表。


4. 避坑指南:新手必踩的5个真实翻车现场与解法

4.1 现象:前端页面空白,Console报Failed to fetch,Network里所有/api/**请求都是ERR_CONNECTION_REFUSED

原因:后端服务根本没启动,或启动端口被占用(如8081端口被IDEA的其他Spring Boot实例占着)。
解决:

  • 打开终端,执行mvn spring-boot:run,观察Console输出是否出现Tomcat started on port(s): 8081;
  • 若报Address already in use: bind,执行netstat -ano | findstr :8081查PID,再用taskkill /PID <PID> /F杀掉进程;
  • 确保后端启动成功后再开前端,不要只开前端。

4.2 现象:登录成功后,点击“我的订单”跳转404,或购物车数量始终为0

原因:前端Vuex store未正确初始化,或Token未持久化。本项目用localStorage存Token,但首次登录后未触发store.dispatch('setUser')。
解决:

  • 在kaic/frontend/src/store/modules/user.js的loginaction中,确认有localStorage.setItem('token', res.data.token)和commit('SET_USER', res.data.user);
  • 若仍无效,在main.js中添加调试:console.log('Token from localStorage:', localStorage.getItem('token')),确认Token存在。

4.3 现象:MySQL执行kaic_db_init.sql报错ERROR 1067 (42000): Invalid default value for 'create_time'

原因:MySQL 5.7严格模式下,DATETIME类型字段不能设默认值为CURRENT_TIMESTAMP(除非是TIMESTAMP)。
解决:

  • 修改kaic/sql/kaic_db_init.sql,将所有create_time DATETIME DEFAULT CURRENT_TIMESTAMP改为create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP;
  • 或关闭MySQL严格模式:在my.cnf中添加sql_mode=STRICT_TRANS_TABLES,NO_ZERO_DATE,NO_ZERO_IN_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION,重启MySQL。

4.4 现象:添加商品到购物车后,页面不刷新,F5刷新才显示;或购物车数量错乱

原因:Vuex中购物车数据未实时响应式更新。本项目用state.cartItems = [...state.cartItems, newItem]方式添加,但若newItem是引用类型,可能造成响应失效。
解决:

  • 在kaic/frontend/src/store/modules/cart.js的addCartItemmutation中,确保用深拷贝:
    state.cartItems.push({ ...payload }) // 而不是直接 push(payload)
  • 或在actions中 commit 前做JSON.parse(JSON.stringify(payload))。

4.5 现象:管理员登录后,点击“商品管理”显示“无权限”,控制台报403 Forbidden

原因:后端Shiro或Spring Security配置了角色拦截,但数据库中管理员用户的role字段值不是ADMIN(可能是admin小写或空格)。
解决:

  • 登录MySQL,执行SELECT id, username, role FROM user WHERE username = 'admin';;
  • 若role值为admin,执行UPDATE user SET role = 'ADMIN' WHERE id = 1;;
  • 本项目角色校验在kaic/src/main/java/com/kaic/config/ShiroConfig.java的shiroFilterFactoryBean中,/admin/** = authc,roles[ADMIN]要求精确匹配。

5. 二次开发实战:从接入微信支付到定制化商品分类

5.1 接入真实支付:替换模拟支付为微信H5支付

当前系统支付是纯前端模拟(OrderConfirm.vue中点击“立即支付”直接跳转成功页)。要接入微信H5支付,只需改3个地方:

第一步:后端新增微信支付接口
在kaic/src/main/java/com/kaic/controller/PayController.java中添加:

@RestController @RequestMapping("/api/pay") public class PayController { @Autowired private OrderService orderService; /** * 微信H5支付统一下单 * @param orderId 订单ID * @return 微信返回的prepay_id等参数 */ @PostMapping("/unifiedOrder") public Result<Map<String, String>> unifiedOrder(@RequestParam Long orderId) { Order order = orderService.getById(orderId); if (order == null || !Objects.equals(order.getStatus(), 1)) { return Result.fail("订单不存在或状态异常"); } // 构造微信支付参数(此处简化,实际需调用微信SDK) Map<String, String> params = new HashMap<>(); params.put("appId", "wx1234567890abcdef"); // 替换为你的AppID params.put("timeStamp", String.valueOf(System.currentTimeMillis() / 1000)); params.put("nonceStr", UUID.randomUUID().toString().replace("-", "")); params.put("package", "prepay_id=wx" + System.currentTimeMillis()); params.put("signType", "MD5"); params.put("paySign", "your-signature-here"); // 实际需按微信规则生成签名 return Result.success(params); } }

第二步:前端调用微信JS-SDK
在OrderConfirm.vue的submitOrder()成功回调中,替换模拟跳转:

// 原模拟代码: // this.$router.push('/order/success') // 新微信支付代码: this.$api.pay.unifiedOrder({ orderId: order.id }).then(res => { if (res.code === 200) { // 调用微信JS-SDK发起支付 WeixinJSBridge.invoke('getBrandWCPayRequest', res.data, function(res) { if (res.err_msg === 'get_brand_wcpay_request:ok') { alert('支付成功'); window.location.href = '/order/success'; } else { alert('支付失败:' + res.err_msg); } }); } });

注意:微信JS-SDK需在公众号后台配置JS接口安全域名,并引入https://res2.wx.qq.com/open/js/jweixin-1.6.0.js。

第三步:支付结果异步通知
新增PayController.notify()接收微信服务器POST的支付结果,更新订单状态为已支付,并触发发货逻辑。这部分需配置微信商户平台的支付回调URL为https://your-domain.com/api/pay/notify。

5.2 商品分类扩展:从单级分类到三级联动菜单

当前商品分类是扁平化设计(goods.category字段存字符串如“手机”)。要支持“数码 > 手机 > iPhone”三级分类,需:

数据库改造:
新增category表:

CREATE TABLE `category` ( `id` bigint NOT NULL AUTO_INCREMENT, `name` varchar(50) NOT NULL, `parent_id` bigint DEFAULT '0', -- 0表示一级分类 `level` tinyint NOT NULL DEFAULT '1', -- 1=一级,2=二级,3=三级 PRIMARY KEY (`id`) ) ENGINE=InnoDB;

修改goods表,将category字段改为category_id外键。

后端接口:
新增CategoryController.listTree()返回树形结构:

@GetMapping("/category/tree") public Result<List<Category>> listTree() { List<Category> allCategories = categoryService.list(); // 构建树:遍历allCategories,按parent_id组装children Map<Long, Category> categoryMap = allCategories.stream() .collect(Collectors.toMap(Category::getId, c -> c)); List<Category> tree = new ArrayList<>(); for (Category c : allCategories) { if (c.getParentId() == 0) { tree.add(c); } else { Category parent = categoryMap.get(c.getParentId()); if (parent != null) { if (parent.getChildren() == null) { parent.setChildren(new ArrayList<>()); } parent.getChildren().add(c); } } } return Result.success(tree); }

前端展示:
在GoodsList.vue中,用el-cascader组件替代原分类筛选:

<el-cascader v-model="selectedCategory" :options="categories" @change="handleCategoryChange" placeholder="请选择商品分类"> </el-cascader>

categories由this.$api.category.tree()获取,handleCategoryChange触发商品列表过滤。

5.3 个性化推荐:基于用户浏览历史的简单协同过滤

不引入AI框架,用纯SQL实现“看了又看”推荐:
在GoodsController.recommendByView()中:

@GetMapping("/recommend/byView") public Result<List<Goods>> recommendByView(@RequestParam Long userId) { // 1. 查出该用户浏览过的商品ID(假设浏览记录存于view_log表) List<Long> viewedIds = viewLogService.getViewedGoodIds(userId); // 2. 查出这些商品的同类商品(同category),且未被该用户浏览过 String sql = "SELECT DISTINCT g.* FROM goods g " + "WHERE g.category IN (SELECT category FROM goods WHERE id IN :viewedIds) " + "AND g.id NOT IN :viewedIds " + "ORDER BY g.sales DESC LIMIT 8"; QueryWrapper<Goods> wrapper = new QueryWrapper<>(); wrapper.apply("g.category IN (SELECT category FROM goods WHERE id IN {0})", viewedIds); wrapper.notIn("g.id", viewedIds); List<Goods> recommends = goodsService.list(wrapper.last("ORDER BY sales DESC LIMIT 8")); return Result.success(recommends); }

前端在首页调用此接口,渲染“猜你喜欢”区块。虽然算法朴素,但胜在零依赖、易理解、可调试。

从那以后我每次接手教学类电商项目,都会先检查kaic_db_init.sql是否能一键建库、application.yml是否有明文密码、package.json的Node版本是否标注清楚——这三处不踩坑,项目就成功了一半。而真正的二次开发,从来不是堆砌新技术,而是看清每行代码在解决什么问题:库存校验防超卖、Token存储保登录、分类树形展现实现导航。希望帮到你。

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