最近接手了一个基于Java的车辆管理系统项目,从需求调研到最终交付,前前后后折腾了大概两个月。这套系统不算复杂,但涉及的模块很多,车辆信息、司机管理、调度派单、维保提醒、油耗统计、费用结算,每一块单独拿出来都不难,但整合在一个系统里还要保证好用、稳定、易扩展,就有一堆细节要处理。今天把这套系统的设计思路、核心模块的实现逻辑、遇到的坑和优化方案都整理出来,如果你是刚接触这类管理系统开发,或者正打算做一个类似的Java Web项目,这篇内容应该能帮你少走不少弯路。
这套系统本质上是典型的CRUD密集型应用,核心是围绕"车辆"这条主数据展开的增删改查以及状态流转。但正因为字段多、状态多、关联多,反而比那些看起来很酷的高并发秒杀系统更考验基本功。我在开发过程中把大部分精力放在了数据模型设计和业务逻辑梳理上,真正写CRUD接口的时间其实并不多。接下来我就从整体设计、技术选型、核心模块实现、常见问题这几个维度,把整个项目拆开来讲。
1. 车辆管理系统的整体设计与需求拆解
1.1 项目背景与业务痛点
这套系统的使用场景是某家运输公司的内部管理,规模大概有200多辆自有车辆,另外还有一些挂靠的外协车辆,司机数量接近300人。在没有系统之前,车辆调度靠电话和Excel表,车辆状态靠人工盯,维保记录散落在纸质档案和微信聊天记录里,财务结算每周对一次账,经常对不上。痛点非常集中:车辆状态不透明、调度效率低、成本核算粗放、维保容易漏。
和业务方反复聊了几天之后,我把核心需求收敛成四个方向:车辆档案管理、实时状态跟踪、调度与任务派发、费用与维保管理。再往细拆,每块又包含若干子功能。比如车辆档案要支持新增、编辑、档案导出,状态跟踪要覆盖空闲、出车、维修、停用四种状态,调度模块要支持按司机、按车辆、按时间段查询可用资源,费用管理要能按月度汇总油耗、维修、保险、罚款等各类支出。
1.2 核心功能模块划分
系统按功能域拆成六个核心模块,每个模块对应一个独立包:
- 车辆信息管理:车辆基本档案、行驶证信息、保险到期日、年检到期日、车辆照片、附属设备等;同时维护车辆的状态字段,用于调度模块的可用性判断。
- 司机管理:司机档案、驾驶证信息、联系方式、紧急联系人、违章记录;司机和车辆是多对多的关系,但同一时间同一辆车只归属于一个司机。
- 调度管理:派车单的创建、审批、指派、出车、回车全流程;支持按时间段查询车辆空闲情况,支持调度日志记录。
- 维保管理:保养计划配置、维保工单创建、维保历史记录、到期提醒;系统里有个定时任务,每天扫描一次维保到期车辆。
- 费用管理:油费、路桥费、维修费、保险费用、罚款等支出登记;按月、按车辆维度的费用汇总统计。
- 系统管理:用户登录认证、角色权限控制、操作日志、数据字典、系统参数配置。
这六个模块之间不是孤立的,最典型的联动场景是:调度员创建派车单时,系统需要实时检查车辆状态是否为"空闲"、车辆是否在维保期内、司机是否已有冲突任务;派车单完成后,系统自动更新车辆总里程、平均油耗,并把相关费用写入费用模块。
1.3 为什么选择Java技术栈
车辆管理系统这一类项目,在技术选型上几乎没有悬念,Java是绝大多数团队的首选。选择Java的原因很简单:生态成熟、招聘市场供应充足、开发效率可控。具体到这套系统,我是基于Spring Boot + MyBatis Plus + MySQL的组合,前端用了Vue2 + Element UI,前后端完全分离。没有引入微服务、消息队列、分布式事务那些重武器,原因很直接,系统并发量不高,日活用户也就几十人,峰值操作可能就十几个并发,用微服务纯属给自己找麻烦。
Java在这个场景下的优势更多体现在稳定性和可维护性上。Spring Boot的自动配置和Starter机制大幅降低了项目搭建成本,MyBatis Plus提供了比较完善的条件构造器和分页插件,针对查询密集型的车辆管理场景,写SQL的工作量比JPA要少很多。另外,Java的类型系统在处理账单金额、日期区间这类数据时比动态语言安全得多,编译期就能挡住一部分低级错误。
2. 系统微服务架构与数据库设计
2.1 项目结构怎么拆
先看一下我采用的模块划分方式。很多人拿到这种项目习惯性只分controller、service、mapper三层,结果写到最后所有代码挤在一起,改一个需求全是牵一发动全身。我这次换了另一种方式:按业务域拆包,每个业务域内部再分controller、service、mapper、model、dto、vo。比如车辆模块的结构大概是这样:
com.company.fleet.module.vehicle ├── controller │ └── VehicleController.java ├── service │ └── VehicleService.java ├── mapper │ └── VehicleMapper.java ├── model │ └── Vehicle.java ├── dto │ └── VehicleQueryDTO.java └── vo └── VehicleDetailVO.java这样做的直接好处是:六大模块并行开发时互不干扰,每个人只管自己那个包,代码review时范围清晰,遇到问题定位也快。公共的部分单独放在common包下,技术无关的比如统一异常处理、统一响应结果封装、JWT工具类、全局跨域配置,都塞进去。
2.2 数据库表结构设计的几个关键点
车辆管理系统的表结构是整个项目的基础,设计得好不好会直接决定后续的业务逻辑好不好写。我建了十几张表,其中几张比较关键的,这里把建表思路讲清楚。
车辆基本信息表,我设计了二十多个字段,主要包括车辆id、车牌号、车辆类型(客车/货车/特种车)、品牌型号、发动机号、车架号、座位数/载重、购买日期、车辆状态、当前里程、所属部门、车辆照片URL、备注等。车牌号做了唯一索引,这个不用多说。但有个细节值得关注:车辆状态字段我用了tinyint类型(0-空闲、1-出车中、2-维修中、3-停用),而不是直接用字符串存,原因有两点,一是节省存储,二是状态判断在代码里用常量类维护,避免字符串拼写出错。
司机信息表和车辆表是一对多的关系,因为一个司机理论上对应一台常用车辆,但外协司机可能临时接车。所以司机表里我没做车辆外键关联,而是用调度表来记录某段时间内司机和车辆的绑定关系。
调度派车单表是业务核心,字段包括:派车单号、车辆id、司机id、用车部门、用车事由、预计出发时间、预计返回时间、实际出发时间、实际返回时间、出发时里程数、返回时里程数、审批状态、创建人、审批人等。派车单状态流转是一个典型的有限状态机:草稿 -> 待审批 -> 已审批待出车 -> 出车中 -> 已完成,中间还可能要支持撤销、驳回。
维保记录表单独说,因为它有一个容易踩坑的点:保养周期和计量单位。有的保养活动按里程算(每5000公里一次),有的按天数算(每6个月一次),还有的按累计运行时间算(特种车辆,每200小时一次)。我的建表方案是:维保类型、计划执行里程、计划执行日期、上次执行里程、上次执行日期、下次执行里程、下次执行日期,这样不同计量单位的保养计划都能覆盖。
2.3 数据完整性与状态一致性
这套系统对数据准确性要求比较高,尤其是里程数、金额这类字段,一旦记录错后面全是连锁反应。我在数据库层面加了几个约束:费用明细表里金额字段用decimal(10,2),不允许为负;里程数字段用int,不允许为负;车牌号、发动机号这些关键字段建唯一索引;所有业务表都加了create_time、update_time、deleted字段用于逻辑删除。
状态一致性方面,调度模块是最容易产生脏数据的地方。举个例子:两个调度员同时看到同一辆车是"空闲"状态,同时去创建派车单,如果没做并发控制,这辆车就可能被同时派给两个司机。我的处理办法是在车辆状态更新时加上版本号控制(乐观锁),或者直接对车辆id做行级锁。MySQL的InnoDB引擎下,更新语句用SELECT ... FOR UPDATE锁住这条记录,防止并发冲突。
-- 车辆状态更新时用乐观锁版本号 UPDATE vehicle SET status = 1, version = version + 1 WHERE vehicle_id = #{vehicleId} AND version = #{version}实战中我把乐观锁版本号和业务状态校验绑定在一起,车辆从"空闲"改为"出车中"时,更新SQL的where条件不仅要带version,还要带status=0,这样即使两个请求同时进来,也只有一条SQL能生效,从根本上堵住了并发派车的漏洞。
3. 核心功能模块的实现与实操细节
3.1 车辆档案管理:CRUD也要注意数据校验
车辆档案管理看起来就是最基础的增删改查,但实际写起来,细节远比想象的多。
首先是参数校验。车辆的新增和编辑涉及大量非空判断、格式校验。车牌号要按省份简写+字母数字组合的正则校验,车架号是17位的字母数字组合,发动机号一般是唯一编码,手机号格式用正则校验。我这次直接用Spring Validation注解加自研组合校验,比如新增车辆时用@NotBlank校验必填字段,编辑时先查数据库确认记录存在再更新,避免出现假更新。
其次是字段联动。车辆的里程数会影响到维保提醒的计算基础,所以车辆信息变更时不能只是简单更新一个表。我当初在编辑车辆里程数时,顺手把该车辆最新的保养进度记录也同步更新了,不然就会出现里程数改了保养计划还按老数据提醒的情况。
再一个附件管理。车辆行驶证、保险单、年检合格证这些需要扫描件上传,由此引出了文件上传的通用能力。系统里做的是一个简单的文件上传接口,支持jpg、png、pdf格式,限制单文件大小不超过5MB,上传到服务器本地磁盘,随后把文件路径保存到数据库字段里。后来考虑到服务器磁盘有限,把本地存储改成了对象存储方案,但接口调用方式不变,这样后续替换成本也比较低。
这个模块总结下来,核心不只是做CRUD,而是保证每一个业务字段都准确、完整、可追溯。
3.2 调度派单:有限状态机是核心玩法
调度管理模块是整个业务系统里复杂度最高的部分,核心就是派车单的状态流转。设计之前我先画了状态流转图(不是代码,是纸上的草稿),确认所有合法路径,然后才动手写代码。状态流转路径如下:
草稿->待审批->已审批->出车中->已完成
路径里的每个环节都有附加操作:草稿可以删除、修改;待审批可以撤销或提交审批;已审批的状态下执行车辆锁定和司机锁定;出车中可以登记实际出发时间,记录出发里程;完成时可以登记返回里程、生成费用记录、更新车辆当前里程。
状态流转的代码我用了一个StateMachineHelper工具类,里面维护一个合法流转Map,key是当前状态,value是可以流转到的目标状态集合。每次发起流转时先校验合法性,不合法直接抛业务异常,防止数据走到不该走的路径。
private static final Map<Integer, Set<Integer>> TRANSITIONS = new HashMap<>(); static { TRANSITIONS.put(0, Set.of(1)); TRANSITIONS.put(1, Set.of(0, 2)); TRANSITIONS.put(2, Set.of(3, 0)); TRANSITIONS.put(3, Set.of(4)); TRANSITIONS.put(4, Set.of()); } public boolean canTransition(int from, int to) { Set<Integer> targets = TRANSITIONS.get(from); return targets != null && targets.contains(to); }这比直接拿if-else判断状态靠谱得多。后来需求加了一个"已审批但未出车"时要能改派车辆,这个逻辑是在已审批状态下增加一条流转到草稿的路径,同时释放车辆锁定。如果当初用if-else写死了,改起来绝对没那么干净。
调度模块还有一个查询功能实现起来复杂:按时间查可用车辆。业务场景是:调度员要派一个明天早上8点到下午6点的出车任务,需要查哪些车辆在这个时间段内是空闲的。这个查询不能只看当前状态,因为当前空闲的车辆明天可能已经被别的任务占用了。我实现的查询逻辑是:先查出所有车辆,然后排除掉在目标时间段内存在"已审批且未完成"派车单的车辆:
SELECT * FROM vehicle WHERE vehicle_id NOT IN ( SELECT vehicle_id FROM dispatch_order WHERE expected_start_time < #{endTime} AND expected_end_time > #{startTime} AND status >= 2 AND status < 4 )这个SQL初看没问题,但实测下来在数据量不大的情况下性能还行,数据一多就慢了,原因是子查询没有利用索引。后来在内层查询的vehicle_id、expected_start_time、expected_end_time、status这几个字段上建了联合索引,查询速度瞬间降下来了。数据量不大时体会不到索引的重要性,到线上数据量大了才知道一个合理的索引能救你一把。
3.3 维保提醒:定时任务与通知机制的落地
维保提醒功能的核心是:每天早上扫描一次数据库,找出未来7天内需要保养或年检的车辆,生成提醒记录,并通过系统站内信。
定时任务我用的Spring的@Scheduled注解实现,简单够用,不依赖分布式任务调度平台。扫描逻辑看起来也直白,但细节容易被忽略。比如"下次保养日期"这个字段,有些车辆是里程触发型,没有固定的时间日期,需要根据里程数判断是否接近上限;有些是日期触发型,直接看日期即可。所以我建立的扫描条件是:next_maintenance_date在7天内 或(current_mileage>=next_maintenance_mileage)。
扫描到需要提醒的车辆后,我不能简单只发一次站内信,否则用户没看到提醒就错过了。所以我在提醒模块里建了一个maintenance_reminder表,每次扫描会先跳过已提醒且状态为"已处理"的记录,避免重复打扰。同时支持维保完成后手动关闭提醒,并把该车维保计划里的下次执行时间/里程数自动更新。
提醒方式的扩展上,初期只做了站内信列表,后面业务方希望能推送到微信或短信。为了将来方便迁移,我写了一个ReminderSender接口,当前只实现站内信一个实现类。后面要接微信模板消息、阿里云短信,只需要新增实现类并在工厂类里注册即可,不必改动业务代码逻辑。
3.4 费用管理:金额计算与统计汇总
费用管理模块是老板最关注的模块,因为直接关系到成本核算。业务上有几个数据入口:油费(来自加油卡充值记录和加油明细),维修费(来自维保工单),保险费用(年度固定支出),路桥费、停车费、罚款(手动登记)。每个入口的数据来源不同,但都统一落在expense_record表里,用expense_type字段区分。
这个模块里最容易出问题的点是统计口径不一致。比如"本月加油费",是按加油时间统计,还是按报销时间统计?有的司机月底加油但下月初才拿来报销,两个口径算出来的数字能差几千块。我后来在费用登记里增加了一个expense_date字段专门表示业务发生时间,和create_time分清楚,统计SQL统一走expense_date,这样数据才说得清。
统计功能我用的是MyBatis Plus的groupBy加selectSum查询,也不涉及太复杂的SQL。一个比较常见但又容易错的场景是车辆月度费用统计报表,需求是列出每辆车每个月的各类费用合计。我最初的做法是查出明细再在Java里循环累加,结果是慢得离谱,200多辆车,每辆车一年几百条记录,数据量不大但循环里又嵌套查询接口,N+1问题严重。后来改成了一条SQL带LEFT JOIN把车辆表、费用表、车辆类型表关联起来,一把查出所有数据,再用groupBy汇总,响应时间从秒级降到毫秒级。
这个经验后来成了我做车辆管理报表的统一准则:能一条SQL解决的事,绝不写Java循环。
4. 系统权限与安全设计
4.1 基于RBAC的权限控制模型
车辆管理系统里用户角色比较固定,典型的是管理员、调度员、司机、财务、维修员。不同的角色能操作的数据范围不一样。比如司机登录后只能看到自己和当前车辆的派单信息,财务只能看费用相关数据,调度员拥有派单审批和车辆状态修改权限。实现层面我用了标准的RBAC模型,用户表、角色表、菜单表、用户角色关联表、角色菜单关联表,从而支持菜单权限和数据权限的配置。
数据权限比较特殊,不是简单通过菜单来控制,而是通过一个数据范围字段控制。比如调度员可以看全部车辆,但普通司机只能看自己绑定的车辆。我在用户表里增加了一个data_scope字段(1-全部数据、2-部门数据、3-仅个人数据),查询车辆列表时动态拼接数据权限过滤条件。这个方案执行下来比较直观,也不难维护。
4.2 登录认证与接口安全
登录认证用的JWT方案,无状态、适合前后端分离的项目。用户登录成功后服务端签发一个token,有效期设定为2小时,前端每次请求在请求头里带上token,后端通过拦截器校验token有效性,并把当前用户信息解析出来放到ThreadLocal里面供业务层使用。密码存储用BCrypt加密,不用MD5这种已经被爆破了几百年的弱哈希算法。
接口安全方面还做了两件事。第一,统一加了请求频控,使用Redis的INCR命令实现,同一个ip在一分钟内的请求次数超过100次就直接拒绝,防止有人拿脚本刷接口。第二,敏感操作接口(删除车辆、调整车牌、审批派车单)在前端二次确认的基础上,后端加了操作日志记录,谁在什么时间操作了什么数据,全部查得到。
4.3 操作日志与数据审计
编辑车辆、修改派车单这种会改动业务关键数据的操作,系统都要有日志记录。我之前见过不少系统日志模块形同虚设,只记录了个操作人名和时间,连改了哪个字段都不知道,出了问题没法追踪。所以这次我做的操作日志明确记录操作前值、操作后值、改动字段名,并且统一封装成切面,用注解声明在接口上。
例如在车辆编辑接口上:
@OperationLog(module = "vehicle", action = "edit", detail = "编辑车辆基本信息")切面里解析出当前用户、操作时间、参数,再对修改前后的值做diff,diff出来的字段和差异存JSON放到日志表。这条日志下来,就算某个人手滑把里程数改错了,也可以快速定位到是谁、在哪一步改的、改前改后各是多少。
5. 开发过程中遇到的典型问题和排查过程
5.1 并发派车问题:不是理论问题而是真实事故
这是上线后遇到的头号问题。两个调度员同时看到一辆车空闲,前后相差一秒提交派车单,结果系统生成了两条派车单,车辆同时派给了两个司机。第一天就出了这种事,好在发生得早,数据没产生太大影响,但给了我们一个非常大的警示。
最初的代码逻辑是这样的:先查车辆状态,判断是空闲,然后插入派车单,更新车辆状态为出车中。这里有个典型的check-then-act竞态条件:两个操作并发进来,都读到车辆状态是"空闲",然后都继续走插入和更新逻辑。
修复方案我采用了乐观锁更新车辆状态,并调整了代码顺序:把"更新车辆状态"作为先决条件来判断。只有车辆状态更新成功,才允许插入派车单。这样即使两个请求同时到达,也只有一条能真正执行成功。
@Transactional public DispatchOrder createDispatchOrder(DispatchOrderCreateDTO dto) { boolean locked = vehicleStatusService.tryLock(dto.getVehicleId(), VehicleStatus.IDLE, VehicleStatus.DISPATCHING); if (!locked) { throw new BizException("车辆已被其他任务占用"); } // 创建派车单逻辑 }这里面的tryLock最终落成一条UPDATE SQL,成功影响行数大于0代表成功锁定。这个方案比用Redis分布式锁简单,因为单应用场景没必要引入额外中间件,而且数据库的行级锁在这个并发量级下完全够用。
5.2 日期查询区间重叠问题
调度模块查可用车辆时遇到的第二个典型问题:区间重叠判断。新建A任务时,需要找出在A任务的开始时间和结束时间之间,没有其他任务的车辆。逻辑本身不难,区间重叠的条件是start < targetEnd AND end > targetStart,但把这个条件写明白挺容易出错的,特别是边界情况。
我在写的时候踩了个坑,一开始写成了start <= targetEnd AND end >= targetStart,这会导致一个问题:某个任务正好在目标开始那一刻结束,会被算作重叠,明明前一单刚回车,车辆应该可以派给新任务。后来改成严格小于和大于,并把日期比较统一到秒级格式,才符合业务预期。
这个坑提醒我,写日期区间查询前,先把边界情况在纸上列清楚:前一个任务结束时间等于本任务开始时间怎么办?本任务结束时间等于前一个任务开始时间怎么办?事实证明,提前花两分钟把边界想透,后面能省半天debug时间。
5.3 慢查询优化与索引设计
系统上线初期数据量不大,所有查询都是秒回,没人在意SQL的性能。随着业务运行了两三个月,数据量逐渐增长到几十万级,几个报表接口开始明显变慢,最夸张的一个月度费用汇总查询跑了8秒多,用户直接在群里吐槽"系统卡死了"。
我用EXPLAIN分析了一下执行计划,发现慢的原因有两个:一是费用表没有建立合适的联合索引,导致查询是ALL全表扫描;二是车辆表和大字段表关联时,因为没有索引做了join缓存,内存里处理了大半天。
最终的优化动作是调整索引策略:
expense_record表上建了(vehicle_id, expense_date)联合索引,用于月度统计和单车辆查询。dispatch_order表上建了(vehicle_id, status, expected_start_time, expected_end_time)联合索引,用于调度可用性查询。vehicle表上对status和plate_number建单独的索引。- 把所有分页查询改成不带
count(*)的深翻页查询,明确分页上限,并限制单页最多50条,防止有人一口气査几千条数据打爆数据库。
优化后主要接口的耗时基本都降到了500毫秒以内,用户反馈明显好转。数据库索引这东西,一定是等到真实数据量上来再调优才最有效,前期过度索引反而会影响插入速度。
5.4 车辆和司机绑定的历史数据回溯
还有一个业务逻辑上的坑:车辆和司机之间的绑定关系不是不变的。调度员经常要查"这辆车上个月是谁在开",但车辆表里只有当前绑定司机的id,历史绑定关系没有落表。
后来我在车辆操作表里增加了一个driver_history记录,每次发生指派时写入一条记录,包含车辆id、司机id、绑定开始时间、绑定结束时间。这样查询历史绑定关系就简单了,只需要按车辆id和时间段去查这个表。派车单审核通过后,自动触发写入;回车完成后,自动关闭当前绑定记录。这套逻辑加完之后,回溯历史猜测问题就再也不存在了。
6. 系统部署、测试与上线经验
6.1 多环境配置管理
开发过程离不开多环境部署。我不喜欢把不同的数据库连接配置写死在application.properties里,而是用Spring Boot的Profile机制,区分application-dev.yml、application-test.yml、application-prod.yml,启动时通过参数--spring.profiles.active=prod来激活不同环境。数据库密码等敏感信息放在环境变量里,而不是明文写在配置文件里。
这套管理方式在车辆管理系统这种量级的项目里刚好合适,不用引入配置中心,也不会出现"明明本地跑得好好的,测试环境就出问题"的情况。
6.2 单元测试覆盖核心业务逻辑
项目交付前我写了一批单元测试和接口级测试。重点测试三个部分:调度状态机的合法流转与非法流转、并发场景下车辆状态更新的幂等性、费用统计按月汇总的正确性。MyBatis相关的测试用H2数据库做嵌入式内存库,启动时自动跑SQL脚本建表。
最大的收获是:写测试暴露了很多边界情况。比如派车单创建时,如果预出时间早于当前时间怎么办,我当时没有判断,还是测试用例提醒了我加了这个校验。测试的核心价值恰恰就在于帮你抓出自己写业务逻辑时没考虑到的那部分分支,而不仅仅是验证功能正确。
6.3 上线后的一周重点监控
项目上线第一周我没有放松,每天早中晚各看一次后台日志和关键接口耗时。重点看调度派车是否出现并发问题,费用统计报表是否有数据异常,定时任务是否正常执行。真就抓到过一个问题:维保提醒的定时任务前三天正常,到第四天突然停了,查日志发现是某个车辆数据异常导致任务链路中抛了未捕获的异常,整个@Scheduled任务直接被中止。
修复的方法是给定时任务整体加try-catch,方法内部对每条车辆数据的处理加独立try-catch,单条数据出错不影响后续数据。另外用了一个定时任务执行记录表,每次任务启动时插入开始时间,结束时更新结束时间和执行状态,第二天早上扫一眼有没有"开始时间有值但结束时间为空"的记录,就能判断是否有任务跑挂了。这套机制非常朴素,但非常有效。
6.4 交接文档与运维手册
项目开发完之后我还补了一份交接文档,不写代码怎么实现,而是写清楚:数据库表结构的含义、关键字段的枚举取值、定时任务的执行周期、部署环境的架构图、常见故障的处理步骤。这份文档后来帮了运维同学不少忙,比如数据库磁盘满了怎么清、定时任务挂了怎么手动触发、调度员误操作改了车辆状态怎么回滚。很多项目复盘时都说"文档缺失导致交接困难",这次提前准备了文档,后面省了很多麻烦。
7. 给你的一些避坑建议和实操心得
最后写几条我这次开发中沉淀下来的实操心得。谈不上多高深,但都是踩过坑才换来的。
第一,先设计状态机,再写代码。凡是涉及多步骤流转的业务(调度、审批、维保),一定花半小时把合法状态路径画清楚,然后代码写死这些路径。没有状态约束的开发,后面改需求时会特别痛苦,因为数据一旦走到一个非预期状态,你就得写一堆异常兼容代码来修补。
第二,金额字段永远是Decimal,永远不用浮点型。这算是行业铁律了,为什么还用多说吗?随便跑一个0.1 + 0.2,用浮点型算出来的精度问题能让你在月底对账时怀疑人生。
第三,能用状态字段就尽量别用物理删除。车辆管理系统的数据是要追责和追溯的,车辆删了但费用还在,司机删了但派单历史还在,所以用逻辑删除不亏,查询过滤条件加一个deleted = 0,成本极低,但能保底。
第四,定时任务一定要有容错和告警。这是多次被坑之后的最大教训。定时任务跑挂了,如果没有监控,可能会连续几天没人发现,等发现的时候数据已经全乱了。加一张任务执行记录表,每天检查任务状态,比啥都稳。
第五,权限设计宁可一开始多花点时间,也别等上线后再补。后补权限往往意味着要改所有的查询接口,相当于把系统重新梳理一遍,那个成本比从一开始就考虑数据权限要高得多。
做一个车辆管理系统,技术难度不在于某个单点功能有多复杂,而在于把一大堆互相牵制的业务规则梳理清楚,把状态流转、数据一致性和权限边界都设计明白。希望这篇内容能让你少踩几个坑,少熬几夜。如果正在做类似的系统,建议把焦点放在业务约束上,框架资源网上有很多,但一个好用的调度流程和一套可靠的数据口径,才是这类系统真正值钱的地方。