资产管理的台账更新,最怕的就是资产编号抄错、盘点数据对不上账。我前阵子基于若依框架把手里的资产管理系统整体升级了一版,核心就一件事:让每一件资产都能“扫一下”完成查询和盘点,不再靠人眼对编号、手工敲键盘。这次升级后,行政、财务、IT几个部门的使用反馈都不错,这篇文章把整个改造过程的思路、代码落地、权限整合和踩坑记录整理出来,给正在做或者准备做类似扫码资产管理的团队一个参考。
1. 资产管理的日常有多痛,扫码方案又是怎么定的
1.1 旧模式下的三个高频崩溃现场
先说旧系统没改造前的样子。资产入库时,管理员在后台手动录入资产编号、名称、责任人、存放位置,然后打印一张A4标签贴在设备上。看着没什么问题,但实际用起来全是坑。
第一个崩溃现场是盘点。每季度盘点,拿着Excel打印出来的资产清单,一间一间办公室跑,看到一台显示器就得弯腰看标签上的编号,然后在一大张表里找这行,手工打个勾。一千多件资产,两个人盘一整天,眼睛都快看花了,回来录入Excel时还有可能抄错行。
第二个崩溃现场是查询。员工打电话问IT:“我这台电脑是什么时候采购的?保修到什么时候?能不能帮我查一下。”IT同事打开后台管理系统,先问资产编号是多少,对方说不知道,再让把机箱上的标签念一遍,一串十几位的编号在电话里念来念去,很容易听错,查出来还不对。
第三个崩溃现场是资产变动。有人调岗,电脑要从A部门调到B部门,资产管理员要在系统里找到这条记录,更改使用人、部门、存放位置。如果资产编号漏记了一位,检索结果就为空,只能拿Excel全文搜索碰运气。
1.2 为何选择“一物一码”而不是RFID或NFC
其实市面上的资产盘点方案不止扫码一种,我当时认真比较过RFID、NFC和扫码三条路线。
RFID的方案是给每件资产贴上无源RFID标签,盘点时拿着手持机在一定范围内扫一圈,可以批量读取。优势是速度快,适合几千上万个资产的大规模盘点,缺点是标签和手持机成本明显更高,而且金属材质的设备(机箱、服务器)对RFID信号有干扰,需要专门抗金属标签,成本还得往上走。
NFC则是近场通信,手机贴上去读取,距离限制在几厘米内,操作上比扫码更麻烦,而且NFC标签贴在金属机箱上同样有读取率问题。
扫码方案的优势就是“零门槛”。资产标签可以打印成条形码或二维码,成本几乎可以忽略,识别设备既可以用USB扫码枪,也可以用手机摄像头。虽然一次只能扫一个,但绝大多数中小企业的资产规模在一两千件左右,这个效率完全够用。
1.3 以若依作为开发底座的原因
选择若依(RuoYi)作为这套系统的开发底座,不是因为它是“国产开源第一框架”所以无脑选,而是看中了它在后台管理系统领域的几项成熟能力。
首先是权限体系。资产管理虽然看起来只是查查台账,但实际使用中有明确的角色边界:普通员工只能查自己的资产,部门主管可以看部门内的资产,资产管理员才能新增、调拨、报废,财务人员可以看资产原值折旧但不能操作。若依的RBAC权限模型刚好能直接覆盖这套需求,不用从零设计用户角色菜单。
其次是代码生成器。资产管理涉及大量基础数据的增删改查,像资产分类、存放地点、供应商、领用人这些字典表,用若依的代码生成功能可以直接从数据库表生成后台管理页面,几天时间就能把后端管理模块铺完,省下大量重复劳动。
第三是前后端分离结构便于扩展。扫码盘点这类需要移动端配合的功能,后期如果要做成微信小程序或者手机H5,前后端分离的接口结构扩展起来很顺畅。
2. 资产编码和标签打印:扫码系统的地基工程
2.1 资产编码规则的设计思路
很多人做资产管理系统,资产编码喜欢用纯流水号,比如A0001、A0002这样。看着简单,但实际使用中问题很大:纯流水号完全没有任何业务信息,看到一串编号,既不知道这是什么类型的资产,也不知道是哪个部门采购的。一旦资产量上来,对照纸质台账找类别都费劲。
我在这次升级中把编码规则调整为:类型码 + 采购年月 + 部门码 + 四位流水号 + 校验位。
例如:PC-202403-IT-0012-X。
- PC代表资产类型(电脑),另外还有MN显示器、SR服务器、NB笔记本等;
- 202403代表采购年月;
- IT代表使用部门;
- 0012是当月该类型资产在该部门下的流水序号;
- X是校验位,由前几位字符通过固定算法算出来,防止手动输入时出现错误。
这个规则的好处是,管理员看到一串编码就能大致判断出资产类型、所属部门和采购时间,电话里让对方念编号时,少一位或者错一位马上能发现。盘点和维修时,扫到码后系统还能自动校验校验位,乱写的编号直接拦在门外。
2.2 用Java生成资产二维码标签的实操
因为若依后端是Java技术栈,我直接用了Zxing库生成二维码和条形码。考虑到二维码信息容量更大,后期还可以在码里携带更多扩展字段,最终选择了二维码方案。
在pom.xml引入依赖:
<dependency> <groupId>com.google.zxing</groupId> <artifactId>core</artifactId> <version>3.5.2</version> </dependency> <dependency> <groupId>com.google.zxing</groupId> <artifactId>javase</artifactId> <version>3.5.2</version> </dependency>生成二维码返回Base64字符串给前端展示,前端用img标签直接渲染。核心工具方法:
public static String generateQRCodeBase64(String content, int width, int height) { QRCodeWriter qrCodeWriter = new QRCodeWriter(); Map<EncodeHintType, Object> hints = new HashMap<>(); hints.put(EncodeHintType.CHARACTER_SET, "UTF-8"); hints.put(EncodeHintType.MARGIN, 1); try { BitMatrix bitMatrix = qrCodeWriter.encode(content, BarcodeFormat.QR_CODE, width, height, hints); BufferedImage image = MatrixToImageWriter.toBufferedImage(bitMatrix); ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); ImageIO.write(image, "png", outputStream); return Base64.getEncoder().encodeToString(outputStream.toByteArray()); } catch (Exception e) { throw new RuntimeException("生成二维码失败", e); } }二维码内容我用的是一个标准化的JSON字符串,而不是纯资产编号。因为后续扫码后除了查资产本身,还得知道当前页面应该跳转到什么操作。比如日常查询、盘点、维修登记需要的数据字段不一样,如果码里只放一个编号,每次扫码后还要在前端再判断场景,很麻烦。用JSON可以把资产编码、资产类型、批次号都放进去,前端扫码后解析即用。
实际生成的二维码内容示例:
{"assetCode":"PC-202403-IT-0012-X","type":"PC","batch":"202403"}2.3 标签打印和粘贴的几个细节经验
标签打印这块,我踩过一次坑。最初图省事,直接用普通A4纸打印后裁剪,再用透明胶带贴在机箱上。结果过了两三个月,胶带发黄变脆,纸张边缘起毛,二维码边缘破损严重,扫码识别率大幅下降。后来换成了PET哑面材质的不干胶标签纸,配一台兄弟或者得力的小型标签打印机,打印效果清晰度和耐用度都好了很多。
粘贴位置也有讲究。台式机机箱统一贴在正面左上角,显示器贴在背面铭牌旁边,笔记本贴在D面不遮挡散热孔的位置。不要在同一个位置贴两层标签,后期撕下来重贴时残胶很难清理,还可能撕坏设备表面的涂层。
另外建议开发一个“补打标签”功能。资产生命周期内标签脱落或者磨损是必然发生的,如果没有补打功能,管理员只能重新录入一条资产或者手工记录编号,数据就有可能重复。我在资产详情页加了重新生成标签的按钮,数据不变,只重新生成一张图片。
3. 扫码核心链路的实现:盘点、查询和资产变动怎么做
3.1 先理解扫码枪的输入机制,再写代码
很多第一次做扫码功能的开发者容易把扫码枪想象成一种“高级设备”,以为需要写串口通信或者SDK对接。实际上,市面上绝大多数USB扫码枪在系统层面就是一把“键盘”,扫描条码时它把码内容转换为键盘按键事件,一个字符一个字符地输入到当前聚焦的输入框里,并且默认在结尾自动带一个回车键。
这意味着前端实现扫码功能极其简单:在输入框聚焦的状态下,直接扫就行,不需要任何驱动。我们做个隐藏的输入框,扫码枪扫完自动回车,触发搜索或者提交事件。
这个机制也带来一个需要注意的问题:如果页面上的某个按钮处于焦点状态,扫码枪回车可能会误触发按钮点击。所以扫码页面一律要管控焦点,手动指定扫码内容输入到哪个字段。
前端vue页面里的实现思路:
handleScannerInput(event) { const value = event.target.value if (event.key === 'Enter') { if (value && value.startsWith('{')) { // 扫码内容,解析跳转或查询 const assetInfo = JSON.parse(value) this.queryAssetByCode(assetInfo.assetCode) } this.$nextTick(() => { this.$refs.scanInput.value = '' }) } }3.2 扫码盘点:批量提交与防重复设计
盘点场景的诉求是拿着扫码枪把所有看到的资产扫一遍,系统自动记录“已盘到”,没扫到的就是盘亏项。
传统做法是扫一件资产就调用一次后台接口,资产量大的时候后台压力很大,而且一旦网络波动,单条提交失败很难发现。我改成前端先把扫码结果存在一个数组里,盘点结束后一次性批量提交。
比如扫到一个资产,先在前端做展示确认,确认无误后push进数组,显示在页面上。盘点结束时,把整个数组提交到后台:
{ "planId": 202403, "items": [ {"assetCode": "PC-202403-IT-0012-X", "scanTime": "2024-03-20 10:30:00"}, {"assetCode": "MN-202310-IT-0023-X", "scanTime": "2024-03-20 10:31:20"} ] }后台处理时需要注意防重复。同一个盘点任务里,用户可能会不小心把同一件资产扫两次。我在Service层用Redis做了去重,扫过的资产编码在任务有效期内不再重复记录:
public void submitStocktake(StocktakeSubmitDTO dto) { List<StocktakeItem> items = new ArrayList<>(); for (StocktakeItemDTO item : dto.getItems()) { String dedupKey = "stocktake:dedup:" + dto.getPlanId() + ":" + item.getAssetCode(); Boolean firstScan = redisTemplate.opsForValue() .setIfAbsent(dedupKey, "1", Duration.ofMinutes(30)); if (Boolean.TRUE.equals(firstScan)) { items.add(buildStocktakeItem(item)); } } stocktakeService.saveOrUpdateBatch(items); }这个做法尤其适合扫码枪这种连续快速输入的设备,后端接口无需处理大量实时写入,只需在批量提交时做最终落库。
3.3 扫码查询:把“查资产”变成“扫出来”
查询功能是最简单但使用频率最高的模块。原来的流程是打开系统、登录、进入资产列表、输入编号回车、查看详情,全部做完最少十几次点击。升级后变成了:打开扫码查询页面、扫一下、直接看到资产详情。
这里有个细节值得说一下:如果扫码查询只做了单件资产查询,效率提升还不够明显。我额外加了一个“关联资产”展示。比如一台电脑,扫码后不仅展示电脑本身的信息,还会带出配套的显示器、键盘鼠标等关联资产。因为资产入库时做了“父资产编码”关联,扫码后一条SQL就能把整组资产带出来。行政在盘点时只需要扫主资产,配件列表同时展示,账实核对效率提升一大截。
3.4 资产领用、调拨和变更的扫码联动
资产变更是资产管理里比较敏感的操作。传统方式是在资产列表找到记录,修改责任人字段。我在这版升级里做成了“扫码 -> 确认当前持有人 -> 扫描接收人二维码/选择接收人 -> 提交变更”三个步骤。
接收人也可以做成一张二维码工牌。员工入职时系统生成一个包含员工ID的水印二维码,贴在工牌上。调拨资产时,扫一下资产码,再扫一下新员工的工牌码,系统自动弹出调拨确认页面,核对资产信息、原使用人、新使用人、变更原因,确认后提交。
这个流程相比原来在系统里搜记录再编辑,至少快三倍,而且每一步都有留痕,谁在什么时间把什么资产给了谁,一清二楚。
4. 和若依框架整合时绕不开的几个关键点
4.1 权限控制:不是每个接口都能给所有人用
若依的权限体系默认是菜单权限和按钮权限两层。我在设计扫码相关接口的时候,没有把所有接口都挂给任何人,而是按角色做了收紧。
后端接口用若依的权限注解控制:
@PreAuthorize("@ss.hasPermi('asset:stocktake:submit')") @PostMapping("/stocktake/submit") public AjaxResult submitStocktake(@RequestBody StocktakeSubmitDTO dto) { return success(stocktakeService.submitStocktake(dto)); } @PreAuthorize("@ss.hasPermi('asset:transfer:create')") @PostMapping("/transfer/create") public AjaxResult createTransfer(@RequestBody AssetTransferDTO dto) { return success(assetTransferService.createTransfer(dto)); }前端按钮也同步加上v-hasPermi指令控制显示。这样不同角色看到的首页功能是不同的。比如普通员工登录后只能看到“我的资产”和“扫码查询”,资产管理员才能看到“资产盘点”、“调拨管理”、“标签补打”这些入口。
4.2 扫码枪免登录场景下的Token处理
扫码枪本身是不带登录界面的设备,直接插在电脑上就是键盘。如果管理员打开盘点页面时登录态已经过期,扫第一个码就会跳回登录页,整个盘点流程就断了。这个问题非常影响实际使用。
针对这个问题,我的方案是给盘点终端配置一个“设备账号”。在若依后台创建一个名为“盘点终端”的专用账号,绑定资产管理员角色,密码设为强密码。盘点电脑首次使用时,管理员手动登录一次,前端把token存到localStorage,并延长有效时间。
若依的token默认存Redis,有效期默认是30分钟。盘点场景一次可能需要一两个小时,我在配置中心单独为这个设备账号调整了token过期时间到8小时,同时在若依的配置项里开启了在线用户管理,方便随时踢掉可疑会话。
还有一个更顺滑的做法:在盘点页面挂一个定时刷新token的定时器,每隔一段时间调用若依的刷新接口,让token一直保持有效。但这样需要前端定时器一直在页面活跃状态下运行,实际上盘点过程本身页面上一直有扫码输入操作,交互不断,token刷新压力不大。
4.3 用若依代码生成器快速搭建资产台账模块
若依的代码生成器是这套系统能快速落地的另一个重要原因。我在数据库中设计好资产主表、分类表、存放地点表、供应商表后,直接在若依管理后台的“代码生成”菜单导入表结构,配置好列表查询字段、表单控件类型、关联查询字典,服务器上执行生成的代码脚本,直接生成前后端代码,再手动补齐业务字段和校验逻辑。
这套流程最大的价值在于基础CRUD几乎不用写代码,能把精力全部集中在扫码盘点、批量导入、资产联动这些核心业务上。实际做下来,资产台账模块从建表到后台管理页面完整可用,只用了不到一天时间。
4.4 多租户和多部门数据隔离的提前设计
若依本身没有内置多租户,但资产管理系统通常天然面临“多分公司”或者“多个独立核算部门”的场景。虽然早期版本可能只有一个公司用,我在设计表的时候还是提前预留了tenant_id字段,所有资产主表和变动流水表都有这个字段,并且在MyBatis-Plus的实体上加了拦截器实现。
@Data public class AssetInfo { private Long id; private String assetCode; private String assetName; private Long tenantId; // 其他字段省略 }这样后期如果扩展成多公司共用一套系统,不需要改动现有表结构,只需要在数据权限拦截上增加租户过滤即可。配合若依本身的数据权限部门过滤,可以实现“分公司隔离、部门内数据可见”的层级权限。
5. 实测过程中的意外情况和修复记录
5.1 连续扫码时最后一个字符丢失
第一轮实测时就遇到了一个很隐蔽的问题。用扫码枪快速连续扫描,前几件资产都能正常录入,扫到第三四件的时候偶尔会出现资产编码少一位的情况。排查了很久,最后发现不是扫码枪的问题,而是前端输入框的change事件在输入过程中被触发了。扫码枪输入速度极快,Vue的input组件在某些浏览器中会将输入过程拆成多个change事件,如果输入框在字符未完整时就被处理了一遍,最后的字符就被截断了。
解决办法是在确认扫码完成之前加上“回车键判断”,不允许在length变化时立刻取值。因为扫码枪输入结束后会自动带回车,而手动键盘输入的回车则不会误判。
5.2 二维码内容中的JSON特殊字符导致解析失败
刚开始生成的二维码内容是用JSON字符串直接编码的,字符串中的引号和花括号在部分扫码枪下会被解析成特殊字符,尤其是低端扫码枪的固件对特殊字符支持不完善,扫出来的内容少了开头的大括号或者引号错乱,导致前端JSON.parse直接抛异常。
后来做了一个容错处理:前端解析失败时进入降级逻辑,直接从扫码内容中提取assetCode字段。因为资产编码的规则是固定的,可以用正则去匹配:
function parseScanResult(rawStr) { try { return JSON.parse(rawStr) } catch (e) { const match = rawStr.match(/assetCode[\"']?\s*:\s*[\"']([^\"']+)[\"']/) if (match) { return { assetCode: match[1] } } throw new Error('无法识别的扫码内容') } }这样即使扫码枪对JSON里的花括号支持不完美,只要assetCode字段能提取出来,盘点流程就能继续。
5.3 大批量盘点的数据库性能优化
第一版盘点提交逻辑直接用MyBatis-Plus的saveBatch逐条插入,一次提交500条记录用时将近20秒,体验很差。后来改为分批批量插入,每批50条,并且关闭自动提交,全部插入完成后统一提交事务,用时从20秒降到4秒左右。
另一个改进是盘点时使用了唯一索引。在盘点明细表上建立了plan_id和asset_code的联合唯一索引,即使前端防重逻辑有遗漏,数据库层面也能兜底,不会出现一条盘点任务里同一资产重复计数的情况。
5.4 网络不稳定环境下的盘点数据暂存
有些单位的机房或者库房处于地下室,网络信号很差。如果盘点到一半断网,这一批扫码数据全部丢失,会非常崩溃。我在前端加了localStorage暂存机制:盘点过程中的数据,扫描成功后先写入本地缓存,每5条自动批量提交一次,提交成功后把已提交的缓存删除。如果检测到断网,已扫码数据继续留在本地,网络恢复后弹窗提示“有未提交的盘点数据,是否继续提交”。
这个改动看起来不复杂,但在实际使用中帮了大忙。库房盘点的场景,手机和移动电脑的信号确实不稳定,有这个兜底机制,盘点人员明显安心很多。
6. 这套系统后续还能怎么扩展
这次基于若依的扫码资产管理升级,核心解决了资产“怎么快速找到、怎么快速盘点、怎么快速变更”三个问题。但从实际运营角度看,还有一些可以继续深入的方向。
第一个方向是移动端。当前扫码主要依赖电脑端的网页配合扫码枪,虽然稳定,但灵活性有限。后期可以做成微信小程序或者钉钉H5,员工手机扫码就能自助查询、报修、提交资产领用申请,把使用门槛再降一档。
第二个方向是资产维保提醒。资产入库时录入保修截止日期,后台用定时任务扫描即将到期或者已过保的资产,主动推送提醒给资产管理员。这个功能对IT设备特别有用,很多显示器、笔记本的保修是按激活日期算的,不记录的话很容易错失免费维修窗口。
第三个方向是资产折旧联动。当前系统的资产原值、入账日期、折旧年限字段已经预留,但资产报废和财务折旧还没有打通。后期可以考虑按资产状态自动推送到财务系统,减少月底对账的重复劳动。
最后再分享一个实际操作中的小技巧:二维码内容里不要放太多字段,够用就行。一开始我想把采购订单号、供应商、存放位置全部编进码里,后来发现二维码越密,打印出来对打印机的清晰度要求越高,稍微磨花一点就扫不出来。现在码里只有三个字段,识别率非常稳定,剩下的信息全部靠后台查询补全。