ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

基于SpringBoot+Vue的电池销售系统开发详解

基于SpringBoot+Vue的电池销售系统开发详解

做系统开发这几年,我帮不少朋友和技术群里的同学捋过SpringBoot+Vue写的业务系统。捋得多了我发现一件事:真正让一套系统显得专业的,往往不是用了多新的技术,而是需求拆解和数据结构设计得够不够贴近真实业务。电池销售系统就是这么一类典型项目,表面看是标准的管理后台,登录、商品、订单、统计、用户权限一应俱全,可一旦你真去管过电池就会知道,它和普通日用品的销售逻辑完全不一样,批次、保质期、售后更换、型号参数,任何一个点没想清楚,后面的表结构和业务流程就得返工。

这篇文章我想把基于SpringBoot+Vue的电池销售系统从头到尾拆一遍,包括业务侧的重点、后端和前端怎么落地、数据库表结构怎么设计、本地怎么跑通、源码和文档怎么用出价值。适合正在做同类型管理系统、准备交作业或交付小项目的人参考,也适合想通过一个完整案例把前后端分离开发过程串起来的人。

1. 电池销售业务的三个特殊点:先想清楚系统到底在卖什么

1.1 电池和普通商品差在哪

普通商品的库存管理,通常只需要一个商品名加一个数量字段就够了,但电池不是这样。电池有明确的物理参数:电压、容量、电池类型、尺寸规格。光一个类型就能拆出铅酸电池、锂电池、镍氢电池、干电池,不同类型的销售对象、存储条件、售后规则都不一样。这些参数如果不在系统里体现,业务方采购时根本没法准确选货。

更麻烦的是批次管理。电池是有保质期和使用周期的,锂电池存放时间过长会影响性能,铅酸电池长期不带电存放可能直接报废。所以库存管理都会要求记录生产批次和生产日期,销售时遵循"先入库先出库"的原则,这些都是普通商品管理系统里没有的。

1.2 售后退换不是附加功能,而是主流程的一部分

电池类商品的售后概率比普通日用品高不少。可能是运输过程磕碰导致外壳变形,可能是库存时间过长性能下降,还有客户买错了型号要换货。退换货流程如果只是简单记一笔"退回货物、退款",那库存、订单、财务数据很快就会对不上。

所以在设计表结构的时候,售后单需要单独一张表,并且要能关联到原始订单和原始商品批次。换货在业务逻辑上等于"退货+新订单"的组合,系统做的时候可以把这两步拆开,但界面层面应该给操作员一个统一的入口。我梳理这类系统时体会很深,很多初版设计都栽在这上面,上线跑了一个月,退货数据一多,库存和订单对不上账,回头再补售后模块,成本就高了。

1.3 用户角色不止是"管理员"

很多管理系统默认就一个管理员账号,页面里所有功能都敞开,演示时看着很全,真放到现场用就不行了。电池销售系统里天然存在三类用户:

  • 管理员:管商品、管价格、管库存调整、看报表
  • 销售员:能开单、能查库存、能录客户,但改不了商品价格
  • 仓库人员:侧重出入库记录和批次管理,不需要销售统计和客户管理权限

这个区别落在实现上就是基于角色的权限控制,后端拦截接口、前端控制菜单按钮,两层都做才算完整。

2. SpringBoot后端落地:分层约定、订单库存逻辑和权限认证

2.1 包结构和分层约定

这个项目用SpringBoot作为后端,整体是经典的三层结构:Controller负责接口,Service负责业务,Mapper负责数据访问,另外会有Entity、DTO、VO三类模型。我的习惯是额外加一个config包,放跨域配置、拦截器配置、MyBatis-Plus分页插件配置。

com.xxx.battery ├── controller ├── service │ └── impl ├── mapper ├── entity ├── dto └── config

Controller里只接收参数、做基础校验、返回统一结果,业务判断全部下沉到Service。这样接口层保持轻薄,后期加缓存、加权限调整,不会到处改代码。数据访问层我倾向于用MyBatis-Plus,单表的增删改查和分页查询几乎不用手写SQL,项目规模不大时开发效率提升非常明显。真正需要自定义SQL的地方往往集中在统计报表、关联查询、库存扣减这几处。

2.2 订单创建与库存扣减的核心逻辑

销售系统的关键流程是"选商品→做订单→扣库存→生成出库记录"。这个流程最容易出错的就是并发下单时的库存超卖。

实现上不能先查库存够不够再执行扣减,因为这两个操作之间有间隙,并发请求会同时看到同一个库存数。正确做法是把判断和扣减放在一条SQL里:

UPDATE product_batch SET stock = stock - #{quantity} WHERE id = #{batchId} AND stock >= #{quantity}

受影响行数如果是0,说明库存不足,整个订单创建事务回滚。这个写法就是典型的乐观锁思路,不靠加锁排队,而是靠条件更新保证数据一致性。

订单创建建议放在一个事务里,用@Transactional注解,保证订单主表、订单明细、库存扣减、出库记录四步要么全部成功,要么全部回滚。订单编号不要用数据库自增ID直接展示给客户,一般用时间戳加随机数,或者日期加当日流水号。

@Transactional public SaleOrder createOrder(OrderCreateDTO dto) { // 1. 校验客户是否存在、商品是否上架 // 2. 对每个订单明细执行带条件的库存扣减 // 3. 累计金额、计算折扣,保存订单主表和明细表 // 4. 生成一条出库记录写入stock_record }

2.3 登录认证:为什么选JWT加拦截器

传统单体后台还在用Session,但前后端分离项目里,前端和后端往往不在同一个域名端口,Session处理起来要额外配置跨域携带Cookie,麻烦。用JWT的思路是登录成功后把用户信息加密签成一个token返回给前端,前端每次请求在请求头里带Authorization,后端用一个拦截器统一解析。

public class JwtInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { // 校验token,解析出用户id和角色,放入请求上下文 // 校验失败时返回401,前端统一跳转登录页 } }

配合拦截路径实现权限控制。简单做法是在WebMvcConfig里对接口路径分组,比如/admin/**需要管理员角色,/sale/**需要销售员角色。权限这块必须做到接口级别,前端菜单显隐只是体验优化,真正的安全边界在后端。

3. Vue前端实现:页面怎么做才不拖后腿

3.1 工程搭建和请求封装

前端用Vue,一般搭配Vue Router做路由、Pinia或Vuex做状态管理、Axios做请求,UI库用Element Plus。后端接口返回格式统一约定成{ code, message, data },Axios在响应拦截器里统一判断code,等于200才取data,非200直接弹错误提示,这样业务代码里就不需要到处写重复判断了。

请求拦截器里统一加token,是前后端分离项目的基本功:

service.interceptors.request.use(config => { const token = getToken() if (token) { config.headers['Authorization'] = 'Bearer ' + token } return config })

路由守卫用来挡未登录的访问:

router.beforeEach((to, from, next) => { if (to.meta.requiresAuth && !getToken()) { next('/login') } else { next() } })

3.2 核心业务页面拆解

商品管理页面:表格分页加多条件筛选,筛选条件一般有商品名称、电池类型、状态。新建商品用弹窗表单,表单里有电压、容量、类型、尺寸等字段,这些字段组合在一起才决定一个SKU。

销售下单页面:上半部分选商品,下方显示已选商品明细,选完后自动带出库存数量和销售价格,数量超过库存时前端直接禁止提交。订单提交成功后跳转订单列表,订单状态从"待出库"流转到"已出库"。

库存页面:要能按批次看到每个商品的采购时间、剩余库存、保质期到期时间,这样操作员才知道哪些批次需要优先出库。前端可以加一个逻辑:到期时间小于30天的批次,在表格里标红提示。

3.3 前后端联调最容易踩的跨域坑

前端开发服务器默认在5173端口或8081端口,后端在8080,端口不同就必然触发跨域。常用解决方案有两个:一个是后端写CORS配置允许指定来源,一个是前端在Vite或Webpack的devServer里配置proxy代理,把/api开头的请求转发到后端。我推荐本地开发用proxy,因为生产部署时通常会用Nginx做同源转发,前端proxy的方式和最终上线形态更接近,遇到问题更容易排查。

4. 数据库设计:整个系统的定盘星

4.1 核心表与关系

数据库是整套系统的底层基础,SpringBoot和Vue写得再流畅,表结构不合理后面全是补丁。核心表我列在下面,照着这个结构设计基本不会返工:

表名作用关键字段
sys_user系统用户username, password, status, role_id
sys_role角色role_name, remark
product_category电池分类category_name, parent_id
product_info商品主表product_name, model, voltage, capacity, type, price, status
product_batch商品批次product_id, batch_no, produce_date, expire_date, stock, purchase_price
customer客户表customer_name, contact, phone, address, level
supplier供应商表supplier_name, contact, phone
sale_order销售订单order_no, customer_id, user_id, total_amount, discount, status, create_time
sale_order_item订单明细order_id, product_id, batch_id, quantity, price, amount
stock_record出入库记录product_id, batch_id, type, quantity, order_no, create_time
return_record退换记录return_no, order_id, product_id, quantity, reason, status

商品主表和批次表分开是有意的。同一个商品型号可能多个批次入库,每个批次的生产日期、采购价、库存量不同,批次表就是解决"同一个SKU多个货源"问题的标准做法。

4.2 电池业务特有的字段设计

product_info表里建议加电压、容量、电池类型、尺寸这几个字段。类型可以用枚举,也可以用字典表,但界面展示和筛选逻辑要统一。product_batch表里的expire_date是核心字段,用来判断批次能不能继续销售,也是报表里"临期库存预警"的数据来源。

sale_order表的status建议用int存状态,而不是直接存中文,例如0待支付、1待出库、2已出库、3已完成、4已退款。后端枚举和数据库表注释里要同步写清楚。订单表还有个容易被忽视的字段:operate_user_id,记录是谁开的单,方便查业绩和做操作追溯。

4.3 初始数据要能撑起演示效果

数据库脚本里一定要包含初始数据,不然前端页面跑起来全是空表格,演示效果大打折扣。初始数据至少要有:一个管理员账号、一个销售员账号,5到10个分类,20个左右电池商品,对应的2至3个批次,3至5个客户,1到2个供应商。商品名称尽量用常见的电动车电池、干电池、纽扣电池、锂电池组这种一眼能看懂的数据。

脚本文件建议分三段写清楚:建库语句、建表语句、插入语句,每段用注释分隔。字符集统一用utf8mb4,避免存emoji或生僻字时出现乱码。

5. 本地跑通源码:环境准备、启动步骤和排坑记录

5.1 环境版本先对齐

我见过太多跑不起来的情况,一半以上是版本不匹配。推荐这一套组合:后端JDK 8或JDK 17,SpringBoot 2.7一般配JDK 8更稳,SpringBoot 3需要JDK 17;Maven 3.6以上;前端Node.js 16或18;MySQL 5.7或8.0。

提示:如果源码里的pom.xml依赖是SpringBoot 2.x版本,不要硬换JDK 17,可能会遇到字节码库的兼容问题,报错会很莫名其妙。

5.2 完整启动步骤

第一步,在MySQL里新建数据库,执行源码里的sql文件,把表结构和初始数据导进去。第二步,打开后端源码,修改application.yml里的数据库地址、账号、密码,确认端口没被占用。第三步,启动后端,看到SpringBoot启动日志没有报错就行。第四步,在前端目录下安装依赖并启动开发服务器:

npm install npm run dev

如果用Vue CLI构建的项目,启动命令是npm run serve。启动后用浏览器访问前端地址,用初始化脚本里的账号登录,第一次登录把默认密码改掉。

5.3 高频报错和处理方法

数据库连不上:优先检查MySQL服务有没有启动、账号密码是否正确,其次检查连接URL里的serverTimezone配置,常见写法是serverTimezone=Asia/Shanghai。

跨域请求被拦截:后端加了CORS还是拦,就改用前端proxy方案。注意前后端二选一,不要两边同时开又配错。

npm install特别慢或者报错:检查是否用了合适的镜像源,同时确认node_modules目录没被之前失败的安装污染,必要时整个删除重装。

前端启动端口被占用:Vite默认5173被占用时会自己换端口,但换完之后如果后端CORS里写死了来源地址,就会出现新的跨域问题,最好直接按proxy方案走。

6. 源码、数据库、文档到手之后怎么用出价值

6.1 三份交付物各自解决什么问题

一套完整的交付物里,数据库脚本解决的是"环境怎么建",源码解决的是"逻辑怎么写",文档解决的是"给别人怎么讲"。拿到东西我建议按这个顺序看:先看需求说明或README里的项目介绍,搞清楚系统给谁用、有哪些角色、处理哪些业务;再看数据库设计说明,理顺表关系;最后才看接口文档,了解每个页面调用了哪些接口。

如果要交作业或者做项目展示,重点不是把每一行代码解释清楚,而是把业务完整串起来。电池销售系统的讲解主线可以这样说:客户打电话要一批电池,销售员登录系统查库存、开销售单,仓库看到待出库订单后批量出库,后台自动扣减批次库存,月底看销售报表和利润统计。这条链路能讲清楚,比逐一遍历菜单有说服力得多。

6.2 设计一条顺畅的演示路径

我建议准备一个五六分钟的演示流程,按业务闭环走:

  1. 用管理员账号登录,展示不同角色登录后菜单的差异
  2. 进入商品管理,新建一种电池商品,设置分类、价格、库存预警值
  3. 进入销售订单,挑选商品下单,确认订单后看库存减少
  4. 打开库存批次页面,展示剩余库存和临期提醒
  5. 录一条退换货记录,看库存回补和订单状态变化
  6. 打开销售统计,展示本月销售额和订单量

这套顺序的价值在于,从"加商品"到"卖商品"到"管库存"到"做售后"再到"看数据",完整覆盖了系统所有核心业务模块,每个环节都能直观看到数据库变化。

6.3 二次开发从哪里切入

如果想把这套系统改成自己的,比较稳妥的路径是先加需求、再做表、再改代码。比如想加一个"客户等级折扣"功能:先在customer表加等级字段,再在下单时根据等级计算折扣比例,最后在订单列表页暴露折扣金额。这样一步步来,不会动到核心结构。也可以把静态报表改成可视化图表,前端引入图表库,后端加一个统计接口,通常一两天就能看到明显效果。

我个人的习惯是,拿到这类项目先别急着换框架,先把原版完整跑通,再把数据库文档打印出来,用笔画一遍表之间的关系,然后按业务需求做增量开发。改到后面对业务理解越深,就越觉得SpringBoot加Vue这套组合虽然基础,但把底层逻辑理顺之后,改出自己想要的功能只是时间问题。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进