我以前刚接触这类项目时,第一个感觉是“汽车库存管理系统?不就是个带增删改查的小程序吗”。真正入行做了几套后才意识到,难的不是CRUD,而是车辆在整个销售周期里状态怎么流转、价格权限怎么控制、多人并发改同一台车怎么办。这篇就结合我做过的微信小程序 + PHP + uniapp架构的汽车销售库存管理系统,把业务拆解、数据建模、接口设计、上线踩坑一次性说清楚。不管你是做毕业设计,还是真给车商做内部工具,这篇都能给你一个可以直接落地的参考。
1. 先搞懂汽车销售库存管理到底在管什么
这个系统叫“汽车销售库存管理系统”,但如果你只按普通电商库存的逻辑去做,做出来大概率是没法用的。我第一版就是这样,把车辆当普通商品,设计成“商品表 + 库存数量 + 上下架”,结果给车商演示时直接被问住了:“我这辆车的钥匙放在哪?这台车是在展厅还是车库?客户试驾完了车为什么变脏了?”
这些问题的背后,其实反映出汽车库存和普通商品库存的本质差异。
1.1 汽车经销商的库存管理,到底在管什么
做系统前我在一家二手车展厅待过两天,也跟几个4S店销售聊过。汽车库存里流转的信息远远超过“数量”本身:
- 车辆基础信息:车系、车型、年款、排量、变速箱、车身颜色、内饰颜色、车辆识别代号(VIN码)、发动机号、生产日期。
- 价格信息:指导价、进货价、底价、挂牌价、成交价。这些价格不是一个人都能看的,底价通常只有店长能看到。
- 位置信息:新车停在车库、展厅、交付区,还是已经在维修车间做PDI检测。
- 状态信息:在库、在展、试驾中、已预订、已销售、已下架。
- 业务单据:入库单、出库单、试驾记录、订单、销售合同、交付记录。
这些信息组合在一起,才构成“这辆车现在能不能卖、以什么价格卖、在哪里能提到车”的完整答案。
1.2 车辆库存与普通商品库存的本质差异
普通商品库存,关注的是数量加减和库存预警。比如:进了100件T恤,卖了30件,剩70件,低于20件就报警。汽车库存完全不同,它有几个特殊性。
单台价值高,所以需要逐台管理。没有哪台车可以模糊地说“库里还有3辆雅阁”,必须精确到哪一台,因为每一辆车的颜色、配置、加装包、生产批次都可能不同。
状态必须保留流转轨迹。一台车不是直接“在库”就跳到“已售”的,中间可能有车主看车后想试驾、试驾完谈了价格先交定金预留,然后走贷款审批,最后才完成过户。每个节点都需要记录下来,否则销售和财务对不上账。
权限粒度必须落到字段。普通库存系统只要区分“管理员”和“普通员工”就行,但汽车库存系统里,普通销售能看到“挂牌价”,未必能看到“底价”,更不应该看到“进货价”。这些字段一旦全局可见,很容易引发内部矛盾。
1.3 系统最终要覆盖的角色与操作路径
我最终梳理出来的角色有四类:超级管理员、店长、销售顾问、财务。每个角色在小程序端看到的页面和操作按钮都不一样。操作路径大致是这样的:
- 车辆入库:销售或库管录入车辆基础信息、上传照片、设置初始状态和库位,系统生成唯一车辆ID。
- 客户看车:销售顾问在车辆列表搜索符合条件的车,查看状态是否“可售”。
- 试驾登记:将车辆状态改为“试驾中”,记录客户信息、试驾时间。
- 预订/销售:客户确定购买后,状态改为“已预订”或“已售”,生成订单,关联客户、价格、付款方式。
- 出库交付:财务确认收款后,车辆状态改为“已交付”,从库存中移除。
这样一套路径下来,核心是“车辆状态机”,而不是几个增删改查的页面。状态设计得好不好,直接决定系统能不能被真正用起来。
2. 技术选型复盘:uniapp + PHP这套组合合适吗
很多人问我,为什么不用Spring Boot + Vue,或者Node + React?我的回答是:技术选型不是越新越好,而是要匹配项目形态、团队能力和部署条件。这套汽车销售库存管理系统,我选的是uniapp做小程序端,PHP做服务端,下面说一下真实考量。
2.1 前端选uniapp,解决的是多端复用问题
汽车销售这个场景,使用角色不只在手机上。销售顾问在店里可能用平板给客户展示,库管在停车场用手机做入库,店长在家里打开手机看库存报表。如果只做微信小程序,起码还要额外做一套H5给临时场景用。
uniapp的价值在于:一套代码可以编译到微信小程序、H5、App(Android/iOS)。我实际开发时,微信小程序是主输出,H5用来给店长在PC浏览器上打开后台管理,安卓App我用同样的代码打了个测试包,几乎没额外花时间。这就是我坚持用uniapp的理由。
2.2 后端选PHP,核心是部署成本低、开发迭代快
这个系统并不是一个高并发的C端产品,它的最高同时在线人数可能也就二三十个销售。对这类内部业务系统,PHP完全够用,而且部署成本极低:
- 一台普通的云服务器,装好Nginx + PHP + MySQL就能跑。
- 代码上传后不需要编译、不需要重启服务,改完直接生效。
- 对不熟悉Docker、K8s的小团队来说,运维门槛低很多。
我在项目中用的是ThinkPHP 6框架,因为它自带路由、ORM、验证器、中间件这些常用能力,又比Laravel更轻量,文档对中文开发者友好。如果你自己从零写,也完全可以,但用框架能省下很多重复劳动。
2.3 整体架构与数据流向
系统整体分三层:
- 客户端层:uniapp开发,编译输出微信小程序。
- 服务端层:PHP(ThinkPHP 6)提供JSON API。
- 数据层:MySQL存储业务数据,Redis缓存登录态和热点数据。
数据流向大致是:小程序端发起请求,经过微信小程序框架的request方法,携带token访问后端接口,PHP先做鉴权和参数校验,再操作数据库,最后统一返回JSON。 我在接口设计里还加了一层拦截器,统一处理跨域、签名校验和日志记录,后面会细说。
选择这套组合还有一个现实原因:好招人、好维护。PHP和uniapp的开发者基数大,后续如果团队换人,接手成本比很多冷门技术栈低。这个话题在技术社区里经常争论,但从实际项目交付角度看,稳定可靠比技术炫耀重要得多。
3. 库存系统的心脏:车辆状态机与数据库表设计
如果你只想看懂一个模块的代码,那一定是车辆状态机;如果你想自己从零写这套系统,那一定要先画好数据表。我前后重构过两次,都是因为状态设计有问题。
3.1 车辆状态机的定义
我在第一版设计里只定义了三个状态:在库、已售、下架。运行了两周就出问题了:销售在列表里看到一台“在库”的车,带客户去车库看,结果车被另一个销售开出去试驾了。根源就是缺少“试驾中”这个中间状态。
最终我定义的完整状态流如下:
| 状态值 | 名称 | 含义 | 可执行操作 |
|---|---|---|---|
| 0 | 未入库 | 信息已录入但未实际到店 | 入库、修改、删除 |
| 1 | 在库 | 车辆在车库,可售 | 转展厅、试驾、预售 |
| 2 | 在展 | 车辆在展厅,可售 | 转车库、试驾、预售 |
| 3 | 试驾中 | 客户正在试驾,暂不可售 | 归还(回到原状态) |
| 4 | 已预订 | 客户已付定金,锁定车辆 | 取消预订、确认销售 |
| 5 | 已售 | 已签合同,未交付 | 财务确认、交付 |
| 6 | 已交付 | 已提走,出库 | 仅查看 |
其中有个细节很重要:试驾中的车辆归还时,不是简单回到“在库”,而是要回到试驾前所在的仓位。如果一辆车从“在展”状态去试驾,归还后应该回“在展”。所以每次状态变更,我都会记录变更前状态和变更后状态,而不是直接在原字段上覆盖。
3.2 车辆主表、状态记录表与订单表设计
数据库我建了这么几张核心表,直接给出建表思路供你参考。
车辆主表vehicle,每辆车一条记录:
CREATE TABLE `vehicle` ( `id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '车辆ID', `vin` varchar(32) NOT NULL COMMENT '车辆识别代号', `brand` varchar(50) NOT NULL COMMENT '品牌', `series` varchar(50) NOT NULL COMMENT '车系', `model_name` varchar(100) NOT NULL COMMENT '车型名称', `model_year` varchar(10) NOT NULL COMMENT '年款', `engine_no` varchar(50) DEFAULT NULL COMMENT '发动机号', `color_out` varchar(20) DEFAULT NULL COMMENT '外观颜色', `color_in` varchar(20) DEFAULT NULL COMMENT '内饰颜色', `guide_price` decimal(12,2) NOT NULL DEFAULT '0.00' COMMENT '指导价', `base_price` decimal(12,2) NOT NULL DEFAULT '0.00' COMMENT '进货价', `floor_price**decimal(12,2) DEFAULT NULL COMMENT '底价', `sale_price` decimal(12,2) DEFAULT NULL COMMENT '挂牌价', `status` tinyint(1) NOT NULL DEFAULT '1' COMMENT '当前状态', `location` varchar(50) DEFAULT NULL COMMENT '库位信息', `created_by` int(11) DEFAULT NULL COMMENT '录入人', `created_at` datetime DEFAULT NULL COMMENT '创建时间', `updated_at` datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', PRIMARY KEY (`id`), UNIQUE KEY `uk_vin` (`vin`), KEY `idx_status` (`status`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;注意两个细节:VIN码必须加唯一索引,这是车辆的唯一身份;status字段要建索引,因为列表页最频繁的就是按状态筛选。
状态记录表vehicle_log,记录每次状态变化和操作人:
CREATE TABLE `vehicle_log` ( `id` int(11) unsigned NOT NULL AUTO_INCREMENT, `vehicle_id` int(11) NOT NULL COMMENT '车辆ID', `from_status` tinyint(1) NOT NULL COMMENT '变更前状态', `to_status` tinyint(1) NOT NULL COMMENT '变更后状态', `remark` varchar(255) DEFAULT NULL COMMENT '备注', `operator_id` int(11) NOT NULL COMMENT '操作人ID', `created_at` datetime NOT NULL COMMENT '操作时间', PRIMARY KEY (`id`), KEY `idx_vehicle_id` (`vehicle_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;订单表order:
CREATE TABLE `order` ( `id` int(11) unsigned NOT NULL AUTO_INCREMENT, `order_no` varchar(32) NOT NULL COMMENT '订单编号', `vehicle_id` int(11) NOT NULL COMMENT '车辆ID', `customer_name` varchar(50) NOT NULL COMMENT '客户姓名', `customer_phone` varchar(20) NOT NULL COMMENT '客户手机号', `deal_price` decimal(12,2) NOT NULL COMMENT '成交价', `deposit_amount` decimal(12,2) DEFAULT '0.00' COMMENT '定金', `pay_status` tinyint(1) NOT NULL DEFAULT '0' COMMENT '收款状态 0未收款 1已收款', `sales_id` int(11) NOT NULL COMMENT '销售顾问ID', `created_at` datetime NOT NULL, PRIMARY KEY (`id`), UNIQUE KEY `uk_order_no` (`order_no`), UNIQUE KEY `uk_vehicle_id` (`vehicle_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;订单表的vehicle_id加了唯一索引,意思非常明确:一台车只能生成一张有效订单,这是防止一车多卖的数据库兜底保障。
3.3 价格权限怎么存:容易被忽视的细节
价格权限是我和客户讨论最多的需求。销售顾问登录后能查看@media挂牌价和底价,但进货价只有店长和财务可见。这个需求不能通过简单的前端v-if判断来实现,因为接口返回的数据一旦包含进货价,前端完全可以通过抓包看到。
正确的做法是:在接口层做字段级别的权限控制。PHP代码里写一个方法,根据当前用户的角色决定返回哪些字段:
protected function getVisiblePriceFields(User $user): array { $base = ['id', 'guide_price', 'sale_price', 'floor_price']; if (in_array($user->role, ['admin', 'finance'])) { $base[] = 'base_price'; // 进货价仅管理员和财务可见 } return $base; }4. 小程序端实现要点:列表、入库、扫码与状态流转
小程序端是整个系统使用频率最高的部分,销售顾问每天打开它查库存、改状态、录客户。界面不需要多炫酷,但要保证:常用操作少点几下、关键信息一眼看到、恶劣网络环境下也不容易出乱子。
4.1 页面结构划分与底部导航
我用uniapp搭建的页面结构如下:
- 工作台首页:展示今日待办、库存总览、最近入库/出库记录。
- 库存列表:作为核心页面,支持按品牌、车系、状态、价格区间筛选。
- 车辆详情:展示一台车的完整档案和状态记录。
- 入库登记:新建车辆表单,支持VIN扫码快速填充。
- 订单管理:订单列表、订单详情、状态流转操作。
底部导航我用的是“工作台、库存、订单、我的”四个Tab。这个设计比较常规,但要注意一个点:入库登记没有放在Tab里,而是放在库存页右上角的按钮,因为入库是低频操作,不该占用底部导航的固定入口。
4.2 库存列表的筛选与状态色块设计
库存列表是销售使用频率最高的页面。我做了筛选栏,条件是品牌、状态、价格区间,用uniapp原生的picker组件实现。列表卡片上,车辆缩略图左侧,右侧是车型、年款、颜色和状态标签。状态标签用不同颜色区分:
- 在库/在展:绿色,表示可售。
- 试驾中:橙色,表示车辆暂时不可售。
- 已预订:蓝色,表示车辆已经锁定。
- 已售/已交付:灰色,基本不会出现在普通列表。
列表性能方面也要注意。车辆图片如果直接用原图,在弱网环境下会非常卡。我做了两件事:一是后端生成缩略图,列表接口只返回小图URL;二是使用uniapp的懒加载属性,只在图片进入可视区域时才请求。
4.3 入库操作:VIN扫码、照片上传与表单校验
入库是整个系统最繁琐的业务,一台车十几项信息要填。为了提高效率,我加了VIN扫码功能,调用uni.scanCode,扫描车辆铭牌上的VIN码条码,自动填充VIN字段。这里有个经验:VIN码第10位代表年款,如果数据库里还没有对应车型,可以提示用户手动选择,但不要自动猜车型,因为同一VIN在不同地区的配置可能不同,猜错的代价很高。
照片上传是入库必备操作。我这里用uni.uploadFile把图片传回PHP服务端,后端存储到服务器目录,并生成缩略图。要注意:uniapp里uploadFile的url需要是后端完整接口地址,文件字段名要与PHP端$_FILES的key一致,否则上传永远失败。
表单校验我放在前端和后端各做一遍。前端校验主要拦截空字段、手机号格式、价格范围;后端校验才是真正的安全底线,尤其是价格字段,必须校验是否为合法数字,防止提交非数字数据。
4.4 出库与销售操作:从选车到生成订单
销售操作的核心流程:销售顾问在车辆详情页点击“销售下单”,系统弹出一个表单,自动带出客户信息输入项。提交后调用后端接口,后端做两步操作:
第一步,检查车辆状态是否为“在库”或“在展”,如果不是则返回错误;第二步,在同一事务里,把车辆状态改为“已预订”,生成订单记录,订单号规则为“XS+年月日+4位随机数”。
这里有个必须注意的点:检查和更新不能分开执行,否则两个销售同时操作同一台车时,可能会同时通过检查。正确做法是用UPDATE语句带状态条件来实现乐观锁:
UPDATE vehicle SET status = 4, updated_at = NOW() WHERE id = #{vehicleId} AND status IN (1, 2)如果影响行数为0,说明车辆状态已经被别人改动过了,直接提示用户“车辆已被预订或状态已变化”。这是防止一车多卖最关键的一行代码。
5. 服务端接口与安全实践:PHP端如何把业务逻辑写清楚
服务端是这套系统的中枢。接口设计得是否清晰,直接决定小程序端开发是否顺畅。我总结了一套适合这类管理系统的接口规范。
5.1 接口设计原则:资源路径与统一返回格式
我所有接口都遵循一个简单的REST风格:资源名用名词复数,操作用动词或HTTP方法。但考虑到内部系统的实际情况,我以POST为主,避免GET请求参数过长和特殊字符转义问题。核心接口如下:
| 接口路径 | 方法 | 功能 |
|---|---|---|
| /api/auth/login | POST | 登录,返回token |
| /api/vehicle/list | POST | 车辆列表(分页+筛选) |
| /api/vehicle/detail | POST | 车辆详情 |
| /api/vehicle/add | POST | 新增车辆(入库) |
| /api/vehicle/changeStatus | POST | 车辆状态变更 |
| /api/order/create | POST | 创建订单(销售) |
| /api/order/list | POST | 订单列表 |
| /api/statistics/overview | POST | 首页统计数据 |
返回格式统一为:
{ "code": 0, "msg": "success", "data": {} }code为0表示成功,非0表示业务错误,比如1001表示未登录,1002表示无权限,2001表示车辆状态不允许当前操作。小程序端封装一个request方法,统一处理这些错误码,遇到1001清除本地登录态并跳转登录页。
5.2 登录与权限控制:token如何签发、校验、续期
微信小程序登录流程是:小程序端调用wx.login获取临时code,传给后端,后端拿着code调用微信接口换取openid,再根据openid找到用户,生成自己的token返回给小程序。
PHP端代码关键思路如下:
public function login(Request $request) { $code = $request->post('code'); $result = $this->wechat->login($code); // code换openid $openid = $result['openid']; $user = User::where('openid', $openid)->first(); if (!$user) { return json(['code' => 1002, 'msg' => '用户不存在,请联系管理员']); } $token = md5($openid . time() . rand(1000, 9999)); cache("token_" . $token, $user->id, 86400 * 7); // 7天有效 return json(['code' => 0, 'data' => ['token' => $token, 'role' => $user->role]]); }我用了一个简单但不失安全性的方案:token存Redis,有效期7天。每个需要鉴权的接口在中间件里取出token,查Redis拿到用户ID,再加载用户的角色和权限。这个方法虽然不如JWT高大上,但好处是可控性强——管理员可以随时把一个用户的token踢下线,只需要删除对应的Redis key。
5.3 统计接口的实现:库存结构的SQL聚合
首页数据统计是店长每次打开小程序最先看的内容。我要统计的是:总库存、可售车辆数、今日入库、今日销售、按品牌分布的库存。这些统计如果分开查,一次首页请求要跑四五条SQL,性能不差但代码很乱。我建议把它们合并到一个接口里,用一条GROUP BY查询统计品牌分布,再用另一条查总量。
public function overview() { $total = Vehicle::where('status', '!=', 0)->count(); $soldToday = Order::where('created_at', '>=', date('Y-m-d 00:00:00')) ->where('pay_status', 1)->count(); $inToday = Vehicle::where('created_at', '>=', date('Y-m-d 00:00:00'))->count(); $brandStats = Vehicle::whereIn('status', [1,2,3]) ->field("brand, count(*) as total") ->group("brand")->select()->toArray(); return json(['code'=>0, 'data'=>[ 'total'=>$total, 'sold_today'=>$soldToday, 'in_today'=>$inToday, 'brand_stats'=>$brandStats ]]); }这里有一个业务口径的坑:统计“可售”时,试驾中的车到底算不算可售?我最终的决定是,试驾中的车不计入可售数量,因为它当前无法被卖给其他客户。但在库存总量里要算进去。这个口径要跟客户确认清楚后再写代码,否则上线后数据对不上,会被财务和销售同时投诉。
5.4 图片上传接口:处理方式与目录规范
车辆图片我最多允许上传9张,存到服务器 /uploads/vehicle/{vehicle_id}/ 目录下,文件名用时间戳加随机字符串。上传接口用ThinkPHP的文件接收方法:
public function upload(Request $request) { $file = $request->file('file'); $validate = ['size'=>2048000, 'ext'=>'jpg,jpeg,png']; if (!$file->check($validate)) { return json(['code'=>2001, 'msg'=>$file->getError()]); } $saveName = date('YmdHis') . '_' . uniqid() . '.' . $file->extension(); $file->move(public_path() . 'uploads/vehicle/', $saveName); return json(['code'=>0, 'data'=>['url'=>"/uploads/vehicle/" . $saveName]]); }存储的URL是相对路径,小程序端拼接上服务器域名即可。千万不要在数据库里存完整URL,因为以后换域名或迁移服务器时,改配置比改数据库容易得多。
6. 联调、打包与上线中的坑,我帮你踩过了
系统功能写完,真正的考验才开始。下面这几类问题是我在这套小程序开发过程中实际遇到并解决的,每个都值得记下来。
6.1 HBuilderX发行微信小程序的关键配置
uniapp项目在HBuilderX开发,最终要发行成微信小程序代码包。操作路径是“发行 - 小程序-微信”,但在此之前,有几处配置不做好,发行后就会各种报错。
manifest.json里的微信小程序配置项必须检查:appid要替换成自己在微信公众平台申请的小程序AppID,不能用测试号;权限声明要根据实际需要勾选,比如用到uni.scanCode就必须在mp-weixin的permission里声明scope.camera权限;ES6转ES5选项建议开启,保证微信低版本客户端也能运行。
域名配置是另一个大坑。微信小程序正式环境要求所有请求域名必须HTTPS,并且在微信公众平台带宽白名单里配置。开发阶段可以勾选“不校验合法域名”,但上线前一定要把PHP服务的HTTPS证书配置好。我是用Nginx配置的免费SSL证书,宝塔面板里直接申请和部署,整个过程不到十分钟。
6.2 uniapp在安卓端的几个兼容性问题
联调时最折磨人的是不同端的表现差异。我遇到的最典型问题是轮播图在安卓手机上有黑边。原因在于uniapp的swiper组件在编译到安卓App端时,渲染机制和微信小程序不同,图片如果没完全贴合容器,就会露出背景色。解决办法是给swiper和swiper-item都设置相同的圆角,并且把图片模式设置为aspectFill:
<swiper class="car-swiper" circular="true"> <swiper-item v-for="(img, idx) in car.images" :key="idx"> <image :src="img" mode="aspectFill" class="car-img" /> </swiper-item> </swiper>还有一个是webview返回问题。如果在uniapp项目里嵌入了webview页面,H5返回时不会直接触发小程序的navigateBack,而是优先让网页内部的history后退,导致用户点返回没有反应。这个问题我最终靠在webview外层的页面包裹层监听返回事件,判断webview的URL是否可以后退,如果可以就调用webview的back方法,否则才关闭页面。
6.3 权限弹窗的实时监听问题
有个需求是希望小程序实时监听系统权限申请框的出现和消失,做同步提示。这是一个很具体的uniapp问题:微信小程序本身没有提供权限弹窗出现/消失的全局事件监听,uniapp也没有封装相关API。我的方案是折中的:在需要调起权限的地方,比如扫码前或选择相册前,由前端主动弹出自己的确认框,告知用户接下来会请求什么权限。系统级的权限框出现和消失无法被全局感知,但可以通过页面的onShow/onHide事件间接判断,因为系统权限框弹出时,小程序页面会触发onHide;用户选择后回到页面会触发onShow。这不能构成完整的弹窗生命周期监听,但已经能满足大部分业务提示需求。
6.4 数据一致性经验:并发状态更新怎么防超卖
这是整套系统里最容易出问题、也最容易被忽略的地方。前面我提到了乐观锁的UPDATE方案,这里再展开说一个实际场景。
假设两个销售同时看到一台在展车辆,客户都有意向。A销售先提交订单,UPDATE语句执行成功,影响行数为1,车辆状态从2变为4。B销售后提交,同一条UPDATE语句执行,但因为WHERE条件里带上了status IN (1,2),而车辆status已经是4,所以影响行数为0,代码判断后返回错误。这样就从数据库层面杜绝了同一台车被卖给两个人的可能。
曾经有一个阶段,我在部分接口里没有用这种带条件的UPDATE,而是先SELECT再UPDATE,结果出现了两个订单同时指向一台车的情况。排查了很久,最后加了这个条件才解决。现在我把这个规则强制写进所有涉及状态变更的接口里,也建议你从第一天就这么做。
7. 上线后的数据验证与持续优化
系统上线不是终点。我整理了一套上线后必须跑的数据验证清单,每一条都是真实业务中踩出来的。
入库出库数据核对:每周导出车辆状态记录,和门店实际台账核对,看有没有状态记录缺失或跳变的情况。如果发现某台车从“在库”直接变成“已售”而没有经过“已预订”,说明销售有绕过流程手动改状态的行为,需要和店长确认是流程问题还是系统设计问题。
价格权限抽查:定期用不同角色的账号登录,检查接口返回的JSON里是否真的不包含本角色不可见的字段。这里我特别强调,不只是小程序端不显示,接口返回的原始数据里也不能包含,因为懂技术的人完全可以看接口返回。
操作日志审计:状态变更表必须保留至少6个月的数据。发生纠纷时,这些日志是判断责任的关键依据。我做了一个管理端查询功能,可以按车辆ID或操作人ID查看完整操作时间线。如果你觉得记录字段还不够,可以再加一个操作前快照字段,把变更前的整个车辆JSON存下来,这样即使后来数据被改乱了,也能恢复现场。
还有一个经常被忽略的是缓存设计。统计接口的数据如果每次都要实时跑GROUP BY,车辆到几百台时影响不大,但如果加到几千台,加上联合查询,响应时间就会明显上升。我后来给统计接口加了Redis缓存,缓存时间5分钟,店长刷首页看到的统计结果最多延迟5分钟,完全能接受。这个优化很简单,但效果非常明显,从原来的800毫秒降到20毫秒。
实际做下来,我发现这套系统最大的难点不是某个技术点,而是怎么把线下业务流程完整转译成数据模型。库存表、状态机、订单表这些设计一旦定好,后面的开发基本就是体力活。包括VIN扫码、照片上传、权限控制、并发防重这些细节,都是从一次次业务沟通和问题排查中沉淀下来的。如果你也在做类似的项目,建议先花一两天时间去了解业务人员的实际操作流程,再动手写代码,这样出来的系统才真正有人愿意用。