加油单据乱飞、油卡余额没人管、司机随口报个数财务只能点头、月底对账变成“拉仇恨大会”——这些看似小事,累积起来就是实打实的损失。更可怕的是,出现问题时无法追溯责任,赔本还丢人。
本文从中小企业可落地的角度出发,为大家讲解如何快速开发车辆管理系统中的加油管理板块,帮助企业降本增效。本文将讲清为什么需要车辆管理、什么是车辆管理系统(VMS),并着重给出“加油管理”模块的完整设计:功能、业务流程、数据库模型、架构、关键代码、开发技巧、结果呈现。
本文那你将了解
注:本文示例所用方案模板:简道云车辆管理系统,给大家示例的是一些通用的功能和模块,都是支持自定义修改的,你可以根据自己的需求修改里面的功能。
许多中小企业在管理车辆燃油费用时常见问题包括:
目标是把这些“模糊成本”变成“可追踪数据流”,减少人工对账,提升透明度,支持可量化的决策。
车辆管理系统(Vehicle Management System,VMS)负责车辆的全生命周期管理:档案、保险、维修、行驶日志、驾驶员管理和费用管理。加油管理是费用管理的核心模块之一,职责包括:
把加油管理做得好,能直接提升财务与业务的协同效率。
这些是能让企业把“看得见的成本”变成“可用的数据”的必要功能。
适合中小企业、可扩展的分层架构如下(文本图):
+-------------------------+ +------------------+
| 前端(Web / 手机) | <--> | API 网关 / Nginx |
+-------------------------+ +------------------+
| |
v v
+----------------------+ +----------------------+
| 后端服务 (Node.js) | <-----> | 消息队列 (RabbitMQ) |
| - Auth / 权限 | +----------------------+
| - FuelService |
| - CardService |
+----------------------+
|
v
+----------------------+ +-------------------+
| 关系型数据库 (MySQL) | | Cache / Redis |
| - oil_cards | +-------------------+
| - fuel_records |
| - card_recharges |
+----------------------+
|
v
+----------------------+
| 外部集成(可选) |
| - OCR 服务 |
| - 油站 API / 充值接口 |
| - 财务系统导出/对接 |
+----------------------+
说明:
下面给出核心表的建议字段,设计关注审计性与对账便捷性。
CREATE TABLE oil_cards (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
card_number VARCHAR(64) NOT NULL UNIQUE,
provider VARCHAR(100),
holder VARCHAR(100),
balance DECIMAL(12,2) DEFAULT 0,
status ENUM('active','frozen','lost','closed') DEFAULT 'active',
created_by BIGINT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);
CREATE TABLE fuel_records (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
vehicle_id BIGINT NOT NULL,
driver_id BIGINT,
card_id BIGINT, -- 使用的油卡,若现金则为 NULL
station VARCHAR(200),
fill_datetime DATETIME,
liters DECIMAL(10,2),
amount DECIMAL(12,2),
odometer BIGINT,
invoice_no VARCHAR(100),
invoice_image VARCHAR(255),
status ENUM('pending','approved','rejected') DEFAULT 'pending',
created_by BIGINT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
UNIQUE KEY uniq_invoice (invoice_no, card_id)
);
CREATE TABLE card_recharges (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
card_id BIGINT NOT NULL,
amount DECIMAL(12,2) NOT NULL,
recharge_datetime DATETIME,
operator_id BIGINT,
voucher_image VARCHAR(255),
source ENUM('bank','cash','third_party'),
status ENUM('pending','completed','failed') DEFAULT 'pending',
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE audit_logs (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
entity VARCHAR(50),
entity_id BIGINT,
action VARCHAR(50),
before_data JSON,
after_data JSON,
operator_id BIGINT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
要点:
主要流程分为:油卡维护 → 充值 → 加油登记 → 自动校验 → 审批 → 扣减余额/入账 → 对账/报表。
用 Mermaid 表示:
flowchart TD
A[维护油卡信息] --> B[油卡充值登记]
B --> C{充值是否成功?}
C -- 是 --> D[余额更新 & 写审计]
C -- 否 --> E[记录失败原因]
D --> F[司机去加油]
F --> G[车辆加油登记(上传发票)]
G --> H[系统校验(发票重复/里程异常/余额)]
H -- 通过 --> I[审批流程] --> J[扣减余额/入账]
H -- 异常 --> K[人工复核/拒绝]
J --> L[对账任务(异步)] --> M[生成报表/告警]
关键业务节点说明:
下面给出几个关键接口与示例实现。实际项目建议使用 NestJS 并在此基础上封装错误处理和权限中间件。
// fuel.controller.ts (Express + Sequelize 简化示例)
import express from 'express';
import { sequelize } from './models'; // Sequelize 实例
const router = express.Router();
router.post('/fuel', async (req, res) => {
const t = await sequelize.transaction();
try {
const { vehicleId, driverId, cardId, station, liters, amount, odometer, invoiceNo, invoiceImage } = req.body;
if (!vehicleId || !amount || !odometer) {
return res.status(400).json({ message: '必填字段缺失' });
}
// 防重复:invoice_no + card_id
const exists = await sequelize.models.fuel_records.findOne({
where: { invoice_no: invoiceNo, card_id: cardId }
});
if (exists) {
return res.status(409).json({ message: '发票已存在,可能重复提交' });
}
// 保存登记(初始 pending)
const fuel = await sequelize.models.fuel_records.create({
vehicle_id: vehicleId,
driver_id: driverId,
card_id: cardId || null,
station, liters, amount, odometer,
invoice_no: invoiceNo, invoice_image: invoiceImage,
status: 'pending', created_by: req.user.id
}, { transaction: t });
// 里程异常检测(若低于历史读数则写审计,但不阻断)
const vehicle = await sequelize.models.vehicles.findByPk(vehicleId);
if (vehicle && odometer < vehicle.current_odometer) {
await sequelize.models.audit_logs.create({
entity: 'vehicle',
entity_id: vehicleId,
action: 'odometer_decrease',
before_data: JSON.stringify({ last: vehicle.current_odometer }),
after_data: JSON.stringify({ odometer }),
operator_id: req.user.id
}, { transaction: t });
}
await t.commit();
res.json({ id: fuel.id, message: '登记成功,待审批' });
} catch (err) {
await t.rollback();
console.error(err);
res.status(500).json({ message: '服务器错误' });
}
});
export default router;
router.post('/cards/:id/recharge', async (req, res) => {
const cardId = req.params.id;
const { amount, rechargeDatetime, source, externalOrderId } = req.body;
const t = await sequelize.transaction();
try {
// 幂等:外部订单号唯一约束(若有)
if (externalOrderId) {
const prev = await sequelize.models.card_recharges.findOne({ where: { external_order_id: externalOrderId }});
if (prev) return res.json({ id: prev.id, message: '已处理' });
}
const card = await sequelize.models.oil_cards.findByPk(cardId, { transaction: t, lock: t.LOCK.UPDATE });
if (!card) {
await t.rollback();
return res.status(404).json({ message: '油卡不存在' });
}
// 创建充值记录
const rec = await sequelize.models.card_recharges.create({
card_id: cardId, amount, recharge_datetime: rechargeDatetime || new Date(),
operator_id: req.user.id, source, status: 'completed', external_order_id: externalOrderId || null
}, { transaction: t });
// 更新余额(同一事务内)
const newBalance = parseFloat(card.balance) + parseFloat(amount);
await card.update({ balance: newBalance }, { transaction: t });
// 写审计
await sequelize.models.audit_logs.create({
entity: 'oil_card',
entity_id: cardId,
action: 'recharge',
before_data: JSON.stringify({ balance: card.balance }),
after_data: JSON.stringify({ balance: newBalance }),
operator_id: req.user.id
}, { transaction: t });
await t.commit();
res.json({ id: rec.id, newBalance });
} catch (err) {
await t.rollback();
console.error(err);
res.status(500).json({ message: '充值失败' });
}
});
要点说明:
以下都是我在项目中踩坑总结出的实战建议,企业在开发时务必考虑:
落地以后,应该关注以下效果与指标来量化收益:
这些指标能对项目 ROI 做估算与持续优化。
把“加油管理”从纸质和记忆中搬到系统里,不只是写几个表单那样简单,而是建立一条从登记到审批、从充值到对账、从告警到分析的闭环。对中小企业来说,优先把发票集中、油卡余额一致性、里程校验和对账自动化做起来,剩下的优化(OCR、油站实时对接、复杂报表)都可以逐步迭代。
开始推进的第一步是分阶段、最小可行产品(MVP)思路。
第一阶段不必一上来就做复杂的 OCR 或与油站对接,而是优先实现:
第二阶段再引入自动化:OCR 提取字段、余额告警、对账自动匹配。最后阶段可以考虑与财务系统/油站 API 对接和复杂报表。
分阶段推进能快速见效、降低实施风险,并逐步树立内部信任。
保证账务一致的核心是事务性操作与对账机制。
技术上,充值与扣减必须在数据库事务内处理,更新余额时加行锁或使用消息队列串行化操作,避免并发冲突。
对账层面,每日导入银行回单或油站对账单,通过交易时间、金额和发票号进行三方匹配;匹配失败的记录进入异常列表并由财务人工核查。
出现历史差异时,保留完整审计日志(谁在什么时候做了什么),并通过人工回溯原始凭证(发票图片、回单)来定位原因。制度上,要求发票与充值凭证必须有扫描件,并建立预算或阈值告警防止超额刷卡。
设计审批策略要在“效率”和“风控”之间找到平衡。建议采用阈值 + 异常触发的组合策略:
审批可以分级:
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。