固定资产这块,看起来是“流水线式”的业务,但实际上牵扯采购、入库、维修、盘点、报废、折旧和权限审计等多个环节。做得好能帮企业把资产“看得清、管得准、用得好”;做得不好就是账实不符、报表不靠谱、审计被揪出问题。下面我把从概念到落地、从架构到流程、再到关键代码都给你一套可直接参考的实现方案 —— 重点聚焦您列的模块:固定资产台账、固定资产申购、固定资产入库、固定资产报修、固定资产处置、固定资产盘点、盘点明细、基础设置。
本文主要内容
很多企业把固定资产当“捡着用”的东西:买了记账、用着不管、盘点靠人工。常见痛点:
收益是直观的:减少资产流失、降低重复采购、提高盘点效率、提升审计合规性、准确计算折旧从而提升财务透明度。对于中大型企业,固定资产系统能降低采购成本与维修成本的叠加收益很可观。
一句话:FAMS 是用来记录和管理企业所有长期使用、价值较高的资产,从采购到处置全流程数字化的系统。核心模块:
下面给一个简洁的三层架构图(可以用 mermaid 渲染或画成图):
graph LR
subgraph Client
WebApp[Web 前端]
Mobile[移动扫码/APP]
end
subgraph Backend
API[REST/GraphQL API]
Auth[鉴权服务(OAuth2/JWT)]
Worker[异步任务/定时任务(折旧、提醒)]
Integrator[ERP/财务集成模块]
end
subgraph DB
Postgres[(PostgreSQL)]
Redis[(Redis 缓存/队列)]
end
Client -->|HTTPS| API
Mobile -->|HTTPS| API
API --> Auth
API --> Postgres
API --> Redis
Worker --> Postgres
Worker --> Redis
API --> Integrator说明:
一个典型资产生命周期(简化):
flowchart TD
A[申购] --> B[审批]
B --> C[采购入库]
C --> D[验收并入固定资产台账]
D --> E[使用(领用/调拨)]
E --> F[报修/维修]
F --> E
E --> G[盘点]
G --> H{是否一致?}
H -- 是 --> E
H -- 否 --> I[调整台账 / 报差异]
E --> J[处置(报废/出售)]
J --> K[财务处理/更新台账]盘点通常是周期性(年中/年末/专项)或随机抽查。每一步都应有审批链与操作人审计记录。
作用:是系统的核心主表,记录资产基础信息及当前状态。建议字段:
要点:资产编码规则要统一(如:YYMM-分类-流水),同时保留条码或二维码字段用于扫码。
作用:由使用部门发起采购申请,包含资产明细、预算、审批流程(多级)。与采购模块或 ERP 对接,审批通过后生成采购单或推送到采购系统。
要点:支持模板、预算检查、审批意见记录。审批流可基于部门/金额/类别自定义。
作用:采购到货后验收并转入资产台账或待验收状态。入库流程包括:检验、打标(贴条码/二维码)、录入序列号/保修期、绑定保管人。
要点:移动端扫码验收效率高;入库要支持批量导入(CSV/Excel)并做幂等性检查(避免重复入库)。
作用:使用者发起报修工单,维修完成后记录维修成本、零件、更换记录以及维修人员信息。
要点:维修单应与资产关联,并写入资产历史(用于成本分析);支持外包维修单据上传与费用结算。
作用:不再使用时进入处置流程,含审批、财务处理(计提折旧/残值)、处置结果入账(如出售收入)。
要点:处置流程要与财务紧密对接,产生凭证。处置前要校验是否存在未完成维修/质押等状态。
作用:周期性核对账面与实物。盘点包含:制定盘点计划、分配盘点任务、移动端扫码盘点、生成盘点明细并对差异进行处理(调整台账或提交申诉)。
要点:
包括:资产类别、科目映射、部门/地点、折旧政策(年限、方法:直线法/加速折旧)、条码规则、审批模板、用户角色权限等。基础设置决定了系统行为,务必做好权限与版本管理。
下面是实战中容易忽视但决定系统稳定性的点:

下面给出可直接用的参考代码片段(Postgres + Node.js (Express + Sequelize))。这只是参考实现,部署前请做安全审核和适配。
-- 资产主表
CREATE TABLE asset (
id BIGSERIAL PRIMARY KEY,
asset_code VARCHAR(64) NOT NULL UNIQUE,
name VARCHAR(200) NOT NULL,
category_id INT NOT NULL,
model VARCHAR(100),
serial_no VARCHAR(100),
purchase_date DATE,
purchase_price NUMERIC(14,2),
current_value NUMERIC(14,2),
accumulated_depreciation NUMERIC(14,2) DEFAULT 0,
location_id INT,
custodian_user_id BIGINT,
status VARCHAR(32) DEFAULT 'IN_USE', -- IN_USE, MAINTENANCE, SCRAPPED, DISPOSED
version INT DEFAULT 0, -- 乐观锁
created_at TIMESTAMP DEFAULT now(),
updated_at TIMESTAMP DEFAULT now()
);
-- 资产历史(变更、维修、处置记录)
CREATE TABLE asset_history (
id BIGSERIAL PRIMARY KEY,
asset_id BIGINT NOT NULL REFERENCES asset(id),
type VARCHAR(50) NOT NULL, -- PURCHASE, MAINTENANCE, TRANSFER, DISPOSAL, INVENTORY
note TEXT,
cost NUMERIC(14,2),
operator_id BIGINT,
created_at TIMESTAMP DEFAULT now()
);
-- 盘点表
CREATE TABLE inventory_count (
id BIGSERIAL PRIMARY KEY,
name VARCHAR(200) NOT NULL,
start_at TIMESTAMP,
end_at TIMESTAMP,
status VARCHAR(20) DEFAULT 'PLANNED' -- PLANNED, IN_PROGRESS, COMPLETED
);
CREATE TABLE inventory_count_detail (
id BIGSERIAL PRIMARY KEY,
inventory_count_id BIGINT REFERENCES inventory_count(id),
asset_id BIGINT REFERENCES asset(id),
scanned BOOLEAN DEFAULT false,
scanned_by BIGINT,
scanned_at TIMESTAMP,
expected_location_id INT,
scanned_location_id INT,
note TEXT
);
建索引:
CREATE INDEX idx_asset_code ON asset(asset_code); CREATE INDEX idx_asset_status ON asset(status); CREATE INDEX idx_inventory_count_id ON inventory_count_detail(inventory_count_id);
// models/asset.js
module.exports = (sequelize, DataTypes) => {
const Asset = sequelize.define('Asset', {
id: { type: DataTypes.BIGINT, primaryKey: true, autoIncrement: true },
asset_code: { type: DataTypes.STRING, unique: true },
name: DataTypes.STRING,
category_id: DataTypes.INTEGER,
model: DataTypes.STRING,
serial_no: DataTypes.STRING,
purchase_date: DataTypes.DATEONLY,
purchase_price: DataTypes.DECIMAL(14,2),
current_value: DataTypes.DECIMAL(14,2),
accumulated_depreciation: { type: DataTypes.DECIMAL(14,2), defaultValue: 0 },
location_id: DataTypes.INTEGER,
custodian_user_id: DataTypes.BIGINT,
status: { type: DataTypes.STRING, defaultValue: 'IN_USE' },
version: { type: DataTypes.INTEGER, defaultValue: 0 }
}, {
tableName: 'asset',
timestamps: true,
updatedAt: 'updated_at',
createdAt: 'created_at',
version: true // Sequelize 会使用 version 字段做乐观锁
});
Asset.associate = (models) => {
Asset.hasMany(models.AssetHistory, { foreignKey: 'asset_id' });
};
return Asset;
};// routes/asset.js
const express = require('express');
const router = express.Router();
router.post('/inbound', async (req, res) => {
const { purchaseOrderId, items, operatorId } = req.body;
const t = await sequelize.transaction();
try {
// 幂等:检查 purchaseOrderId 是否已处理
const existing = await AssetHistory.findOne({ where: { note: `PO:${purchaseOrderId}`, type: 'PURCHASE' }, transaction: t });
if (existing) {
await t.rollback();
return res.status(409).json({ message: '采购单已入库' });
}
const createdAssets = [];
for (const it of items) {
const code = generateAssetCode(it.category_id);
const asset = await Asset.create({
asset_code: code,
name: it.name,
category_id: it.category_id,
model: it.model,
serial_no: it.serial_no,
purchase_date: it.purchase_date,
purchase_price: it.price,
current_value: it.price,
location_id: it.location_id,
custodian_user_id: it.custodian_user_id
}, { transaction: t });
await AssetHistory.create({
asset_id: asset.id,
type: 'PURCHASE',
note: `PO:${purchaseOrderId}`,
cost: it.price,
operator_id: operatorId
}, { transaction: t });
createdAssets.push(asset);
}
await t.commit();
res.json({ created: createdAssets.length, assets: createdAssets });
} catch (err) {
await t.rollback();
console.error(err);
res.status(500).json({ message: '入库失败', error: err.message });
}
});generateAssetCode 可简单实现为 AS-YYYYMM-category-流水,但需保证线程安全(可用数据库序列或 Redis 计数器)。
盘点时若发现差异,需要在事务中更新台账与写入 asset_history:
async function processInventoryResult(inventoryId, detailList, operatorId) {
const t = await sequelize.transaction();
try {
for (const d of detailList) {
const asset = await Asset.findByPk(d.asset_id, { transaction: t, lock: t.LOCK.UPDATE });
if (!asset) {
// 新发现的实物(账外资产)
const newAsset = await Asset.create({
asset_code: generateAssetCode(d.category_id || 0),
name: d.name || '未知',
category_id: d.category_id || null,
current_value: d.estimated_value || 0,
status: 'IN_USE'
}, { transaction: t });
await AssetHistory.create({ asset_id: newAsset.id, type: 'INVENTORY_FOUND', note: `Found in inventory ${inventoryId}`, operator_id: operatorId }, { transaction: t });
} else {
// 若位置或状态不一致,记录并更新
if (asset.location_id !== d.scanned_location_id) {
await AssetHistory.create({ asset_id: asset.id, type: 'INVENTORY_ADJUST', note: `Location mismatch. Expected ${asset.location_id}, found ${d.scanned_location_id}`, operator_id: operatorId }, { transaction: t });
asset.location_id = d.scanned_location_id;
await asset.save({ transaction: t });
}
// 标记扫描
await InventoryCountDetail.update({ scanned: true, scanned_by: operatorId, scanned_at: new Date() }, { where: { id: d.detail_id }, transaction: t });
}
}
await InventoryCount.update({ status: 'COMPLETED', end_at: new Date() }, { where: { id: inventoryId }, transaction: t });
await t.commit();
} catch (err) {
await t.rollback();
throw err;
}
}报废处置要生成审批流并在批准后写资产历史、更新状态,并把残值/处置价写入财务接口。示例:
// 审批通过后调用
await sequelize.transaction(async (t) => {
await AssetHistory.create({ asset_id, type: 'DISPOSAL', note: disposalNote, cost: disposalPrice, operator_id }, { transaction: t });
await Asset.update({ status: 'DISPOSED', current_value: 0 }, { where: { id: asset_id }, transaction: t });
// 推送会计凭证到财务系统(异步)
queuePush('finance.disposal', { asset_id, disposalPrice, operator_id });
});在这里我给大家推荐一个业务人员就能够直接上手的高性价比、零代码平台——简道云固定资产管理系统,简道云背靠国内BI龙头帆软,在数据处理、数据展示上的能力有绝对优势,数据分析支持高度自定义,任何分析需求都可以快速制作仪表盘,简道云固定资产管理系统提供全面的资产信息管理功能,用户可以实时跟踪资产的使用情况和位置,确保资产利用的最大化。
实施后可以用这些 KPI 来评估系统价值:
举例:某公司上线 FAMS 后一年内盘点差异率从 6% 降到 1.2%,重复采购成本下降 18%,折旧与财务对账工作量减少 40%。
资产编码建议采用规则化、结构化的方式,例如 AS-YYYYMM-分类-流水号。这样既可通过编码快速定位采购时间和资产类别,也便于分表和检索。条码/二维码是编码的载体,不是替代规则。通常做法是:系统生成唯一 asset_code,然后将该编码制成二维码或条码贴在设备上。盘点和验收时只需要扫码,就能把条码映射回 asset_code。如果以后要支持 RFID,也可以把 asset_code 存入标签的 UID 映射表。注意编码生成要保证幂等,最好用数据库序列或 Redis 原子计数来生成流水,避免并发冲突。
一个成熟的盘点实践是:先做风险评估并制定盘点计划,分区域和时间窗口执行,优先处理高价值/高风险资产。采用移动扫码工具可以显著缩短停机时间,支持夜间或非高峰期盘点,另外支持抽样盘点(对低价值或不常移动资产),并在盘点过程中对发现的问题采用“先记录后处理”的策略:立即记录盘差并标记责任人,后续通过事务性调整来修正台账,以免盘点过程中阻塞生产。盲盘和公开盘的混用能减少人为作假风险,并且将盘点与维护、巡检工作结合起来,减少重复工作。
折旧通常按会计准则(例如直线法)在系统中按期计算:每月或每年运行折旧任务,计算当期折旧额并写入资产累计折旧与当期凭证。系统应支持配置折旧政策(年限、残值率、起算日、折旧方法)。与财务系统的对接非常必要:折旧结果要推送给财务系统生成凭证,避免人工手动录入导致误差。对接方式可以用 API 或消息队列异步传输折旧分录,确保财务凭证与资产台账一致并保留相关凭证号以备审计。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。