资讯动态

Spring Boot + Vue电池销售管理系统:从数据库到答辩的全栈实战解析

发布时间:2026/9/24 23:22:24 来源:尧图企业网站定制
每年一到期末周总有一批人在各个代码仓库里搜“基于springboot vue的XX管理系统”。我见过不少同学下载了一堆源码结果要么数据库脚本导入报错要么前端依赖装不上最后在宿舍里对着满屏报错日志发呆。今天要聊的这套电池销售系统恰好是一个覆盖面很全、业务逻辑又足够直观的全栈项目源码、数据库脚本、配套文档三件套齐全特别适合用来应对课程设计、毕业设计或者作为你第一个完整的springboot vue练手项目。这套系统本质上做的是商品销售中最核心的一环电池从入库到卖出库存怎么变、订单怎么记、客户和供应商怎么管、销售数据怎么统计。我见过太多人拿了一套源码会运行但是启动之后不知道怎么讲、不知道怎么改、不知道怎么回答老师的追问。这篇文章我不打算只给你报流水账我会把这套电池销售系统从技术选型、数据库设计、后端接口、前端联调到最后的验收答辩一条线全部拆开讲清楚尤其是那些“你以为会了、一被追问就翻车”的细节。1. 电池销售系统这类全栈课设为什么年年都有人做销售类管理系统几乎是课设和毕设里最经久不衰的一类选题而电池销售系统又是销售系统里很适合入手的一个方向。它的业务模型足够直观商品、客户、供应商、订单、库存这五件事把一套典型的进销存业务全串起来了没有模糊不清的业务边界不需要复杂的算法又能把数据库设计、后端接口、前端页面这三层知识全部覆盖到。电池销售这个具体场景还有一个天然优势就是它自带“属性维度”。同样是卖电池有的按品牌分有的按型号分有的按容量和电压分还有的涉及铅酸、锂电、镍氢等不同类别。这意味着你在设计商品表的时候会自然想到属性字段怎么加、分类怎么建而不是像“通用商品系统”那样只能抽象地空谈。对于课设来说这种能落到具体场景的设计比泛泛的“网上商城”更容易讲出东西。再说回技术点覆盖。这套系统看起来就是一个普通的增删改查项目但它实际上覆盖了多数老师爱问的技术点用户登录和权限控制对应拦截器、JWT、密码加密商品管理对应文件上传、条件查询、分页排序订单管理对应多表关联、事务操作、库存联动销售统计对应聚合查询、日期处理、图表渲染。所以你会发现一套电池销售系统做下来你不是在做一个页面而是在走一遍完整的全栈业务闭环。这也是为什么这类项目在仓库里永远有人找、永远有人在做的原因。2. 技术栈选型的底层逻辑Spring Boot 2 Vue 2 MyBatis-Plus这套组合赢在哪很多同学拿到源码之后第一反应是看版本号然后开始焦虑Spring Boot 3都出了你怎么还用2.xVue都到3.x了项目怎么还是Vue 2这里我可以很直接地说课设和毕设场景下稳定压倒一切资料量压倒一切而不是“最新技术”压倒一切。2.1 版本搭配为什么这么选我先给出一套比较推荐的版本组合也说明它们各自的原因。组件推荐版本原因JDK1.88u201均可学校机房和绝大多数电脑的主流环境兼容性最好不用额外折腾Spring Boot2.7.x稳定、资料海量支持JDK 8不需要像Spring Boot 3那样必须上JDK 17MySQL5.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说一下持久层框架的选型。JPASpring Data JPA的抽象程度确实高但它有一个问题很多中国学生接触得少出了复杂查询报错之后很难自己排查。原生MyBatis的SQL自由度最高但单表CRUD也要手写XML效率低代码量大。MyBatis-Plus正好卡在中间单表操作直接用内置方法复杂查询用LambdaQueryWrapper特殊需求还能自己写XML属于“能偷懒的地方绝不手写要展示能力的地方也能展示”。这套电池销售系统里商品列表的条件查询、订单的分页查询、统计报表里的分组聚合用MyBatis-Plus写起来都非常顺手。比如查询电池列表按名称模糊搜索、按品牌筛选、按库存排序代码可以写成LambdaQueryWrapperBattery wrapper new LambdaQueryWrapper(); wrapper.like(StringUtils.hasText(name), Battery::getName, name) .eq(StringUtils.hasText(brand), Battery::getBrand, brand) .orderByAsc(Battery::getStock); PageBattery 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) ) ENGINEInnoDB AUTO_INCREMENT1 DEFAULT CHARSETutf8mb4 COMMENT电池商品表;这里面第一个设计重点是stock_warn这个字段。它是库存预警阈值当stock stock_warn时前端列表页会把库存数字标红提示管理员补货。这个功能虽然实现起来只有一两行判断但它体现了“系统能主动辅助业务”是一个非常容易在答辩时拿分的点。第二个设计重点是deleted字段也就是逻辑删除。为什么不用物理删除因为订单明细表里的商品是通过battery_id关联到商品表的如果商品被物理删除了历史订单明细里的商品信息就悬空了。更严重的是你按商品维度做销售统计的时候数据会直接缺失。逻辑删除只在查询时加一个deleted0的条件既能保证数据可用也不影响历史订单和统计报表的完整性。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) ) ENGINEInnoDB AUTO_INCREMENT1 DEFAULT CHARSETutf8mb4 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) ) ENGINEInnoDB AUTO_INCREMENT1 DEFAULT CHARSETutf8mb4 COMMENT订单明细表;为什么要有battery_name、battery_model、price这些“冗余字段”因为商品的价格和名称是会变的。今天这个电池卖50块钱明天搞活动改成45块钱如果订单明细里不存当时的快照你查历史订单就只能看到“关联商品ID3”这种没有任何业务含义的数据甚至商品改名之后历史订单也跟着变这显然不合理。快照字段的存在让订单数据在时间轴上保持稳定这是实际业务系统里非常常见的冗余设计思路。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) ) ENGINEInnoDB AUTO_INCREMENT1 DEFAULT CHARSETutf8mb4 COMMENT库存变动日志表;before_stock和after_stock是精华所在。有了这两个字段你可以在页面上做“库存流水”功能也可以精确回溯任何一次库存变动的上下文。这个表也让“入库”和“销售下单”这两个操作有了可解释的落点整套系统在逻辑上就完整了。4. 后端接口实现里最容易翻车的三个点鉴权、分页、库存扣减后端这块很多同学喜欢上来就写接口写完才发现登录拦截没做分页查出来全是重复数据库存扣减在并发下直接变负数。下面这三个点是我看这套系统源码时最关注的环节也是你能不能“把项目讲明白”的分水岭。4.1 登录鉴权JWT中间件还是简单的拦截器电池销售系统里管理员和销售员登录后应该只能访问自己有权限的接口。最简单的做法是Spring Boot的拦截器加JWTJSON 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对象。比如PageBattery page batteryMapper.selectPage(new Page(pageNum, pageSize), wrapper);如果你在Service层写的是ListBattery 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 :datatableData border el-table-column propname label电池名称 / el-table-column propbrand label品牌 / el-table-column propmodel label型号 / el-table-column propprice label单价 / el-table-column label库存 template slot-scopescope span :class{ warn-stock: scope.row.stock scope.row.stockWarn } {{ scope.row.stock }} /span /template /el-table-column /el-table el-pagination :current-pagequeryParams.pageNum :page-sizequeryParams.pageSize :totaltotal current-changehandlePageChange /配套的查询逻辑是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脚本。导入前先确认字符集建议建库时指定utf8mb4CREATE 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 DATABASE和USE直接在Navicat里运行即可如果脚本里没有建库语句就自己先建一个库再执行选择库。6.3 第三步配置后端并启动打开后端项目的application.yml核心要改的就是数据库连接server: port: 9090 spring: datasource: url: jdbc:mysql://localhost:3306/battery_sales?useUnicodetruecharacterEncodingutf8mb4useSSLfalseserverTimezoneAsia/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不用手写条件。启动后端前先确认端口没被占用。后端的启动类在源码包里通常叫BatteryApplication或SalesApplication带SpringBootApplication注解的那个类直接运行main方法即可。6.4 第四步前端安装依赖并启动前端目录下执行npm install npm run serve如果npm install特别慢可以换淘宝镜像npm config set registry https://registry.npmmirror.com如果node版本太高出现了node-sass相关的ERESOLVE错误最常见的两种解法是删除node_modules和package-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缓存热点商品库存。选一个你觉得有把握的方向认真讲一两句具体思路就够了。写在最后我从技术选型一路讲到了答辩应对基本把这套电池销售系统从“源码包”变成“你的项目”的完整链路过了一遍。我个人做这套系统复盘时最大的体会是一份源码最大的价值不在于“能跑”而在于你能讲清楚它的每一个设计决策。数据库里为什么有这个字段、后端接口为什么这样写、前后端对接时哪个配置最要命这些东西想明白了源码才是你的而不是停留在下载文件夹里的一个压缩包。如果你正准备拿这个项目去交课设或者毕设我的建议是别急着改代码先花半天时间照着这篇的思路把表结构和核心流程在纸上画一遍再动手跑项目。你越早把“运行链路”和“设计决策”变成自己的语言后面不管是被老师追问还是被面试官考察都会越来越顺。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑