Spring Boot + Vue电池销售管理系统:从数据库到答辩的全栈实战解析
2026/9/24 23:22:21 网站建设 项目流程

每年一到期末周,总有一批人在各个代码仓库里搜“基于springboot + vue的XX管理系统”。我见过不少同学下载了一堆源码,结果要么数据库脚本导入报错,要么前端依赖装不上,最后在宿舍里对着满屏报错日志发呆。今天要聊的这套电池销售系统,恰好是一个覆盖面很全、业务逻辑又足够直观的全栈项目,源码、数据库脚本、配套文档三件套齐全,特别适合用来应对课程设计、毕业设计,或者作为你第一个完整的springboot + vue练手项目。

这套系统本质上做的是商品销售中最核心的一环:电池从入库到卖出,库存怎么变、订单怎么记、客户和供应商怎么管、销售数据怎么统计。我见过太多人拿了一套源码会运行,但是启动之后不知道怎么讲、不知道怎么改、不知道怎么回答老师的追问。这篇文章我不打算只给你报流水账,我会把这套电池销售系统从技术选型、数据库设计、后端接口、前端联调,到最后的验收答辩,一条线全部拆开讲清楚,尤其是那些“你以为会了、一被追问就翻车”的细节。

1. 电池销售系统这类全栈课设,为什么年年都有人做

销售类管理系统几乎是课设和毕设里最经久不衰的一类选题,而电池销售系统又是销售系统里很适合入手的一个方向。它的业务模型足够直观:商品、客户、供应商、订单、库存,这五件事把一套典型的进销存业务全串起来了,没有模糊不清的业务边界,不需要复杂的算法,又能把数据库设计、后端接口、前端页面这三层知识全部覆盖到。

电池销售这个具体场景还有一个天然优势,就是它自带“属性维度”。同样是卖电池,有的按品牌分,有的按型号分,有的按容量和电压分,还有的涉及铅酸、锂电、镍氢等不同类别。这意味着你在设计商品表的时候会自然想到属性字段怎么加、分类怎么建,而不是像“通用商品系统”那样只能抽象地空谈。对于课设来说,这种能落到具体场景的设计比泛泛的“网上商城”更容易讲出东西。

再说回技术点覆盖。这套系统看起来就是一个普通的增删改查项目,但它实际上覆盖了多数老师爱问的技术点:

  • 用户登录和权限控制,对应拦截器、JWT、密码加密;
  • 商品管理,对应文件上传、条件查询、分页排序;
  • 订单管理,对应多表关联、事务操作、库存联动;
  • 销售统计,对应聚合查询、日期处理、图表渲染。

所以你会发现,一套电池销售系统做下来,你不是在做一个页面,而是在走一遍完整的全栈业务闭环。这也是为什么这类项目在仓库里永远有人找、永远有人在做的原因。

2. 技术栈选型的底层逻辑:Spring Boot 2 + Vue 2 + MyBatis-Plus这套组合赢在哪

很多同学拿到源码之后第一反应是看版本号,然后开始焦虑:Spring Boot 3都出了,你怎么还用2.x?Vue都到3.x了,项目怎么还是Vue 2?这里我可以很直接地说,课设和毕设场景下,稳定压倒一切,资料量压倒一切,而不是“最新技术”压倒一切。

2.1 版本搭配为什么这么选

我先给出一套比较推荐的版本组合,也说明它们各自的原因。

组件推荐版本原因
JDK1.8(8u201+均可)学校机房和绝大多数电脑的主流环境,兼容性最好,不用额外折腾
Spring Boot2.7.x稳定、资料海量,支持JDK 8,不需要像Spring Boot 3那样必须上JDK 17
MySQL5.7或8.0两种版本都兼容,8.0的utf8mb4和窗口函数更强,5.7更老牌
MyBatis-Plus3.5.x内置单表CRUD、分页插件、逻辑删除,省掉大量重复SQL,答辩也好讲
Node.js14或16和Vue 2 + Element UI搭配最稳,Node版本太高容易出现node-sass编译失败
Vue2.6.x配合Element UI 2.x,官方文档和问答资料量是最大的,踩坑基本都能搜到方案

这里我想重点强调一下为什么不是Spring Boot 3。Spring Boot 3最大的变化是javax包名换成了jakarta,一部分老教程和老代码直接迁移会有问题。做课设的实际情况是:你的参考代码可能来自某个学长、某个培训机构视频、某个开源仓库,这些资源里绝大多数用的是javax。如果你非要用Spring Boot 3,可能参考代码里的一半依赖都要重新配置,纯属给自己增加工作量。

Vue 2和Vue 3的问题也类似。Vue 3 + Element Plus确实是当前趋势,但Element Plus的组件写法和Element UI有细节差异,很多“直接抄作业”的代码片段不能无缝复用。对于时间紧迫的课程设计来说,用最成熟、资料最全的组合把项目跑通,比追新更重要。

2.2 为什么选MyBatis-Plus而不是JPA或原生MyBatis

说一下持久层框架的选型。JPA(Spring Data JPA)的抽象程度确实高,但它有一个问题:很多中国学生接触得少,出了复杂查询报错之后很难自己排查。原生MyBatis的SQL自由度最高,但单表CRUD也要手写XML,效率低,代码量大。MyBatis-Plus正好卡在中间:单表操作直接用内置方法,复杂查询用LambdaQueryWrapper,特殊需求还能自己写XML,属于“能偷懒的地方绝不手写,要展示能力的地方也能展示”。

这套电池销售系统里,商品列表的条件查询、订单的分页查询、统计报表里的分组聚合,用MyBatis-Plus写起来都非常顺手。比如查询电池列表,按名称模糊搜索、按品牌筛选、按库存排序,代码可以写成:

LambdaQueryWrapper<Battery> wrapper = new LambdaQueryWrapper<>(); wrapper.like(StringUtils.hasText(name), Battery::getName, name) .eq(StringUtils.hasText(brand), Battery::getBrand, brand) .orderByAsc(Battery::getStock); Page<Battery> page = batteryMapper.selectPage(new Page<>(current, size), wrapper);

这段代码既没有拼接SQL的字符串噩梦,也能非常直白地解释给答辩老师听:第一个参数是布尔值,条件为真时才拼上这个查询条件,这就是MyBatis-Plus的条件构造器。

3. 数据库设计决定项目上限:库存、订单、明细三张表怎么建才耐得住追问

数据库设计这套系统里最值得讲的部分。很多人数据库表建得很随意,项目跑起来倒是没问题,但答辩时被老师问一句“为什么订单表要冗余商品价格?”就愣住。下面我按这个项目的核心链路,把表结构设计的思路一条一条说清楚。

3.1 核心表有哪些

电池销售系统从业务上划分,至少需要这几张表:

  • 用户表(sys_user):登录账号、密码、角色,支撑后台登录和权限区分;
  • 电池商品表(battery):存储商品的基础信息和库存;
  • 客户表(customer):购买方信息,B端销售场景下客户档案很关键;
  • 供应商表(supplier):电池进货来源,和商品入库产生关联;
  • 销售订单表(sale_order):一次销售的主记录;
  • 订单明细表(sale_order_item):一笔订单里的每项电池商品;
  • 库存变动日志表(stock_log):记录每次入库、出库的来龙去脉。

这七张表基本就够用了。有些同学喜欢再多加一堆表,比如电池分类表、角色权限表、留言反馈表,我的建议是量力而行:表越多,外键和逻辑越复杂,课设时间紧张的时候反而容易把自己绕进去。

3.2 商品表设计里的两个关键点

以电池商品表为例,最基本的建表SQL是这样的:

CREATE TABLE `battery` ( `id` bigint NOT NULL AUTO_INCREMENT, `name` varchar(100) NOT NULL COMMENT '电池名称', `brand` varchar(50) DEFAULT NULL COMMENT '品牌', `model` varchar(50) DEFAULT NULL COMMENT '型号', `capacity` int DEFAULT NULL COMMENT '容量(mAh)', `voltage` decimal(5,2) DEFAULT NULL COMMENT '电压(V)', `price` decimal(10,2) NOT NULL COMMENT '销售单价', `stock` int NOT NULL DEFAULT '0' COMMENT '当前库存', `stock_warn` int DEFAULT NULL COMMENT '库存预警阈值', `status` tinyint DEFAULT '1' COMMENT '状态 1上架 0下架', `deleted` tinyint DEFAULT '0' COMMENT '逻辑删除标记', `create_time` datetime DEFAULT NULL, `update_time` datetime DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COMMENT='电池商品表';

这里面第一个设计重点是stock_warn这个字段。它是库存预警阈值,当stock <= stock_warn时,前端列表页会把库存数字标红,提示管理员补货。这个功能虽然实现起来只有一两行判断,但它体现了“系统能主动辅助业务”,是一个非常容易在答辩时拿分的点。

第二个设计重点是deleted字段,也就是逻辑删除。为什么不用物理删除?因为订单明细表里的商品是通过battery_id关联到商品表的,如果商品被物理删除了,历史订单明细里的商品信息就悬空了。更严重的是,你按商品维度做销售统计的时候,数据会直接缺失。逻辑删除只在查询时加一个deleted=0的条件,既能保证数据可用,也不影响历史订单和统计报表的完整性。

3.3 订单头与订单明细为什么要拆两张表

这是数据库设计里最经典的一道答辩题。你卖电池,不可能每一笔订单只卖一件商品,客户可能一次买了5种不同规格的电池。如果所有信息都放在一张表里,会出现同一个订单号重复出现5次,字段里有大量重复数据,改单也麻烦。所以标准做法是拆成订单主表和订单明细表:主表存一次交易的公共信息(客户、总金额、状态、时间),明细表存每一条商品行。

订单主表的示例:

CREATE TABLE `sale_order` ( `id` bigint NOT NULL AUTO_INCREMENT, `order_no` varchar(32) NOT NULL COMMENT '订单编号', `customer_id` bigint DEFAULT NULL COMMENT '客户ID', `user_id` bigint DEFAULT NULL COMMENT '操作员ID', `total_amount` decimal(10,2) DEFAULT NULL COMMENT '订单总金额', `pay_type` tinyint DEFAULT NULL COMMENT '支付方式 1现金 2微信 3支付宝 4对公转账', `status` tinyint DEFAULT '1' COMMENT '订单状态 1已完成 2已退款 3已作废', `remark` varchar(255) DEFAULT NULL COMMENT '备注', `create_time` datetime DEFAULT NULL, PRIMARY KEY (`id`), KEY `idx_order_no` (`order_no`) ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COMMENT='销售订单表';

订单明细表在设计上有一个值得专门讲的细节,就是“快照”字段:

CREATE TABLE `sale_order_item` ( `id` bigint NOT NULL AUTO_INCREMENT, `order_id` bigint NOT NULL COMMENT '订单ID', `battery_id` bigint DEFAULT NULL COMMENT '商品ID', `battery_name` varchar(100) DEFAULT NULL COMMENT '商品名称快照', `battery_model` varchar(50) DEFAULT NULL COMMENT '型号快照', `price` decimal(10,2) DEFAULT NULL COMMENT '成交单价快照', `num` int NOT NULL COMMENT '购买数量', `amount` decimal(10,2) DEFAULT NULL COMMENT '小计金额', PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COMMENT='订单明细表';

为什么要有battery_namebattery_modelprice这些“冗余字段”?因为商品的价格和名称是会变的。今天这个电池卖50块钱,明天搞活动改成45块钱,如果订单明细里不存当时的快照,你查历史订单就只能看到“关联商品ID=3”这种没有任何业务含义的数据,甚至商品改名之后历史订单也跟着变,这显然不合理。快照字段的存在,让订单数据在时间轴上保持稳定,这是实际业务系统里非常常见的冗余设计思路。

3.4 库存变动日志表的价值

最后说一下库存日志表。很多课设项目里库存只是商品表里一个stock字段,卖出就减,入库就加,从来不留痕迹。但老师如果追问一句“怎么证明某天某笔操作把库存从100变成了80?”,你没有日志表就完全答不上来。

库存日志表的核心字段是这几个:

CREATE TABLE `stock_log` ( `id` bigint NOT NULL AUTO_INCREMENT, `battery_id` bigint NOT NULL, `change_type` tinyint NOT NULL COMMENT '变动类型 1入库 2销售出库 3盘点调整', `change_num` int NOT NULL COMMENT '变动数量', `before_stock` int NOT NULL COMMENT '变动前库存', `after_stock` int NOT NULL COMMENT '变动后库存', `remark` varchar(255) DEFAULT NULL, `create_time` datetime DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COMMENT='库存变动日志表';

before_stockafter_stock是精华所在。有了这两个字段,你可以在页面上做“库存流水”功能,也可以精确回溯任何一次库存变动的上下文。这个表也让“入库”和“销售下单”这两个操作有了可解释的落点,整套系统在逻辑上就完整了。

4. 后端接口实现里最容易翻车的三个点:鉴权、分页、库存扣减

后端这块很多同学喜欢上来就写接口,写完才发现登录拦截没做,分页查出来全是重复数据,库存扣减在并发下直接变负数。下面这三个点,是我看这套系统源码时最关注的环节,也是你能不能“把项目讲明白”的分水岭。

4.1 登录鉴权:JWT中间件还是简单的拦截器

电池销售系统里,管理员和销售员登录后应该只能访问自己有权限的接口。最简单的做法是Spring Boot的拦截器加JWT(JSON Web Token):登录成功后后端生成一个带用户信息和过期时间的token返回给前端,前端每次请求在请求头带上Authorization: Bearer <token>,后端通过拦截器校验token是否合法。

关键实现分三步:

第一步,登录接口里校验用户名密码,密码存储使用BCrypt加密,不能明文存储:

@PostMapping("/login") public Result login(@RequestBody LoginDTO dto) { User user = userService.lambdaQuery() .eq(User::getUsername, dto.getUsername()) .one(); if (user == null || !BCrypt.checkpw(dto.getPassword(), user.getPassword())) { return Result.error("用户名或密码错误"); } String token = JwtUtil.createToken(user.getId(), user.getUsername(), user.getRole()); return Result.success(token); }

第二步,写一个拦截器校验token,并且通过HandlerInterceptor把用户信息放入请求上下文:

public class JwtInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String token = request.getHeader("Authorization"); // 如果token为空或校验失败,直接返回401 // 校验通过则把userId放入request的attribute中 return true; } }

第三步,注册拦截器,并放行登录接口和静态资源:

@Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new JwtInterceptor()) .addPathPatterns("/**") .excludePathPatterns("/api/login", "/error"); }

这里有个小坑要提醒你:如果前端把登录请求的URL也放在/api下,拦截器放行的路径记得和前端请求路径严格对应,否则会出现“登录接口都被拦截,前端永远登不进去”的情况。我自己排查过好几个同学的代码,最后都是这个放行路径不一致导致的。

4.2 分页查询:只配了插件还不够

分页是管理后台的高频需求。MyBatis-Plus的分页插件配置很简单,就是一个配置类:

@Configuration public class MybatisPlusConfig { @Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; } }

但真正的坑不在这里。分页插件生效有一个前提:你的查询方法的第一个参数必须是Page对象。比如:

Page<Battery> page = batteryMapper.selectPage(new Page<>(pageNum, pageSize), wrapper);

如果你在Service层写的是:

List<Battery> list = batteryMapper.selectList(wrapper);

再把list手动截取一段,那分页插件等于没用,数据量一大性能就完蛋,而且表里的数据变了,分页的total还会对不上。这个点是答辩时最容易暴露“你不是自己写的”细节,因为自己写过一遍的人绝对不会犯。

4.3 库存扣减:一不留神就超卖

电池销售系统最核心的业务操作就是下单减库存。很多初学的写法是这样:

// 错误的示例 Battery battery = batteryMapper.selectById(batteryId); if (battery.getStock() >= num) { battery.setStock(battery.getStock() - num); batteryMapper.updateById(battery); }

这段代码看着没问题,但它不是原子操作。如果两个人同时下单,两个线程都查出库存是100,都判断“够卖”,然后都执行减库存,最终结果就可能是98甚至更少,明明只剩最后一件商品却被卖出去了两次。

正确的做法是把库存扣减写进一条UPDATE语句里,利用数据库的行锁保证原子性:

UPDATE battery SET stock = stock - #{num} WHERE id = #{batteryId} AND stock >= #{num}

在MyBatis-Plus的Mapper里可以这样做:

@Update("UPDATE battery SET stock = stock - #{num}, update_time = NOW() " + "WHERE id = #{batteryId} AND stock >= #{num}") int deductStock(@Param("batteryId") Long batteryId, @Param("num") Integer num);

这个方法的返回结果是受影响行数,如果返回0,说明库存不足或者商品不存在,Service层拿到0就应该抛业务异常并回滚事务。这种“乐观锁思想 + 数据库条件更新”的写法,既能防止超卖,代码又很简洁,而且是面试、答辩里非常加分的一个亮点。

除了扣减库存,下单流程还应该包在事务里。在Service方法上加上@Transactional(rollbackFor = Exception.class),这样写订单主表、写订单明细、扣库存、写库存日志任何一步失败,整个操作都会回滚,不会出现“订单建了但库存没扣”的脏数据。

4.4 销售统计的SQL怎么写

销售统计模块通常包括:按天统计销售额、按月统计出货量、按品牌统计销量排名。这些都是基于订单和明细表的聚合查询。

一个典型的近7天销售统计SQL可以长这样:

SELECT DATE_FORMAT(o.create_time, '%Y-%m-%d') AS order_date, COUNT(DISTINCT o.id) AS order_count, IFNULL(SUM(i.amount), 0) AS total_amount FROM sale_order o LEFT JOIN sale_order_item i ON o.id = i.order_id WHERE o.status = 1 AND o.create_time >= DATE_SUB(CURDATE(), INTERVAL 6 DAY) GROUP BY DATE_FORMAT(o.create_time, '%Y-%m-%d') ORDER BY order_date;

这个SQL里有个注意点:LEFT JOIN时如果一笔订单没有任何明细,SUM返回NULL,所以要用IFNULL兜底,否则前端图表里会出现空白数据。统计这块不要求SQL写得天花乱坠,但至少要让老师看到你懂“聚合函数 + 时间处理 + 条件过滤”这三件事。

5. 前端Vue对接后端的完整链条:路由、axios封装与本地代理

前端部分在整个项目里占比很大,但我不打算把所有页面都罗列一遍,那会把篇幅拖得又臭又长。我更想说的是那些“不配好就会反复出问题”的基础设施。

5.1 前端工程结构先理顺

拿到前端代码之后,先搞清楚src目录下的职责划分:

src ├── api # 每个模块的请求函数 ├── assets # 静态资源 ├── components # 公共组件 ├── router # 路由配置 ├── store # 状态管理(Vuex) ├── utils # 工具函数,axios实例一般在这 ├── views # 页面组件 ├── App.vue └── main.js

这套电池销售系统的页面,大体上包括:登录页、首页(仪表盘)、商品管理、客户管理、供应商管理、入库管理、订单管理、销售统计、库存流水。每新增一个功能模块,就是“router加路由 + api加请求 + views加页面”三板斧。

5.2 axios统一封装:不要每个页面都单独发请求

前端写请求最忌讳的是每个组件里直接axios.get,一旦后端接口地址变了或者要统一处理token过期,你得把所有页面翻个底朝天。正确的做法是把axios实例封装在utils/request.js里,统一做三件事:

  • 设置baseURL/api,这样开发环境可以走代理,生产环境可以走网关;
  • 请求拦截器里自动从localStorage取token并放到请求头;
  • 响应拦截器里统一处理code,如果遇到401自动跳转登录页。

核心代码大概是:

import axios from 'axios' import { Message } from 'element-ui' import router from '@/router' const request = axios.create({ baseURL: '/api', timeout: 10000 }) request.interceptors.request.use(config => { const token = localStorage.getItem('token') if (token) { config.headers['Authorization'] = 'Bearer ' + token } return config }) request.interceptors.response.use( response => { const res = response.data if (res.code !== 200) { Message.error(res.msg || '请求失败') return Promise.reject(new Error(res.msg)) } return res }, error => { if (error.response && error.response.status === 401) { localStorage.removeItem('token') router.push('/login') } Message.error(error.response?.data?.msg || '网络异常') return Promise.reject(error) } ) export default request

封装好之后,每个模块的api文件只需要关心接口本身,比如:

// api/battery.js import request from '@/utils/request' export function getBatteryPage(params) { return request({ url: '/battery/page', method: 'get', params }) } export function saveBattery(data) { return request({ url: '/battery', method: 'post', data }) }

这里有个细节:响应拦截器里已经返回了res,所以页面里调用getBatteryPage拿到的直接是后端返回的数据体,不需要再res.data.data套娃,这个约定一定要和后端设计的统一返回结构对齐。拿这套系统举例,后端统一返回{ code: 200, msg: 'success', data: ... },那前端的封装就按这个结构写。

5.3 跨域问题:用本地代理而不是乱开CORS

前后端分离项目必问的问题是跨域。前端跑在localhost:8080,后端跑在localhost:9090,端口不同就存在跨域。最省事也最规范的开发方案是在前端vue.config.js里配置代理:

module.exports = { devServer: { port: 8080, proxy: { '/api': { target: 'http://localhost:9090', changeOrigin: true, pathRewrite: { '^/api': '' } } } } }

注意这里的pathRewrite:如果后端接口路径本身不带/api前缀,代理时就要把/api重写掉,否则请求发到后端会变成/api/battery/page,后端没有这个映射就直接404了。很多前端页面白屏,打开控制台一看全是404,问题就出在这一行配置上。

当然,也有同学选择在后端加一个全局CORS配置类,这也行,但只适合做本地调试。真到了部署联调阶段,代理方案明显更灵活,而且它不需要在后端代码里引入额外的“测试环境专用”逻辑。

5.4 列表页和表单页的Element UI套路

后端接口都通之后,前端页面本质上就是三板斧:表格上做查询,弹窗里做表单,分页条和查询条件联动。以商品管理列表页为例,核心结构是:

<el-table :data="tableData" border> <el-table-column prop="name" label="电池名称" /> <el-table-column prop="brand" label="品牌" /> <el-table-column prop="model" label="型号" /> <el-table-column prop="price" label="单价" /> <el-table-column label="库存"> <template slot-scope="scope"> <span :class="{ 'warn-stock': scope.row.stock <= scope.row.stockWarn }"> {{ scope.row.stock }} </span> </template> </el-table-column> </el-table> <el-pagination :current-page="queryParams.pageNum" :page-size="queryParams.pageSize" :total="total" @current-change="handlePageChange" />

配套的查询逻辑是:

async loadData() { const res = await getBatteryPage(this.queryParams) this.tableData = res.data.records this.total = res.data.total }

这里有一个常见的低级错误:把res.data整个数组直接绑给表格。如果后端返回的是分页对象{ records: [], total: 100 },你直接绑一个数组肯定什么都显示不出来。先看一下后端返回结构再决定怎么取值,能省下大量对着控制台发呆的时间。

6. 拿源码后从0到1跑通整个项目的实操记录

讲完设计逻辑,接下来是整个过程的实操记录。我先把拿到源码包之后的运行过程按步骤拆开,每一步都标出容易踩的坑。

6.1 第一步:解压并确认目录结构

源码包解压后,通常会有两个子文件夹,一个后端(比如叫backend或者battery-server),一个前端(比如叫frontend或者battery-web),另外还有数据库脚本目录和文档目录。建议先花五分钟扫一遍结构,不要急着双击打开IDE。

6.2 第二步:导入数据库

用Navicat或者命令行执行SQL脚本。导入前先确认字符集,建议建库时指定utf8mb4:

CREATE DATABASE battery_sales DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; USE battery_sales; SOURCE battery_sales.sql;

导入报错最常见的两种:一是SQL脚本在旧版本MySQL上用了新语法,二是脚本里有DROP TABLE IF EXISTS却因为外键约束报错。如果是后者,可以先关闭外键检查:

SET FOREIGN_KEY_CHECKS = 0; -- 执行导入脚本 SET FOREIGN_KEY_CHECKS = 1;

另外,数据库脚本如果首行是CREATE DATABASEUSE,直接在Navicat里运行即可;如果脚本里没有建库语句,就自己先建一个库再执行选择库。

6.3 第三步:配置后端并启动

打开后端项目的application.yml,核心要改的就是数据库连接:

server: port: 9090 spring: datasource: url: jdbc:mysql://localhost:3306/battery_sales?useUnicode=true&characterEncoding=utf8mb4&useSSL=false&serverTimezone=Asia/Shanghai username: root password: 你自己的密码 driver-class-name: com.mysql.cj.jdbc.Driver mybatis-plus: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl global-config: db-config: logic-delete-field: deleted logic-delete-value: 1 logic-not-delete-value: 0

注意serverTimezone一定要设置成Asia/Shanghai,否则JDBC连接MySQL 8.0的时候会因为时区问题直接报错。还有logic-delete-field的配置,它对应表里的deleted字段,配置好之后,你调selectList查询时,MyBatis-Plus会自动追加AND deleted = 0,不用手写条件。

启动后端前,先确认端口没被占用。后端的启动类在源码包里通常叫BatteryApplicationSalesApplication,带@SpringBootApplication注解的那个类,直接运行main方法即可。

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

前端目录下执行:

npm install npm run serve

如果npm install特别慢,可以换淘宝镜像:

npm config set registry https://registry.npmmirror.com

如果node版本太高,出现了node-sass相关的ERESOLVE错误,最常见的两种解法是:

  • 删除node_modulespackage-lock.json,重新npm install
  • 如果还报错,就用set NODE_OPTIONS=--openssl-legacy-provider再启动(Windows下是set NODE_OPTIONS=--openssl-legacy-provider && npm run serve)。

前端启动成功后,浏览器访问http://localhost:8080,正常情况会跳转到登录页。如果页面能打开但接口请求全是404,优先检查代理配置里的路径是否和后端一致,再看后端启动日志有没有报错。

6.5 常见运行错误对照表

现象大概率原因处理方式
后端启动报Access denied for user数据库账号或密码错误检查application.yml的username/password
后端启动报Unknown database数据库没有创建成功检查库名是否一致,大小写也算
后端启动报Communications link failureMySQL没启动或端口不对确认MySQL服务已启动,端口3306未被占用
前端页面请求报404代理路径或者后端接口路径不匹配检查vue.config.js的pathRewrite和后端Controller路径
前端报Port 8080 was already in use8080被占用在vue.config.js里改端口,比如8088
登录成功但列表页面始终转圈token没带上或接口报500打开浏览器控制台看接口返回,重点看响应拦截器

这组对照表如果能在项目启动前先看一遍,大概率能省下半天到一天的排错时间。

7. 验收答辩时最常被追问的几个技术点,怎么答不露怯

代码跑通只是第一步,最后的答辩才是决胜局。老师不一定会亲自敲代码,但一定会问几个“为什么”,这几个问题你心里有底,基本就稳了。

7.1 为什么订单表和订单明细表要分成两张表

答法要点:一次销售订单包含多个商品项,如果放在一张表里,同一笔订单必须重复存储订单公共信息多次,数据冗余且不利于修改。拆成主表和明细表后,订单头管“这笔交易的整体”,明细表管“这笔交易具体买了什么”。一张订单在明细表里可以有1行或N行,这就是典型的一对多关系设计。

7.2 商品删除为什么用逻辑删除而不是物理删除

答法要点:因为历史订单和销售统计需要关联商品信息,逻辑删除通过deleted字段标记,既能在业务列表里“删除”数据,又能保证历史记录的完整性。同时,订单明细表里保存了商品名称和价格的快照,这也是为了让历史数据不因商品信息的变更而失真。

7.3 库存扣减怎么防止超卖

答法要点:不是“先查再改”,而是把库存判断放在UPDATE语句的WHERE条件里,让数据库行锁保证原子性。如果影响行数为0,说明库存不足,整个下单操作回滚。这样即使多人同时下单,也不会出现库存卖成负数的情况。回答时如果能带出“乐观锁”和“事务回滚”这两个词,老师的印象分会更高。

7.4 前端的token存在哪里,过期了怎么办

答法要点:token存储在localStorage中,每次请求通过axios请求拦截器添加到请求头。token过期时,后端接口返回401,响应拦截器统一捕获,清除本地存储并跳转到登录页。这里可以多补一句“如果要求更安全,可以存到内存或cookie并设置HttpOnly”,显得你有安全意识。

7.5 如果让你继续扩展这个系统,你会加什么

这个问题没有标准答案,但千万别回答“不知道”。合理的回答方向包括:增加基于ECharts的销售趋势图表、引入商品分类和扫码入库、增加Excel导入导出、把登录改成RBAC多角色权限控制,或者引入Redis缓存热点商品库存。选一个你觉得有把握的方向,认真讲一两句具体思路就够了。

写在最后

我从技术选型一路讲到了答辩应对,基本把这套电池销售系统从“源码包”变成“你的项目”的完整链路过了一遍。我个人做这套系统复盘时最大的体会是:一份源码最大的价值不在于“能跑”,而在于你能讲清楚它的每一个设计决策。数据库里为什么有这个字段、后端接口为什么这样写、前后端对接时哪个配置最要命,这些东西想明白了,源码才是你的,而不是停留在下载文件夹里的一个压缩包。

如果你正准备拿这个项目去交课设或者毕设,我的建议是:别急着改代码,先花半天时间照着这篇的思路把表结构和核心流程在纸上画一遍,再动手跑项目。你越早把“运行链路”和“设计决策”变成自己的语言,后面不管是被老师追问还是被面试官考察,都会越来越顺。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询