SpringBoot+Vue3 企业资产管理系统设计:建账·领用·调拨·盘点·折旧·处置全生命周期拆解
🌐 演示地址:https://ruoyioffice.com | 📦 源码1·GitHub:ruoyi-office | 📦 源码2·GitCode:ruoyi-office | 📦 源码3·Gitee:ruoyi-office | 💬 微信:17156169080(备注「RuoYi Office」)
固定资产管理是企业管理里"看着简单、做起来全是坑"的典型场景:一台笔记本电脑从采购入库、分配给员工、出故障送修、跨部门调拨、每月计提折旧,到最后报废处置,整个生命周期可能跨越 5 年、经手 6 张单据、状态变化十几次。如果只用一张 Excel 维护,结果往往是"账实不符、折旧靠手算、谁在用不知道"。RuoYi Office 的资产管理模块以"资产卡片(台账)"为唯一真相源,用入库、领用借用归还、调拨、维修、盘点、处置 6 类单据驱动资产状态流转,配合策略模式的折旧引擎与贯穿全程的变动日志,构建了一套真正可落地的固定资产全生命周期闭环。

▲ 全景图:以 asset_card 卡片台账为中心,分类/位置/配置定规则,6 类单据驱动 6 种状态流转,折旧引擎按月计提,asset_log 变动日志贯穿全程
引言:固定资产管理到底难在哪?
"不就是记一下公司有哪些东西吗?"——很多人对资产管理的第一印象。但真正实施后会发现,它的复杂度来自"一物多态、状态多变、账要算钱":
一个资产,多种身份:同一台设备,今天闲置在仓库,明天被领用给市场部,后天送修,下个月调拨到分公司,年底报废。它的"部门、使用人、位置、状态"全都在变,必须有一个权威记录来追踪当前状态,而不是散落在多个表格里。
状态不能手改,必须单据驱动:如果允许管理员随手把状态从"使用中"改成"闲置",账实必然失控。正确做法是——状态只能由单据(领用单、调拨单、维修单、处置单)的审批结果来改变,人工不直接动状态。
折旧要算钱,还要每月算:固定资产的净值随时间递减,财务每月要计提折旧。手算容易错,且不同资产折旧方法(平均年限法、双倍余额递减法)不同。必须有一个可配置、可扩展、可重算的折旧引擎。
盘点要对账:每年至少一次资产盘点,逐台核对"账面位置/状态"与"实物位置/状态"是否一致,统计盘盈、盘亏、异常。盘点不能影响日常业务,需要冻结一份"账面快照"逐条核对。
每一次变动都要留痕:资产是企业资产负债表的一部分,审计要求每次状态变化、归属变化都可追溯——谁在什么时间因为哪张单据,把资产从什么状态改成了什么状态。
本文以 RuoYi Office 的 yudao-module-asset 模块为例,基于真实源码,完整拆解资产卡片建账、单据驱动状态、折旧引擎、盘点对账、变动日志的设计与实现。
一、业务设计:以"资产卡片"为中心的全生命周期模型
1.1 核心抽象:资产卡片(台账)是唯一真相源
结论先行:整个资产模块只有一个"主数据"——资产卡片 asset_card,其余所有单据都是"改变卡片状态/归属"的事件。 资产卡片即一物一卡的电子台账,记录某项固定资产的全部静态信息(编号、名称、规格、品牌)和动态信息(当前状态、使用人、所在部门、位置、累计折旧、净值)。
这样设计的好处是:无论资产经历多少次领用、调拨、维修,"它现在归谁、在哪、值多少钱"永远只查一张表。单据只负责"发起变更",卡片负责"承载当前真相"。
1.2 六种资产状态与驱动单据
资产状态用 AssetStatusEnum 定义了 6 个值,每种状态由特定单据的审批结果驱动:
| 状态 | 枚举值 | 进入方式 | 退出方式 |
|---|---|---|---|
| 闲置 | IDLE(1) | 建账默认 / 归还 / 维修完工 | 领用、借用、调拨、维修、处置 |
| 使用中 | IN_USE(2) | 领用单 / 借用单审批通过 | 归还、维修、处置 |
| 维修中 | REPAIRING(3) | 维修单审批通过 | 维修完工还原 |
| 调拨中 | TRANSFERRING(4) | 调拨流转中 | 调拨完成 |
| 报废中 | DISPOSING(5) | 处置单审批通过 | 处置完成 |
| 已报废 | DISPOSED(6) | 处置完成(completeDispose) | 无(终态) |
关键设计:状态机的逻辑不集中在卡片 Service,而是分散在各单据 Service 的流程回调里。领用/借用由 AssetAllocateServiceImpl 把卡片置为"使用中",维修由 AssetRepairServiceImpl 置为"维修中"并在完工时还原,处置由 AssetDisposeServiceImpl 置为"报废中→已报废"。卡片自己不做状态机,只被动接受变更。
1.3 资产从哪里来:建账的两条入口
资产卡片的诞生有两条路径,都最终落到 asset_card:
路径 A:手工建账(盘盈、历史资产录入)
asset/card 新增 → createCard() → 状态默认闲置 → 写入库日志
路径 B:入库单审批通过自动建卡(采购入库主流程)
入库单 asset_receive(含明细 asset_receive_item)
→ BPM 审批通过 → onProcessApproved()
→ 按明细数量逐台拆分生成 asset_card
→ 记录 sourceBillType=asset_receive + sourceBillId(溯源)TODO 文档原写"采购入库单",实际源码中入库单的表名是
asset_receive(资产入库单),审批通过后由AssetReceiveServiceImpl.onProcessApproved按明细行的数量批量拆分建卡——一行明细"笔记本电脑 × 5"会生成 5 张独立的资产卡片,每张一个唯一编号。这才是真实的"建账入口"。
二、系统设计:模块组成与核心决策
2.1 模块组成
资产管理模块由"维度配置 + 卡片台账 + 6 类单据 + 折旧 + 统计"几部分组成:
| 子模块 | 数据表 | 功能 | 是否走 BPM 审批 |
|---|---|---|---|
| 资产分类 | asset_category | 树形分类,定义折旧方法/年限/残值率默认值 | 否 |
| 存放位置 | asset_location | 树形位置,资产存放地点 | 否 |
| 模块配置 | asset_config | 编号规则、折旧默认、提醒天数 | 否 |
| 资产卡片 | asset_card | 一物一卡电子台账(核心主数据) | 否 |
| 入库单 | asset_receive(+item) | 采购入库,审批通过自动建卡 | 是(701) |
| 领用/借用/归还 | asset_allocate(+item) | 领用占用、借用挂归还、归还释放 | 是(702/703) |
| 调拨单 | asset_transfer(+item) | 跨部门/公司/位置转移 | 是(704) |
| 维修单 | asset_repair | 故障送修,完工还原状态 | 是(705) |
| 盘点单 | asset_stocktake(+item) | 逐条对账,统计盘盈盘亏 | 否 |
| 处置单 | asset_dispose(+item) | 报废/处置,审批+完成两段 | 是(706) |
| 折旧记录 | asset_depreciation_record | 按月计提折旧记录 | 否 |
| 变动日志 | asset_log | 全程留痕,状态/归属变化 | 否 |
2.2 核心设计决策
| 决策点 | 方案 | 理由 |
|---|---|---|
| 真相源 | 卡片台账 asset_card 唯一权威 | "现在归谁、在哪、值多少"只查一张表 |
| 状态变更 | 单据审批驱动,卡片不做状态机 | 杜绝人工随手改状态导致账实不符 |
| 单据接入流程 | 6 类单据实现 FlowBillService 回调 | 统一对接 BPM,新增单据零成本 |
| 折旧计算 | 策略模式 DepreciationCalculator | 多种折旧方法可插拔,按月计提可重算 |
| 资产编号 | 前缀+日期+Redis自增序号 | 分布式唯一、规则可配置 |
| 来源溯源 | 卡片记 sourceBillType + sourceBillId | 可追溯由哪张入库单生成,幂等防重 |
| 全程留痕 | asset_log 记 before/afterStatus | 满足审计追溯要求 |
三、PC 端功能实现
3.1 资产台账卡片列表
资产台账是模块的入口页面,采用 VxeGrid 表格 + 多条件搜索 的标准布局,状态列按字典渲染为彩色标签。

▲ 资产台账列表:资产状态以彩色标签区分(闲置=灰、使用中=绿、维修中=橙、调拨中=蓝、报废中=红),原值/净值随折旧动态变化,支持新增、查看、编辑、删除
列表设计要点:
- 状态彩色标签:前端用
statusMap把 6 个状态值映射成颜色标签——使用中绿色、维修中橙色、调拨中蓝色、报废中红色,一眼分辨资产当前态。 - 详情路由跳转:点击操作列跳转到
/asset/card-info,通过query.type(create/edit/view)区分新增、编辑、只读三种模式,复用同一个详情页。 - 财务字段联动:原值、累计折旧、净值、残值在列表直接展示,净值由折旧引擎每月回写,无需手算。
3.2 资产盘点单列表
盘点是"账实对账"的核心场景。盘点单 asset_stocktake 冻结一份应盘资产的账面快照,盘点员逐条核对实物位置/状态,系统自动统计盘盈、盘亏、异常。

▲ 资产盘点单列表:"一季度资产盘点计划"覆盖全部资产,状态为"待执行",应盘 10 台、已盘 0 台,点击"执行盘点"进入逐条核对界面
列表设计要点:
- 账面快照冻结:创建盘点单时把当前应盘资产的"账面位置、账面状态"写入盘点明细
asset_stocktake_item,作为逐条核对的基准,盘点过程不影响日常业务。 - 进度可视:列表直接展示"应盘数 / 已盘数",盘点进度一目了然,状态机为待盘点 → 盘点中 → 已完成 / 已取消。
- 结果自动判定:实盘录入后由
resolveCheckResult自动判定正常 / 盘盈 / 盘亏 / 异常,完成时汇总写回主表。
3.3 资产统计报表
统计报表是资产管理的"全局视图",从分类、部门、状态、折旧趋势四个维度对资产进行实时聚合,辅助资产配置决策与财务核算。

▲ 资产统计报表(分类统计):办公设备、笔记本电脑、服务器、网络设备等各类资产的数量与原值合计实时聚合,另有部门统计、状态统计、折旧趋势三个 Tab
统计设计要点:
- 多维聚合:提供分类统计、部门统计、状态统计、折旧趋势四个维度,金额按
originalValue原值合计,数量按卡片计数。 - 实时计算:统计直接基于
asset_card台账聚合,无需额外维护汇总表,账动即报表动。 - 决策支撑:通过"哪类资产最多、哪个部门占用最重、多少资产已报废"快速定位资产配置与处置重点。
四、后端核心实现
4.1 资产编号生成:前缀 + 日期 + Redis 自增
资产编号格式由 asset_config 配置(前缀、日期格式、序号长度均可改),序号用 Redis 原子自增保证分布式唯一。默认生成形如 ZC20260609-0001:
@Override
public String generateAssetCode() {
AssetConfigDO config = configService.getConfig();
String prefix = config.getCodePrefix() != null ? config.getCodePrefix() : "ZC";
String dateFormat = config.getCodeDateFormat() != null ? config.getCodeDateFormat() : "yyyyMMdd";
int seqLength = config.getCodeSeqLength() != null ? config.getCodeSeqLength() : 4;
String dateStr = LocalDate.now().format(DateTimeFormatter.ofPattern(dateFormat));
String seqKey = String.format(ASSET_CODE_SEQ_KEY, prefix, dateStr); // asset:code:seq:%s:%s
Long seq = stringRedisTemplate.opsForValue().increment(seqKey);
String seqStr = String.format("%0" + seqLength + "d", seq);
return prefix + dateStr + "-" + seqStr;
}手工建账时 createCard 调用本方法生成编号,并把状态默认置为"闲置",同时写一条"资产入库"变动日志。
4.2 入库单审批通过自动建卡
这是"建账"的主流程入口。入库单 asset_receive 通过 BPM 审批后,onProcessApproved 回调按明细数量逐台拆分生成资产卡片——一行"× 5"的明细会生成 5 张独立卡片,并把单价/原值平摊,记录来源溯源信息:
@Override
@Transactional(rollbackFor = Exception.class)
public void onProcessApproved(String businessKey) {
Long id = Long.parseLong(businessKey);
AssetReceiveDO receive = validateReceiveExists(id);
// 1. 幂等:同一入库单已建卡则直接返回,避免重复
Long existed = cardMapper.selectCount(new LambdaQueryWrapperX<AssetCardDO>()
.eq(AssetCardDO::getSourceBillType, SOURCE_BILL_TYPE)
.eq(AssetCardDO::getSourceBillId, id));
if (existed != null && existed > 0) {
return;
}
List<AssetReceiveItemDO> items = receiveItemMapper.selectListByReceiveId(id);
if (CollUtil.isEmpty(items)) {
return;
}
// 2. 按明细数量逐台拆分建卡,金额平摊到每台
List<AssetCardDO> cards = new ArrayList<>();
for (AssetReceiveItemDO item : items) {
int qty = item.getQuantity() == null || item.getQuantity() < 1 ? 1 : item.getQuantity();
BigDecimal lineAmount = item.getAmount() != null ? item.getAmount() : BigDecimal.ZERO;
BigDecimal perOriginal = qty > 0
? lineAmount.divide(BigDecimal.valueOf(qty), 2, RoundingMode.HALF_UP)
: lineAmount;
for (int i = 0; i < qty; i++) {
cards.add(buildCardFromReceiveLine(receive, item, perOriginal));
}
}
cardMapper.insertBatch(cards);
// 3. 每张卡片写一条"资产入库"变动日志
for (AssetCardDO card : cards) {
logService.createLog(card.getId(), card.getAssetCode(), 1, "资产入库",
"资产【" + card.getAssetName() + "】由入库单【" + receive.getBillCode() + "】生成", null, null);
}
}buildCardFromReceiveLine 把入库单的供应商、合同、公司、部门、附件等信息复制到卡片,并设置 sourceType=PURCHASE、sourceBillType=asset_receive、sourceBillId=入库单ID,从而实现"卡片可溯源到入库单"。若入库单含保修月数,还会自动推算保修到期日 warrantyExpireDate = receiveDate + warrantyMonths。
4.3 折旧计算:策略模式 + 平均年限法(核心代码)
折旧是资产模块最有技术含量的部分。 RuoYi Office 用策略模式把"折旧方法"抽象成 DepreciationCalculator 接口,目前落地了平均年限法(直线法)StraightLineCalculator,未来新增双倍余额递减法只需再写一个 Calculator,无需改动调度逻辑。
折旧调度方法 calculate(period) 按期次扫描应折旧资产,逐台计提并回写卡片的累计折旧与净值:
@Override
@Transactional(rollbackFor = Exception.class)
public int calculate(String period) {
validatePeriod(period); // 校验 yyyy-MM 格式
YearMonth ym = YearMonth.parse(period, PERIOD_FORMAT);
LocalDate periodEnd = ym.atEndOfMonth();
List<AssetCardDO> cards = cardMapper.selectListForDepreciation(periodEnd);
int created = 0;
List<AssetDepreciationRecordDO> batch = new ArrayList<>();
for (AssetCardDO card : cards) {
// 1. 幂等:本期已计提则跳过
if (depreciationRecordMapper.selectByAssetIdAndPeriod(card.getId(), period) != null) {
continue;
}
// 2. 按卡片折旧方法选择策略,计算本期折旧额
Integer method = card.getDepreciationMethod() != null ? card.getDepreciationMethod() : 1;
DepreciationCalculator calculator = resolveCalculator(method);
BigDecimal original = card.getOriginalValue() != null ? card.getOriginalValue() : BigDecimal.ZERO;
BigDecimal accumulatedBefore = card.getAccumulatedDepreciation() != null
? card.getAccumulatedDepreciation() : BigDecimal.ZERO;
BigDecimal amount = calculator.calculate(original, card.getResidualRate(),
card.getUsefulLife(), accumulatedBefore);
if (amount.compareTo(BigDecimal.ZERO) <= 0) {
continue; // 已折满则不再计提
}
// 3. 累计折旧↑、净值↓,回写卡片并生成折旧记录
BigDecimal accumulatedAfter = accumulatedBefore.add(amount).setScale(2, RoundingMode.HALF_UP);
BigDecimal netValue = original.subtract(accumulatedAfter).setScale(2, RoundingMode.HALF_UP);
batch.add(AssetDepreciationRecordDO.builder().assetId(card.getId()).assetCode(card.getAssetCode())
.depreciationPeriod(period).depreciationMethod(method).originalValue(original)
.accumulatedBefore(accumulatedBefore).depreciationAmount(amount)
.accumulatedAfter(accumulatedAfter).netValue(netValue).build());
AssetCardDO upd = new AssetCardDO();
upd.setId(card.getId());
upd.setAccumulatedDepreciation(accumulatedAfter);
upd.setNetValue(netValue);
cardMapper.updateById(upd);
created++;
}
if (!batch.isEmpty()) {
depreciationRecordMapper.insertBatch(batch);
}
return created;
}平均年限法的具体算法在 StraightLineCalculator 中——先算残值,再算可折旧总额,除以使用年限(单位是月)得到月折旧额,最后用"剩余可折旧额"兜底,确保不会折旧超过应折金额:
@Override
public BigDecimal calculate(BigDecimal originalValue, BigDecimal residualRate, Integer usefulLife,
BigDecimal accumulatedBefore) {
if (originalValue == null || usefulLife == null || usefulLife <= 0) {
return BigDecimal.ZERO;
}
BigDecimal rate = residualRate != null ? residualRate : BigDecimal.ZERO;
// 残值 = 原值 × 残值率%;可折旧额 = 原值 - 残值
BigDecimal residualValue = originalValue.multiply(rate)
.divide(BigDecimal.valueOf(100), 2, RoundingMode.HALF_UP);
BigDecimal depreciable = originalValue.subtract(residualValue).max(BigDecimal.ZERO);
// 月折旧额 = 可折旧额 / 使用年限(月)
BigDecimal monthly = depreciable.divide(BigDecimal.valueOf(usefulLife), 2, RoundingMode.HALF_UP);
// 剩余可折旧额兜底,避免最后一期折超
BigDecimal accumulated = accumulatedBefore != null ? accumulatedBefore : BigDecimal.ZERO;
BigDecimal remaining = depreciable.subtract(accumulated).max(BigDecimal.ZERO);
return monthly.min(remaining).max(BigDecimal.ZERO);
}策略解析 resolveCalculator 从 Spring 注入的 List<DepreciationCalculator> 中找到 supports(method) 为真的实现,找不到则抛出"折旧方法不支持"错误——这正是策略模式可插拔的体现。
4.4 盘点对账:账面快照 + 逐条核对
盘点是"账实对账"的过程。创建盘点单时,系统把当前应盘资产的"账面位置、账面状态"冻结成快照写入盘点明细 asset_stocktake_item:
for (AssetCardDO c : cards) {
AssetStocktakeItemDO row = AssetStocktakeItemDO.builder()
.stocktakeId(head.getId())
.assetId(c.getId())
.assetCode(c.getAssetCode())
.assetName(c.getAssetName())
.expectedLocationId(c.getLocationId()) // 账面位置快照
.expectedStatus(c.getAssetStatus()) // 账面状态快照
.build();
rows.add(row);
}
stocktakeItemMapper.insertBatch(rows);盘点员逐条录入"实盘位置、实盘状态"后,resolveCheckResult 自动判定盘点结果——位置和状态都匹配为"正常(1)",否则"异常(4)",实物缺失(实盘状态约定为 -1)则判"盘亏(3)":
private Integer resolveCheckResult(AssetStocktakeItemDO book, Long actualLocationId, Integer actualStatus) {
if (actualStatus != null && actualStatus == ACTUAL_STATUS_LOSS) {
return RESULT_LOSS; // 盘亏:无实物
}
if (actualLocationId == null || actualStatus == null) {
return null; // 信息不全,暂不判定
}
boolean locMatch = Objects.equals(book.getExpectedLocationId(), actualLocationId);
boolean stMatch = Objects.equals(book.getExpectedStatus(), actualStatus);
return (locMatch && stMatch) ? RESULT_NORMAL : RESULT_ABNORMAL;
}盘点单状态机为:待盘点(1) → 盘点中(2,首次录入触发) → 已完成(3) / 已取消(4)。完成时校验所有明细均已盘点,再汇总正常/盘盈/盘亏数量写回盘点主表。
4.5 单据驱动状态:FlowBillService 标准回调
6 类单据都实现了框架统一的 FlowBillService 回调接口,BPM 流程状态变化时自动触发业务逻辑。以维修单为例——审批通过把资产置为"维修中"并快照送修前状态,完工时还原:
@Override
@Transactional(rollbackFor = Exception.class)
public void onProcessApproved(String businessKey) {
Long id = Long.parseLong(businessKey);
AssetRepairDO row = validateRepairExists(id);
// 维修单 → 维修中
repairMapper.updateById(new AssetRepairDO().setId(id).setRepairStatus(REPAIR_STATUS_REPAIRING));
// 资产卡片 → 维修中
AssetCardDO cardUpd = new AssetCardDO();
cardUpd.setId(row.getAssetId());
cardUpd.setAssetStatus(AssetStatusEnum.REPAIRING.getStatus());
assetCardMapper.updateById(cardUpd);
}提交维修单时会先把卡片当前状态存入 beforeAssetStatus(送修前快照);维修完工 complete 时用这个快照还原资产状态——如果送修前是"使用中"就还原为"使用中",否则回到"闲置",避免"修完一律变闲置"导致使用人信息丢失。调拨单的回调 onProcessApproved 则更新卡片的部门、公司、位置归属;处置单审批通过置"报废中"、completeDispose 置"已报废"。
五、RuoYi Office 的创新设计
5.1 卡片台账作"真相源",单据只做"事件"
传统资产系统常把状态散落在多张单据里,查"这台设备现在归谁"要联多表。RuoYi Office 坚持单一真相源:当前归属、状态、净值永远以 asset_card 为准,单据只是"改变卡片"的事件流。这让查询极简、账实一致,也让盘点有了明确的"账面"对照基准。
5.2 折旧引擎的策略模式:方法可插拔
折旧方法用 DepreciationCalculator 接口抽象,调度层只认接口不认实现。当前内置平均年限法 StraightLineCalculator,DepreciationMethodEnum 已预留双倍余额递减法、年数总和法、工作量法四种枚举。新增一种方法只需写一个实现 supports() 与 calculate() 的 @Component,调度代码一行不改——这是开闭原则的标准落地。
5.3 入库建卡的"按数量拆分 + 幂等溯源"
一行"笔记本 × 5"的入库明细,审批通过后自动拆成 5 张独立卡片,金额平摊、各自生成唯一编号。同时每张卡片记录 sourceBillId 溯源到入库单,且入库回调先查"是否已建卡",保证流程重复回调不会重复建卡——这种幂等设计在 BPM 异步回调场景下至关重要。
5.4 维修的"送修前状态快照"
维修前先把资产当前状态存进 beforeAssetStatus,完工时按快照还原。这个细节解决了一个真实痛点:一台"使用中"的设备送修,修完应该回到"使用中"(仍归原使用人),而不是粗暴地变成"闲置"。快照机制让状态还原精准无损。
5.5 借用的归还闭环与逾期管理
领用是永久占用,借用则需要归还。借用审批通过后挂 returnStatus=待归还,归还动作校验"必须是借用单、必须已审批通过、未归还"后释放资产回到闲置并记实际归还日期。配合配置项 borrowOverdueNotify,可对超过 expectedReturnDate 的借用做逾期提醒。
5.6 变动日志贯穿全程
asset_log 用 beforeStatus/afterStatus 记录每一次状态迁移,relatedBillType/relatedBillId 关联触发变更的单据,logType 区分入库、变更等类型,并自动记录操作人。这让资产的每一次变动都可追溯,满足审计要求。
六、数据结构
6.1 表结构:asset_card(资产卡片台账主表,节选核心字段)
| 字段 | 类型 | 说明 |
|---|---|---|
id | bigint | 主键 |
asset_code | varchar | 资产编号(前缀+日期+流水,唯一) |
asset_name | varchar | 资产名称 |
category_id | bigint | 分类ID |
asset_status | int | 资产状态(1闲置 2使用中 3维修中 4调拨中 5报废中 6已报废) |
original_value | decimal | 原值 |
accumulated_depreciation | decimal | 累计折旧 |
net_value | decimal | 净值 |
residual_rate | decimal | 残值率(%) |
residual_value | decimal | 残值 |
depreciation_method | int | 折旧方法(1平均年限 2工作量 3双倍余额 4年数总和) |
useful_life | int | 使用年限(月) |
use_user_id / use_dept_id | bigint | 使用人 / 使用部门 |
location_id | bigint | 存放位置 |
source_type | int | 来源(1采购 2调入 3捐赠 4盘盈 5WMS转固 9其他) |
source_bill_type / source_bill_id | varchar/bigint | 来源单据类型 / ID(溯源) |
warranty_expire_date | date | 保修到期日 |
6.2 表结构:asset_depreciation_record(折旧记录)
| 字段 | 类型 | 说明 |
|---|---|---|
id | bigint | 主键 |
asset_id / asset_code | bigint/varchar | 资产ID / 编号 |
depreciation_period | varchar | 折旧期次(yyyy-MM) |
depreciation_method | int | 折旧方法 |
original_value | decimal | 原值 |
accumulated_before | decimal | 期初累计折旧 |
depreciation_amount | decimal | 本期折旧额 |
accumulated_after | decimal | 期末累计折旧 |
net_value | decimal | 期末净值 |
6.3 表结构:asset_allocate(领用/借用/归还单)
| 字段 | 类型 | 说明 |
|---|---|---|
id | bigint | 主键 |
bill_code | varchar | 单据编号 |
allocate_type | int | 类型(1领用 2借用 3归还) |
allocate_date | date | 操作日期 |
apply_user_id / apply_dept_id | bigint | 申请人 / 申请部门 |
expected_return_date | date | 预计归还日期(借用) |
actual_return_date | date | 实际归还日期 |
return_status | int | 归还状态(1待归还 2已归还 3逾期) |
process_instance_id / process_status | varchar/int | 流程实例 / 审批状态 |
6.4 表结构:asset_log(变动日志)
| 字段 | 类型 | 说明 |
|---|---|---|
id | bigint | 主键 |
asset_id / asset_code | bigint/varchar | 资产ID / 编号 |
log_type | int | 日志类型(1入库 10信息变更 等) |
log_title / log_content | varchar | 标题 / 内容 |
related_bill_type / related_bill_id | varchar/bigint | 关联单据类型 / ID |
before_status / after_status | int | 变更前 / 变更后状态 |
operator_id / operator_name | bigint/varchar | 操作人 |
6.5 设计要点
- 多租户:所有表继承
TenantBaseDO,自动隔离租户数据。 - 金额精度:原值、净值、折旧额统一用
BigDecimal并setScale(2, HALF_UP),避免浮点误差。 - 使用年限单位是月:
useful_life以月为单位,配合月折旧逻辑,因此 5 年设备填60。 - 附件 JSON 存储:单据的
file_urls、盘点照片photo_urls用JacksonTypeHandler以 JSON 数组存储。
七、技术亮点总结
| 设计要点 | 实现方式 | 价值 |
|---|---|---|
| 卡片台账真相源 | asset_card 承载当前状态/归属/净值 | 查"归谁/在哪/值多少"只查一张表 |
| 状态单据驱动 | 6 类单据 FlowBillService 回调改状态 | 杜绝人工改状态,账实一致 |
| 入库自动建卡 | onProcessApproved 按数量拆分 | 一行"×5"自动生成 5 张卡片 |
| 建卡幂等溯源 | sourceBillId + 已建卡校验 | BPM 重复回调不重复建卡 |
| 折旧策略模式 | DepreciationCalculator 接口 | 折旧方法可插拔,符合开闭原则 |
| 平均年限法 | (原值-残值)/年限(月) + 剩余兜底 | 月折旧精准,不折超应折额 |
| 折旧计提幂等 | 同资产同期次只计提一次 | 重复点击不重复扣减 |
| 盘点账面快照 | 创建时冻结 expectedLocation/Status | 账实对照有明确基准 |
| 维修状态快照 | beforeAssetStatus 完工还原 | "使用中"设备修完仍归原用户 |
| 借用归还闭环 | returnStatus + 归还释放 | 借用资产可追踪、可逾期提醒 |
| 资产编号 Redis 自增 | 前缀+日期+原子序号 | 分布式唯一,规则可配置 |
| 变动日志全程留痕 | asset_log before/afterStatus | 满足审计追溯 |
八、快速体验
在线演示:https://ruoyioffice.com/web/(账号 admin / 密码 admin123)
操作路径:资产管理 → 资产台账 / 入库管理 / 领用借用 / 调拨 / 维修 / 盘点 / 折旧 / 处置
推荐体验流程:
- 配置规则:进入"资产分类"维护折旧方法/年限/残值率,"模块配置"设置资产编号前缀与折旧默认值。
- 建账入口 A:进入"资产台账"点击新增,手工录入一台设备,观察自动生成的编号(如
ZC20260609-0001)与默认"闲置"状态。 - 建账入口 B:在"入库管理"新建入库单,录入"笔记本 × 5"明细并提交审批,审批通过后回到资产台账,观察自动生成的 5 张独立卡片。
- 领用占用:在"领用借用"发起领用单,选择闲置资产并提交审批,通过后该资产状态变为"使用中",使用人/部门回填。
- 调拨与维修:发起调拨单变更资产归属部门;发起维修单,观察状态变为"维修中",完工后还原。
- 月度折旧:进入"折旧管理",输入期次
2026-06一键计提,观察资产净值下降、生成折旧记录。 - 资产盘点:创建盘点单,逐条录入实盘位置/状态,完成后查看盘盈/盘亏/异常汇总。
- 报废处置:发起处置单审批,通过后状态"报废中",完成处置后变为"已报废"终态。
源码仓库:
| 平台 | 地址 |
|---|---|
| GitHub | https://github.com/yuqing2026/ruoyi-office |
| GitCode | https://gitcode.com/zhouzhongyan/ruoyi-office |
| Gitee | https://gitee.com/yqzy1688/ruoyi-office |
结语
固定资产管理的本质,是让"一物多态、状态多变、账要算钱"这三件难事各归其位。RuoYi Office 的答案是:用资产卡片台账做唯一真相源,用单据审批驱动状态变更而非人工硬改,用策略模式的折旧引擎把"算钱"标准化、可扩展、可重算,用变动日志把每一次变化留痕——最终把入库建账、领用借用、调拨、维修、盘点、折旧、处置串成一个真正闭环的全生命周期。
这套"主数据 + 事件单据 + 策略计算 + 全程留痕"的建模思路,不止适用于固定资产,也可以推广到设备台账、IT 资产、车辆管理、低值易耗品等任何"实体随时间和事件改变状态与价值"的管理场景。
如果你正在设计企业资产或设备管理系统,对"单据驱动状态机""折旧策略引擎""账实盘点对账"感兴趣,欢迎参考源码实现,也欢迎在评论区聊聊:你们公司的固定资产盘点,现在还在用 Excel 吗?
常见问题(FAQ)
RuoYi Office 的资产管理是开源免费的吗?
是。资产管理模块(yudao-module-asset)基于 RuoYi-Vue-Pro / Yudao 架构,后端 Spring Boot 3.5 + 前端 Vue3,开源可商用、无 license 限制,本地约 10 分钟即可启动体验。
资产折旧支持哪些方法?怎么计算?
折旧采用策略模式,DepreciationMethodEnum 定义了平均年限法、工作量法、双倍余额递减法、年数总和法四种枚举,当前内置实现平均年限法(直线法)。计算公式为:残值 = 原值 × 残值率%,月折旧额 = (原值 − 残值) / 使用年限(月),按期次 yyyy-MM 计提并回写累计折旧与净值,同一资产同期次幂等。
资产状态可以手动修改吗?
不建议、也不应该。资产的 6 种状态(闲置/使用中/维修中/调拨中/报废中/已报废)由领用、借用、调拨、维修、处置等单据的审批结果驱动,卡片本身不做状态机,从而保证账实一致、变更可追溯。
资产入库后台账是手动录的吗?
可以手动建账,但主流程是自动建卡:入库单(asset_receive)走 BPM 审批通过后,onProcessApproved 会按明细数量逐台拆分生成资产卡片,金额平摊、各自生成唯一编号,并溯源到入库单,且做了幂等防重处理。
资产管理支持流程审批和多租户吗?
支持。入库、领用、借用、调拨、维修、处置 6 类单据通过统一的 FlowBillService 回调接入 Flowable BPM 引擎,可在流程设计器自定义审批节点;所有数据表继承 TenantBaseDO,天然支持多租户数据隔离。
💡 想要体验 RuoYi Office 的强大功能?
🌐 在线演示:https://ruoyioffice.com/web/(账号 admin / admin123)
📦 源码仓库:GitHub | GitCode | Gitee
💬 技术咨询:添加微信 17156169080,备注「RuoYi Office」
⭐ 如果觉得不错,请给个 Star 支持一下!
