Java+若依固定资产管理源码:企业资产台账全生命周期系统

发布时间:2026/11/6 6:44:56
Java+若依固定资产管理源码:企业资产台账全生命周期系统

简介:这份资源是面向Java后端开发者与企业信息化实施人员的一套固定资产管理系统完整源码,基于Spring Boot的若依框架搭建后端,配合layui前端构建界面,可用于学习企业级权限架构或作为资产管理类项目的二次开发底座。压缩包共1883个文件、约8.88MB,其中317个java文件承载资产登记、领用借用、归还维修、调拨转移、报废及统计报表等业务逻辑与实体服务,180个html与90个js、40个css构成layui页面与交互脚本,38个xml、3个yml和1个sql脚本负责依赖配置与数据库建表,另有vm模板、字体图标及图片资源,目录划分相对规整。系统同时内置组织结构维护与角色权限分配,能按部门、岗位控制资产归属与操作范围,贴近真实企业场景。资源已有563人学习,适合具备Spring Boot与MyBatis基础、希望理解若依权限体系与业务流程落地的中初级开发者,也可作为课程设计或毕设参考,帮助快速把握资产全生命周期管理的表结构、接口组织与前后端协作方式。

1. 固定资产管理源码:一套基于 Java 与若依框架的资产台账系统

去年年底帮一个做行政主管的朋友盘固定资产,她打开一个 40 多 MB 的 Excel,里面光是「电脑」就按采购年份拆了 11 张 sheet,找一台 2021 年买的笔记本序列号翻了六分钟。这种场景其实是大量中小公司在用的真实办法,问题不在人,在于没有一个能把资产卡片、领用、调拨、报废、折旧串起来的系统。这份固定资产管理源码就是干这件事的:基于 Java 和若依框架,把资产从入库到报废的全生命周期落在一套带权限、带菜单、带代码生成器的后台里。适合两类人——想把公司那套 Excel 台账换掉的后端开发,以及拿若依当脚手架做二次开发、需要一个现成业务模块练手的同学。它不是一个玩具 Demo,资产主表、流水表、折旧表、盘点单是拆开设计的,后面几章会顺着代码往下拆。

2. 技术栈与工程结构:若依骨架里资产模块挂在哪

先讲清楚整体结构,再谈具体代码。固定资产管理这类后台系统有个特点:业务复杂度中等,但基础能力要求高——登录、权限、菜单、字典、部门、操作日志一个都不能少。若依恰好把这一层全部做完了,所以这套源码的重心不是「重新造后台」,而是「在成熟后台里长出一个资产域」。

2.1 技术选型:为什么固定资产系统大多落在若依前后端分离版上

一谈起后台管理就有人推荐自己搭 Spring Boot 空工程,我一般会劝住。固定资产系统里真正值钱的是资产域名模型和状态流转,不是登录鉴权。若依前后端分离版常见的技术组合大致是这样:

层次技术选型在资产系统里的作用
后端框架Spring Boot 2.x提供 REST 接口与自动装配
持久层MyBatis + Mapper XML资产复杂查询、多表联查
安全Spring Security + JWT登录、接口鉴权、按钮级权限
缓存Redis验证码、字典、在线用户、防重复提交
数据源Druid连接池与 SQL 监控
定时任务Quartz每月折旧计提
前端Vue2 + Element UI列表、表单、弹窗、导出
数据库MySQL 5.7/8.0资产、流水、折旧、盘点四类表

这个组合最大的价值在于「资产模块只需要关心资产本身」。你写一个AssetInfoController,继承若依的BaseController,startPage()、getDataTable()、toAjax()这些方法直接可用;导出 Excel 只需一个@Excel注解;部门下拉、用户选择、字典翻译前端都有现成组件。所以这份源码重点不在框架层,而在业务层怎么设计。

一个典型的若依主启动类长这样,资产模块不会单独起应用,而是作为业务包挂进去:

@SpringBootApplication(exclude = { DataSourceAutoConfiguration.class }) public class RuoYiApplication { public static void main(String[] args) { // 启动时打印 banner 与当前 profile SpringApplication.run(RuoYiApplication.class, args); System.out.println("(♥◠‿◠)ノ゙ 资产管理系统启动成功 ლ(´ڡ`ლ)゙"); } }

这里exclude = DataSourceAutoConfiguration.class是因为若依用了 Druid 多数据源配置,不需要 Spring Boot 自动装配单数据源。资产模块的@MapperScan通常配在ruoyi-framework的ApplicationConfig里,扫描范围一般会写成com.ruoyi.**.mapper,如果你新建了com.ruoyi.asset包,记得确认这个通配符能覆盖到,不然 Mapper 注入报NoSuchBeanDefinitionException是常有的事。

我一般会建议把资产模块单独建一层包,而不是零散塞进ruoyi-system里,原因是后面要加盘点、折旧、审批流,业务会越滚越大,早期合并后期拆会很痛。这也是二次开发里最值钱的一个决策。

2.2 五个 Maven 模块与资产代码的落位

若依前后端分离版的目录结构本身不复杂,复杂的是新人不知道自己的业务代码该放哪。源码包解开后大致是下面这个结构:

ruoyi ├── ruoyi-admin # 启动模块,Controller 与配置文件在这 │ └── src/main/resources │ ├── application.yml │ └── application-druid.yml ├── ruoyi-common # 通用工具、注解、常量、BaseEntity ├── ruoyi-framework # Security、Redis、Druid、拦截器配置 ├── ruoyi-system # 系统模块(用户、部门、字典、菜单) ├── ruoyi-quartz # 定时任务 ├── ruoyi-generator # 代码生成器 └── ruoyi-ui # Vue 前端

资产模块的落位有两种常见做法,这份源码走的是第二种:

  • 做法一:完全塞进ruoyi-system,包名com.ruoyi.system.domain.asset,好处是不用改 pom,坏处是 system 模块越来越胖。
  • 做法二:新建ruoyi-asset模块,ruoyi-admin依赖它,包名com.ruoyi.asset。业务包内再拆domain、mapper、service、service.impl、controller。源码里用的是这套。

对应资产包内部结构:

com.ruoyi.asset ├── domain │ ├── AssetInfo.java # 资产主表实体 │ ├── AssetCategory.java # 资产分类 │ ├── AssetFlow.java # 领用/调拨/报废流水 │ └── AssetDepreciation.java # 折旧记录 ├── mapper │ ├── AssetInfoMapper.java │ └── AssetInfoMapper.xml ├── service │ ├── IAssetInfoService.java │ └── impl/AssetInfoServiceImpl.java └── controller └── AssetInfoController.java

前端对应的目录一般在ruoyi-ui/src/views/asset/下,按业务再拆info、category、flow、depreciation四个子目录,每个子目录一个index.vue加一个api.js。这里有个约定:api.js里接口前缀要和后端@RequestMapping完全对齐,否则联调阶段最常见的 404 就是从这里冒出来的。

提示:新增模块要在父级pom.xml的<modules>里登记,并在ruoyi-admin/pom.xml加依赖,否则 IDE 能识别但 Maven 打包时会报找不到符号,这个坑不少见。

2.3 数据库设计:资产主表、流水表、折旧表怎么拆

资产系统的数据结构是整个项目的地基。很多人第一步就会踩坑——把「领用记录」直接塞进asset_info表里,结果一台设备领用调拨过三五次就写不下,只能改历史。正确做法是「主表存现状、流水表存轨迹」:

  • asset_info:一台资产一行,记录当前状态、当前保管人、当前部门。
  • asset_flow:一次操作一行,记录领用、归还、调拨、维修、报废。
  • asset_depreciation:每月一条折旧记录,避免重复计提。
  • asset_category:分类树,支持父子级。
  • asset_inventory、asset_inventory_item:盘点单主表与明细。

核心建表 SQL 大致如下(只摘录主表和流水表关键字段):

CREATE TABLE `asset_info` ( `asset_id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '资产ID', `asset_code` VARCHAR(64) NOT NULL COMMENT '资产编码,全局唯一', `asset_name` VARCHAR(128) NOT NULL COMMENT '资产名称', `category_id` BIGINT NOT NULL COMMENT '分类ID', `spec_model` VARCHAR(128) DEFAULT NULL COMMENT '规格型号', `serial_no` VARCHAR(128) DEFAULT NULL COMMENT '序列号', `original_value` DECIMAL(12,2) DEFAULT 0.00 COMMENT '原值', `residual_rate` DECIMAL(5,2) DEFAULT 5.00 COMMENT '净残值率(%)', `use_years` INT DEFAULT 5 COMMENT '使用年限', `status` CHAR(1) DEFAULT '0' COMMENT '0待入库 1在库 2领用 3维修 4报废', `dept_id` BIGINT DEFAULT NULL COMMENT '当前使用部门', `keeper` VARCHAR(64) DEFAULT NULL COMMENT '当前保管人', `del_flag` CHAR(1) DEFAULT '0' COMMENT '0存在 2删除', `create_by` VARCHAR(64) DEFAULT NULL, `create_time` DATETIME DEFAULT NULL, PRIMARY KEY (`asset_id`), UNIQUE KEY `uk_asset_code` (`asset_code`), KEY `idx_category` (`category_id`), KEY `idx_status` (`status`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='资产主表'; CREATE TABLE `asset_flow` ( `flow_id` BIGINT NOT NULL AUTO_INCREMENT, `asset_id` BIGINT NOT NULL COMMENT '资产ID', `flow_type` CHAR(1) NOT NULL COMMENT '1领用 2归还 3调拨 4维修 5报废', `from_dept` BIGINT DEFAULT NULL, `to_dept` BIGINT DEFAULT NULL, `from_keeper` VARCHAR(64) DEFAULT NULL, `to_keeper` VARCHAR(64) DEFAULT NULL, `oper_time` DATETIME NOT NULL, `remark` VARCHAR(255) DEFAULT NULL, PRIMARY KEY (`flow_id`), KEY `idx_asset` (`asset_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='资产流转流水表';

两个设计点值得单独说。第一,asset_code加了唯一索引,这是资产系统里最容易被忽略的一条约束——资产编码重复会直接导致后续折旧、盘点的对账错乱,靠业务代码判重迟早出问题。第二,status用一个字符位表达状态,而不是用布尔字段叠加,好处是状态机推进时只需一次校验,扩展新状态(比如「审批中」)时也只需加一个字母,不用改表结构。这两个细节在后面第 3 章的状态机里会继续用到。

3. 核心模块拆解:一台设备从入库到报废的完整链路

结构讲完就要看业务。固定资产管理系统的核心链路其实不长:入库建卡 → 领用分发 → 调拨转移 → 维修 → 报废出账,中间穿插每月折旧。但每一步都牵扯「主表状态」和「流水表」两处写入,一旦事务没管住,会出现资产状态是「在库」、流水却停在「已报废」的错乱数据。这份源码把这套链路按状态机的方式做了收敛,拆开看比较清晰。

3.1 资产台账 CRUD:Controller 到 Mapper 的调用链

先从最普通的列表查询看。若依脚手架上的 Controller 写法高度统一,资产模块基本沿用,重点在于权限注解和分页处理:

@RestController @RequestMapping("/asset/info") public class AssetInfoController extends BaseController { @Autowired private IAssetInfoService assetInfoService; /** 资产列表:支持编码、名称、分类、状态、部门多条件过滤 */ @PreAuthorize("@ss.hasPermi('asset:info:list')") @GetMapping("/list") public TableDataInfo list(AssetInfo assetInfo) { // startPage 会从请求里读取 pageNum 和 pageSize,交给 PageHelper startPage(); List<AssetInfo> list = assetInfoService.selectAssetInfoList(assetInfo); return getDataTable(list); } /** 资产详情:进入编辑页或者查看页时调用 */ @PreAuthorize("@ss.hasPermi('asset:info:query')") @GetMapping(value = "/{assetId}") public AjaxResult getInfo(@PathVariable Long assetId) { return success(assetInfoService.selectAssetInfoById(assetId)); } /** 新增资产:一般用于入库建卡 */ @PreAuthorize("@ss.hasPermi('asset:info:add')") @Log(title = "资产入库", businessType = BusinessType.INSERT) @PostMapping public AjaxResult add(@Validated @RequestBody AssetInfo assetInfo) { return toAjax(assetInfoService.insertAssetInfo(assetInfo)); } }

startPage()是若依封装好的分页入口,它读的是PageDomain里解析出来的pageNum、pageSize、orderByColumn和isAsc,本质上是PageHelper.startPage(...)的包装;getDataTable()再把PageInfo转成前端需要的rows、total结构。这里最关键的一点是分页方法必须紧贴selectXxxList调用,中间如果插了别的数据库操作,分页会错位到下一次查询上,列表页码对不上是这类问题最典型的症状。

@PreAuthorize里的asset:info:list是权限标识,对应sys_menu表里配置的perms字段。这套权限点是需要提前在菜单管理里录入的,否则非超级管理员登录后进不去这个接口。二次开发时新增接口,别忘了菜单表补一行,否则 4.3 节启动验证时会看到「已登录但接口 403」。

Service 层的写法通常是标准的三段式,但在资产场景里,新增那一步必须顺手把资产编码生成掉:

@Service public class AssetInfoServiceImpl implements IAssetInfoService { @Autowired private AssetInfoMapper assetInfoMapper; @Override public List<AssetInfo> selectAssetInfoList(AssetInfo assetInfo) { return assetInfoMapper.selectAssetInfoList(assetInfo); } @Override public int insertAssetInfo(AssetInfo assetInfo) { // 1. 生成资产编码:分类前缀 + 年月 + 4 位序列,例如 PC-202506-0007 assetInfo.setAssetCode(generateAssetCode(assetInfo.getCategoryId())); // 2. 入库建卡默认状态为「在库」,避免前端漏传 assetInfo.setStatus("1"); assetInfo.setCreateTime(DateUtils.getNowDate()); return assetInfoMapper.insertAssetInfo(assetInfo); } private String generateAssetCode(Long categoryId) { // 实际项目里通常查 category 表拿前缀,再用 Redis 自增保证并发唯一 String prefix = categoryMapper.selectPrefixById(categoryId); String yyyymm = DateUtils.parseDateToStr("yyyyMM", new Date()); Long seq = redisCache.increment("asset:seq:" + prefix + ":" + yyyymm, 1); return String.format("%s-%s-%04d", prefix, yyyymm, seq); } }

这里有两个经验点。第一,编码生成千万别用COUNT(*)+1,并发下必然撞唯一索引,Redis 自增是最省事也最稳的方案,若依本身已经整合 Redis,直接用RedisCache就够。第二,status在 Service 里强制赋值,比默认值更可靠——前端表单一旦被二次开发改过字段名,setStatus往往是第一个被漏掉的,导致新数据进来状态为空,列表里一片空白。

Mapper XML 用的是若依一贯风格,动态条件查询通过<if>拼装,是固定资产场景下最实用的部分:

<select id="selectAssetInfoList" parameterType="AssetInfo" resultMap="AssetInfoResult"> select a.asset_id, a.asset_code, a.asset_name, a.category_id, a.spec_model, a.original_value, a.status, a.dept_id, a.keeper, a.create_time, c.category_name, d.dept_name from asset_info a left join asset_category c on c.category_id = a.category_id left join sys_dept d on d.dept_id = a.dept_id <where> a.del_flag = '0' <if test="assetCode != null and assetCode != ''"> and a.asset_code like concat('%', #{assetCode}, '%') </if> <if test="assetName != null and assetName != ''"> and a.asset_name like concat('%', #{assetName}, '%') </if> <if test="categoryId != null"> and a.category_id = #{categoryId} </if> <if test="status != null and status != ''"> and a.status = #{status} </if> <if test="deptId != null"> and a.dept_id = #{deptId} </if> </where> order by a.create_time desc </select>

资产列表页是典型的「多条件过滤 + 多表联查 + 排序」场景,一般不要用SELECT *,因为资产主表字段较长,spec_model、remark这类大字段在列表页里根本没有用,全查会明显拖慢分页。另外left join sys_dept拿部门名称,比在 Java 里循环调一次部门接口要划算得多。

3.2 领用与调拨:用状态机管住资产生命周期

资产系统里最容易出错的地方不是增删改,是状态流转。一份 JS 校验不完的资产设备,可以被三个部门同时点「领用」,如果服务端只判一次status = '1',两并发请求进来,两条领用流水都会落库,最后账上就一台、手上两台。

这份源码处理这个场景的办法是「状态机 + 数据库条件更新」:

@Override @Transactional(rollbackFor = Exception.class) public int borrowAsset(Long assetId, String toKeeper, Long toDept) { // 1. 先查当前资产,做业务层校验,给用户友好的报错 AssetInfo asset = assetInfoMapper.selectAssetInfoById(assetId); if (asset == null) { throw new ServiceException("资产不存在"); } if (!"1".equals(asset.getStatus())) { throw new ServiceException("资产当前不在库,无法领用"); } // 2. UPDATE 时带上 status 条件,等价于乐观锁,防止并发双写 int affected = assetInfoMapper.updateStatusFromTo(assetId, "1", "2", toKeeper, toDept); if (affected == 0) { throw new ServiceException("资产状态已变化,请刷新后重试"); } // 3. 写流水,recordType 固定为「领用」 AssetFlow flow = new AssetFlow(); flow.setAssetId(assetId); flow.setFlowType("1"); flow.setFromDept(asset.getDeptId()); flow.setToDept(toDept); flow.setFromKeeper(asset.getKeeper()); flow.setToKeeper(toKeeper); flow.setOperTime(DateUtils.getNowDate()); assetFlowMapper.insertAssetFlow(flow); return 1; }

关键在updateStatusFromTo这条 SQL,它是「先查后改」范式里最能保命的一句:

<update id="updateStatusFromTo"> update asset_info set status = #{toStatus}, dept_id = #{deptId}, keeper = #{keeper}, update_time = now() where asset_id = #{assetId} and status = #{fromStatus} and del_flag = '0' </update>

参数含义很直白:fromStatus是期望的前置状态,只有真匹配才更新;toStatus是目标状态。这一句把检查与更新合并成原子操作,两个人同时点领用,第一个人affected = 1,第二个人affected = 0直接抛业务异常,不会双写。配合@Transactional(rollbackFor = Exception.class),主表更新成功但流水插入失败时,主表状态会回滚,不会出现「状态已改但没流水」的黑匣子数据。

领导调拨是同一套逻辑,只是fromStatus和toStatus都改成2,然后update条件换成部门/保管人不一致,避免「调给自己」。报废更简单:fromStatus = 1 or 2,toStatus = 4,同时在流水表记一笔flowType = 5。整套状态推进固定为「待入库(0) → 在库(1) → 领用(2) → 维修(3) → 报废(4)」,中间不允许跳级,这样盘点时只要查状态字段就能出「在库资产明细」。

3.3 折旧计算:定时任务与直线法折旧流水

折旧是固定资产区别于普通物资管理的核心功能。源码采用的方法是「直线法 + 月度计提」,公式是:

月折旧额 = (原值 - 原值 × 净残值率) ÷ (使用年限 × 12)

这个公式看起来简单,坑的是「重复计提」和「不满整月」两类情况。源码的做法是给asset_depreciation表加唯一约束uk_asset_period (asset_id, period),其中period是yyyy-MM,每月定时任务跑之前先按 period 删掉重算,或者直接依赖唯一索引把重复插入挡回去。定时任务本身可以用 Quartz(若依已经集成好任务管理页),也可以用 Spring 的@Scheduled,后者更适合固定资产这种场景固定、不需要人工在后台改 cron 的。

@Component public class AssetDepreciationTask { @Autowired private AssetDepreciationMapper depreciationMapper; @Autowired private AssetInfoMapper assetInfoMapper; /** 每月 1 号凌晨 2 点跑一次,对上个月做计提 */ @Scheduled(cron = "0 0 2 1 * ?") public void runMonthlyDepreciation() { String period = DateUtils.parseDateToStr("yyyy-MM", DateUtils.addMonths(new Date(), -1)); // 只处理在库/领用/维修中三种状态,报废和待入库的不计提 List<AssetInfo> list = assetInfoMapper.selectDepreciableAssets(); for (AssetInfo asset : list) { // 如果这一期已存在记录,跳过(双保险,唯一索引兜底) if (depreciationMapper.existsByAssetAndPeriod(asset.getAssetId(), period) > 0) { continue; } BigDecimal monthly = calcMonthly(asset); AssetDepreciation d = new AssetDepreciation(); d.setAssetId(asset.getAssetId()); d.setPeriod(period); d.setAmount(monthly); d.setAccumulated(calcAccumulated(asset.getAssetId()).add(monthly)); d.setNetValue(asset.getOriginalValue().subtract(d.getAccumulated())); depreciationMapper.insertAssetDepreciation(d); } } private BigDecimal calcMonthly(AssetInfo asset) { // 分母用 BigDecimal,避免 int 除法把小数截掉 BigDecimal years = new BigDecimal(asset.getUseYears()); BigDecimal rate = asset.getResidualRate().divide(new BigDecimal("100")); BigDecimal depreciable = asset.getOriginalValue().multiply(BigDecimal.ONE.subtract(rate)); return depreciable.divide(years.multiply(new BigDecimal("12")), 2, RoundingMode.HALF_UP); } }

calcMonthly里有两个细节。第一,除法一定要显式指定scale和RoundingMode,否则BigDecimal除以一个不能整除的数会抛ArithmeticException,这种报错在测试环境不容易复现,因为只有小数除不尽的资产才会触发。第二,累计折旧和净值是冗余字段,从性能考虑可以直接放在折旧记录里,但一旦发现对不上,第一反应应该是跑一次全量重算,而不是逐条修数据——这个习惯后面会反复强调。

折旧记录的展示一般放在「资产详情 → 折旧明细」tab 下,前端按period倒序展示就行。如果要出一张「本月折旧汇总表」,再按period和dept_id分组聚合,别每次明细都重新算。

4. 跑起来:从改配置到登录后台的完整启动流程

代码看明白不等于跑得起来。这份源码是典型的若依工程,跑通的关键不在业务代码,在环境对齐和配置文件。我在不同机器上部署过几次,每次卡住的点都集中在 JDK、MySQL 连接、Redis 地址三处。

4.1 环境准备与版本对齐

固定资产源码对环境的要求基本等于若依本身,常见的推荐版本:

组件推荐版本说明
JDK8 或 11若依 3.x 大多跑在 8 上,11 也能跑
Maven3.6+需要能成功拉取私服依赖
MySQL5.7 或 8.08.0 要注意时区和caching_sha2_password
Redis5.0+一定要先起,否则启动会报连接超时
Node14 或 16前端 Vue2 + Element UI 对高版本有兼容问题

JDK 这里容易翻车:用 JDK 17 跑若依旧版本常见javax包找不到,因为部分依赖没有适配 Jakarta 命名空间。省心的办法是直接切到 JDK 8。MySQL 8.0 两个高频坑——连接 URL 必须带serverTimezone=Asia/Shanghai&useSSL=false&allowPublicKeyRetrieval=true,否则 Druid 初始化直接失败;MySQL 8 默认的加密插件不认老驱动,需要确认依赖里mysql-connector-java版本至少 8.0.x。

Node 版本这块我一般会给一个保底建议:nvm use 16。前端npm install慢是常态,配置一下国内镜像能省不少时间。

4.2 数据库初始化与配置修改

数据库这一步分三段:建库 → 导 SQL → 改配置。

先用 root 登录 MySQL,建库和授权:

mysql -u root -p
-- 建库,字符集按 utf8mb4,避免资产名称里出现特殊字符乱码 CREATE DATABASE `asset_admin` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; -- 建一个专用账号,不建议直接用 root 跑业务 CREATE USER 'asset_user'@'%' IDENTIFIED BY 'Asset@2025'; GRANT ALL PRIVILEGES ON `asset_admin`.* TO 'asset_user'@'%'; FLUSH PRIVILEGES;

SQL 脚本一般分两个文件:ry_2025xxxx.sql(若依基础表与菜单)和asset_module.sql(资产模块表与字典)。导入顺序不能倒,因为资产模块要往sys_menu里插菜单,得先有基础表:

mysql -u asset_user -p asset_admin < sql/ry_2025xxxx.sql mysql -u asset_user -p asset_admin < sql/asset_module.sql

导完sys_menu里应该能看到「资产管理」这个一级目录,下面挂着资产台账、资产分类、资产流水、折旧记录四个菜单。如果只导了基础表你会发现前端登录后看不到资产菜单,不是前端问题,是菜单数据没插进去。

接下来改配置,主要是两处:application.yml里的 Redis 和application-druid.yml里的 MySQL。

# application.yml 片段 spring: redis: host: 127.0.0.1 port: 6379 database: 0 password: # 本地没设密码就留空,别写 null timeout: 10s
# application-druid.yml 片段 spring: datasource: druid: master: url: jdbc:mysql://127.0.0.1:3306/asset_admin?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true username: asset_user password: Asset@2025 driver-class-name: com.mysql.cj.jdbc.Driver

这里最容易出错的就是 URL 参数。少了serverTimezone若依的 Quartz 初始化会失败,少了allowPublicKeyRetrieval=true在 MySQL 8 上会报Public Key Retrieval is not allowed,useSSL=false不带的话某些机器会在握手阶段卡住。另外spring.redis.password留空时不要写password: ""引号,某些配置版本会认为空字符串是密码,反而报鉴权失败。

4.3 前后端启动与验证

后端启动走ruoyi-admin模块的RuoYiApplication,命令行方式:

cd ruoyi mvn clean package -DskipTests java -jar ruoyi-admin/target/ruoyi-admin.jar --spring.profiles.active=druid

看到控制台打印出「启动成功」并且端口 8080 起来了,就可以启动前端。

cd ruoyi-ui npm install --registry=https://registry.npmmirror.com npm run dev

前端默认 80 端口,如果被占用,Vue CLI 一般会自动切换,或者改vue.config.js里的devServer.port。启动后访问http://localhost:80,默认账号密码通常是admin/admin123(源码里如有改动以实际为准)。

登录成功后按下面三步验证项目是否真正跑通:

  1. 进入「资产管理 → 资产台账」,点击「新增」建一张资产卡,观察编码是否按前缀-年月-序号生成。
  2. 在列表上点「领用」,弹窗里选部门和保管人,提交后资产状态应从「在库」变成「领用」,同时「资产流水」里多一条记录。
  3. 进入「系统监控 → 定时任务」,找到折旧任务手动执行一次,看「折旧记录」里是否有当月数据。

如果第 2 步提交时提示 403,多半是菜单权限点没有刷新——退出重登一次让sys_menu的perms重新进 Redis 缓存;如果第 3 步任务执行了但没数据,去检查资产状态是不是全在「待入库」,那种状态是被折旧逻辑主动排除的。

注意:首次启动前确认 Redis 已经跑起来。若依在启动时就会去读字典和参数配置,Redis 连不上会直接抛RedisConnectionFailureException,而不是等到业务接口才报错。

5. 避坑:部署和二次开发里最容易翻车的 5 个点

这一章是我自己部署和二次开发过程中真踩过的记录,不是罗列知识点。每条都按「现象 → 原因 → 解决」写,能对上就用,对不上看看思路也是好的。

5.1 启动直接报 Redis 连接失败

现象:java -jar刚跑起来控制台就中断,Caused by: io.lettuce.core.RedisConnectionException: Unable to connect to localhost:6379。

原因:本机没装 Redis,或者 Redis 起了但监听的127.0.0.1、端口被改,application.yml里还写着默认值。还有一种是 Redis 有密码但配置没填,客户端连上后被NOAUTH踢掉,报错信息也类似。

解决:先redis-cli ping看返回是否PONG,确认服务端口;有密码就在配置里补上password;若依用 Lettuce 客户端,timeout别设太短,设10000比较稳。生产环境直接改配置指向正式 Redis 地址,不要图省事在本地硬编码。

5.2 资产编码重复或者序号乱跳

现象:新增资产时报Duplicate entry 'PC-2025xx-0007' for key 'uk_asset_code',或者月末第一天编码直接从 0007 跳到 0037。

原因:用COUNT(*) + 1或MAX(asset_code) + 1拼序号,高并发下两个请求读到同一个值;月份切换时如果序列前缀按分类 + 年月拼接,历史序列没有独立隔离,就会串到上个月的计数上。

解决:编码生成统一走 RedisINCR,key 用asset:seq:{prefix}:{yyyyMM},每个分类每个月一条独立序列。如果有历史数据,初始化时把当月序列值SET到当前最大序号上,避免前缀冲突。数据库那边唯一索引必须保留,作为最后一道闸门。

5.3 折旧任务重复执行

现象:同一个月在「折旧记录」里出现两条相同资产的记录,汇总表金额翻倍。

原因:Quartz 的 cron 配成了每天跑,或者集群部署时两台机器同时触发同一个任务,没有做幂等。

解决:第一,任务本身只做「按月」频次,cron 写成0 0 2 1 * ?;第二,给asset_depreciation表加uk_asset_period (asset_id, period)唯一索引,插入时用INSERT IGNORE或捕获唯一键异常跳过;第三,若依的 Quartz 任务表里status字段是0表示正常、1表示暂停,集群部署时只让一台机器跑,任务配置里避免双活写同一个job_name。

5.4 权限注解不生效,接口谁都能调

现象:Controller 上写了@PreAuthorize("@ss.hasPermi('asset:info:add')"),但用普通账号调这个接口居然成功了。

原因:大概率是安全配置里开了匿名放行,比如SecurityConfig里antMatchers("/asset/**").permitAll(),或者根本没启用@EnableGlobalMethodSecurity(prePostEnabled = true)。另一种情况是@PreAuthorize里的权限标识和sys_menu里的perms拼写不一致,Spring Security 解析时只认字面量,不报错、只放行。

解决:检查SecurityConfig的configure(HttpSecurity http)有没有把/asset/**放行;检查方法级安全开关有没有开;最后到「系统管理 → 菜单管理」把perms字段和注解逐一核对,前后端联调前先跑通「无权限用户调接口应该 403」这个用例。

5.5 前端打包后刷新页面菜单 404

现象:npm run build打出来的dist放到 Nginx 上,首页能进去,刷新任意子路由就 404。

原因:Vue Router 用的是 history 模式,Nginx 没配try_files,刷新时请求的是真实 URL,服务器找不到对应文件。

解决:Nginx 站点配置里加一段:

location / { root /usr/share/nginx/html; index index.html index.htm; try_files $uri $uri/ /index.html; }

再确认vue.config.js里publicPath和部署子路径一致。若依部署在子目录下,还要改router/index.js的base,否则资产菜单点进去地址对不上,表现也是 404 或空白页。

6. 二次开发:把资产台账接进自定义审批流

资产系统跑到后面,十有八九会被要求「报废/调拨必须走审批」。直接往asset_flow里加个approve_status字段,再写几个 if 判断,是最省事的做法,但撑不过三个月——审批节点会加,会签、或签、退回、加批都会来,最后状态字段变成一大串枚举,if判断嵌套到没法看。我给建议是分两步改。

第一步先把审批流和资产状态解耦:新增一张asset_approve表,字段大致是approve_id、asset_id、flow_type、current_node、apply_user、apply_time、approve_status、result,主表asset_info里只加一个approving标志位(1表示审批中)。这样主表状态机维持原样,审批流程完全独立。

第二步在 Service 里留钩子。报废方法改为「先生成审批单、把资产状态推到approving、真正出账时再推进status = 4」,审批通过的回调只做一件事:调用原来的scrapAsset方法,把之前那段状态机推进逻辑原封不动复用。这样无论审批流是用若依自带的简单流程、还是接一个工作流引擎、甚至后面换成外部流程平台,资产核心状态机都不用改。

验证方法很简单,准备 3 个账号、2 个资产,跑一次「发起报废 → 主管驳回 → 修改再提交 → 财务通过」,然后查三处数据是否对得上:

检查点预期结果
asset_info.status审批中保持1,通过后变4
asset_approve每次提交一条记录,result字段记录通过/驳回
asset_flow只在审批通过后追加一条flow_type=5的记录

三处都能对上,说明钩子位置拆对了。这里我踩过一次坑:一开始把资产状态推进逻辑塞进审批回调里,回调一旦超时重试,状态机会被推两次,status从1直接跳到4之前还多了一条流水。从那以后我每次加回调入口,都强制要求回调端先判approve_status是否已经是result,只有状态还没终结的才允许继续推进——后悔药没有,幂等校验一定要在写业务之前。希望帮到你。

本文还有配套的精品资源,点击获取