1. 项目背景与核心价值
企业资产管理(EAM)系统是现代企业数字化转型的基础设施之一。传统资产管理依赖人工台账和Excel表格,存在数据孤岛、流程割裂、追溯困难等痛点。我们团队最近为一家中型制造企业实施的这套系统,通过前后端分离架构实现了资产全生命周期管理,上线后使设备利用率提升27%,盘点效率提高近10倍。
这个系统的核心价值在于三个维度:一是通过标准化编码体系解决资产标识混乱问题;二是借助工作流引擎实现从采购到报废的闭环管理;三是利用数据分析看板辅助决策。特别在制造行业,设备停机成本每小时可能高达数万元,预防性维护功能直接关系到企业效益。
2. 技术架构设计解析
2.1 前端技术选型
采用Vue 3组合式API开发管理后台,主要基于以下考量:
- 相比React更温和的学习曲线,适合企业IT团队后续维护
- Element Plus组件库提供丰富的业务组件(如资产树形选择器)
- ECharts对设备状态可视化支持更友好
关键配置示例:
// vite.config.js 生产环境优化 build: { rollupOptions: { output: { manualChunks(id) { if (id.includes('element-plus') || id.includes('echarts')) { return 'vendor' } } } } }2.2 后端架构设计
Spring Boot采用分层架构:
com.asset ├── config # 安全/持久化配置 ├── controller # RESTful API ├── service # 业务逻辑 │ ├── impl # 实现类 ├── repository # JPA接口 └── model # 实体类特别设计了双数据源:
- 主库:MySQL 8.0(InnoDB集群)
- 从库:Redis缓存热点数据(如部门资产统计)
2.3 数据库关键设计
资产主表结构优化要点:
CREATE TABLE `asset` ( `id` BIGINT NOT NULL AUTO_INCREMENT, `asset_code` VARCHAR(20) NOT NULL COMMENT '资产编码规则: DEPT+YYYYMM+5位序列', `name` VARCHAR(100) NOT NULL, `category_id` INT NOT NULL COMMENT '关联资产分类表', `status` TINYINT NOT NULL DEFAULT 1 COMMENT '1-在用 2-闲置 3-维修', `purchase_date` DATE NOT NULL, `net_value` DECIMAL(12,2) COMMENT '净值计算=原值-累计折旧', `location_id` INT COMMENT 'GIS坐标编码', `qr_code` VARCHAR(255) COMMENT '二维码存储路径', PRIMARY KEY (`id`), UNIQUE INDEX `udx_code` (`asset_code`), INDEX `idx_status` (`status`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;3. 核心功能实现细节
3.1 资产全生命周期工作流
采用Activiti引擎驱动状态变更:
graph TD A[采购申请] -->|审批| B[入库登记] B --> C[领用分配] C --> D[日常使用] D -->|故障| E[维修流程] D -->|闲置| F[调拨流程] D -->|报废| G[处置流程]关键状态机实现:
@Transactional public void changeAssetStatus(Long assetId, AssetStatus newStatus) { Asset asset = repository.findById(assetId) .orElseThrow(() -> new BusinessException("资产不存在")); // 状态校验逻辑 if (asset.getStatus() == AssetStatus.SCRAPPED) { throw new BusinessException("已报废资产不可操作"); } // 记录变更日志 AssetLog log = new AssetLog(); log.setAssetId(assetId); log.setOldStatus(asset.getStatus()); log.setNewStatus(newStatus); logRepository.save(log); asset.setStatus(newStatus); repository.save(asset); }3.2 移动端盘点方案
混合开发方案选型对比:
| 方案 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 纯Web | 跨平台 | 扫码性能差 | 简单盘点 |
| UniApp | 开发快 | 功能受限 | 原型验证 |
| React Native | 性能好 | 学习成本高 | 大型项目 |
| Flutter | 体验优 | 生态较弱 | 重UI需求 |
最终采用Cordova+WebView方案:
- 使用ZXing插件实现毫秒级扫码
- 本地SQLite存储离线盘点数据
- 通过WebSocket实现实时进度同步
3.3 折旧计算模块
财务部门特别要求的双余额递减法:
public BigDecimal calculateDepreciation(Asset asset) { BigDecimal cost = asset.getOriginalValue(); BigDecimal salvage = asset.getSalvageValue(); int years = asset.getUsefulLife(); // 双倍余额递减法公式 BigDecimal rate = BigDecimal.valueOf(2.0) .divide(BigDecimal.valueOf(years), 10, RoundingMode.HALF_UP); BigDecimal bookValue = cost.subtract(asset.getAccumulatedDepreciation()); BigDecimal depreciation = bookValue.multiply(rate); // 最后两年转为直线法 if (asset.getUsedYears() >= years - 2) { depreciation = bookValue.subtract(salvage) .divide(BigDecimal.valueOf(2), 2, RoundingMode.HALF_UP); } return depreciation.compareTo(BigDecimal.ZERO) > 0 ? depreciation : BigDecimal.ZERO; }4. 性能优化实战记录
4.1 千万级数据查询优化
资产历史表分库方案:
- 按年度水平分片(2023_asset_history)
- 建立时间维度索引
- 冷数据归档到ClickHouse
查询优化前后对比:
| 优化措施 | 查询耗时(ms) | 内存占用(MB) |
|---|---|---|
| 原始查询 | 4200 | 850 |
| 添加索引 | 1200 | 320 |
| 分页优化 | 800 | 150 |
| 结果缓存 | 50 | 80 |
4.2 高并发领用控制
分布式锁实现方案对比:
// Redisson分布式锁实现 public boolean acquireAsset(Long assetId, Long userId) { RLock lock = redissonClient.getLock("asset:" + assetId); try { if (lock.tryLock(3, 10, TimeUnit.SECONDS)) { Asset asset = repository.findById(assetId); if (asset.getStatus() == AssetStatus.AVAILABLE) { asset.setUserId(userId); repository.save(asset); return true; } } } finally { lock.unlock(); } return false; }4.3 文件导入性能提升
Excel导入优化策略:
- 使用EasyExcel替代POI(内存降低60%)
- 采用批量插入(batchSize=1000)
- 异步处理机制
@Async public void importAssets(MultipartFile file) { EasyExcel.read(file.getInputStream()) .head(AssetImportDTO.class) .registerReadListener(new AssetImportListener()) .sheet() .doRead(); } // 自定义监听器实现分批入库 class AssetImportListener implements ReadListener<AssetImportDTO> { private List<Asset> cache = new ArrayList<>(1000); @Override public void invoke(AssetImportDTO data, AnalysisContext context) { Asset asset = convertToEntity(data); cache.add(asset); if (cache.size() >= 1000) { batchInsert(); } } private void batchInsert() { repository.saveAll(cache); cache.clear(); } }5. 部署与运维方案
5.1 容器化部署
Docker Compose编排示例:
version: '3' services: mysql: image: mysql:8.0 environment: MYSQL_ROOT_PASSWORD: ${DB_PASSWORD} volumes: - ./mysql/data:/var/lib/mysql - ./mysql/conf:/etc/mysql/conf.d redis: image: redis:6 ports: - "6379:6379" backend: build: ./backend ports: - "8080:8080" depends_on: - mysql - redis frontend: build: ./frontend ports: - "80:80"5.2 监控体系搭建
Prometheus监控指标示例:
- 资产API成功率
- 折旧计算耗时
- 并发领用等待数
- 盘点任务队列深度
Grafana看板配置关键点:
- 设置资产状态分布饼图
- 折旧计提进度时序图
- 接口响应时间热力图
6. 典型问题排查实录
6.1 扫码枪兼容性问题
现象:部分工业扫码枪无法触发Web页面事件 排查过程:
- 抓取键盘事件发现部分设备发送KeyCode异常
- 使用input事件替代keydown事件
- 添加300ms防抖处理
最终解决方案:
// 扫码枪专用输入组件 <template> <input v-model="scanValue" @input="handleScan" ref="scannerInput" class="scanner-input" /> </template> <script setup> const scanValue = ref('') let timer = null const handleScan = () => { clearTimeout(timer) timer = setTimeout(() => { if (scanValue.value.length >= 8) { submitScan(scanValue.value) scanValue.value = '' } }, 300) } </script>6.2 折旧计算精度问题
故障现象:年末资产净值出现0.01元差异 根本原因:BigDecimal除法未统一精度 修复方案:
// 统一配置计算精度 @Bean public ArithmeticUtils arithmeticUtils() { return new ArithmeticUtils(6, RoundingMode.HALF_UP); } // 所有计算使用工具类 public BigDecimal calculate(Asset asset) { return arithmeticUtils.divide( asset.getOriginalValue(), BigDecimal.valueOf(asset.getUsefulLife()) ); }6.3 并发领用超发问题
复现步骤:
- 用户A和B同时申请同一资产
- 系统检查时都显示可用
- 最终两人都获得使用权
解决方案对比:
| 方案 | 实现复杂度 | 性能影响 | 可靠性 |
|---|---|---|---|
| 数据库乐观锁 | 低 | 小 | 中等 |
| SELECT FOR UPDATE | 中 | 较大 | 高 |
| 分布式锁 | 高 | 中等 | 最高 |
最终采用组合方案:
- 前端限制重复提交
- 后端加Redis分布式锁
- 数据库最终校验
7. 扩展方向建议
- 物联网集成:通过MQTT协议接入设备实时数据
- 预测性维护:基于设备历史数据训练LSTM模型
- 数字孪生:Three.js实现资产3D可视化
- 区块链存证:关键操作上链存证
实际项目中我们接入了PLC设备数据采集,通过以下指标预测故障:
- 电机振动频率
- 轴承温度变化率
- 电流波动系数
# 简化的预测模型示例 from tensorflow.keras.models import Sequential from tensorflow.keras.layers import LSTM, Dense model = Sequential([ LSTM(64, input_shape=(30, 5)), # 30个时间步,5个特征 Dense(1, activation='sigmoid') ]) model.compile(loss='binary_crossentropy', optimizer='adam')