
去年年底我们部门做例行盘点仓库里那台ThinkPad T14死活找不到。登记表上写的领用人早就离职了翻了一圈交接记录也没人更新过借用状态。三个月后这台机器在另一个项目组的柜子里被翻出来资产标签早就磨掉了全靠序列号才确认身份。这种场景干过IT行政或者设备管理的人应该都不陌生——Excel台账在人员频繁流动、设备不断调拨的现实面前基本就是一张废纸。后来我花了两个多星期基于LayuiMini框架和PHP把公司内部的资产管理系统从零搭了出来专门针对IT办公行业的固定资产与设备管理场景。这套系统现在跑了大半年盘点时打开手机扫码枪对着标签扫一圈差异清单十分钟就能拉出来。这篇文章就把这套源码从选型、数据建模、业务拆解到部署排错的过程完整复盘一遍给打算自己做资产系统或者正在选型同类源码的朋友做个参考。1. 为什么选LayuiMini搭配PHP做资产台账1.1 先说说我要解决的具体问题IT办公行业的资产管理和工厂仓库那种进销存是两码事。工厂管的是原材料和成品进出库流程固定、批次清晰但IT部门的资产是另一类逻辑设备品类杂——台式机、笔记本、显示器、打印机、网络设备、软件License甚至显示器支架和扩展坞都有人要登记生命周期长且跳跃——一台电脑从入库到报废中间可能被领用、归还、调拨、维修反复走好几个来回责任人变动频繁——入职领电脑、离职还电脑、调岗换机器这几个动作每年发生上百次。在这个场景下Excel台账主要有三个致命伤一是多人在线编辑时版本冲突改着改着就覆盖了二是资产状态全靠人工同步借用中维修中这类中间态经常漏记三是缺乏明细流水出了问题没有追溯依据。所以这套系统最核心的目标不是做出一个炫酷的界面而是把每台设备从入库到报废的所有状态变化都完整记录在案。1.2 LayuiMini到底解决了哪一部分界面上的事情我用的是LayuiMini。接触过Layui的人应该知道它本身是一套基于jQuery的UI组件库有表格、表单、弹出层、树形控件这些现成组件写后台管理系统的交互足够用了。但Layui有个问题——它只管组件不管后台的整体骨架。菜单、顶部导航、多标签页、主题切换这些壳子还得自己拼。LayuiMini就是把这个壳子直接做好的一套后台管理模板开箱即用左边菜单栏、顶部标题栏、内容区域多标签页切换都有。我在选型时也对比过Vue全家桶加Element UI的方案如果团队有专职前端那套工程化体系确实强大。但现实是很多做PHP源码交付的开发者、集成商只有一两个人后端写完还得兼顾页面。LayuiMini配PHP的思路是把大部分页面逻辑放在服务端渲染表格和表单交互用Layui组件解决不需要额外维护一套前端工程也不用考虑跨域接口、Token刷新这些前后端分离才能带来的复杂度。实话实说选这套组合不是因为它技术上有碾压性优势而是因为它和中小团队内部IT资产管理这个需求完美匹配开发周期短、部署要求低、维护成本可控。就我个人的经验一个人一周时间能把基础功能全部跑通这在Vue方案里是很难想象的。1.3 PHP在这个场景里的位置后端选择PHP理由其实和LayuiMini类似——务实。这套系统的使用方大概率是公司IT部门或者中小企业服务器往往就是一台普通的CentOS/Ubuntu或者干脆用宝塔面板管理。PHP能跑在几乎任何虚拟主机上不需要常驻进程没有JVM内存压力遇到问题重启一下PHP-FPM就完事。我在项目里用的是原生PHP加PDO没有引入Laravel或者ThinkPHP这种重量级框架。这个选择可能有人觉得反常规但对于一个资产管理系统来说核心逻辑是清晰的CRUD加状态流转真正的复杂度在数据模型和业务流程设计上而不在框架本身的抽象能力。原生PDO的好处是代码透明、定位问题直接、部署时少一层Composer依赖。当然如果你习惯了ThinkPHP的ORM用它也完全没问题——关键是把业务逻辑想清楚这个后面会细说。2. 资产不是一条记录而是一串事件数据模型才是系统地基2.1 两张核心表资产主表和事件流水表很多人在设计资产系统时容易犯一个错误就是把资产设计成一张字段特别多的静态记录表里面塞了使用人、位置、状态、借用日期好几个冗余字段。这样做看着简单但实际用起来全是坑今天张三借走你把使用人改成张三明天张三还回来你又把使用人改回空。整个过程中谁借的、什么时候借的、凭据是什么全部丢失。我的设计思路是拆成两张表资产主表assets和事件流水表asset_logs。资产主表只保存当前快照也就是这台设备此时此刻归属于谁、在什么位置、处于什么状态。事件流水表存这台设备从入库开始发生过的每一个动作。任何状态变更都同步写一条流水流水里记录动作类型、操作人、时间、变更前后的值。资产主表的核心字段设计如下字段说明类型id自增主键intasset_no资产编号业务唯一键varchar(32)asset_name资产名称varchar(100)category_id资产分类IDintbrand品牌varchar(50)model型号varchar(50)sn设备序列号加唯一索引varchar(64)price购置价格decimal(10,2)purchase_date购买日期datesupplier供应商varchar(100)location存放位置varchar(100)owner_user_id当前使用人intowner_dept_id当前归属部门intstatus状态码tinyinttag_printed标签是否打印tinyintremark备注varchar(255)created_at入库时间datetimeupdated_at最后更新时间datetimedeleted_at软删除时间datetime流水表更简单关键是变更前和变更后两个JSON字段。我在实际操作中发现这两个字段非常有用——比如盘点时发现资产被人从3号仓库挪到了行政部直接看流水里变更前后的location字段就能定位是谁干的、什么时候干的不需要再去翻聊天记录。2.2 资产编号一物一码是怎么生成的资产编号是这套系统的身份证逻辑好坏直接决定后续扫码、盘点、检索的体验。我采用的规则是分层拼接资产类别前缀 年份 流水号。比如一台2024年入库的笔记本编号就是NB-2024-0001显示器是MN-2024-0001服务器是SR-2024-0001。这样做的优势很明显看到编号就知道大类不用进系统就能快速判断打印机或者电脑报废后编号不会复用所有历史记录保持连续扫标签时一眼能看出这台设备大概是什么时候进的货。生成编号的逻辑我放到入库接口里用事务加行锁保证唯一核心代码如下public function generateAssetNo($categoryCode, $year) { $db new PDO($dsn, $user, $pass, [PDO::ATTR_ERRMODE PDO::ERRMODE_EXCEPTION]); $db-beginTransaction(); // 行锁防止并发重复 $sql SELECT curr_no FROM asset_no_sequence WHERE category_code ? AND year ? FOR UPDATE; $stmt $db-prepare($sql); $stmt-execute([$categoryCode, $year]); $row $stmt-fetch(); if ($row) { $nextNo $row[curr_no] 1; $db-prepare(UPDATE asset_no_sequence SET curr_no ? WHERE category_code ? AND year ?) -execute([$nextNo, $categoryCode, $year]); } else { $nextNo 1; $db-prepare(INSERT INTO asset_no_sequence (category_code, year, curr_no) VALUES (?, ?, 1)) -execute([$categoryCode, $year]); } $db-commit(); return sprintf(%s-%d-%04d, $categoryCode, $year, $nextNo); }注意流水号用了%04d也就是从0001开始。如果公司一年入库的设备超过9999台把格式改成%05d就行这个看实际规模调整。另外我把序列号字段加了唯一索引因为IT设备的SN通常在厂家那边是唯一的这能有效防止同一条设备被重复录入台账。2.3 状态机和软删除约定资产状态的设计我用的是一个tinyint字段目前定义了六个状态码状态码含义说明0在库已入库但未领用存放在仓库1在用已被员工领用正常使用中2维修中故障送修不参与盘点考核3借用中临时借用区别于正式领用4报废已走完报废流程等待处理5遗失盘亏锁定的状态用整型不用字符串主要是历史和统计方便。比如按月统计在库数量status 0直接条件查询后面如果想加状态再加一个整型值就行不需要改表结构。软删除字段我保留了。资产数据和普通业务数据不一样它涉及固定资产账面审计物理删除等于销毁痕迹。所以删除操作一律走逻辑删除查询默认过滤deleted_at IS NULL后台保留一个管理员可见的已删除资产列表方便审计追溯。3. 业务模块拆解从入库到盘点我把流程分成了五个环节3.1 资产入库成批登记的高效姿势入库是整个系统的起点也是我最早做完的模块。新采购的设备到货后仓库管理员需要把每台设备的信息录入系统。如果只用表单手动录入一台设备十几个字段打下来来一百台机器就得加班到深夜。所以入库模块我做了两种途径单台手动新增和Excel批量导入。批量导入的模板我固定成了标准列资产名称、分类、品牌、型号、序列号、价格、购买日期、供应商、存放位置、备注。导入时后端用PHPExcel库读取文件每一行逐一校验错了的在返回结果里标出第几行、哪一列、为什么错方便用户修正后再传。我在此处踩过一个坑批量导入时如果一次性处理上千行PHP脚本会执行超时。解决的办法是分批处理每一百条commit一次事务并且在调用前设置set_time_limit(0)。虽然不优雅但在这个场景里确实管用。入库还有一个容易被忽略的伴随动作——生成资产标签。我的做法是入库成功后系统自动生成对应条码/二维码图片管理员可以选择打印标签操作调用浏览器打印功能把标签纸打出来。等标签贴到设备上这台设备才算真正进入可领用状态。所以我在assets表里加了tag_printed字段盘点时就能一眼看出哪台机器没贴标签。3.2 领用与归还把人-资产绑定状态变更做成闭环领用和归还是使用频率最高的两个操作直接对应员工借电脑和员工离职还电脑这两个典型场景。领用流程我在页面上做成了一个下拉搜索扫描的混合模式管理员输入或扫描资产编号系统自动带出这台设备的当前状态、位置、配置信息确认无误后选择领用人、部门点击提交。接口做的事情有两步——更新资产主表的使用人和状态写一条类型为领用的流水记录。归还流程类似只不过方向相反员工还回设备管理员把使用人清空、状态改回在库。这里我特意做了一个功能模块叫离职资产交接清单——选择离职的员工姓名后系统自动列出他名下所有资产管理员逐台勾选确认已归还一次性批量更新。这个功能在实际使用中反馈最好因为IT部门在员工离职时最头疼的往往就是这人手里到底有哪些设备、哪些已经找不到了。3.3 维修和调拨把异常状态也记录在案设备不可能永远不出故障。我单独做了维修模块因为维修这个动作有几个特点周期不确定、费用可能产生、中间状态需要特殊标识。维修流程是从在用/在库状态发起维修单填写故障描述、送修日期、维修方、预估费用提交后资产状态自动变为维修中归还时更新维修结果和实际费用状态恢复到原来的在库/在用。调拨和借用是两个不同概念。调拨是资产从一个部门正式转到另一个部门所有权归属变化需要走确认流程借用是临时的跨部门使用不算永久变更。这两种我分开建模调拨单有发起人和接收人审批通过后变更部门和位置借用单则记录借出时间、预计归还时间、实际归还时间。从数据模型的角度看它们本质上都是给资产主表的状态切换加了一层业务审批但没有审批单就是普通修改有审批单才能保证操作合规。3.4 盘点模块码枪和手机扫码的两种实现方案盘点功能是这套系统区别于普通台账的核心亮点。传统Excel台账没法做盘点因为没有一个数字化的账面清单可以和物理资产实时比对。我的设计是发起一个盘点计划系统生成当前时间点的账面快照盘点过程中盘到一台设备就标记一次盘点结束后系统自动生成差异报告——盘亏账面有实盘无、盘盈实盘有账面无、位置不符、状态不符几类。盘点时的扫码有两条路线一是扫码枪模式。市面上常见的一维扫码枪实际上是个键盘模拟器扫到条码后直接向焦点输入框发送一串字符再加一个回车。我在盘点页面做了一个聚焦搜索框扫码枪扫到资产编号自动搜索命中后直接打勾不需要点按钮。这个方案的优点是硬件便宜、速度快缺点是必须坐在电脑前。二是手机H5模式。手机浏览器调用摄像头扫码扫到后通过接口补充资产状态。这里我直接用HTML5的getUserMedia加一个二维码识别库实现在线扫码不依赖微信的JS-SDK任何浏览器都能打开。实际测试下来iPhone自带Safari和安卓Chrome兼容性都不错。手机模式的方便之处在于人可以直接走到工位旁边扫不需要把设备搬回仓库。盘点结束后生成差异报告并支持导出Excel作为财务审计依据。这里必须补充一句盘点模块的准确性取决于日常操作是否规范如果平时领用归还都嫌麻烦不录系统盘点差异百分之百会爆表。3.5 资产标签打印模板化设计的细节标签打印是我开发到快一半时补上的模块但实际价值很大。没有标签的设备就像没有门牌号的房间管理全靠脑子和记忆人一多就乱。我选用的是热敏标签打印机标签纸规格50mm乘30mm打印内容包括资产编号、资产名称、类别、加一个二维码。二维码里编码的是资产编号文本不是URL这样就算扫出来是一串字符在哪都能解码。打印功能用浏览器的原生的print能力配一份固定的CSS模板一行一台标签放上打印机选好标签纸尺寸直接打。模板HTML里要注意把打印边距设成0避免标签内容偏移。实际操作中有一个经验不要把品牌型号这些信息印在标签上。标签空间有限而且如果设备调拨或者重新归类标签上的信息就对不上了。资产编号是唯一识别标识其他信息在系统里查就行。这也是后来复盘觉得做得对的一个决定。4. LayuiMini集成过程中我反复踩过的几个坑4.1 iframe多标签页下的登录态和路径问题LayuiMini默认的内容区域是iframe多标签页也就是每个菜单打开都是一个独立的iframe窗口。这个设计在体验上不错但开发时会带来两个坑。第一个坑是会话Cookie在iframe里的传递问题。如果主域名和接口域名不一致或者浏览器开启了严格的SameSite限制iframe里的请求可能不带Cookie导致登录态失效。项目里我在登录后统一把用户信息写入Session并在所有接口入口处做登录校验。被SameSite卡过几次之后我直接在PHP的session初始化处设置Cookie的SameSite属性。第二个坑是相对路径。iframe里的页面URL层级和主框架不同如果在页面里用相对路径引用CSS、JS或者接口地址很容易出现404或者请求打歪。我的处理方式是定义一个全局JS变量window.__BASE__在入口文件中用PHP输出当前项目的根URL所有Ajax请求、静态资源路径都用它拼接。这个习惯在iframe多层嵌套时能避免大量莫名其妙的问题。4.2 表格工具栏事件与数据重载的配合Layui的table组件自带工具栏功能可以放批量领用批量调拨导出数据这些操作按钮。这里容易踩的坑是table.reload之后自定义按钮的事件绑定会失效。我第一次是把click事件直接绑在按钮DOM上结果一刷新列表按钮就点了没反应。调试半天才发现table.reload会重新渲染工具栏HTML之前绑定的DOM对象已经被替换了。正确的姿势是用Layui的table事件监听机制不直接操作DOM而是通过table.on(toolbar(资产列表))来捕获工具栏点击事件行内操作按钮同理用table.on(tool(资产列表))监听。事件对应哪个表通过lay-filter值精准定位。这套事件机制学会了之后table的增删改查交互就都通了。4.3 表单弹窗提交成功后刷新列表的老问题在用Layui做管理后台时最常用的交互流程是点击新增打开layer弹窗弹窗里是form表单填完提交成功后关闭弹窗并刷新父页面的表格。这个流程看起来简单但新手常在刷新这一步卡住。原因在于弹窗里的form是独立页面提交成功后怎么去刷新父页面的表格我在开发时的标准写法是接口返回JSON约定{code:0, msg:操作成功}前端在success回调里执行parent.layer.closeAll()然后调用父页面暴露的刷新方法// 父页面定义一个全局刷新方法 window.refreshAssetTable function() { table.reload(asset_table); }; // 弹窗页面提交成功回调 success: function(res) { if (res.code 0) { parent.layer.closeAll(); parent.refreshAssetTable(); } }这套逻辑的前提是接口返回的数据结构统一。如果字段一会儿叫status一会儿叫code前端洗数据会洗到怀疑人生。所以从项目一开始就定死接口返回规范所有接口统一{code, msg, data}三个字段前端只需要处理同一个结构。4.4 权限控制不能只看前端菜单LayuiMini的菜单是按用户角色动态渲染的这本身没有任何问题。但很多从模板改出来的系统权限校验只在前端做了菜单隐藏后端接口完全是裸奔状态——知道接口地址的人直接请求就能操作。这在内部系统也许问题不大但如果这套源码要交付给其他公司用权限漏洞就是致命缺陷。我的做法是加了一个简单的RBAC权限表用户表、角色表、菜单表、用户角色关联表、角色菜单关联表。每个菜单项对应一个权限标识比如asset:add、asset:delete、log:export。后端接口在入口处统一校验权限标识没有权限直接返回code:403。前端菜单只是能不能看到的问题后端权限才是能不能做的边界。这类权限设计不需要做得像互联网应用那么复杂但对内管理系统的场景已经绰绰有余了。我建议做源码交付的朋友哪怕客户没提权限要求也要把后端校验加上这是做系统的基本底线。5. 部署上线与日常维护把能踩的坑留在开发期5.1 环境组合与Nginx伪静态配置这套系统我最终跑在CentOS 7加宝塔面板上PHP版本用了7.4数据库MySQL 5.7Web服务器Nginx 1.18。部署流程非常简单源码上传到站点根目录导入SQL文件修改config.php里的数据库连接信息然后就完事了。没有Composer安装环节也没有编译过程这也是当时选择原生PHP的便利之处。Nginx下要注意伪静态重写问题。因为系统没有用框架的路由直接走的xxx.php?action这类传统PHP模式其实天然不依赖伪静态。但为了URL好看我做了重写把/index.php/asset/list这类路径指向入口文件。如果你不打算做重写完全不需要理会伪静态配置PHP文件直接访问就行。文件权限方面有一个容易忽略的坑如果把整个站点目录权限设成777固然省事但安全性堪忧。我的做法是程序目录设为755属主设为www用户或者你PHP-FPM运行的那个用户upload目录单独设为775因为标签图片和导入的Excel临时文件需要写入。数据库配置文件的权限收得最紧确保PHP能读即可。5.2 数据库备份与数据安全策略资产管理系统里的数据往小了说是设备台账往大了说是公司的固定资产账面记录。一旦丢失重新盘点可能需要好几个工作日财务那边也没法交代。所以备份方案从上线第一天就定了MySQL定时任务每天凌晨全量备份保留最近30天的备份文件每个季度把备份文件下载到本地冷存储一份。具体操作上宝塔面板有计划任务功能一行mysqldump命令加压缩就可以mysqldump -u[用户名] -p[密码] asset_system | gzip /www/backup/asset_$(date %Y%m%d).sql.gz查询量大了之后建议再加一行清理命令删除30天前的备份避免备份文件把磁盘写满。另外要提醒一点如果公司内部对数据敏感备份文件所在目录要做好权限管理默认不要让所有人都可读。5.3 上线后根据实际使用做的三个调整系统做完不等于优化完。跑了大半年后我根据日常使用反馈陆续做了三个调整这里一并分享出来。第一个调整是把资产状态的展示从普通下拉框改成了状态色块。原本列表页就是一个下拉筛选框但实际用时大家反馈眼睛看不过来。后来我在表格里把在用显示成绿色、维修中显示成橙色、报废显示成灰色信息一眼就能抓到重点。Layui的table支持自定义模板列改起来不复杂但体验提升明显。第二个调整是增加了我的资产视图。原来系统是管理员视角所有操作都围着管转。但实际上每季度经常有员工问我名下有哪些设备。我单独做了一个入口登录用户只看到自己名下的资产列表打印出来签字确认就行。这个功能看似简单但对降低咨询量、明确保管责任特别有效。第三个调整是盘点差异报告加了一个打印版样式。原来的盘点结果页面是网页布局用浏览器打印时会乱七八糟。后来我单独写了一个打印专用模板只保留表格线和差异标记打印出来贴在公司资产公示栏里各部门对账时直接拍照沟通省去了反复解释的麻烦。最后的几条实在话这套系统从开发到现在已经跑了不短时间最大的感受就是选型求稳、数据模型想清楚、权限别裸奔。LayuiMini加PHP这套组合虽然看起来朴素但在中小团队的内部管理系统场景里开发效率、部署门槛、可维护性三个维度综合下来依然是很能打的方案。如果你的需求也是给公司内部做一个资产生命周期管理系统或者想在这类源码的基础上做二次开发这套技术路线我是推荐的。最后再分享一个小技巧给资产系统做字段注释这件事一定要在开发期就做好。数据库每个字段写清楚含义状态码在注释里写明白等到上线半年后再去翻代码你会感谢当时认真写注释的自己。