简介一套基于Java若依框架与layui开发的固定资产管理系统源码面向Java Web学习者、课程设计与毕业设计开发者以及需要资产管理系统二次开发参考的技术人员。系统围绕资产全生命周期管理展开覆盖资产登记、领用、借用、归还、维修、调拨、转移、报废和统计报表等业务并包含组织结构维护与角色权限分配可用于理解若依权限体系和layui前后端交互的组织方式。压缩包共1883个文件约8.88MB以317个Java后端源码、180个HTML页面、90个JavaScript脚本、40个CSS样式及38个XML配置为主另含SQL脚本和图片等资源便于按后端服务、前端页面与配置模块分层查阅。已有563人学习适合用来对照若依框架的权限管理、代码生成与前端组件用法也可作为资产类管理系统的功能拆解和二次开发底稿。1. 从三千行 Excel 到一套能跑的后台接手一个设备租赁公司的资产盘点时我在三千多行 Excel 里翻出 17 台笔记本同时挂在两个人名下而对应的实物早就不知道去哪儿了——用表格管资产出问题是迟早的事。这份 Java 固定资产管理源码就是来收拾这类局面的它以若依RuoYi前后端分离框架为底座后端 Spring Boot MyBatis Redis前端 Vue Element UI把资产台账、分类树、领用借用、调拨报废、折旧计提和盘点核销做成了一套开箱能跑的后台。权限、部门、数据字典、定时任务这些基础设施框架里现成就有不用从零搭一遍 RBAC。它适合三种人给中小客户交付资产管理模块的外包团队、想拿它当练手项目做二次开发的后端新手以及手上已经有一堆若依项目、想把资产域逻辑抽出来复用的老手。2. 工程骨架拆解若依的多模块结构怎么容纳一个资产域2.1 六个 Maven 模块各自负责什么拿到源码先别急着找启动类先看父 POM 的 modules 顺序和 dependencyManagement。若依前后端分离版是典型的 Maven 多模块依赖方向单向收敛理解这张表能省掉后面一半的困惑。模块主要职责资产域改造时要不要动ruoyi-admin启动类、Controller、主配置、日志要动放资产 Controller 并加模块依赖ruoyi-frameworkSpring Security 配置、JWT 过滤器、AOP 日志、数据源基本不动除非加自定义注解切面ruoyi-system用户、角色、菜单、部门、字典、参数不改只调用它的 DeptServiceruoyi-commonBaseEntity、AjaxResult、注解、工具类、常量要加放资产相关的枚举和常量ruoyi-quartzQuartz 调度封装ITask 接口折旧定时任务挂这里最省事ruoyi-generator代码生成器Velocity 模板出 CRUD用一次出完骨架就改造这套拆法的好处是边界清楚业务代码写崩了不会污染框架层框架升级时也只动 framework 和 common。代价是新手容易在「Controller 放哪个模块」「Mapper 能不能被扫描到」这两个问题上卡住后面避坑章会专门讲。2.2 资产模块挂在哪一层新建 ruoyi-asset 还是塞进 ruoyi-system源码里两种做法都常见。塞进 ruoyi-system 的 com.xxx.system.asset 包下最省事不用改 POM 不用加扫描但如果资产模块要交付给多个项目复用或者后续可能做微服务拆分新建独立模块更干净。我一般选后者因为资产域的表和逻辑一旦超过十张表跟系统管理混在一起会很难维护。新建模块的 POM 大致长这样project artifactIdruoyi-asset/artifactId description资产管理业务模块/description dependencies !-- 依赖 common 拿 BaseEntity、AjaxResult 和工具类 -- dependency groupIdcom.ruoyi/groupId artifactIdruoyi-common/artifactId /dependency !-- 需要用到部门、用户的查询所以依赖 system -- dependency groupIdcom.ruoyi/groupId artifactIdruoyi-system/artifactId /dependency /dependencies /project依赖只能单向asset 依赖 system反过来不行。然后在 ruoyi-admin 的 POM 里加上 ruoyi-asset并且在启动类上把扫描包放宽到 com.ruoyi通常框架默认就是 com.ruoyi.**多数情况不需要改。包结构建议按领域分不要按技术分asset/controller、asset/service/impl、asset/mapper、asset/domain、asset/domain/vo这样后面加盘点、加折旧都只是新增一个子包不动老代码。2.3 启动前必改的四处配置源码能直接跑起来的前提是配置对得上。application.yml 里我通常先看这四个地方少一个都启动不了。# 1. 服务端口与上下文 server: port: 8080 servlet: context-path: / # 2. 数据源主库指向若依的 ry 库从库可以留空 spring: datasource: druid: master: url: jdbc:mysql://127.0.0.1:3306/ry_asset?useUnicodetruecharacterEncodingutf8zeroDateTimeBehaviorconvertToNulluseSSLfalseserverTimezoneGMT%2B8 username: root password: 你的密码 # 初始化连接数本地调试设小一点免得占满连接 initialSize: 5 minIdle: 10 maxActive: 20 # 3. Redis登录 token、字典缓存、验证码都依赖它 redis: host: 127.0.0.1 port: 6379 database: 0 password: # 4. 文件上传路径资产二维码、发票附件落这里 ruoyi: profile: D:/ruoyi/uploadPath数据库初始化有两个 SQL框架自带的 ry_xxx.sql 建系统表并插入菜单和字典资产模块自己那份 asset.sql 建业务表并插入分类初始数据和菜单按钮。很多人只跑了第一个结果登录进去发现左边没菜单这不是权限问题是菜单表没数据。Redis 最好用同一个库别跟别的项目混我看过把 database 都设成 0 导致 token 互相顶掉的事故查了整整一下午才发现是本地环境串了。3. 资产台账主链路一条资产从入库到报废要落几张表3.1 五张表撑起整个资产模型台账是整个系统的地基表设计错了后面全是补丁。核心是先分清「静态属性」和「动态流转」资产本身的名称、型号、原值这些不常变状态、使用人、部门这些每动一次就要留痕所以主表只存当前状态流转走独立记录表。-- 资产主表一条记录代表一件实物资产 CREATE TABLE asset_info ( asset_id bigint(20) NOT NULL AUTO_INCREMENT COMMENT 资产ID, asset_code varchar(64) NOT NULL COMMENT 资产编码二维码内容, asset_name varchar(128) NOT NULL COMMENT 资产名称, category_id bigint(20) NOT NULL COMMENT 分类ID对应 asset_category, spec_model varchar(128) DEFAULT NULL COMMENT 规格型号, purchase_date date DEFAULT NULL COMMENT 购置日期折旧起算依据, original_value decimal(18,2) NOT NULL DEFAULT 0.00 COMMENT 原值, residual_rate decimal(5,4) NOT NULL DEFAULT 0.0500 COMMENT 残值率0.05 即 5%, usable_years int(4) NOT NULL DEFAULT 5 COMMENT 使用年限, accumulated_dep decimal(18,2) NOT NULL DEFAULT 0.00 COMMENT 累计折旧, net_value decimal(18,2) NOT NULL DEFAULT 0.00 COMMENT 账面净值, asset_status char(1) NOT NULL DEFAULT 0 COMMENT 0在库1在用2借出3维修4报废, user_id bigint(20) DEFAULT NULL COMMENT 当前使用人, dept_id bigint(20) DEFAULT NULL COMMENT 归属部门数据权限按它过滤, location varchar(128) DEFAULT NULL COMMENT 存放地点, del_flag char(1) NOT NULL DEFAULT 0 COMMENT 0存在2删除逻辑删除, create_by varchar(64) DEFAULT NULL, create_time datetime DEFAULT NULL, update_by varchar(64) DEFAULT NULL, update_time datetime DEFAULT NULL, remark varchar(500) DEFAULT NULL, PRIMARY KEY (asset_id), UNIQUE KEY uk_asset_code (asset_code), KEY idx_category (category_id), KEY idx_dept (dept_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT资产台账;asset_code 上的唯一索引不能省这是资产系统里唯一能防重复录入的东西。老系统迁移时最常见的脏数据就是同一个编码存了两条靠业务层查重根本挡不住并发索引才是最后一道。另外金额字段一律 decimal(18,2)别用 double——0.1 0.2 那个经典问题放到折旧上就是净值变成 -0.01月底对账时够你难受的。剩下四张表各管一段-- 流转记录每次状态变化追加一条只增不改 CREATE TABLE asset_flow ( flow_id bigint(20) NOT NULL AUTO_INCREMENT, asset_id bigint(20) NOT NULL COMMENT 资产ID, flow_type char(2) NOT NULL COMMENT 01入库02领用03借用04归还05调拨06报废07维修, from_status char(1) DEFAULT NULL COMMENT 变更前状态, to_status char(1) DEFAULT NULL COMMENT 变更后状态, from_dept_id bigint(20) DEFAULT NULL, to_dept_id bigint(20) DEFAULT NULL, oper_id bigint(20) DEFAULT NULL COMMENT 操作人, oper_time datetime DEFAULT NULL, remark varchar(500) DEFAULT NULL, PRIMARY KEY (flow_id), KEY idx_asset (asset_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT资产流转记录; -- 折旧明细按期间落账唯一键保证幂等 CREATE TABLE asset_depreciation ( id bigint(20) NOT NULL AUTO_INCREMENT, asset_id bigint(20) NOT NULL, period char(6) NOT NULL COMMENT 会计期间 yyyyMM, dep_amount decimal(18,2) NOT NULL COMMENT 本期折旧额, accumulated decimal(18,2) NOT NULL COMMENT 累计折旧, net_value decimal(18,2) NOT NULL COMMENT 期末净值, PRIMARY KEY (id), UNIQUE KEY uk_asset_period (asset_id, period) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT折旧明细;asset_depreciation 上的 uk_asset_period 是整个方案里我最看重的一处设计。折旧任务被重复触发在运维上是常态调度挂掉重启、人工补跑、多节点部署都会撞上没有这个唯一键重跑一次账面净值就少一截而且没人能立刻发现。另外两张是盘点asset_inventory_task 存批次任务编号、部门范围、计划起止、任务状态asset_inventory_item 存明细快照盘点时把资产编码、名称、应有状态抄一份进去扫码后填实际状态和差异标记。快照这一步别偷懒直接关联主表否则盘点期间有人做了调拨你的盘点结果就永远对不上。3.2 Mapper 层的写法与数据权限拼接MyBatis 这块若依的约定是 XML 放 resources/mapper/ 下namespace 指向接口全限定名。列表查询几乎都要带部门数据权限写法有固定套路select idselectAssetList parameterTypeAssetInfo resultMapAssetInfoResult select a.asset_id, a.asset_code, a.asset_name, a.original_value, a.net_value, a.asset_status, a.purchase_date, c.category_name, d.dept_name, u.nick_name as user_name from asset_info a left join asset_category c on a.category_id c.category_id left join sys_dept d on a.dept_id d.dept_id left join sys_user u on a.user_id u.user_id where a.del_flag 0 if testassetCode ! null and assetCode ! and a.asset_code like concat(%, #{assetCode}, %) /if if testassetStatus ! null and assetStatus ! and a.asset_status #{assetStatus} /if if testcategoryId ! null and a.category_id #{categoryId} /if if testparams.beginTime ! null and params.beginTime ! and a.purchase_date gt; #{params.beginTime} /if !-- 数据权限片段由 DataScope 切面注入别名必须与注解里的 deptAlias 一致 -- ${params.dataScope} /where order by a.asset_id desc /select三个参数要说清楚#{assetCode}是预编译占位符用户输入走它${params.dataScope}是字符串拼接看起来危险但它的值由框架切面生成内容是and (a.dept_id 100 or a.dept_id in (...))这种结构不接受外部输入别一时手快把它换成${assetCode}就行。params来自 BaseEntity 里的 Map前端传params[beginTime]2024-01-01这样的参数就能自动进到这个 Map不用额外定义字段。3.3 Service 层状态机把非法流转挡在代码里资产流转最容易写成一堆 if-else加一个「送修」动作就得改三处。我的做法是一张流转矩阵加一个统一的变更入口所有动作走同一个方法。public enum AssetStatusEnum { IN_STORE(0, 在库), IN_USE(1, 在用), BORROWED(2, 借出), REPAIRING(3, 维修中), SCRAPPED(4, 已报废); private final String code; private final String desc; // 构造、getter 略 public static AssetStatusEnum of(String code) { return Arrays.stream(values()) .filter(e - e.code.equals(code)) .findFirst() .orElseThrow(() - new ServiceException(未知的资产状态 code)); } }矩阵单独放一个类改规则只改这一个地方public final class AssetFlowRules { // key 是当前状态value 是允许流转到的目标状态集合 private static final MapAssetStatusEnum, SetAssetStatusEnum RULES new HashMap(); static { RULES.put(AssetStatusEnum.IN_STORE, EnumSet.of(AssetStatusEnum.IN_USE, AssetStatusEnum.BORROWED, AssetStatusEnum.SCRAPPED)); RULES.put(AssetStatusEnum.IN_USE, EnumSet.of(AssetStatusEnum.IN_STORE, AssetStatusEnum.REPAIRING, AssetStatusEnum.SCRAPPED)); RULES.put(AssetStatusEnum.BORROWED, EnumSet.of(AssetStatusEnum.IN_STORE, AssetStatusEnum.REPAIRING)); // 维修中只能回在库不能直接报废避免漏掉维修成本结算 RULES.put(AssetStatusEnum.REPAIRING, EnumSet.of(AssetStatusEnum.IN_STORE, AssetStatusEnum.SCRAPPED)); RULES.put(AssetStatusEnum.SCRAPPED, EnumSet.noneOf(AssetStatusEnum.class)); } public static boolean allowed(AssetStatusEnum from, AssetStatusEnum to) { return RULES.getOrDefault(from, Collections.emptySet()).contains(to); } }Service 里只留一个口子事务边界也在这里Service public class AssetFlowServiceImpl implements IAssetFlowService { Autowired private AssetInfoMapper assetMapper; Autowired private AssetFlowMapper flowMapper; /** * 统一状态变更入口 * param assetId 资产ID * param targetCode 目标状态编码 * param flowType 流转类型用于记录是领用还是调拨 * param toUserId 目标使用人可空 * param remark 备注 */ Override Transactional(rollbackFor Exception.class) public void changeStatus(Long assetId, String targetCode, String flowType, Long toUserId, String remark) { AssetInfo asset assetMapper.selectAssetById(assetId); if (asset null) { throw new ServiceException(资产不存在或已删除); } AssetStatusEnum from AssetStatusEnum.of(asset.getAssetStatus()); AssetStatusEnum to AssetStatusEnum.of(targetCode); if (!AssetFlowRules.allowed(from, to)) { throw new ServiceException(String.format(资产[%s]不允许从%s变更为%s, asset.getAssetCode(), from.getDesc(), to.getDesc())); } // 1. 更新主表当前状态 AssetInfo update new AssetInfo(); update.setAssetId(assetId); update.setAssetStatus(to.getCode()); update.setUserId(toUserId); update.setUpdateTime(DateUtils.getNowDate()); assetMapper.updateAsset(update); // 2. 追加流转记录from 状态取自变更前的快照 AssetFlow flow new AssetFlow(); flow.setAssetId(assetId); flow.setFlowType(flowType); flow.setFromStatus(from.getCode()); flow.setToStatus(to.getCode()); flow.setFromDeptId(asset.getDeptId()); flow.setToDeptId(asset.getDeptId()); flow.setOperId(SecurityUtils.getUserId()); flow.setOperTime(DateUtils.getNowDate()); flow.setRemark(remark); flowMapper.insertAssetFlow(flow); } }这段的逻辑顺序不能颠倒先查当前状态再校验然后更新主表最后落流水。两个写操作在同一个事务里rollbackFor Exception.class是为了让受检异常也回滚若依里很多默认只回滚 RuntimeException补上这个参数是常识。SecurityUtils.getUserId()从 JWT 里取当前登录人匿名接口调用会抛异常所以这个入口只能挂在需要登录的 Controller 后面。3.4 Controller 与权限标识的对应关系Controller 写得薄逻辑全在 Service权限标识跟菜单表里的按钮权限一一对应RestController RequestMapping(/asset/info) public class AssetInfoController extends BaseController { Autowired private IAssetInfoService assetService; Autowired private IAssetFlowService flowService; /** 分页列表自动带部门数据权限 */ PreAuthorize(ss.hasPermi(asset:info:list)) GetMapping(/list) public TableDataInfo list(AssetInfo assetInfo) { startPage(); // 数据权限在 Service 里加 DataScope 注解 ListAssetInfo list assetService.selectAssetList(assetInfo); return getDataTable(list); } /** 状态变更领用、借用、归还、调拨、报废共用 */ PreAuthorize(ss.hasPermi(asset:info:edit)) Log(title 资产流转, businessType BusinessType.UPDATE) PutMapping(/changeStatus) public AjaxResult changeStatus(RequestBody AssetFlowDto dto) { flowService.changeStatus(dto.getAssetId(), dto.getTargetStatus(), dto.getFlowType(), dto.getToUserId(), dto.getRemark()); return success(); } }PreAuthorize(ss.hasPermi(asset:info:list))里的字符串必须跟 sys_menu 表里按钮的 perms 字段完全一致前端v-hasPermi[asset:info:edit]也要一致三处有一处写错按钮就点不动。Log会往 sys_oper_log 写一条操作日志资产报废这类动作建议都加上出了纠纷能拿出操作人和时间。前端页面在 ruoyi-ui 的 src/views/asset/info/index.vueAPI 请求在 src/api/asset/info.js路由不用手写菜单表里配好 component 路径component 填 asset/info/index后端动态下发就行。4. 折旧计提与盘点两个最容易写错的业务环节4.1 直线法折旧的幂等实现折旧这块如果只能记住一件事那就是别让同一期间计提两次。计算本身不难难的是重跑。下面这个计算器把残值、最后一期倒挤、边界保护都收进去了Component public class AssetDepreciationCalculator { /** * 计算指定期间的月折旧额 * param originalValue 原值 * param residualRate 残值率0.05 表示 5% * param usableYears 使用年限 * param accumulated 已累计折旧 */ public BigDecimal monthly(BigDecimal originalValue, BigDecimal residualRate, Integer usableYears, BigDecimal accumulated) { if (originalValue null || originalValue.compareTo(BigDecimal.ZERO) 0) { return BigDecimal.ZERO; } int totalMonths usableYears * 12; if (totalMonths 0) { return BigDecimal.ZERO; } // 预计净残值四舍五入到分 BigDecimal residual originalValue.multiply(residualRate).setScale(2, RoundingMode.HALF_UP); // 应计提总额 原值 - 净残值 BigDecimal depreciable originalValue.subtract(residual); BigDecimal monthly depreciable.divide(BigDecimal.valueOf(totalMonths), 2, RoundingMode.HALF_UP); // 最后一期倒挤本期最多只能提剩余的应计提额否则净值会低于残值 BigDecimal remain depreciable.subtract(accumulated); if (remain.compareTo(BigDecimal.ZERO) 0) { return BigDecimal.ZERO; } return monthly.min(remain); } }参数说明一下residualRate用小数不要用百分数0.05 而不是 5数据库那张表里也是 decimal(5,4) 默认 0.0500。四舍五入统一用HALF_UP别用默认的HALF_EVEN银行家舍入在批量场景下会跟财务的手工账对不上。remain那段就是血泪经验——每月除不尽的小数余量攒到最后会差几毛钱财务一核账就露馅倒挤到最后一期是最省事的解法。落账的服务加上幂等判断和事务Service public class AssetDepreciationServiceImpl implements IAssetDepreciationService { Autowired private AssetInfoMapper assetMapper; Autowired private AssetDepreciationMapper depMapper; Autowired private AssetDepreciationCalculator calculator; /** * 对全部可折旧资产计提指定期间 * param period 会计期间 yyyyMM * return 实际计提的资产条数 */ Override Transactional(rollbackFor Exception.class) public int depreciateAll(String period) { // 只取未报废、净值大于净残值的资产 ListAssetInfo list assetMapper.selectDepreciableList(); int count 0; for (AssetInfo asset : list) { // 先查明细表已存在说明本期已计提直接跳过 if (depMapper.countByPeriod(asset.getAssetId(), period) 0) { continue; } BigDecimal amount calculator.monthly(asset.getOriginalValue(), asset.getResidualRate(), asset.getUsableYears(), asset.getAccumulatedDep()); if (amount.compareTo(BigDecimal.ZERO) 0) { continue; } BigDecimal accumulated asset.getAccumulatedDep().add(amount); BigDecimal netValue asset.getOriginalValue().subtract(accumulated); // 1. 写折旧明细 AssetDepreciation dep new AssetDepreciation(); dep.setAssetId(asset.getAssetId()); dep.setPeriod(period); dep.setDepAmount(amount); dep.setAccumulated(accumulated); dep.setNetValue(netValue); depMapper.insertAssetDepreciation(dep); // 2. 回写主表累计折旧和净值 AssetInfo update new AssetInfo(); update.setAssetId(asset.getAssetId()); update.setAccumulatedDep(accumulated); update.setNetValue(netValue); assetMapper.updateAsset(update); count; } return count; } }查重 唯一索引是双保险只靠查重在多节点并发下还是可能重复插但有了 uk_asset_period 最坏情况是抛异常回滚不会污染账面数据。循环里逐条更新在资产量上千时会慢真到那个量级再改成批量 insert别一上来就优化。4.2 挂到 Quartz 定时任务上若依的定时任务约定是 Bean 实现 ITask 接口调用目标字符串写成beanName.run(参数)/** * 折旧计提任务 * 在「系统监控 - 定时任务」里新增 * 调用目标字符串assetDepreciationTask.run(202501) * cron0 0 2 1 * ?每月 1 号凌晨 2 点 */ Component(assetDepreciationTask) public class AssetDepreciationTask implements ITask { Autowired private IAssetDepreciationService depreciationService; Override public void run(String params) { // 参数传了期间就按传入的算没传默认取当月方便手工补跑 String period StringUtils.isNotEmpty(params) ? params.trim() : DateUtils.parseDateToStr(yyyyMM, DateUtils.getNowDate()); int count depreciationService.depreciateAll(period); log.info(折旧计提完成期间{}计提资产数{}, period, count); } }Component(assetDepreciationTask)里的名字必须和调用字符串前缀一模一样大小写都不能差。参数里别写中文和空格Quartz 的解析对这个比较敏感。4.3 盘点任务生成与差异核销盘点的核心动作只有两个生成时快照核销时比对。Override Transactional(rollbackFor Exception.class) public Long createInventory(InventoryTaskDto dto) { // 1. 先落任务头 AssetInventoryTask task new AssetInventoryTask(); task.setTaskCode(PD DateUtils.dateTimeNow(yyyyMMddHHmmss)); task.setTaskName(dto.getTaskName()); task.setDeptId(dto.getDeptId()); task.setPlanStart(dto.getPlanStart()); task.setPlanEnd(dto.getPlanEnd()); task.setTaskStatus(0); // 0待盘点 1盘点中 2已完成 inventoryTaskMapper.insertAssetInventoryTask(task); // 2. 按部门和分类圈定范围把资产当前信息抄进明细表 AssetInfo query new AssetInfo(); query.setDeptId(dto.getDeptId()); query.setCategoryId(dto.getCategoryId()); ListAssetInfo assets assetMapper.selectAssetList(query); for (AssetInfo a : assets) { AssetInventoryItem item new AssetInventoryItem(); item.setInventoryId(task.getInventoryId()); item.setAssetId(a.getAssetId()); item.setAssetCode(a.getAssetCode()); item.setAssetName(a.getAssetName()); item.setExpectStatus(a.getAssetStatus()); // 快照应有状态 item.setDiffFlag(0); inventoryItemMapper.insertAssetInventoryItem(item); } return task.getInventoryId(); }核销时比对actualStatus和expectStatus不一致就把 diffFlag 置 1再由管理员决定是改台账还是标记盘亏。注意明细里的 assetCode 是快照不要为了「保持同步」去关联主表刷新它快照失去了独立性盘点就没有意义了。5. 二次开发避坑五个让人对着日志发呆半天的坑5.1 启动报 Invalid bound statement (not found)现象Mapper 接口和 XML 都写了启动不报错一调接口就抛Invalid bound statement (not found)。原因新建模块的 XML 不在 MyBatis 的 mapperLocations 范围内或者接口和 XML 的 namespace 对不上。若依默认配置是classpath*:mapper/**/*.xml你如果把 XML 放在 resources/mapper/asset/ 下是能被扫到的但放到别的目录就得自己加。解决先确认 XML 在 resources/mapper 下面且打包后进了 target/classes再核对 namespace 是接口全限定名、id 是方法名。多模块项目还要确认模块被 admin 依赖了否则编译出来的 jar 里根本没有这个 XML。5.2 按钮点了 403接口单独测又是通的现象页面上的「报废」按钮点下去返回 403用 Postman 带 token 直接调接口却成功。原因前端v-hasPermi的权限标识和后端PreAuthorize的字符串不一致或者 sys_menu 里只配了菜单没配按钮角色上没勾这个按钮权限。前端指令只控制显隐真正拦截在后端。解决去 sys_menu 表查 perms 字段把三处字符串对齐菜单表的 perms、后端的 PreAuthorize、前端的 v-hasPermi然后在角色管理里重新勾选保存一次让权限缓存刷新。改完记得让用户重新登录权限是登录时进缓存的。5.3 列表能看到别的部门数据现象A 部门账号登录后列表里出现了 B 部门的资产。原因Service 方法上没加DataScope(deptAlias d)或者加了但 XML 里没写${params.dataScope}切面生成了片段却没地方拼。解决注解和 XML 必须成对出现而且别名要跟 SQL 里部门表的别名一致。如果查询是自定义 SQL 没走 BaseEntity 参数还要确认方法第一个参数是继承了 BaseEntity 的实体否则切面拿不到 params。5.4 定时任务报「任务不存在」或「找不到方法」现象定时任务保存成功执行时报NoSuchBeanDefinitionException或者MethodNotFound。原因调用目标字符串写的是assetDepreciationTask.run()但 Bean 注册名写成了assetDepreciationTaskJob或者方法签名不是run(String)。解决Component(xxx)里的名字、调用字符串前缀、ITask接口的run(String params)方法三处对齐。改完类名或者 Bean 名之后要去定时任务列表把旧任务删掉重新加Quartz 里存的是老的 JobDetail不会自动更新。5.5 折旧补跑一次净值少了一大截现象上个月的任务失败手工补跑了一次结果发现某批资产的累计折旧翻倍。原因明细表没有唯一索引或代码里没做期间查重补跑就是纯插入。解决加UNIQUE KEY uk_asset_period (asset_id, period)服务里循环前先countByPeriod。已经产生的脏数据要按资产和期间分组保留 id 最小的一条把多出来的金额从累计折旧里减回去这个修复脚本一定要在测试库跑通了再动生产。6. 二次开发把流转动作抽成策略再加一种处置方式不用改三处资产系统上线后最常见的需求不是新功能而是「再加一种处置方式」——捐赠、内部调拨、以旧换新每次都在 Service 里堆 if-else一个季度下来那个方法就没人敢动了。我的改法是把「动作」和「状态校验」拆开状态矩阵管能不能转策略管转的时候还要做什么附带操作比如报废要停折旧、调拨要改部门、领用要写使用人。public interface AssetFlowHandler { /** 对应的流转类型如 02 领用 */ String flowType(); /** 状态变更成功后执行的附加动作 */ void afterChange(AssetInfo asset, AssetFlowDto dto); } Service public class AssetFlowHandlerFactory { private final MapString, AssetFlowHandler handlers; // 构造注入Spring 会把所有实现类塞进 List public AssetFlowHandlerFactory(ListAssetFlowHandler list) { this.handlers list.stream() .collect(Collectors.toMap(AssetFlowHandler::flowType, h - h)); } public AssetFlowHandler get(String flowType) { AssetFlowHandler handler handlers.get(flowType); if (handler null) { throw new ServiceException(不支持的流转类型 flowType); } return handler; } }新增一种处置方式只需要新建一个实现类标上Service再在 AssetFlowRules 里加一条允许的流转原来那个 changeStatus 方法一行都不用碰。这种改法的验证也简单写一个SpringBootTest注入工厂断言get(08)能取到新处理器再跑一遍从在库到新状态的接口看 asset_flow 表是否落了正确记录。本地验证我习惯按这个顺序走一遍步骤操作观察点1新建资产并入库asset_info 一条asset_flow 一条2领用给某个用户状态变 1使用人正确3手工触发折旧任务连点两次第二次应该 0 条计提4建盘点任务并核销差异diff_flag 正确置 15报废后再尝试领用接口报「不允许从已报废变更」最后这一步最容易被忽略很多人只测正向流程结果报废的资产还能被领出去月底盘点又是一堆烂账。从那以后我每次加完新状态都会先把「已报废、已删除」这类终态的反向用例跑一遍再提测宁可多花十分钟也别让财务拿着报表来找我。希望帮到你。本文还有配套的精品资源点击获取