Skip to content

SpringBoot+Vue3 企业资产管理系统设计:建账·领用·调拨·盘点·折旧·处置全生命周期拆解

🌐 演示地址https://ruoyioffice.com | 📦 源码1·GitHubruoyi-office | 📦 源码2·GitCoderuoyi-office | 📦 源码3·Giteeruoyi-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 表格 + 多条件搜索 的标准布局,状态列按字典渲染为彩色标签。

资产台账卡片列表 - 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

java
@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 张独立卡片,并把单价/原值平摊,记录来源溯源信息:

java
@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=PURCHASEsourceBillType=asset_receivesourceBillId=入库单ID,从而实现"卡片可溯源到入库单"。若入库单含保修月数,还会自动推算保修到期日 warrantyExpireDate = receiveDate + warrantyMonths

4.3 折旧计算:策略模式 + 平均年限法(核心代码)

折旧是资产模块最有技术含量的部分。 RuoYi Office 用策略模式把"折旧方法"抽象成 DepreciationCalculator 接口,目前落地了平均年限法(直线法)StraightLineCalculator,未来新增双倍余额递减法只需再写一个 Calculator,无需改动调度逻辑。

折旧调度方法 calculate(period) 按期次扫描应折旧资产,逐台计提并回写卡片的累计折旧与净值:

java
@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 中——先算残值,再算可折旧总额,除以使用年限(单位是月)得到月折旧额,最后用"剩余可折旧额"兜底,确保不会折旧超过应折金额:

java
@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

java
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)":

java
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 流程状态变化时自动触发业务逻辑。以维修单为例——审批通过把资产置为"维修中"并快照送修前状态,完工时还原:

java
@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 接口抽象,调度层只认接口不认实现。当前内置平均年限法 StraightLineCalculatorDepreciationMethodEnum 已预留双倍余额递减法、年数总和法、工作量法四种枚举。新增一种方法只需写一个实现 supports()calculate()@Component,调度代码一行不改——这是开闭原则的标准落地。

5.3 入库建卡的"按数量拆分 + 幂等溯源"

一行"笔记本 × 5"的入库明细,审批通过后自动拆成 5 张独立卡片,金额平摊、各自生成唯一编号。同时每张卡片记录 sourceBillId 溯源到入库单,且入库回调先查"是否已建卡",保证流程重复回调不会重复建卡——这种幂等设计在 BPM 异步回调场景下至关重要。

5.4 维修的"送修前状态快照"

维修前先把资产当前状态存进 beforeAssetStatus,完工时按快照还原。这个细节解决了一个真实痛点:一台"使用中"的设备送修,修完应该回到"使用中"(仍归原使用人),而不是粗暴地变成"闲置"。快照机制让状态还原精准无损。

5.5 借用的归还闭环与逾期管理

领用是永久占用,借用则需要归还。借用审批通过后挂 returnStatus=待归还,归还动作校验"必须是借用单、必须已审批通过、未归还"后释放资产回到闲置并记实际归还日期。配合配置项 borrowOverdueNotify,可对超过 expectedReturnDate 的借用做逾期提醒。

5.6 变动日志贯穿全程

asset_logbeforeStatus/afterStatus 记录每一次状态迁移,relatedBillType/relatedBillId 关联触发变更的单据,logType 区分入库、变更等类型,并自动记录操作人。这让资产的每一次变动都可追溯,满足审计要求。


六、数据结构

6.1 表结构:asset_card(资产卡片台账主表,节选核心字段)

字段类型说明
idbigint主键
asset_codevarchar资产编号(前缀+日期+流水,唯一)
asset_namevarchar资产名称
category_idbigint分类ID
asset_statusint资产状态(1闲置 2使用中 3维修中 4调拨中 5报废中 6已报废)
original_valuedecimal原值
accumulated_depreciationdecimal累计折旧
net_valuedecimal净值
residual_ratedecimal残值率(%)
residual_valuedecimal残值
depreciation_methodint折旧方法(1平均年限 2工作量 3双倍余额 4年数总和)
useful_lifeint使用年限(月)
use_user_id / use_dept_idbigint使用人 / 使用部门
location_idbigint存放位置
source_typeint来源(1采购 2调入 3捐赠 4盘盈 5WMS转固 9其他)
source_bill_type / source_bill_idvarchar/bigint来源单据类型 / ID(溯源)
warranty_expire_datedate保修到期日

6.2 表结构:asset_depreciation_record(折旧记录)

字段类型说明
idbigint主键
asset_id / asset_codebigint/varchar资产ID / 编号
depreciation_periodvarchar折旧期次(yyyy-MM)
depreciation_methodint折旧方法
original_valuedecimal原值
accumulated_beforedecimal期初累计折旧
depreciation_amountdecimal本期折旧额
accumulated_afterdecimal期末累计折旧
net_valuedecimal期末净值

6.3 表结构:asset_allocate(领用/借用/归还单)

字段类型说明
idbigint主键
bill_codevarchar单据编号
allocate_typeint类型(1领用 2借用 3归还)
allocate_datedate操作日期
apply_user_id / apply_dept_idbigint申请人 / 申请部门
expected_return_datedate预计归还日期(借用)
actual_return_datedate实际归还日期
return_statusint归还状态(1待归还 2已归还 3逾期)
process_instance_id / process_statusvarchar/int流程实例 / 审批状态

6.4 表结构:asset_log(变动日志)

字段类型说明
idbigint主键
asset_id / asset_codebigint/varchar资产ID / 编号
log_typeint日志类型(1入库 10信息变更 等)
log_title / log_contentvarchar标题 / 内容
related_bill_type / related_bill_idvarchar/bigint关联单据类型 / ID
before_status / after_statusint变更前 / 变更后状态
operator_id / operator_namebigint/varchar操作人

6.5 设计要点

  • 多租户:所有表继承 TenantBaseDO,自动隔离租户数据。
  • 金额精度:原值、净值、折旧额统一用 BigDecimalsetScale(2, HALF_UP),避免浮点误差。
  • 使用年限单位是月useful_life 以月为单位,配合月折旧逻辑,因此 5 年设备填 60
  • 附件 JSON 存储:单据的 file_urls、盘点照片 photo_urlsJacksonTypeHandler 以 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

操作路径:资产管理 → 资产台账 / 入库管理 / 领用借用 / 调拨 / 维修 / 盘点 / 折旧 / 处置

推荐体验流程

  1. 配置规则:进入"资产分类"维护折旧方法/年限/残值率,"模块配置"设置资产编号前缀与折旧默认值。
  2. 建账入口 A:进入"资产台账"点击新增,手工录入一台设备,观察自动生成的编号(如 ZC20260609-0001)与默认"闲置"状态。
  3. 建账入口 B:在"入库管理"新建入库单,录入"笔记本 × 5"明细并提交审批,审批通过后回到资产台账,观察自动生成的 5 张独立卡片。
  4. 领用占用:在"领用借用"发起领用单,选择闲置资产并提交审批,通过后该资产状态变为"使用中",使用人/部门回填。
  5. 调拨与维修:发起调拨单变更资产归属部门;发起维修单,观察状态变为"维修中",完工后还原。
  6. 月度折旧:进入"折旧管理",输入期次 2026-06 一键计提,观察资产净值下降、生成折旧记录。
  7. 资产盘点:创建盘点单,逐条录入实盘位置/状态,完成后查看盘盈/盘亏/异常汇总。
  8. 报废处置:发起处置单审批,通过后状态"报废中",完成处置后变为"已报废"终态。

源码仓库

平台地址
GitHubhttps://github.com/yuqing2026/ruoyi-office
GitCodehttps://gitcode.com/zhouzhongyan/ruoyi-office
Giteehttps://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 支持一下!

联系我们

获取报价、演示和二开方案

微信咨询二维码

微信咨询

17156169080

添加时备注「RuoYi Office」

在线体验商业版