SpringBoot构建服务器销售管理平台:SKU库存与审批流程实战

发布时间:2026/10/7 16:51:47
SpringBoot构建服务器销售管理平台:SKU库存与审批流程实战 说起企业内部的信息管理平台很多开发者第一反应就是“SpringBoot 项目无非是 CRUD套模板改改就行”。但真等接到“服务器销售”这种单价高、配置杂、审批链长、还要管库存和客户账期的业务情况就完全不一样了。我今年为公司内部从头搭了一套基于 SpringBoot 的服务器销售信息管理平台核心就是把三件事管清楚服务器产品的 SKU 和库存、销售从报价到下单的过程、订单从审批到回款的状态流转。这篇文章就当项目复盘。我会把需求拆解、数据库建模、核心模块实现、部署上线以及踩过的坑全部摊开讲。如果你正准备用 SpringBoot Vue 做企业级后台系统或者正在处理“型号多、价格敏感、流程有审批”的销售业务这篇东西应该比单纯看框架文档更有参考价值。我不打算堆一堆项目截图或者“系统功能图”这类复盘帖真正值钱的是设计决策背后的为什么以及代码落不了地时那些让人挠头的问题。1. 项目到底在解决什么问题1.1 业务现状与需求拆解先交代一下业务背景。这家公司做系统集成客户以政企和中小科技公司为主卖的服务器覆盖塔式、机架式和刀片式品牌涉及主流厂商和国产品牌单台价格从两三千到二三十万都有。以前没有系统的时候销售把服务器配置单发在微信群里报价全靠 Excel仓库有没有货靠打电话问订单走到哪一步也靠挨个催。这样搞的直接后果是同一个配置在不同销售手里报了不同价格客户问交期没人能答得准月底财务对账能对到加班。所以系统的首要目标很朴素不是做多炫的界面而是让“配置、价格、库存、状态”这四件事从靠人记忆变成靠系统记录。具体拆成三类需求业务操作需求销售要能按品牌、型号、CPU、内存、硬盘、GPU 等条件快速找到可售配置能创建报价单并提交审批审批通过后一键转订单。仓库要能维护实际库存和到货批次。财务要能看到应收和已收数据。管理追溯需求每一张报价单和订单都要留下操作日志谁创建、谁改价、谁审批、谁发货必须能追溯。价格不能随便改改价要有审批记录。统计决策需求月底要能按品类、客户、销售员三个维度拉出销量和毛利库存不足时要预警不然销售把货卖出去了仓库根本没货。很多内部系统失败就是因为一开始只把线下表格搬到了线上没有把流程理清楚。我建议任何人在动代码之前先找业务方坐下来把“从客户咨询到财务回款”的整体链路画一遍。我们这个项目最终画出来的主链路是客户咨询 - 销售创建报价 - 业务主管审批 - 客户确认 - 生成订单 - 部门经理审批 - 仓库发货 - 销售跟进回款 - 财务核销。系统所有模块都是围着这条链转的。1.2 为什么选择 SpringBoot 做骨架技术选型其实没有太多悬念。团队最熟的就是 Java而 SpringBoot 恰好把企业内部 Web 系统需要的大部分能力都覆盖了起步快。Spring Initializr 几分钟就能拉出工程内置的自动配置省掉一堆 XML对于以业务逻辑为主的系统开发效率直接决定交付周期。生态完整。持久层可以用 MyBatis-Plus 或 Spring Data JPA权限可以用 Spring Security 或更轻的 Sa-TokenExcel 导出有 Apache POI / EasyExcel参数校验有 Bean Validation。这些库全部在一套 Spring 体系下整合成本很低。交付和运维简单。SpringBoot 打包成可执行 jar 后在内网服务器上一条 java -jar 命令就能跑也可以直接扔进 Docker不需要折腾复杂的应用服务器。对中小型公司的运维能力来说这非常重要。当时也考虑过 Node.js 的 NestJS 和 Python 的 Django这两套做内部系统同样很高效。但公司后续计划把系统接入 ERP 财务模块Java 生态在传统企业软件里更好找维护人选所以最终定了 SpringBoot。这里有个选型上的经验别一上来就上微服务。企业内部管理系统用户量撑死几百人单应用完全是够的。我们当时明确立了规矩模块边界在代码里分清楚但不拆服务。等真到单表百万级数据、并发过千的规模再拆也来得及。过度设计是这类项目最常见的失败原因。2. 功能模块设计与数据库建模2.1 六大功能模块划分需求确认后我把整个平台分成六个模块这个划分不是拍脑袋而是顺着业务主链路来的系统管理用户、角色、菜单、操作日志、数据权限。销售和主管看到的订单范围不一样就是靠数据权限控制。商品中心维护服务器品牌、系列、型号以及每个型号对应的详细配置CPU、内存、硬盘、阵列卡、GPU、电源、机架高度等同时记录成本价、销售指导价、最低限价和当前库存。客户管理客户基本资料、联系人、信用额度、所属行业、跟进记录。服务器销售里客户信用很重要回款能力差的大客户反而容易拖垮现金流。报价管理销售创建报价单里面有明细行、折扣、最终成交价提交后走审批。审批通过后可以一键转订单并保留原报价版本。订单管理从报价转单或直接建单包含订单状态、发货信息、回款计划、关联合同。订单和库存联动下单或审核通过时占用库存发货时扣减库存。统计报表月度销售趋势、产品销量排行、销售员业绩、客户贡献度、毛利汇总、库存预警。这些报表是管理层最看重的部分项目能否验收往往就看这里。模块之间不是各管各的。比如报价单转订单不是简单复制字段而是要重新校验价格是否在限价范围内库存是否足够客户信用额度是否超限。所以设计模块时一定要把“模块之间的交互规则”写清楚。2.2 核心表结构与字段设计要点数据库我选了 MySQL 8.0。字符集统一 utf8mb4排序规则用 utf8mb4_general_ci。建表时有一个最能救命的习惯金额字段一律 decimal绝对不要用 float 或 double。服务器商品表是最核心的表之一。注意它不是简单的“商品名称 价格”而是一张“配置可列举、价格有区间”的 SKU 表CREATE TABLE server_product ( id BIGINT PRIMARY KEY AUTO_INCREMENT, sku VARCHAR(64) NOT NULL COMMENT SKU编码, brand VARCHAR(32) NOT NULL COMMENT 品牌, model_no VARCHAR(64) NOT NULL COMMENT 型号, cpu_conf VARCHAR(255) COMMENT CPU配置, mem_conf VARCHAR(255) COMMENT 内存配置, disk_conf VARCHAR(255) COMMENT 硬盘配置, gpu_conf VARCHAR(255) COMMENT GPU配置, network_conf VARCHAR(128) COMMENT 网络配置, rack_units TINYINT COMMENT 机架高度U数, cost_price DECIMAL(12,2) NOT NULL COMMENT 成本价, list_price DECIMAL(12,2) NOT NULL COMMENT 指导价, min_price DECIMAL(12,2) NOT NULL COMMENT 最低限价, stock_qty INT NOT NULL DEFAULT 0 COMMENT 当前库存, status TINYINT NOT NULL DEFAULT 1 COMMENT 1上架 0下架, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, UNIQUE KEY uk_sku (sku), KEY idx_model (brand, model_no) ) COMMENT 服务器商品表;为什么配置字段用 varchar 而不是拆多张表因为销售和客户沟通时看到的是一整句配置描述比如“2U 机架式 / 双路至强 6330 / 256GB 内存 / 4TB*4 企业盘”。拆太细反而让录入成本变高。但如果你要做“按内存容量搜索”这种需求建议加一个 JSON 字段存结构化配置用 MySQL 的 JSON 函数配合普通字段一起查询两条腿走路。订单表和订单明细表也是核心。服务器订单经常一台机器就是一个订单但明细表一定要存在因为客户可能一次性采购多批次不同批次型号不同。订单明细里要冗余商品快照信息包括型号、配置、单价。这么设计是因为商品表的价格和配置会变但订单上的历史信息不能跟着变这就是“业务快照”思想。报价单表和订单表结构类似但多一个 version 字段和 audit_status 字段用来区分不同报价版本和审批状态。客户对价格不满意销售改了价再提交就是 insert 一条新 version而不是覆盖旧记录。这样以后能随时看价格谈判轨迹。2.3 状态机与业务审批流转服务器销售里最忌讳的就是订单状态想改就改。我们直接把状态设计成了状态机用枚举类统一管理public enum OrderStatus { DRAFT(0, 草稿), SUBMITTED(1, 待审批), APPROVED(2, 已审核), DELIVERED(3, 已发货), COMPLETED(4, 已完成), REJECTED(5, 已驳回), CANCELLED(6, 已取消); }所有状态流转都封装在 OrderService 里Controller 只能调用方法不能自己 set 状态。比如“驳回”只能从 SUBMITTED 流转“发货”只能从 APPROVED 流转逻辑上非法跳转直接抛业务异常。这样看起来多写了一点代码但能防止鬼畜式状态变动。在数据库里状态字段用 tinyint不用 varchar 存中文。枚举名称和数字的对应关系写死在枚举类上查询结果展示时再转成中文这样既节省空间也不会出现“待审”“审批中”“审核中”这种同一含义多种写法。订单表还要加一个 audit_user_id 和 audit_time做审批溯源。我后来给系统做数据修复时发现很多订单的审批人信息都是空的就是因为表设计阶段没有强制约束审批完成必须写审批人这个教训非常实际。3. 技术选型与工程架构3.1 分层架构与包结构实际编码时我搭的工程结构没有用什么高大上的微服务框架就是标准的单体分层但每个层职责要清楚com.example.server-sales ├── common // 统一返回体、全局异常、工具类 ├── config // 配置类MyBatis-Plus、Sa-Token、CORS、Jackson ├── controller // 接收请求参数校验返回 Result ├── service // 业务逻辑层事务边界 ├── mapper // MyBatis-Plus Mapper 接口 ├── entity // 数据库实体 ├── dto // 入参和出参对象 └── enums // 枚举集合分层最关键的纪律是Controller 里不写业务逻辑只做“收参-校验-调 service-返回”。Service 里不写 SQL通过 Mapper 操作。这样做的原因很实在业务逻辑一旦被塞进 Controller后面加审批、加库存校验时你就是拿着放大镜在几百行接口方法里捞代码谁维护谁想骂人。持久层我用了 MyBatis-Plus而不是原生 MyBatis。原因很简单这个项目百分之六十以上的操作是单表 CRUD 和分页MyBatis-Plus 的 LambdaQueryWrapper 能少写大量 XML。但涉及到订单、客户、商品的联合报表仍然手写 SQL保证复杂查询可控。项目里两类技术并存并不冲突关键是使用场景要分清楚。实体类上统一加了 TableLogic 做逻辑删除。企业销售数据不能物理删除删了以后审计和财务对账直接变成无底洞。所有删除操作都变成 update 设置 deleted1查询时 MyBatis-Plus 自动过滤。3.2 统一返回体与全局异常处理不同接口如果各返回各的结构前端对接会非常痛苦。我定义了一个全局统一返回体 Resultpublic class ResultT { private int code; // 0 成功非 0 业务异常码 private String msg; // 提示信息 private T data; // 数据负载 private long timestamp; }配合 RestControllerAdvice 做全局异常处理。业务异常、参数校验异常、数据库异常分别处理前端拿到 Result 后只看 code 和 msg。这样做最直接的好处是前端不用每个接口写一套错误处理后端想加日志、加埋点也只要在这一层统一处理。全局异常里一定不要把敏感技术信息直接抛给前端。比如数据库连接失败内部日志要记录堆栈但返回给用户的话术只能是“系统繁忙请稍后再试”。这一点在做企业内网系统时尤其重要用户不会因为你暴露了 SQL 错误信息就觉得你技术强只会觉得你系统烂。3.3 登录鉴权与数据权限控制登录鉴权这块我没有用 Spring Security 那一整套重型方案选的是 Sa-Token。Sa-Token 对内部系统来说足够轻量登录后签发 token拦截器统一校验还支持踢人下线。Spring Security 功能强大但配置成本和学习成本对这个项目来说偏高很多时候杀鸡用了牛刀。数据权限是内部销售系统的敏感点。普通销售登录后只能看自己创建的客户和订单销售主管能看本部门财务和总经理看全部。实现上不搞复杂的规则引擎就在用户表加一个部门字段和一个角色字段查询时根据角色拼接数据权限条件。比如if (userRole Role.SALES) { queryWrapper.eq(Order::getCreateUserId, currentUserId); } else if (userRole Role.SALES_MANAGER) { queryWrapper.in(Order::getDeptId, user.getDeptId()); }数据权限控制一定要在服务层做不能只在前端隐藏按钮。很多系统被内部员工绕过页面直接调接口看到全量数据就是只做了前端权限没做后端权限。4. 核心业务实现实录4.1 商品与库存管理实现商品管理是基础模块但它一点都不简单。服务器这种 SKU 有一个明显特点同一条产品线的不同配置价格差异极大而且库存是分批次到货的。只是简单设计一个 stock_qty 字段完全不够仓库还要看到“这批货到了多少”“哪些配置库存被冻结”。我们设计了库存流水表 stock_log每条记录绑定 product_id、变更数量、变更类型入库、订单占用、发货扣减、取消释放、盘点调整、关联业务单号。这样月底盘点时库存对不上就能顺着流水查到哪里出了问题而不只是看一个干巴巴的数字。下单占用库存是我特别想强调的一步。假如客户下了 10 台服务器仓库里正好有 10 台等到发货时发现其中已经被别的订单占用那就只剩扯皮了。所以在订单审核通过时就调库存占用接口把货锁住发货时才真正扣减订单取消时释放占用。这个机制在并发高的场景下尤其重要。MySQL 里做扣减我加了条件更新防超卖int count serverProductMapper.update(null, new LambdaUpdateWrapperServerProduct() .setSql(stock_qty stock_qty - {0}, quantity) .eq(ServerProduct::getId, productId) .ge(ServerProduct::getStockQty, quantity)); if (count 0) { throw new BizException(库存不足或商品已下架); }这个 update 是原子操作不会出现两个事务同时把库存扣成负数。很多新手写代码时喜欢先 select 再判断后 update在高并发下分分钟超卖。省略了一步 select性能和安全都赚了。4.2 订单下单与事务控制订单模块是整个平台的心脏。从报价单转订单时业务规则很多要校验审批是否通过、客户信用额度是否足够、订单明细价格是否低于最低限价、库存是否可占用整串操作必须在一个事务里完成。Service 方法上直接用 Transactional 标注。事务边界放在 Service 层而不是 Controller是因为一个订单创建涉及订单主表、订单明细表、库存占用表、操作日志表四张表的变更任何一个环节失败前面写的都不能留。Transactional(rollbackFor Exception.class) public Order createOrderFromQuotation(Long quotationId) { Quotation quotation quotationService.getById(quotationId); if (quotation.getAuditStatus() ! AuditStatus.APPROVED) { throw new BizException(报价单未通过审批不能转订单); } Order order new Order(); order.setCustomerId(quotation.getCustomerId()); order.setTotalAmount(quotation.getTotalAmount()); order.setStatus(OrderStatus.DRAFT); orderMapper.insert(order); ListQuotationItem items quotationItemMapper.selectList( new LambdaQueryWrapperQuotationItem() .eq(QuotationItem::getQuotationId, quotationId)); for (QuotationItem item : items) { OrderItem orderItem new OrderItem(); BeanUtils.copyProperties(item, orderItem); orderItem.setOrderId(order.getId()); orderItemMapper.insert(orderItem); } logService.log(创建订单, 报价单转订单, quotationId, order.getId()); return order; }用 Transactional 时容易犯一个错误事务内部 try-catch 把异常吞掉。比如你在 Service 里捕获了异常打日志然后继续往下走这时候 Spring 根本不知道事务要回滚脏数据就落库了。我踩过一次坑查了半天没找到问题最后才意识到是把异常吃掉了。所以业务代码里要抛出去统一由全局异常处理事务回滚才会生效。订单列表的分页查询我用的是 MyBatis-Plus 的分页插件 PaginationInnerInterceptor。分页要特别注意大偏移量问题比如第 100 万条数据 LIMIT 1000000, 20 非常慢。但企业内网几千条订单完全不需要过度优化只要记住加好索引就行。订单表索引我加了 create_time、customer_id、status、create_user_id这几种组合查询基本都能覆盖。4.3 统计报表与 Excel 导出统计报表在内部系统里是“存在感最强”的功能。管理层不看你的架构多优雅只看月底能不能快速导出一张准确率 100% 的销售表。我实现报表的思路是复杂统计全部走手写 SQL不用 MyBatis-Plus 强行拼接数据量小就直接实时查数据库数据量大了再做汇总表。举个实际例子月度销售统计要按销售员分组汇总成交金额和毛利SELECT u.real_name AS sales_name, COUNT(DISTINCT o.id) AS order_count, SUM(o.total_amount) AS total_amount, SUM(o.total_amount - o.cost_amount) AS gross_profit FROM sales_order o LEFT JOIN sys_user u ON o.create_user_id u.id WHERE o.status IN (3, 4) AND o.create_time BETWEEN #{start} AND #{end} GROUP BY o.create_user_id ORDER BY total_amount DESC这里需要走 LEFT JOIN 而不是 INNER JOIN因为有种边界情况是订单的创建人可能在系统里被离职删除了但历史订单还得出现在报表里。用 LEFT JOIN 就算员工信息缺失订单数据也不会丢。Excel 导出我用的 EasyExcel比 POI 好用太多。大数据量导出时用 EasyExcel 的 write 方法配合分批查询可以做到几十万行数据导出不爆内存。导出接口要设计成异步的生成文件后给下载链接。同步导出遇到大数据量会让 HTTP 请求超时用户体验极差。后端生成的文件要记得定时清理临时目录文件不清理内网服务器磁盘很快就满了。4.4 部署与打包上线开发完成后部署其实也踩了不少坑。项目用的 SpringBoot 3.x打包命令很简单但实际部署过程有几个关键点构建时用 Maven 的 package 命令在 pom.xml 里配好 spring-boot-maven-plugin 和 finalName生成一个 server-sales.jar。Dockerfile 写得尽量小基础镜像选用 openjdk:17-jdk-slim避免用臃肿的 full 镜像FROM openjdk:17-jdk-slim WORKDIR /app COPY target/server-sales.jar /app/app.jar EXPOSE 8080 ENTRYPOINT [java, -jar, app.jar, --spring.profiles.activeprod]配置上要注意环境隔离application.yml 里放公共配置application-prod.yml 里放内网数据库地址和密码。密码不写死用环境变量注入比如${DB_PASSWORD}这样代码仓库里不会泄露数据库口令。内网部署时建议先跑一遍生产配置文件确认数据库连接池能连上再让同事注册账号做冒烟测试。上线后我用 Prometheus Grafana 盯了一段时间 JVM 内存和接口响应时间虽然这个体量不需要很复杂但至少要知道系统是不是活着。5. 常见问题与排查速查5.1 几个高频问题做这个项目过程中我和同事遇到过不少问题有些问题现在想起来都觉得哭笑不得。我把高频问题整理成一张表照着排查能省很多时间。现象根本原因解决方案订单金额对不上账用了 float 存金额精度丢失金额字段统一改 decimal(12,2)代码里用 BigDecimal 计算前端调接口报跨域前后端分离部署在不同端口写 CorsConfig 配置允许的 origin并按环境区分导入导出时间差 8 小时时区未设置MySQL 默认 serverTimezone 和系统不一致JDBC URL 加 serverTimezoneAsia/ShanghaiJackson 设置日期格式删除数据后列表还能看到逻辑删除但查询没过滤MyBatis-Plus 实体加 TableLogic复杂 SQL 里手动加 deleted0SpringBoot 版本太高老插件不兼容用了过时的 starter 或依赖版本冲突统一用 Spring Initializr 生成的依赖不要盲目复制老项目 pom打包 jar 后静态资源 404前端构建产物没放进 resources/static 或没配静态路径Vue 打包后会生成 dist拷贝到 resources/static或单独用 Nginx 部署前端5.2 两个印象深刻的 Bug第一个是数据库字段名踩了大坑。我给订单明细表设计字段时用了一个叫 rank 的列用来表示排序序号。在 MySQL 8 里倒没出问题但后来内网数据库用的版本是早期版本rank 在部分语义下不算关键字但加上业务 SQL 里的子查询之后就报了莫名其妙的语法错误。这个问题的教训是字段命名别用 order、rank、select、transaction 这类敏感词前面加前缀更安全比如 sort_no。第二个是事务吞异常导致库存凭空变多。库存回滚接口在 Service 内部 catch 了异常打日志结果事务没有回滚库存被扣了但订单显示创建失败两边数据状态不一致。当时查这个问题花了一整天最后用日志一比对才发现异常被吞了。之后我给项目定了个规矩Service 内部禁止随意 catch 可抛出的业务异常所有业务异常必须继续向上抛由全局异常处理统一响应。6. 后续扩展与我的体会6.1 还能往哪些方向扩展这套系统上线稳定运行后我一直在想它后续能怎么扩展。最直接的是接入企业微信或钉钉通知订单提交审批时自动推消息给主管审批完成后推给销售替代现在还要登录系统看的模式。这个用 SpringBoot 的异步消息加 Webhook 就能做不复杂但体验提升很大。第二个值得做的是价格审批工作流。现在审批只是单一主管节点但真实的服务器销售里低于成本价或高于某个折扣阈值的订单可能需要多级审批。可以引入 Flowable 或 Activiti 这类工作流引擎但我要提醒一句别为了用工作流而用工作流。内部系统如果审批节点不超过三层自己用状态机加审批记录表更好维护直接上工作流引擎反而增加学习和部署成本。第三个是配置兼容性校验。服务器选型最怕的就是客户要的 CPU 和主板不匹配、内存类型和 RAID 卡不兼容。如果后续把配置结构化数据做得更完整可以在选配时做自动校验销售没下单就能发现配置冲突。这个小功能对业务方来说感知极强。第四个是跟财务系统对接。订单回款之后自动生成收款单推送给财务软件减少人工二次录入。这个接口不复杂但对接时对方系统字段文档往往很水要留出足够时间做联调。6.2 个人经验小结最后聊点实在的。做完这个项目我最大的体会是企业级 SpringBoot 系统难点从来不是框架本身而是业务规则能不能被严谨地落到代码里。你先要把业务流程画到连业务员都点头认可再开始建表写代码否则后面返工成本是几何级增长的。第二个经验是状态和金额是两个绝对不能糊弄的地方。订单状态一律状态机控制金额一律 BigDecimal权限一律走后端校验。这三条铁律坚持下来系统很难出大乱子。第三个经验是日志和审计要早做不要等上线后补。操作日志、状态变更日志、价格变更日志一开始就在核心表旁边设计好。很多内部系统上线半年后领导想追责某个历史订单是谁改的结果查不到这种信任危机一次就够。我比较建议你在接手同类需求时先用一周时间把业务调研和表结构设计做扎实再开始写代码。SpringBoot 只负责让你写得快真正决定项目成败的是你对自己业务理解有多深。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询