1. 项目整体设计与技术选型思路1.1 为什么要做企业资产管理系统先说一个现实场景。我接触过的不少公司在资产盘点时全靠Excel设备借出去没人登记部门调动后资产责任人还是旧人一年一度的折旧核算全靠财务手工算。小公司还能凑合一旦资产数量上了千账实不符的问题就会越来越严重。所以企业资产管理系统从需求端来说就是解决资产在哪、谁在用、状态如何、值多少钱这四个核心问题。这个项目以企业级为定位意味着它不是简单的增删改查Demo而是要走一整套完整的业务流程资产入库、领用、归还、调拨、维修、折旧、盘点、报废再加上用户权限管理和操作日志留痕。我当时接到这个需求时第一反应就是把所有业务线理清然后围绕资产台账这个核心表去扩展周边表避免各模块之间数据割裂。1.2 技术栈选型理由技术栈选型是SpringBoot Vue MyBatis MySQL这也是目前中小型管理系统里最主流、最容易招到人、也最容易上手的组合。我逐一说说为什么这样选。后端用SpringBoot是因为它把Spring生态里大量繁琐的配置都自动化了内嵌Tomcat打出一个jar包就能跑部署成本极低。对于资产管理这类典型的CRUD密集型业务SpringBoot的约定优于配置能够大幅压缩开发周期。前端用Vue是因为它的渐进式框架特性很适合中后台管理系统。我要在一个项目里同时管理资产台账、审批流程、统计报表等多个页面Vue的组件化开发让页面复用变得很舒服加上Element UI组件库表格、表单、弹窗、分页这些高频场景基本不用自己造轮子。MyBatis在持久层框架里是半自动的代表。它不像JPA那样帮你把SQL全生成好而是把SQL控制权交给你。资产管理系统里涉及大量多表联查比如资产列表要关联部门、人员、分类、状态等字段MyBatis的XML方式写复杂SQL非常直观而且方便DBA审查优化。至于MySQL它是开源数据库里生态最成熟、性能足够稳、运维成本最低的选择支撑几千上万条资产记录的中型系统完全没压力。2. 核心功能模块与数据库设计2.1 功能模块划分资产管理系统的功能不能拍脑袋堆要以资产全生命周期为主线来拆。我把整个系统分为六大模块。第一是资产台账管理这是整个系统的心脏。所有资产登记到这里生成唯一的资产编号就像人的身份证号一样后续所有业务流程都围绕这个编号流转。资产字段包括名称、分类、规格型号、品牌、购置日期、原值、净值、存放地点、使用部门、责任人等。第二是资产变动管理包括资产的领用、归还、调拨。员工入职要领电脑部门间要转移设备这些操作都要有流程记录谁在什么时候把什么资产从什么地方拿到了什么地方全程可追溯。第三是资产维护管理包括维修和保养记录。设备出故障了就登记维修单记录故障描述、维修费用、维修结果既方便追溯设备健康状态也为后续资产更新换代提供数据依据。第四是资产盘点管理。这个模块解决账实相符的问题管理员发起盘点任务各责任人确认资产在位情况盘点结束后系统自动生成盘盈盘亏记录。有了这个模块年终盘点就从翻Excel找记录变成了跟着系统核对。第五是折旧管理。财务需要按资产类别设置残值率和折旧年限系统每月自动计算折旧额生成折旧明细和汇总报表减轻财务手工核算的负担。第六是系统管理包括用户管理、角色管理、菜单权限、操作日志和字典管理。资产数据涉及企业内部信息必须有严格的权限控制普通员工只能看自己名下的资产部门主管看整个部门的资产管理员和系统管理员才有全量权限。2.2 数据库表设计数据库设计是整个系统的地基我的经验是先按业务域划分表组再逐个细化字段最后统一处理索引和关联关系。核心表是资产信息表我给它命名为asset_info字段设计如下。字段名类型说明idbigint主键自增asset_codevarchar(50)资产编号唯一索引asset_namevarchar(100)资产名称category_idbigint资产分类IDbrandvarchar(50)品牌modelvarchar(100)规格型号serial_novarchar(100)出厂序列号purchase_datedate购置日期original_valuedecimal(12,2)资产原值net_valuedecimal(12,2)净值由折旧模块更新statustinyint状态1在库2在用3维修4报废department_idbigint使用部门IDuser_idbigint责任人IDlocationvarchar(200)存放地点remarkvarchar(500)备注create_timedatetime创建时间update_timedatetime更新时间围绕这张主表我设计了资产分类表asset_category、部门表sys_department、用户表sys_user、领用记录表asset_allocate_record、维修记录表asset_repair_record、折旧记录表asset_depreciation_record、盘点任务表asset_inventory_task。领用记录表里需要记录allocate_type区分是领用还是归还再记录operator_id和operate_time作为操作留痕。折旧记录表按资产ID和折旧月份联合唯一防止同一资产同一月份重复计提。表设计有个容易忽略的坑就是金额字段。资产原值和净值必须用decimal而不是float不然累积折旧算到后面会出现精度问题。另一个坑是状态字段我建议用tinyint存数字字典值而不要直接用中文这样后续扩展状态、做国际化以及写SQL条件判断都会方便得多。在设计命名规范时我统一用下划线命名法Java实体里用驼峰MyBatis开启驼峰映射后自动对应省去大量手工字段映射配置。3. 后端实现SpringBoot MyBatis 关键技术点3.1 工程结构与代码分层后端工程用Maven管理依赖。我习惯按照启动类在最外层、包名按模块划分的方式组织代码结构大致如下com.example.asset ├── AssetApplication.java ├── controller ├── service │ └── impl ├── mapper ├── entity ├── dto ├── vo ├── config └── commoncontroller层只负责接收参数和返回结果不写业务逻辑。service层处理具体业务流程mapper层对应MyBatis的接口SQL写XML或注解里。dto用于接收前端传参vo用于返回给前端的展示数据entity和数据库表一一对应三层对象分离能避免接口字段暴露问题。common包里放统一返回结果类Result、分页结果类PageResult、全局异常处理器GlobalExceptionHandler、JWT工具类和登录拦截器。这样一个分层的好处是职责清晰改数据库表结构时只需要动entity和mapper改接口逻辑时只动service前端对接时不关心后端内部逻辑只要接口返回结构稳定就行。我在实际项目里深深体会到分层带来的最大收益不是代码变少了而是排查问题的时候定位特别快。3.2 MyBatis 配置与缓存MyBatis的配置核心在application.yml里。我给出一个典型的配置片段标注了每个关键项的作用。spring: datasource: url: jdbc:mysql://localhost:3306/asset_db?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/Shanghai username: root password: yourpassword driver-class-name: com.mysql.cj.jdbc.Driver mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.example.asset.entity configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImpl这里有几个配置项必须提醒大家注意。第一url里的serverTimezone必须设置MySQL 8.0之后的驱动默认要求时区不设会直接报错。第二map-underscore-to-camel-case必须开启否则数据库的下划线字段映射不到实体的驼峰属性。第三开发阶段把log-impl设为StdOutImplSQL和参数会直接打印到控制台排查问题事半功倍但生产环境要关掉否则日志量太大。MyBatis的缓存分一级缓存和二级缓存默认一级缓存是开启的作用域是SqlSession。很多人在这里踩过坑在一个事务里先查一次资产列表再往表里插入一条新资产又查一次列表结果第二次查返回的是缓存里的旧数据新插入的记录看不到。这就是一级缓存捣的鬼。解决办法其实很简单执行了增删改操作后MyBatis会自动清空一级缓存所以只要插入操作发生在同一个SqlSession里第二次查询不会命中旧缓存。但关键是要理解SqlSession的生命周期在Spring管理下每个请求通常对应一个新的SqlSession一级缓存的共享范围不会跨请求。因此在实际项目中一级缓存对脏读的影响通常不大反而是二级缓存如果不做细致配置容易出现数据不一致。我的建议是资产管理这类并发写操作不算极端的系统不要开启MyBatis二级缓存数据库层面做好索引比缓存更可靠。3.3 核心业务代码实现资产列表查询是系统里最核心的接口它要支持按资产名称模糊搜索、按部门过滤、按状态过滤、分页返回。我直接给出一个带条件的查询SQL写在XML里。select idselectAssetPage resultTypecom.example.asset.vo.AssetVO SELECT a.id, a.asset_code, a.asset_name, c.category_name, a.brand, a.model, a.original_value, a.net_value, a.status, d.department_name, u.real_name AS user_name, a.location, a.purchase_date FROM asset_info a LEFT JOIN asset_category c ON a.category_id c.id LEFT JOIN sys_department d ON a.department_id d.id LEFT JOIN sys_user u ON a.user_id u.id where if testassetName ! null and assetName ! AND a.asset_name LIKE CONCAT(%, #{assetName}, %) /if if testdepartmentId ! null AND a.department_id #{departmentId} /if if teststatus ! null AND a.status #{status} /if /where ORDER BY a.update_time DESC /select这段SQL里最关键的就是用了动态标签。资产列表页通常有多个筛选条件用户勾选哪些就拼哪些用标签能优雅地处理这种多条件组合查询。另一个细节是查询列表时LEFT JOIN关联出分类名、部门名和用户名前端表格直接展示不必再逐个发请求翻译ID这是提升页面性能非常有效的手段。领用资产时的业务逻辑相对复杂一些。我在service层里核心逻辑是三步走校验资产状态为在库才能领用、更新资产信息的状态和责任人、写入领用记录。状态校验这一步很容易被忽略如果两个人同时提交领用同一台设备没有状态校验就会出现超卖式的数据错乱。所以我在更新状态的SQL里加上了条件用乐观锁的思路保证并发安全update idupdateStatus UPDATE asset_info SET status #{newStatus}, user_id #{userId}, department_id #{departmentId}, update_time NOW() WHERE id #{id} AND status #{oldStatus} /update这个update语句的WHERE条件里带上oldStatus只有当前状态确实是在库状态才能更新成功返回的影响行数为1表示更新成功为0说明已经被别人抢占了此时抛出业务异常提示该资产状态已变化请刷新后重试。这种简单有效的并发控制手段在资产管理系统中比引入Redis分布式锁轻量得多也足够可靠。3.4 分页与批量操作实现分页是每个后台管理系统的刚需。我用的是PageHelper插件在pom里引入依赖后直接在service层查询前调用一行代码即可PageHelper.startPage(pageNum, pageSize); ListAssetVO list assetMapper.selectAssetPage(queryDTO); PageInfoAssetVO pageInfo new PageInfo(list);PageHelper的原理是基于MyBatis拦截器自动拼接LIMIT语句。有个必须记住的禁忌startPage之后必须紧跟第一条查询语句中间不能穿插其他查询否则分页会作用到错误的SQL上。我在联调阶段就遇到过一次列表数据莫名其妙不全的问题最后定位到是startPage和实际查询之间多了一次字典查询导致的。批量操作的需求也不少比如资产盘点时一次性更新多条资产的盘盈盘亏状态。MyBatis批量更新有两种常见方式一种是在XML里用foreach拼接多条update语句另一种是循环调用单条更新。我测试下来数据量在几十上百条时两种方式性能差异不大但第一种方式的SQL日志更清晰事务也更好控制。用foreach时要注意MySQL对SQL语句长度有限制一次性拼接几千条会报错建议用ExecutorType.BATCH批量提交或者控制每次批量的大小在500条以内。4. 前端实现Vue 核心技术点4.1 前端工程初始化与基础配置前端部分我用Vue 3配合Vite构建。对比Vue CLIVite的开发服务器启动速度快了一个量级热更新也快对现代前端工程来说体验好很多。初始化命令如下npm create vitelatest asset-web -- --template vue cd asset-web npm install npm install vue-router4 axios element-plus项目装好后我在src下建了api、router、store、views、components几个目录。axios封装放在api目录下我用拦截器统一处理请求头携带token、响应码异常提示和401跳转登录。这里直接给出核心封装代码。// api/request.js import axios from axios import { ElMessage } from element-plus import router from ../router const request axios.create({ baseURL: /api, timeout: 10000 }) request.interceptors.request.use(config { const token localStorage.getItem(token) if (token) { config.headers.Authorization token } return config }) request.interceptors.response.use( response { const res response.data if (res.code ! 200) { ElMessage.error(res.msg || 请求失败) return Promise.reject(new Error(res.msg)) } return res.data }, error { if (error.response error.response.status 401) { localStorage.removeItem(token) router.push(/login) } ElMessage.error(error.message || 网络异常) return Promise.reject(error) } )这里的设计思路是前端不关心后端返回的具体数据结构细节统一从response.data里取出业务数据再返回给调用方。这样页面里的代码就非常干净例如资产列表接口的调用就变成一行赋值操作。4.2 资产台账页面的关键交互实现资产台账列表页是整个前端最核心的页面。我用Element Plus的el-table展示数据el-form实现搜索栏el-pagination实现分页再加一个el-dialog承载新增和编辑表单。针对vue播放m3u8这类热搜词场景虽然资产管理系统本身不涉及视频但如果你想在系统里做资产图片预览或简单的视频资产展示Vue集成m3u8播放确实是个常见的需求方向这里顺带提一句m3u8是HLS协议的播放列表格式浏览器原生video标签不能直接播放需要引入hls.js或video.js配合hls插件来解析。操作步骤是安装依赖后在mounted生命周期里初始化播放器把m3u8地址传入即可。不过要注意m3u8地址要做跨域配置否则播放请求会被浏览器拦截。回到资产页面本身搜索和分页的状态管理我用一个query对象统一维护const query reactive({ assetName: , departmentId: null, status: null, pageNum: 1, pageSize: 10 }) const loadData async () { const list await getAssetPage(query) tableData.value list.records total.value list.total }搜索按钮触发时把pageNum重置为1再调用loadData分页组件改变页码时更新query.pageNum后再调用loadData。这个模式虽然简单但非常实用几乎所有列表页都可以照搬。我自己在实际项目中总结下来列表页的代码模式基本一致所以会把这些通用逻辑抽成组合式函数useTable每个页面只需要传入查询接口和查询参数就能自动拥有搜索、重置、分页、刷新这四件套能力大大减少了重复代码。4.3 路由权限与打包部署前端路由我用vue-router的createWebHistory模式创建这样URL比较美观。但history模式有个部署陷阱如果服务器没有配置try_files回退到index.html用户直接访问某个详情页URL时会返回404。配合SpringBoot部署时需要在后端提供forward转发把不存在的路径转发到index.html。路由级权限的实现我基于后端的菜单权限数据动态添加路由。后端返回当前用户可访问的菜单列表和按钮权限标识前端在路由守卫里判断用户是否已登录、token是否过期、目标路由是否需要权限标记以及当前用户是否拥有该权限。核心代码如下router.beforeEach((to, from, next) { const token localStorage.getItem(token) if (!token) { if (to.path /login) { next() } else { next(/login) } return } if (to.path /login) { next(/) return } if (!isRoutesLoaded.value) { initDynamicRoutes().then(() { next({ ...to, replace: true }) }) return } next() })动态路由初始化后需要调用next({ ...to, replace: true })重新进入一次目标路由因为此时新路由还没来得及注册直接next()会导致首次进入时页面空白。这个问题我排查过很久才搞明白当时的现象是刷新页面后点击菜单跳转正常但直接输入URL访问则白屏。查下来就是route的addRoute之后需要重新触发一次导航。页面打包使用npm run build产物在dist目录。我习惯把dist目录里的静态文件直接复制到SpringBoot的src/main/resources/static目录下然后把后端配置为所有未匹配接口的请求都走到index.html。这样编译出一个jar包就能完整运行部署和运维都非常省事。5. 常见问题与排查技巧实录5.1 SpringBoot 版本兼容问题热点词里有一个springboot版本太高这是我在项目中真实遇到过的情况。SpringBoot从2.x升级到3.x底层Java版本要求从8升到17对应的MyBatis Starter也需要换坐标原来的mybatis-spring-boot-starter是2.2.x配合SpringBoot 3.x会直接启动报错。新坐标是mybatis-spring-boot-starter 3.0.x版本才能兼容SpringBoot 3.x。我的建议是如果团队成员对SpringBoot 3.x还不熟或者项目里有一些老依赖依赖javax命名空间那就老老实实用SpringBoot 2.7.x搭配JDK 8这个组合的生态最成熟资料也最多。企业级管理系统追求的是稳定交付没必要追新技术。如果非要上SpringBoot 3.x要注意两个大坑第一javax.的包要改成jakarta.第二Java 8的API有些在Java 17里被移除了比如SecurityManager相关代码第三方库如果用了这些API会直接报找不到类。还有一个常见问题是IDEA新建SpringBoot项目时Spring Initializr默认选择的SpringBoot版本偏高如果你的JDK版本不够创建后跑不起来。解决办法是创建时自定义Server URL或者直接在pom.xml里把parent版本改成匹配本机JDK的版本。这些版本问题的核心排查思路就一句话看启动日志的报错堆栈第一行如果是NoSuchMethodError或ClassNotFoundException基本都是依赖版本不匹配导致的。5.2 MyBatis 相关坑点汇总MyBatis的坑主要集中在SQL映射和参数处理。热搜词里mybatis 单个数字字符比较是高频问题因为在XML文件里写条件判断时0会被MyBatis判定为空串。原因在于MyBatis对OGNL表达式的判断规则当参数是字符或字符串类型时它会做空值检查。解决办法是加上! -之类的显式判断或者专门写一个判断条件处理。避免踩坑最好的方式就是写Mapper里的条件判断时对可能为0的参数直接用 ! null来做判断尽量不要用空串判断。MyBatis还有一个比较隐蔽的坑就是当查询参数是单个非String类型时如果不在接口方法的参数上使用Param注解XML里引用参数名会报错。例如AssetInfo selectById(Long id);select idselectById resultTypeAssetInfo SELECT * FROM asset_info WHERE id #{id} /select这个写法在旧版MyBatis下会报There is no getter for property named id因为单参数情况下MyBatis不知道参数名是什么需要加Param(id)才能明确绑定。如果你遇到这种报错不要怀疑人生直接给参数加Param注解即可解决。5.3 MySQL 相关配置与架构注意事项MySQL安装和配置也是热搜词里大量出现的需求。我建议到MySQL官网下载MSI安装包按向导安装即可。安装MySQL 8.0之后一定要做两件事第一创建专用数据库账号不要一直用root操作业务库第二设置正确的字符集推荐utf8mb4因为它能存储完整的Unicode字符包括生僻字和表情符号。在数据库架构层面资产管理系统的查询模式是典型的单条记录更新多、范围查询多、关联查询固定几张表这种模式对MySQL非常友好。我给资产信息表的常用查询字段加索引asset_code唯一索引、status普通索引、department_id普通索引、category_id普通索引。注意不要给每一个字段都加索引因为索引会拖慢写入性能特别是资产信息表这种写入也不算少的业务表索引冗余反而有害。MySQL还有一个容易被忽视的点就是连接数配置。SpringBoot默认连接池是HikariCP默认最大连接数为10。如果你们的系统并发不算高其实够用但如果前端页面某个接口查询特别慢占用了连接不释放其他请求就会排队等待。我在部署生产环境时会把maximum-pool-size调到20到50之间同时设置connection-timeout为30000毫秒防止长时间等待。这样配置后再配合慢查询日志数据库层面基本就不会成为瓶颈了。5.4 前后端联调与部署踩坑联调阶段最容易出的问题就是跨域。前端开发服务器跑在localhost:5173后端接口跑在localhost:8080两个端口不同必然产生跨域。解决方式有两种一种是在后端写一个CorsConfiguration配置类放行所有请求另一种是在Vite的vite.config.js里配置proxy代理。我更推荐proxy方式因为代理只作用于开发环境生产环境前端打包后和后端同源不需要处理跨域。vite.config.js里的配置如下export default defineConfig({ server: { port: 5173, proxy: { /api: { target: http://localhost:8080, changeOrigin: true, rewrite: path path.replace(/^\/api/, ) } } } })部署阶段还有一个坑就是Vue打包后布局异常。很多人遇到过npm run build之后页面样式错乱或者图片不显示最常见的原因是静态资源路径问题。在Vue项目里打包时的base配置默认是/如果你的应用是部署在域名根路径下没问题但如果部署在子路径下比如http://localhost:8080/asset-web/所有静态资源就会404。这时候把base改成./或者子路径即可。另外要检查CSS里的背景图路径尽量用绝对路径或者相对路径时确保层级正确。登录后首页白屏的问题也经常遇到通常就是路由模式导致的。history模式需要后端配合如果你没有配置静态资源转发改用hash模式是最省事的方案。6. 一点实操心得与扩展方向做完这个系统后我个人最大的体会是企业级管理系统拼的不是酷炫技术而是业务细节的完整性和稳定性。把资产生命周期的每一步都走通把权限控制做到位把并发场景下的数据一致性处理好这个项目就已经比市面上很多半吊子系统强很多了。最后再分享一个小经验。资产管理系统做完基础功能后最有扩展价值的方向是数据可视化报表比如按部门统计资产分布、按月份统计资产采购趋势、按状态统计资产健康度。这些图表如果用ECharts实现前端工作量也不大但给管理层的观感提升是巨大的。另外一个方向是引入二维码标签每个资产打印一个二维码贴到设备上扫码直接查看资产信息和历史记录仓库盘点时会省非常多的人力。这两个功能不需要改底层架构在现有SpringBoot Vue的框架上做增量开发就行难度可控但实际价值非常明显。如果你正在上手这个项目建议按照台账、领用、维修、折旧、盘点这个顺序逐个模块推进每个模块先跑通主流程再补异常分支。这样下来一个能真正交给用户使用的资产管理系统两条三周时间是可以稳稳交付的。
