ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

二手车销售管理系统实战:Spring Boot+MySQL+业务设计全复盘

二手车销售管理系统实战:Spring Boot+MySQL+业务设计全复盘 做二手车销售管理系统这个项目的时候我第一反应不是急着建工程、写接口而是先想明白一个问题这类系统跟普通进销存到底差在哪。二手车行业最核心的资产是车辆信息但真正决定成交的其实是客户的跟进过程。一辆车从收进来到卖出去中间要经历入库、定价、拍照上架、客户咨询、预约看车、议价、成交过户链条特别长而且每一环都依赖销售人员的线下动作。如果系统只做车辆登记和展示那跟Excel表格没什么区别没法解决管理问题。所以这个系统的关键不是做出多少页面而是能不能把业务链条完整串起来车源信息存下来客户线索跟着走销售跟进留痕成交数据可回溯。用Spring Boot做后端服务MySQL存业务数据几乎是这类项目最稳妥的组合。Spring Boot的自动配置和starter机制能快速搭出可运行的工程MySQL关系型模型天然适合描述车辆、客户、订单之间的关联关系。这篇文章我会从需求分析、表结构设计、核心接口实现到部署踩坑完整复盘整个系统的搭建过程。不管你是拿它做毕业设计、课程设计还是公司内部真要落地一套二手车销售管理工具里面的设计思路和实现细节都可以直接参考。1. 需求梳理二手车销售系统到底要管什么1.1 业务场景与核心痛点先还原一下二手车行的日常。店里收了一台车评估师记录车况、给出收购价行政人员拍照录入系统销售顾问开始对外发布车源信息。接下来客户通过网站、门店、朋友介绍等各种渠道来咨询销售得记下客户意向、定期回访、约看车、谈判价格最后成交过户财务收款开票。这中间最痛的点有三个。第一车辆信息分散。这台车什么配置、什么车况、收购价多少、挂售价多少、放了多久如果靠人记或者靠Excel管理很容易出错尤其车多了以后库存情况根本摸不清。第二客户跟进全凭自觉。销售手头几十个客户哪个该回访了、哪个有强烈意向、哪个已经跟丢了没有系统提醒和记录完全依赖个人习惯公司层面无法管控。第三经营数据不透明。老板想知道这个月卖了几台车、毛利是多少、哪款车卖得快如果靠月底人工汇总既慢又容易漏。针对这些痛点这个系统的功能边界就很清晰了车辆全生命周期管理、客户线索管理和跟进记录、销售数据统计。再往细了拆分就是车源登记、车辆上下架管理、客户信息维护、跟进记录写入、预约看车安排、成交订单生成、基础数据报表这几大块。1.2 功能模块拆解与优先级我习惯先把功能按优先级分层优先保证核心闭环跑通再做辅助功能。第一优先级是车辆管理和客户管理。车辆管理要支持录入车辆基础信息品牌、车型、年份、里程、排量、变速箱、颜色、收购价、销售价、车况描述、上传车辆图片、修改车辆状态在库、已预约、已售、下架。客户管理要记录客户姓名、电话、意向车型、意向预算、客户来源网站咨询、门店到访、老客转介绍同时给客户打上意向等级标签方便销售重点跟进。第二优先级是跟进和预约。跟进记录是销售过程管理的核心每次电话、微信聊天、到店接待都要留下记录并且设置下次跟进时间。预约看车要关联客户和车辆安排具体时间支持反馈看车结果。第三优先级是订单和统计。客户成交后生成销售订单记录最终成交价、合同编号、经手销售。统计报表至少要有本月销量、本月销售额、库存车辆数、各品牌销售占比、各销售员业绩排名。至于用户权限简单一点就分两个角色管理员和销售员。管理员可以看全部数据、管理车辆上下架、调整价格销售员只能看自己录入的客户和跟进记录可以新增车辆信息但不能下架车辆。这个权限模型在课程设计和中小型企业内部都够用了。2. 技术选型与项目分层设计2.1 为什么是Spring Boot MySQL MyBatis-Plus技术选型这块不追求新追求稳。Spring Boot我用的2.7.18。这是Spring Boot 2.x最后一个稳定版本坑少、资料多、兼容性好。Spring Boot 3虽然已经出来很久但对JDK版本有要求必须17很多教学环境和服务器上的JDK还是1.8没必要给自己挖坑。如果公司环境允许用3.x也没问题但2.7.18做这类管理系统绰绰有余。持久层框架选了MyBatis-Plus。为什么不用原生MyBatis因为这个项目里有大量单表CRUD操作用原生MyBatis要写一堆重复的Mapper XML效率低。MyBatis-Plus提供BaseMapper内置了insert、selectById、updateById等常用方法分页插件也成熟复杂的多条件查询再用自定义SQL解决。两者结合开发速度提升一大截。数据库用MySQL 8.0。MySQL 5.7目前已经停止官方维护新项目没必要再用老版本。8.0在性能、JSON支持、窗口函数等方面都有明显优势而且安装配置资源多遇到问题容易搜到答案。数据库连接池用HikariCPSpring Boot 2.x默认集成性能好、配置简单不用额外引入Druid。前端这块如果做纯后端管理系统的项目我建议用Thymeleaf模板引擎加Bootstrap服务端渲染开发效率高不用单独搭前端工程。如果公司有专门的前端资源那可以拆成前后端分离Spring Boot只提供RESTful API前端用Vue或React。本文后面讲的是单体方案更适合个人开发者或者小团队自己维护。2.2 项目结构规划与分层职责项目结构我按经典的分层架构来拆controller、service、mapper、entity四层再加config、common、dto几个辅助包。这样的好处是职责清晰后期维护不迷路。src/main/java/com/example/carsales/ ├── controller/ # 接口层接收请求、参数校验、返回结果 │ ├── CarController.java │ ├── CustomerController.java │ ├── FollowUpController.java │ ├── OrderController.java │ └── UserController.java ├── service/ # 业务逻辑层核心业务判断都在这里 │ ├── CarService.java │ ├── CustomerService.java │ └── ... ├── mapper/ # 数据访问层MyBatis-Plus的Mapper接口 ├── entity/ # 实体类对应数据库表结构 ├── dto/ # 请求/响应对象避免实体类直接暴露 ├── config/ # 配置类拦截器、跨域、MyBatis-Plus分页插件等 ├── common/ # 通用结果封装、异常处理、工具类 └── CarsalesApplication.java分层的核心原则是Controller只做参数接收和结果返回不写业务逻辑Service承载业务规则Mapper只做数据读写。比如车辆下架操作Controller拿到车辆ID后调ServiceService里先查车辆是否存在、当前状态是否允许下架、再执行更新最后返回统一结果对象。如果有人图省事把业务判断写在Controller里短期看代码量少后期一旦加需求就会非常痛苦。统一返回结果这个细节很多人会忽略。我建议定义一个Result 类包含code、message、data三个字段成功返回200失败返回具体错误码。这样前端或者调用方只需要判断code就能知道请求是否成功不用每次解析HTTP状态码。异常处理统一用RestControllerAdvice做全局异常捕获业务异常、参数校验异常、未知异常分别处理避免把异常堆栈直接抛给调用方。3. 数据库设计7张表撑起整个业务3.1 核心数据表结构与设计思路数据库设计是整个系统的地基。我每设计一张表都会想一个问题这张表要回答什么业务问题用户表sys_user存登录账号和权限信息。密码字段用BCrypt加密存储不要明文存这是最基本的底线。角色字段用字符串区分admin和staff简单直白不引入复杂的权限框架。CREATE TABLE sys_user ( id BIGINT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(100) NOT NULL, real_name VARCHAR(50), role VARCHAR(20) DEFAULT staff, status TINYINT DEFAULT 1, create_time DATETIME DEFAULT CURRENT_TIMESTAMP );车辆表car_info是系统最核心的表字段多而且关键字段要建索引。品牌、车系、年份、里程、排量、变速箱、颜色、收购价、销售价、车辆状态、上架时间、描述、图片地址。状态字段用TINYINT0表示在库1表示已售2表示下架用数字比用字符串状态更高效也方便做条件筛选。客户表customer存客户基础信息和意向信息。电话字段一定要建索引因为销售查客户基本都是按电话号码搜。意向等级level字段用1到3表示高、中、低方便销售按重要程度排序跟进。跟进记录表follow_up每一条记录关联一个客户和一个销售员记录跟进内容、下次跟进时间。这个表的数据量会比较大所以要建联合索引(customer_id, create_time)查某个客户的跟进历史时性能有保障。预约看车表appointment关联客户和车辆记录预约时间、状态待确认、已到店、已取消、已完成。销售订单表sale_order记录成交信息。订单编号order_no要唯一建议用业务规则生成不用自增ID。成交价deal_price单独存因为实际成交价往往低于挂牌价这部分差价就是议价空间。车辆调价记录表price_log每次车辆价格变动都记录旧价、新价、操作人、原因。这个表特别重要二手车价格波动频繁如果调价没有记录后面对账时会说不清楚。3.2 字段设计中的细节与索引规划字段类型的选择容易踩坑。价格字段不要用FLOAT或DOUBLE准确性问题会在金额计算时暴露必须用DECIMAL(10,2)。里程字段用INT单位统一为公里。日期时间字段用DATETIME注意连接参数里要设置serverTimezoneAsia/Shanghai避免时区偏差。图片字段存储我建议只保存图片URL地址不要用BLOB把图片二进制塞进数据库。原因很简单BLOB字段会让表体积膨胀查询性能下降而且应用服务器和数据库之间的网络传输压力会大。图片文件存在本地磁盘或OSS对象存储上数据库只保存资源路径。索引规划上car_info表给brand、status建索引高频查询是按品牌和状态筛选。sale_order表给sale_date和user_id建索引月度统计和按销售员排名的查询会用到。customer表给phone建唯一索引。建索引要克制不要给每个字段都建MySQL里索引不是越多越好写操作会被拖慢而且占磁盘空间。有一类典型问题查询慢。排查思路是先EXPLAIN看执行计划看有没有走索引、扫描了多少行。我记得做过一个统计报表接口按月份分组统计销量数据量才几千条就要几百毫秒。EXPLAIN一看发现查询条件里的状态字段没走索引因为表里建了索引给的是statusTINYINT但SQL里用了status ! 1导致全表扫描。改法是把不等值条件改写为等值条件或者直接建覆盖索引。4. 核心业务逻辑的实现细节4.1 车辆管理入库、上下架与价格调整车辆入库逻辑不算复杂但有一步容易忽视自动生成车辆编号。我采用规则CAR-年月日-四位序号比如CAR-20250318-0001。具体实现是用一个DayCounter表或者查询当天已有车辆数加1然后用String.format补零。这样生成的编号在业务沟通里直观好用销售报编号就知道是哪天收的车。上下架操作要写状态流转校验。车辆状态是一个有限状态机在库→上架→预约中→已售在库/上架→下架。状态流转必须校验合法性不能允许已售车辆直接再上架。MyBatis-Plus的updateById可以做更新但要注意在Service层先查一次当前状态再判断是否允许流转不能盲目更新。价格调整这块二手车跟新车不一样价格变动频繁。调价要记录原因比如车况整备完成加价2000或者库存超60天降价促销。每次调价写一条price_log记录同时更新car_info表的当前销售价。如果后面要分析调价频率是否有助于成交这个表就是数据基础。4.2 销售线索与跟进记录的设计客户从录入到成交中间围绕的核心是跟进记录。我一直强调跟进记录是整个销售过程管理的抓手这事的价值常被低估。很多团队做管理系统只关心车辆数据录入忽略了跟进记录的设计实际上这才是销售管理者最想看的。跟进记录表设计上内容字段要允许较长的文本因为销售可能记录详细对话细节。重要的是next_follow_time这个字段这是下次跟进时间。系统每天早上的任务列表就是查当天需要跟进的客户关联条件是next_follow_time在当前日期。这样销售一打开系统就知道今天要联系谁管理者也能看到哪些客户好久没人跟进了。客户意向等级调整也是一个有效功能。销售在跟进过程中判断客户意向变化把等级从低改到高或者标记为已成交已流失。等级变化也可以记录日志但这个功能优先级可以放低第一版可以先不做。预约看车功能要设置冲突校验。同一台车在同一时间段不能被两个人预约。简单做法是查appointment表里有没有该车辆、预约时间重叠且状态为待确认或已到店的记录。这个判断放在Service层做事务控制避免并发问题。4.3 成交流程与数据归档成交是整个流程的终点也是数据价值最高的节点。成交操作要做的事情把车辆状态改成已售生成销售订单更新客户状态为已成交。这三步要在同一个事务里只要一步失败整体回滚。我曾经遇到过因为没加事务控制车辆状态改了销售订单没生成的情况月底对账时发现库里少了一辆车非常狼狈。事务这件事Service层方法上标注Transactional并注意抛出RuntimeException才触发回滚Exception类型默认不触发。成交价与挂牌价的设计。挂牌价是车辆上架时填的销售价成交价是最终实际卖出去的价格两者往往不一样。议价空间就是这两者之差。统计毛利的时候要用成交价减去收购价成本价不能用挂牌价计算。订单表里同时存sale_price和deal_price两个字段报表统计时按deal_price算这个细节看起来小但对报表准确性影响很大。订单编号用ORD-日期-序号生成跟车辆编号类似。合同编号如果线下有纸质合同系统里存合同编号字段便于对应。销售员ID存user_id后面统计业绩按这个字段分组即可。5. 实操过程从零搭建到可运行5.1 环境准备与工程骨架环境建议JDK 1.8或11Maven 3.6MySQL 8.0IDEA。Spring Boot 2.7.18对应Maven依赖从中央仓库拉取即可需要注意Maven镜像源国内环境建议配置阿里云镜像不然依赖下载能卡半小时。创建工程我用Spring InitializrGroup填com.exampleArtifact填car-sales选择Java 8版本勾选Spring Web、MyBatis Framework、MySQL Driver、Thymeleaf依赖。Lombok可选如果团队里有不熟悉注解的新手可以先不用手写getter/setter也就多几行代码。MyBatis-Plus需要单独在pom.xml里添加依赖Initializr里不带这个。!-- pom.xml 中核心依赖 -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-thymeleaf/artifactId /dependency dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version3.5.3.1/version /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-j/artifactId scoperuntime/scope /dependency5.2 数据库初始化与核心配置数据库先手动执行建表SQL初始化不推荐用程序自动建表生产环境更不能用。项目里加一个schema.sql放在resources目录方便团队新成员快速初始化环境。application.yml关键配置项spring: datasource: url: jdbc:mysql://localhost:3306/car_sales?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/ShanghaiallowPublicKeyRetrievaltrue username: root password: yourpassword driver-class-name: com.mysql.cj.jdbc.Driver thymeleaf: cache: false 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这里几个配置项解释一下。serverTimezoneAsia/Shanghai解决时区问题不然插入的时间会比实际少8小时。allowPublicKeyRetrievaltrue解决MySQL 8.0的SHA256密码认证插件导致的连接失败问题。useSSLfalse是本地开发环境跳过SSL验证。log-impl配置是让MyBatis在控制台打印SQL排错利器正式环境记得关掉。5.3 权限控制与登录模块实现登录模块不引入Spring Security用拦截器就够了。Spring Security配置繁琐对这个体量的项目属于过度设计。我实现一个自定义拦截器LoginInterceptor继承HandlerInterceptor接口在preHandle里检查session中有没有登录用户。排除登录接口、注册接口和静态资源路径其余接口一律拦截。用户登录时用BCryptPasswordEncoder的matches方法比对密码比对成功后把用户对象放进session。修改密码功能里新密码也要BCrypt加密再存库。密码加密这一点没有商量的余地哪怕只在这个系统里跑也不能明文存。角色鉴权我做一个简易方案自定义RequireRole注解标注在Controller方法上管理员接口标注RequireRole(admin)拦截器里读取注解并对比当前用户角色。比在每个方法里手动判断role更优雅切面代码更容易维护。6. 常见问题与排查技巧实录6.1 数据库连接与环境类问题运行最常见的当属数据库连不上。控制台报CommunicationsException或者Access denied先别急着怀疑网络三步排查第一步看数据库服务是否启动Windows下用服务管理器查MySQL服务状态第二步看连接URL能不能ping通navicat能连上说明数据库本身没问题第三步看账号权限URLL里填的账号是否有权限访问对应库。MySQL 8.0的Public Key Retrieval问题报错信息是Public Key Retrieval is not allowed解决方案就是URL参数加allowPublicKeyRetrievaltrue。这个问题是因为MySQL 8.0默认使用caching_sha2_password认证插件客户端第一次连接时需要获取服务器公钥默认禁止自动获取。加上这个参数就通了。中文乱码问题。页面显示问号或者乱码先检查数据库表字符集是不是utf8mb4再检查连接URL里有没有characterEncodingutf8最后看页面编码有没有meta标签设置charset。三处统一了基本不会乱。强调一点在建库建表时统一用utf8mb4utf8mb4是utf8的超集能存emoji和生僻字MySQL 8.0默认就是utf8mb4。6.2 Spring Boot与MyBatis-Plus的坑MyBatis-Plus的逻辑删除有一个经典坑配置了logic-delete-field之后所有查询自动过滤已删除数据但唯一索引会出问题。假设给手机号字段建了唯一索引用户删除一条记录后再次插入同一个手机号的数据会报唯一索引冲突因为那条已删除的记录还在表里占着索引。解决思路要么用联合唯一索引包含deleted字段要么删除时不走逻辑删除而是真正DELETE要么用状态字段区分而非唯一索引。我通常选择联合唯一索引方案。分页插件一定要在配置类里注册MybatisPlusInterceptor并添加PaginationInnerInterceptor不注册的话Page查询不会生效查出来的数据量不受限制。这个很多人第一次用都踩过。另外分页查询统计总条数的SQL如果涉及多表关联count语句可能要走一遍大表关联性能会慢。优化手段是自定义count SQL或者干脆只依赖第一页的数据量用加载更多代替分页条。LocalDateTime序列化问题。Spring Boot默认用Jackson序列化时间LocalDateTime类型默认输出格式是一长串数字不好看也不方便前端处理。在application.yml里配置spring.jackson.date-formatyyyy-MM-dd HH:mm:ss对LocalDateTime是不生效的需要用JsonFormat注解标注在实体类时间字段上或者自定义一个Jackson的LocalDateTime序列化器。这个坑很隐蔽我一开始也被绕进去了。6.3 业务逻辑中的典型Bug事务失效是高频Bug最常见的两种场景同类内方法自调用失效和异常被吞掉。同类中方法A调用方法BB上有Transactional注解B的事务不会生效因为Spring事务基于AOP代理自调用不走代理对象。解决办法是拆到不同Service类里或者注入自身代理对象。异常被吞的情况方法里catch住Exception没有往外抛事务当然不会回滚数据就写了一半。还有一个并发问题容易被忽视成交操作和另一个人同时操作同一辆车。比如销售A提交订单把车辆状态改成已售销售B同时也在给这辆车创建订单。解决办法是在sale_order表创建时用SELECT ... FOR UPDATE锁住对应car_info记录或者给car_info表加version字段做乐观锁。MyBatis-Plus支持Version注解做乐观锁配置拦截器加OptimisticLockerInnerInterceptor。二手车行同时抢一台车的情况不多但为了防止数据错误还是加上更稳妥。关于返回给第三方的接口设计出现过因为接口参数校验不完善导致的数据问题。比如新增客户接口没有校验手机号格式销售录入12345这种无效号码后期打电话发现打不通客户数据质量非常差。解决办法是使用Spring的Validated注解和NotBlank、Pattern等注解做参数校验统一处理校验异常返回。结尾一些实践经验做这个系统最大的体会是技术点本身都不难难的是把业务流程想清楚再动手写代码。我在做第二版的时候重构了跟进记录表的设计加了下次跟进时间字段就因为回收访数据时发现Excel里压根没法快速筛出今天该跟谁。一个字段的差别对实际管理效率的影响是质变的。最后分享一个小技巧开发这类管理系统时每个核心业务的SQL语句我都会先手动在Navicat里跑通确认结果没问题之后才写到Mapper里。这样可以避免调试接口时还要同时排查SQL语法错误把变量控制在单一的维度内。另外车辆图片的本地存储路径要提前规划好不要放在项目工程目录下打包发布时会把图片覆盖掉。单独配置一个file.upload.path属性指向服务器固定目录线上部署时改配置文件的路径就可以避免这类问题。
返回列表