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.Driverurl中的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。
改造实战:替换商品图片路径
假设你想把默认图片换成自己服务器上的图片,只需改两处:
- 前端:在
GoodsList.vue的img标签中,将:src="good.imgUrl"改为:src="'https://your-cdn.com/' + good.imgUrl"; - 后端:确保数据库
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存储保登录、分类树形展现实现导航。希望帮到你。
本文还有配套的精品资源,点击获取