多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

基于微信小程序与PHP的汽车库存管理系统设计与实现

基于微信小程序与PHP的汽车库存管理系统设计与实现 我以前刚接触这类项目时第一个感觉是“汽车库存管理系统不就是个带增删改查的小程序吗”。真正入行做了几套后才意识到难的不是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、AppAndroid/iOS。我实际开发时微信小程序是主输出H5用来给店长在PC浏览器上打开后台管理安卓App我用同样的代码打了个测试包几乎没额外花时间。这就是我坚持用uniapp的理由。2.2 后端选PHP核心是部署成本低、开发迭代快这个系统并不是一个高并发的C端产品它的最高同时在线人数可能也就二三十个销售。对这类内部业务系统PHP完全够用而且部署成本极低一台普通的云服务器装好Nginx PHP MySQL就能跑。代码上传后不需要编译、不需要重启服务改完直接生效。对不熟悉Docker、K8s的小团队来说运维门槛低很多。我在项目中用的是ThinkPHP 6框架因为它自带路由、ORM、验证器、中间件这些常用能力又比Laravel更轻量文档对中文开发者友好。如果你自己从零写也完全可以但用框架能省下很多重复劳动。2.3 整体架构与数据流向系统整体分三层客户端层uniapp开发编译输出微信小程序。服务端层PHPThinkPHP 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) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;注意两个细节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) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;订单表orderCREATE 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) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;订单表的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/loginPOST登录返回token/api/vehicle/listPOST车辆列表分页筛选/api/vehicle/detailPOST车辆详情/api/vehicle/addPOST新增车辆入库/api/vehicle/changeStatusPOST车辆状态变更/api/order/createPOST创建订单销售/api/order/listPOST订单列表/api/statistics/overviewPOST首页统计数据返回格式统一为{ 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([code0, 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 [size2048000, extjpg,jpeg,png]; if (!$file-check($validate)) { return json([code2001, msg$file-getError()]); } $saveName date(YmdHis) . _ . uniqid() . . . $file-extension(); $file-move(public_path() . uploads/vehicle/, $saveName); return json([code0, 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都设置相同的圆角并且把图片模式设置为aspectFillswiper classcar-swiper circulartrue swiper-item v-for(img, idx) in car.images :keyidx image :srcimg modeaspectFill classcar-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扫码、照片上传、权限控制、并发防重这些细节都是从一次次业务沟通和问题排查中沉淀下来的。如果你也在做类似的项目建议先花一两天时间去了解业务人员的实际操作流程再动手写代码这样出来的系统才真正有人愿意用。
返回列表