ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

Java+Vue电池销售管理系统实战:前后端分离、库存事务与部署全解析

Java+Vue电池销售管理系统实战:前后端分离、库存事务与部署全解析 1. 先搞清楚这是一套什么样的系统1.1 电池销售业务的真实痛点在哪每年一到课程设计或者毕业设计的季节就能看到大量同学在到处找管理系统源码。Java Vue 的组合这两年特别火因为它既有后端的业务逻辑、事务处理、数据库设计又有前端的页面交互、路由控制、状态管理一套做下来基本覆盖了企业级开发的主要环节写进简历里也拿得出手。电池销售系统就是这类项目里很典型的一个选题——表面上看是一个「商品进销存」但真的把需求拆开之后你会发现它牵扯到的问题远比想象的要多。电池这个品类本身有三个很特殊的属性决定了销售系统的设计不能照搬通用的「商品 订单」模型第一个是型号维度。电池不是按「SKU 单品」管理的而是按「型号 规格」管理。比如同样是 18650 锂电池容量有 2000mAh、2600mAh、3500mAh 之分还有平头尖头、带保护板不带保护板的区别。库存和价格必须挂在型号规格这一层而不是笼统地挂在「电池」这个大类上。第二个是批次和日期。电池是有保质期和循环寿命的实体销售中经常会遇到批次管理的问题。虽然课程设计项目通常不会做到完整的批次追溯但数据库设计时预留生产日期、批次号字段是一个很加分的细节。第三个是客户与价格的关系。电池销售中有大量批发客户批发价和零售价往往不一样。所以订单表里的成交单价必须从商品表里冗余出来不能实时去查商品表——否则商品调价之后历史订单的金额就全错了。所以这套系统的核心业务闭环是商品管理电池型号规格→ 客户管理 → 下单 → 扣库存 → 记录流水 → 销售统计。这个链路想清楚之后整个项目的骨架就定了。1.2 系统要覆盖哪些角色和流程从角色来看这个系统通常分两类用户管理员和普通销售员。管理员负责商品维护、客户管理、查看全局报表销售员负责日常下单、查看库存、处理自己的订单。权限控制在真实场景里是必要的但在课程设计或毕设里有些同学会做成「单角色 简单登录」我建议还是做成两套角色——答辩时老师问「为什么需要权限设计」这个问题你能给出清晰回答这就是一个亮点。从流程来看最核心的是销售下单流程选择客户 → 选择电池型号 → 输入数量 → 校验库存 → 计算金额 → 生成订单 → 扣减库存 → 记录流水。这个流程里最容易做错的地方就是「校验库存」和「扣减库存」这两个动作。很多同学直接在 Service 里先查一次库存判断够不够然后插入订单再 update 库存——这看起来没问题实际上在高并发或者多线程环境下会出大乱子。后面我会专门讲这里为什么必须用事务和锁。除了销售主流程还有采购入库的逆流程。采购入库时增加库存并写流水销售出库时减少库存并写流水。只要把「库存变化」这件事单独拿一张表来记录系统就天然具备了对账的基础。1.3 这个项目交付物的正确理解方式标题里写了「源码 数据库 文档」这意味着它不只是一个代码仓库而是一套完整可交付的项目。源码负责功能实现数据库脚本负责环境初始化文档负责让别人能看懂、能复现、能二次开发。我接触过很多拿到这类项目却跑不起来的同学问题往往不出在代码本身而是出在环境割裂数据库脚本用 MySQL 5.7 写的本地装的是 MySQL 8密码加密方式变了导致连不上前端用的 Node 14 启动本地装的是 Node 18依赖装完直接报错后端端口被占用、Redis 没启动、JDK 版本不对……这些问题占了排障时间的一大半。所以这篇文章打算用「做项目的人」的视角把技术选型的依据、数据库拆表的过程、前后端关键实现、部署过程中最容易卡壳的地方全部串一遍。看完你不仅知道这套系统每行代码在干什么还能真正理解为什么这样设计遇到问题怎么排查。2. 技术栈选型的考量为什么偏要是 Java Vue2.1 后端选型的真实逻辑Java 后端在这个项目里几乎没有悬念地选了 Spring Boot MyBatis Plus MySQL这不是跟风而是这套组合在「教学项目」和「企业实际开发」之间达到的平衡点是最好的。Spring Boot 解决的是配置地狱的问题。如果回到 SSH 时代光是一个 Spring 的 XML 配置就能劝退一大半初学者。Spring Boot 的自动配置让项目可以从零快速跑起来需要自定义的地方用 application.yml 就能处理这对课程设计和毕设来说太重要了——因为你的时间应该花在业务逻辑上而不是花在配置容器上。MyBatis 是另一个关键点。国内企业用 MyBatis 的比例相当高尤其是涉及复杂 SQL 的场景它的灵活度比 JPA/Hibernate 高得多。而 MyBatis Plus 又在 MyBatis 基础上补足了单表 CRUD 的痛点BaseMapper 自带增删改查分页插件一行配置搞定这写代码的效率提升不是一星半点。我在实际做这个项目时80% 的数据库操作都用 MyBatis Plus 内置方法解决了只有订单统计、多表关联查询才自己写 XML 里的 SQL。数据库用 MySQL 8 而不是 5.7 的原因很简单MySQL 8 支持窗口函数做「按月统计销售趋势」这类报表会方便很多。虽然这个项目用不到太复杂的窗口函数但既然是新项目没必要选一个即将过时的版本。2.2 前端为什么用 Vue 2 而不是直接上 Vue 3这是个好问题也是很多人在选型时纠结过的问题。Vue 3 已经是主流了Composition API 也让代码组织更灵活但如果你是在做课程设计或毕业设计我反而建议根据你的基础来选。如果你之前完全没接触过 Vue或者项目要求里没有明确指定版本Vue 2 Element UI 的生态是最成熟的。Element UI 的组件文档、案例数量、问题解决方案存量都极其丰富你遇到一个表格操作卡住或者弹窗不显示的 bug搜一下基本都有现成答案。Vue 3 的 Element Plus 虽然也不错但社区沉淀的教程数量还是比 Vue 2 时代少一些对新手更不友好。另外还有一个很现实的原因很多院校的课程大纲里教的还是 Vue 2期末答辩时老师更熟悉的是 Vue 2 的写法。用 Vue 2 做出来的东西老师在代码里一眼能看懂你在干什么用 Vue 3 的 setup 语法老师反而要反应一下。当然如果你已经有 Vue 3 基础直接上 Vue 3 也完全没问题技术栈本身不影响系统功能。这个项目的技术栈最终定下来是后端Spring Boot 2.7 MyBatis Plus 3.5 MySQL 8 Maven前端Vue 2 Vue Router Vuex Axios Element UI鉴权JWTJSON Web Token开发工具IDEA VS Code Navicat这套组合覆盖了前后端分离开发的核心环节而且都是国内企业里真实在用的东西不是那种「只存在于课本里」的技术。2.3 前后端分离项目的基础目录结构拿到源码之后第一件事应该是搞清楚目录结构而不是急着启动。后端代码的典型分包方式是controller接收 HTTP 请求校验参数调用 serviceservice业务逻辑事务边界在这里mapper数据访问层MyBatis Plus 的 Mapper 接口entity数据库表对应的实体类dto前端传参的数据对象跟 entity 分离vo返回给前端的视图对象config配置类比如跨域、MyBatis Plus 分页插件utils工具类比如 JWT 工具、日期工具common统一返回值、统一异常处理、常量前端的基本目录是views页面组件比如Login.vue、OrderList.vuerouter路由配置storeVuex 状态管理api封装所有后端接口请求components公共组件utilsaxios 实例、token 存储等工具layout主布局框架一般是左侧菜单 右侧内容区这个结构本身就是一个「标准答案」。答辩时老师问「你的项目是怎么组织的」你能把这个目录结构复述一遍就已经能说明你真的理解了项目。3. 数据库设计从订单反推出来的表结构3.1 五张核心表是怎么拆出来的数据库设计是整个系统最见功底的部分。很多同学的数据库是「想到哪建到哪」表之间没有外键逻辑字段命名混乱导致后面写代码满头包。我设计这个系统时用的是「从订单反推」的思路——先确定整个业务最核心的事件是「产生一笔销售订单」然后围绕这个事件拆解数据需求。一笔订单发生之前需要什么需要知道是哪个客户买的所以有customer表。需要知道买的是什么商品所以有battery_product表。订单发生之后要记录什么订单编号、下单时间、总金额、经手人所以有sales_order表。一笔订单里可能同时包含多个型号的电池所以订单和商品是多对多的关系必须拆一张order_item明细表出来。库存变化怎么追踪单独建stock_record流水表记录每一次入库和出库的变化。这么一推五张核心表就出来了表名用途核心字段sys_user系统用户用户名、密码加密存储、角色battery_product电池商品型号、类型、容量、电压、单价、库存量customer客户信息客户名称、联系人、电话、地址、客户等级sales_order销售订单主表订单编号、客户 ID、总金额、订单状态、下单时间order_item订单明细表订单 ID、商品 ID、数量、成交单价、小计金额stock_record库存流水表商品 ID、变动类型入库/出库、变动数量、关联订单号凌乱的业务需求一旦落到表上就变得非常清晰了。我在做这个项目时有个体会如果数据库设计阶段花了一整天去推敲后面写代码反而特别快如果数据库随便建一建就动手写代码后面改表结构的时间会让你怀疑人生。3.2 库存流水为什么值得单独建表很多简化版的管理系统会把库存数量直接存在商品表里出库时直接update商品表的库存字段。这样做的问题是你只知道现在的库存是多少但不知道这些库存是怎么变化的。单独建一张stock_record流水表本质上是把「状态」和「变化」分开存储。商品表里的库存是一个「状态」它告诉系统当前还剩多少流水表里记录的是「变化」它告诉系统每一次库存变动的来龙去脉。有了这张表就可以回答很多真实业务里必须回答的问题这个月进了多少货卖了哪些型号哪笔订单导致某个型号库存告急这也是实际企业系统里普遍采用的做法。库存流水听起来是额外的复杂度但长远来看它是系统对账、审计、复盘的基础。对于电池这种有保质期、有批次概念的品类流水表的存在让未来的批次追溯变得可能。3.3 建表时容易忽略的细节字段建表时每个人的习惯不一样但有几个字段我建议不管什么表都加上项目答辩时能体现专业性第一个是创建时间和更新时间。MySQL 里可以直接用datetime类型MyBatis Plus 里通过TableField(fill FieldFill.INSERT)自动填充。有了这两个字段排查数据问题时会轻松很多。第二个是逻辑删除标记。用deleted字段配合 MyBatis Plus 的逻辑删除功能执行 delete 操作时实际上是 update数据不会物理消失。这个设计在企业里的标准做法学生在项目中能主动用上本身就说明对数据安全有意识。第三个是remark备注字段。电池销售过程中有太多临时性的信息需要记录比如「这批 18650 电池是特价清仓」「这个客户要求月底开票」有备注字段可以让系统应对各种非结构化的需求。另外特别提醒一个常见问题金额字段用decimal而不是double或float。浮点数在数据库里的精度问题是经典老坑搞过支付系统的人都知道一分钱能差出天大的事。电池批发订单金额动辄上万用decimal(10,2)是最稳妥的选择。4. 后端实现的几个关键环节4.1 登录鉴权为什么用 JWT 而不是 Session前后端分离的架构下登录状态怎么保持是一个必须解决的问题。传统单体应用的 Session 方案里服务器要保存用户的登录状态前端通过 Cookie 里的 SessionID 来识别身份。但前后端分离之后前端可能跑在 8080 端口后端跑在 8081 端口跨域请求下 Cookie 处理本身就麻烦而且如果未来要做多个服务实例Session 同步更是噩梦。JWT 的思路完全不同。用户登录成功后服务器签发一个加密的 Token 返回给前端前端把它存在本地每次请求时放在请求头里带上。后端通过解析 Token 来确认用户身份不需要在服务端保存任何会话数据。这个项目的 JWT 实现逻辑是这样的public class JwtUtils { private static final String SECRET battery-sale-secret-key; public static String generateToken(Integer userId, String username, String role) { return Jwts.builder() .setSubject(username) .claim(userId, userId) .claim(role, role) .setExpiration(new Date(System.currentTimeMillis() 24 * 60 * 60 * 1000)) .signWith(SignatureAlgorithm.HS256, SECRET) .compact(); } public static Claims parseToken(String token) { return Jwts.parser().setSigningKey(SECRET).parseClaimsJws(token).getBody(); } }Token 的有效期设置为 24 小时前端拦截到 401 状态码时自动跳转到登录页。这个方案写起来简单逻辑清晰答辩时也容易讲明白。4.2 订单事务扣库存和生成订单必须捂在一个事务里这是整个项目最容易出错同时也是最值得深入理解的一个点。前面提到销售下单流程的几个步骤校验库存、生成订单、扣减库存、写流水。如果这些操作不是原子的——比如订单生成了但库存扣减失败或者库存扣了但订单没生成——那么系统就处于一个数据不一致的状态。这种不一致在金额和库存相关的系统里是绝对不允许出现的。Spring 解决这个问题的方式就是Transactional注解。给方法加上这个注解之后方法内的所有数据库操作会放在同一个数据库事务里执行要么全部成功要么全部回滚。但Transactional不是加上就万无一失。有一个非常经典的坑如果库存校验和扣减操作之间有并发问题两个请求同时读到库存还有 10 件都认为可以卖 10 件结果库存变成了负数。解决方案有好几种课程设计层面最容易被忽视但最实用的一种是加数据库行锁Override Transactional(rollbackFor Exception.class) public boolean createOrder(OrderCreateDTO dto) { // 1. 生成订单主表记录 // 2. 遍历商品明细逐个扣库存 for (OrderItemDTO item : dto.getItems()) { BatteryProduct product batteryProductMapper.selectByIdForUpdate(item.getProductId()); if (product.getStock() item.getQuantity()) { throw new BusinessException(商品 [ product.getModel() ] 库存不足); } product.setStock(product.getStock() - item.getQuantity()); batteryProductMapper.updateById(product); } // 3. 写入库存流水 // 4. 返回订单号 }selectByIdForUpdate使用了SELECT ... FOR UPDATE语句查询记录的这一刻就把这条商品记录锁住了其他事务必须等当前事务提交后才能继续操作同一行数据。这是一个在并发场景下非常实用的手段也是面试官喜欢深挖的点。4.3 Controller、Service、Mapper 各层的边界划分分层的本质是「职责分离」。很多初学同学把业务逻辑全写在 Controller 里一个方法几百行看起来也能跑但一旦项目变大代码根本没法维护。我的习惯是这么分的Controller 层只干三件事接收参数、简单校验、调用 Service。它不应该出现任何if (xxx null)之类的业务判断。心里默念Controller 的每个方法应该短到一屏能看完。Service 层是业务逻辑的核心也是事务注解所在的地方。订单生成、库存扣减、金额计算、数据分析这些实际业务规则全都应该在这里。Service 里可以调用多个 Mapper 方法也可以调用其他 Service 的方法但不要写 SQL。Mapper 层只做数据访问。单表操作用 MyBatis Plus 内置方法多表关联或者复杂统计就在 XML 里写 SQL。一个 Mapper 方法只对应一个数据库操作不要在一个方法里塞太多逻辑。这种划分在执行效率上不是最优的——多一层就多一点调用开销——但在可维护性和可理解性上收益巨大。做课程设计或者刚入行写代码养成「职责分明」的习惯比追求性能重要得多。4.4 统一返回值和全局异常处理前端调用后端接口时最不喜欢的就是后端返回的数据格式五花八门有的接口返回 JSON 对象有的返回数组出错时有的返回字符串有的直接报 500。前端对接时每个接口都要单独处理这种体验很糟糕。解决方式是定义一个统一的结果对象比如ResultTpublic class ResultT { private Integer code; private String message; private T data; // 静态方法success(data)、error(message) }所有接口都返回Result前端的 Axios 拦截器里做一个统一的响应处理。成功时code为 200取出data传给页面失败时code非 200弹出message提示用户。这样前端的错误处理代码只写一遍后端接口的返回格式也统一了。全局异常处理用RestControllerAdvice配合ExceptionHandler业务异常抛BusinessException参数校验失败抛MethodArgumentNotValidException未知异常兜底捕获返回通用的服务器错误提示。这样数据库报错、空指针异常这些不该暴露给用户的东西都不会直接摔到前端页面上。5. 前端实现的组织方式5.1 路由、权限和页面骨架前端是一个标准的 Vue 2 单页应用整体页面骨架采用「左侧菜单 右侧内容区」的布局使用 Element UI 的el-container组件搭建。路由配置要区分两种情况不需要登录就能访问的比如登录页以及必须登录才能访问的业务页面。实现方式是 Vue Router 的全局前置守卫router.beforeEach((to, from, next) { const token localStorage.getItem(token); if (to.path /login) { next(); } else if (!token) { next(/login); } else { next(); } });这个逻辑看起来简单但有细节要补充从localStorage里取到 token 只代表「前端认为用户登录过」token 是否过期、是否被篡改必须让后端在接口层面去校验。前端守卫只是用户体验层面的拦截真正的安全边界在后端。菜单和权限的联动方式有两类做法一类是后端根据用户角色返回菜单列表前端动态生成路由另一类是前端写死所有菜单根据角色控制显隐。课程设计项目用后者就足够了因为角色只有管理员和销售员两种菜单差异不大。前者是更「企业级」的做法但复杂度提升明显没有太多必要。5.2 Axios 封装和请求拦截前端跟后端通信的入口统一在api目录下封装一个 axios 实例而不是每个页面直接import axios from axios。所有请求都通过这个实例发出好处是可以统一处理三件事请求头带 token、响应返回统一处理、错误状态码统一拦截。import axios from axios; import { Message } from element-ui; const service axios.create({ baseURL: /api, timeout: 10000 }); service.interceptors.request.use(config { const token localStorage.getItem(token); if (token) { config.headers[Authorization] token; } return config; }); service.interceptors.response.use( response { const res response.data; if (res.code ! 200) { Message.error(res.message || 请求失败); return Promise.reject(new Error(res.message)); } return res.data; }, error { if (error.response error.response.status 401) { localStorage.removeItem(token); window.location.href /login; } Message.error(error.message || 网络异常); return Promise.reject(error); } );注意请求拦截器里把 token 放进了Authorization请求头后端拦截器会从请求头里解析 token。响应拦截器里统一判断code字段成功时直接把data返回给页面方法这样页面里的代码可以很简洁const data await getOrderList(params); this.tableData data;这个封装做好了前端每个页面组件可以少写几十行重复代码。5.3 订单页面从选商品到提交的完整交互订单功能是前端最核心的页面也是交互最复杂的一块。页面上通常分几个区域第一步是客户选择。用el-select下拉框支持远程搜索——输入关键字调后端接口模糊查询客户名称选中后显示客户详情。第二步是选择商品。推荐用表格加「添加」按钮的方式点击添加后弹出一个商品选择对话框展示商品列表支持按型号、类型过滤选中后填充到订单明细表格里。明细表格每一行包含商品型号、库存余量、单价、数量、小计金额数量列用el-input-number控制设置最小值 1最大值不超过库存。第三步是金额计算。监听明细行的数量变化动态重新计算每行的小计和订单总金额展示在页面底部。这一步要特别注意前端展示的金额只是「预估金额」真正权威的金额必须以服务端计算为准。所以哪怕前端把金额算得很精确提交订单时后端仍然会重新遍历明细、重新算一遍金额。前后端都计算的意义在于前端即时反馈后端保证正确。第四步是提交订单。点击提交后调用后端创建订单接口成功后清空明细并跳到订单列表页同时在列表页刷新出最新订单。整个流程在el-dialog或单独页面里完成都可以我比较建议用单独页面代码逻辑清晰也方便后续跳转。5.4 数据统计页面的 ECharts 图表展示销售管理系统的「管理」二字很大程度体现在数据统计上。这个项目里我做了三个图表近 7 天销售趋势折线图、电池类型销售占比饼图、各型号销量排行柱状图。图表用 ECharts 实现推荐按需引入而不是全量引入否则打包体积会大不少。统计接口的后端实现是典型的聚合查询SQL 里按日期分组select idselectSalesTrend resultTypemap SELECT DATE(create_time) AS date, SUM(total_amount) AS amount, COUNT(*) AS order_count FROM sales_order WHERE create_time #{startDate} AND create_time lt; #{endDate} GROUP BY DATE(create_time) ORDER BY date /select前端拿到聚合数据后直接组装成 ECharts 需要的格式。图表渲染的常见坑在于容器没有高度时图表不显示、数据更新后图表不刷新。解决方式是给图表容器设置固定高度数据变化后用setOption并传入notMerge: true。6. 从源码到能跑起来环境配置与部署指南6.1 后端启动前必须改的几个配置很多同学拿到源码的第一步是兴奋地双击启动然后被一堆报错泼冷水。后端启动失败的绝大多数原因集中在配置文件上。打开src/main/resources/application.yml你需要确认这几个配置项数据库连接相关spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/battery_sale?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/Shanghai username: root password: 你的数据库密码有几点提醒数据库名battery_sale要先用 Navicat 或命令行执行建库脚本创建serverTimezoneAsia/Shanghai必须要加否则报时区错误MySQL 8 的驱动要用com.mysql.cj.jdbc.DriverMySQL 5.7 用的com.mysql.jdbc.Driver在新驱动里会报错。端口默认是 8080如果你本机有别的服务占用了 8080改成 8081 之类不冲突的端口。Token 密钥、Token 过期时间这类配置在application.yml里可以预留出来不要写死在代码里。6.2 前端开发和构建的关键命令前端项目拿到手依次执行这几条命令就能跑起来npm install npm run devnpm install安装依赖时经常会卡住或者报错。常见解法是切换到国内镜像源。比如用npx cnpm或者配置.npmrcregistryhttps://registry.npmmirror.com如果安装过程中出现 node-sass 相关的报错大概率是 Node 版本和 node-sass 版本不匹配。这是前端项目最经典的坑解决方案是查看package.json里声明的 node-sass 版本然后确认本地 Node 版本与之兼容。最省事的方式是安装项目作者推荐的 Node 版本。npm run dev启动后默认监听 8080 端口但这里有个前后端联调的关键问题前端页面跑在 8080后端接口在 8081浏览器会直接发起跨域请求。开发环境的跨域问题通过 Vite 或 Vue CLI 的 devServer 代理配置解决把/api前缀的请求转发到后端地址。生产环境的跨域问题则在 Nginx 反向代理层解决——前端静态文件和控制台由 Nginx 处理/api开头的请求转发到 Java 后端端口。6.3 前后端整合部署的一次完整演示前面讲的是开发环境的启动方式如果你希望把整个系统部署到一台服务器上方案是这样用 Maven 打包后端项目执行mvn clean package -DskipTests在target目录下得到一个 jar 包服务器上执行java -jar battery-sale.jar就能启动后端。前端执行npm run build打包产物在dist目录里。把dist文件夹里的静态文件放到 Nginx 的html目录下Nginx 配置里增加反向代理规则location /api/ { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; }这样部署完成之后用户访问 Nginx 的 80 端口页面加载前端静态文件接口请求自动转发到后端的 8080 端口。前后端不再有跨域问题因为所有请求的域名和端口在浏览器看来都是同一个。如果不想用 Nginx也可以用 Spring Boot 直接托管前端静态文件——把dist目录复制到项目的src/main/resources/static下再重新打包后端 jar 包就同时包含前后端代码。这种方式部署更简单但不符合前后端分离的最佳实践自己练手可以项目里演示一下就够了。7. 实际开发中踩过的坑每个都是血泪换来的7.1 日期时间字段的时区问题这个坑我印象太深了。数据库里存的create_time是北京时间但查询结果返回给前端时发现所有时间都多了 8 个小时。原因就是 JDBC 连接串没有指定时区默认用了服务器的 UTC 时区。解决方案就是配置里加serverTimezoneAsia/Shanghai同时把 Jackson 反序列化的时区也设置成东八区spring: jackson: time-zone: GMT8另一个相关的问题是日期格式。前端表格里显示的是类似于2024-06-01T10:30:00这种带字母 T 的格式不够友好。解决方案是在实体类的日期字段上加JsonFormat注解JsonFormat(pattern yyyy-MM-dd HH:mm:ss, timezone GMT8) private LocalDateTime createTime;这类问题对系统的功能不产生致命影响但就是这些细节决定了别人愿不愿意用你的系统。7.2 跨域配置的两个方案跨域问题几乎每个前后端分离项目都会遇到。浏览器控制台刷出一片红色的 CORS 报错是很多同学最崩溃的时刻。解决方案有两个。开发阶段最推荐是配置前端 devServer 代理因为代理模式下浏览器认为所有请求都是同源的根本不触发 CORS。做法是在 Vue CLI 的vue.config.js里配置module.exports { devServer: { proxy: { /api: { target: http://localhost:8080, changeOrigin: true, pathRewrite: { ^/api: } } } } };同时后端接口路径不要写/api前缀前端请求写/api/xxx代理把/api去掉后再转发到后端。这样前端代码和后端接口互不干扰联调体验最顺。第二个方案是后端加 CORS 配置。用CrossOrigin注解或者实现WebMvcConfigurer接口统一配置允许的跨域来源。这个方案可行但需要把允许的来源、请求方法、请求头都配清楚。写错了不但不能解决跨域反而会引入新的安全问题。实际项目里我一般是「开发用代理部署用 Nginx后端 CORS 兜底」。7.3 数据库字段命名导致的 MyBatis 映射问题这是一个非常隐蔽的坑。MySQL 里字段命名通常是snake_case比如customer_nameJava 实体类属性是customerName。MyBatis Plus 默认开启了下划线转驼峰的映射规则所以大部分情况下能自动对应。但如果某个字段的命名没按规范来或者 SQL 查询使用了别名结果集列名和实体属性对应不上就会查出null。这类问题排查起来特别难受因为代码不报错只是字段值是空的。排查思路是先去数据库执行一遍同样的 SQL 看结果集再检查实体类属性和查询结果列名的对应关系。很多同学在这个问题上耗掉半天时间最后发现只是少写了一个别名。7.4 前端表格里操作按钮的权限控制前端列表页常见操作是「编辑」「删除」「查看详情」。在销售系统中不同的角色应该看到不同的操作按钮。比如普通销售员只能查看自己的订单不能删除订单而管理员可以作废异常订单。这个权限控制的实现方法很简单Vuex 里存储当前登录用户的角色信息模板中通过指令或者v-if判断el-button v-ifrole admin typedanger sizemini删除/el-button el-button v-ifrole admin typewarning sizemini作废/el-button但要注意的是前端按钮隐藏只是「体验层」的权限控制真正的权限校验必须放在后端。否则一个懂点技术的人直接调接口就能删订单了。后端每个接口都应该通过注解或代码校验当前用户的角色权限双管齐下才是安全的做法。7.5 文档与源码保持一致才是最难的标题里提到了文档这也是我最后想专门说的一点。项目的文档一般包含三份需求文档、数据库设计文档、系统部署说明文档。课程设计的文档往往是一份大而全的「说明书」毕业设计则要求更规范一些。我的经验是写文档最忌讳的是「写完代码再回忆着写」。正确做法是开发过程中随手记录——新建一张表记录一下字段含义写完一个接口记录一下入参出参和业务规则跑通一个页面记录一下操作流程。比如在订单事务那一节除了说明Transactional的作用还会配上「SELECT ... FOR UPDATE 为什么能防止并发超卖」这段排查过程这类内容是普通资料里不容易找到的。文档里要特别包含的是如何初始化数据库、如何修改配置、如何启动前后端这三个部分是让其他同学能把项目跑起来的前提条件。很多时候源码是对的但文档里写的数据库密码过期了、前端 Node 版本提示要求不对照做就跑不起来。把这类文本写好你的交付物才是真正可复用的。最后再分享一个小技巧如果你拿到的项目里没有数据库初始化脚本千万不要自己手动建表。手动建表不仅耗时而且还容易漏掉字段。正确的做法是让后端框架自己完成建表在application.yml里临时开启 MyBatis Plus 的自动建表和自动填充mybatis-plus: global-config: db-config: table-prefix: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl配置完之后启动一次项目控制台会输出所有的建表日志和 SQL 执行日志。这些日志既可以帮助你确认表结构是否创建成功也可以帮助你调试接口执行的具体 SQL 语句。查完再把日志打印关掉就好。这个技巧在你拿到任何一套 Spring Boot MyBatis 项目源码时都通用。说到底像电池销售系统这样的管理类项目真正的价值不在于功能有多炫而在于你通过它理解了完整的前后端数据流前端页面发起请求路由到后端 ControllerService 处理业务逻辑Mapper 操作数据库数据再反向流回前端渲染。把这条链路吃透了你会发现绝大多数管理系统本质上都是在做同样的事。下一次即使换一个业务场景你也能很快从这套骨架里迁移过去。
返回列表