ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

Java固定资产管理系统设计与实现:从数据模型到避坑指南

Java固定资产管理系统设计与实现:从数据模型到避坑指南 简介这份资源是面向高校计算机专业学生与Java初学者的一份固定资产管理系统课程设计文档围绕浏览器/服务器模式下的资产信息化管理需求展开帮助读者理解如何用Java技术栈替代传统人工管理方式。压缩包内共1个doc文件约2.77MB内容为完整的系统设计与实现说明涵盖摘要、绪论、项目背景与设计原则等章节可直接作为课程设计或毕业设计的参考模板。文档以JSP、Struts、Hibernate和Spring组合架构为主线按表现层、业务层、数据访问层三层结构组织并展开资产管理与用户管理两大模块涉及资产增删改查、报表打印、职工信息维护及操作员与管理员权限划分等具体功能点。目前已有214人学习下载适合需要梳理系统分层设计思路、了解SSH框架整合方式或撰写同类设计文档的读者参考借鉴。1. 固定资产管理系统到底在管什么从一台笔记本的领用到报废说起公司行政把一台笔记本发给新同事这台机器在财务账上是「固定资产」在 IT 台账里是「设备」在采购系统里是「订单行」。三个月后这位同事离职笔记本转给另一个人半年后键盘坏了送修一年后彻底报废。这中间每一次流转如果没有一套系统把「谁在用、在哪、值多少钱、什么时候该折旧、什么时候该报废」串起来最后一定是一笔糊涂账。Java固定资产管理系统设计与实现要解决的就是这条从采购入库到报废清理的完整链路核心是三件事资产台账、流转记录、折旧计算。它适合两类人一类是课程设计或毕设需要落地一个能跑起来的完整系统另一类是中小企业想用一套可控的 Java 后端把资产管起来。下面按「先想清楚数据模型再动手写代码最后处理那些一定会翻车的地方」的顺序讲。2. 先把资产台账的数据模型定死五张表撑起整个系统2.1 为什么资产、分类、部门、员工、流转记录要拆成五张表很多人一上来就设计一张大宽表把资产名称、使用人、部门、分类全塞进去。这样做的直接后果是资产转交一次就要改主表历史记录全丢分类改名要批量更新部门调整要全表扫描。正确做法是按职责拆开。核心实体有五个资产分类asset_category笔记本、台式机、打印机、办公家具。分类决定了默认折旧年限和残值率。部门department资产归属的组织单元支持树形结构总公司-分公司-部门。员工employee资产使用人关联部门。资产主表asset一条记录对应一件实物资产含资产编号、名称、分类、原值、购入日期、当前状态、当前使用人。流转记录asset_transfer每一次领用、转交、维修、报废都写一条只增不改。这样拆的好处是资产主表只存「当前状态」流转记录存「历史轨迹」两者通过 asset_id 关联。查当前归属看主表查历史看流转表互不干扰。2.2 建表 SQL 与关键字段说明下面这套建表语句是我在 MySQL 8 上实际用过的字段类型和索引都调过-- 资产分类表 CREATE TABLE asset_category ( id BIGINT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(64) NOT NULL COMMENT 分类名称, depreciable_years INT NOT NULL DEFAULT 3 COMMENT 折旧年限, residual_rate DECIMAL(5,4) NOT NULL DEFAULT 0.05 COMMENT 残值率, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_name (name) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT资产分类; -- 部门表parent_id 支持树形 CREATE TABLE department ( id BIGINT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(64) NOT NULL, parent_id BIGINT NOT NULL DEFAULT 0, KEY idx_parent (parent_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT部门; -- 员工表 CREATE TABLE employee ( id BIGINT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(32) NOT NULL, dept_id BIGINT NOT NULL, status TINYINT NOT NULL DEFAULT 1 COMMENT 1在职 0离职, KEY idx_dept (dept_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT员工; -- 资产主表 CREATE TABLE asset ( id BIGINT PRIMARY KEY AUTO_INCREMENT, asset_no VARCHAR(32) NOT NULL COMMENT 资产编号业务唯一, name VARCHAR(128) NOT NULL, category_id BIGINT NOT NULL, original_value DECIMAL(12,2) NOT NULL COMMENT 原值, purchase_date DATE NOT NULL, status TINYINT NOT NULL DEFAULT 0 COMMENT 0闲置 1在用 2维修 3报废, holder_id BIGINT NULL COMMENT 当前使用人, dept_id BIGINT NULL COMMENT 当前归属部门, version INT NOT NULL DEFAULT 0 COMMENT 乐观锁, UNIQUE KEY uk_asset_no (asset_no), KEY idx_status (status), KEY idx_holder (holder_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT资产主表; -- 流转记录表只增不改 CREATE TABLE asset_transfer ( id BIGINT PRIMARY KEY AUTO_INCREMENT, asset_id BIGINT NOT NULL, from_holder BIGINT NULL, to_holder BIGINT NULL, action TINYINT NOT NULL COMMENT 1领用 2转交 3维修 4报废, remark VARCHAR(255), operator VARCHAR(32) NOT NULL, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, KEY idx_asset (asset_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT资产流转记录;参数说明original_value用 DECIMAL(12,2) 而不是 FLOAT因为金额计算不能有浮点误差version字段是乐观锁防止两个人同时转交同一台设备asset_transfer表没有 update 语句只允许 insert这是审计的基本要求。逻辑说明资产主表的holder_id和dept_id是冗余字段目的是避免每次查「这台设备现在谁在用」都要去流转表里找最后一条。冗余的代价是转交时必须在一个事务里同时更新主表和插入流转记录。2.3 折旧计算放在哪一层折旧有两种做法一是每次查询时实时算二是每天定时任务把折旧额写进一张折旧明细表。实时算的公式是「月折旧额 原值 - 原值×残值率/折旧年限×12」累计折旧 月折旧额 × 已使用月数。实时算的优点是简单缺点是资产多了以后每次列表查询都要算而且历史月份的折旧额会随当前日期变化对不上账。我一般用定时任务方案每天凌晨跑一次把当月折旧额写入asset_depreciation表主表只存累计折旧。这样财务报表直接读表不用算。定时任务用 Spring 的Scheduled就够不需要引入 Quartz 这种重框架。Scheduled(cron 0 30 1 * * ?) // 每天凌晨1:30执行 public void monthlyDepreciation() { // 1. 查出所有状态为在用或闲置的资产 ListAsset assets assetMapper.selectDepreciable(); for (Asset asset : assets) { // 2. 已计提月数 当前月 - 购入月 int usedMonths calcUsedMonths(asset.getPurchaseDate()); int totalMonths asset.getCategory().getDepreciableYears() * 12; if (usedMonths totalMonths) continue; // 已提完 // 3. 月折旧额保留两位 BigDecimal monthly asset.getOriginalValue() .multiply(BigDecimal.ONE.subtract(asset.getCategory().getResidualRate())) .divide(BigDecimal.valueOf(totalMonths), 2, RoundingMode.HALF_UP); depreciationMapper.insertOrUpdate(asset.getId(), monthly); } }参数说明cron表达式0 30 1 * * ?表示每天 1:30避开业务高峰RoundingMode.HALF_UP保证金额四舍五入一致insertOrUpdate用ON DUPLICATE KEY UPDATE实现防止重复跑任务时插入多条。逻辑说明折旧任务必须幂等也就是同一天跑两次结果一样。做法是给asset_depreciation表加(asset_id, period)唯一索引重复插入时更新而不是新增。3. 用 Spring Boot MyBatis 把资产领用和转交跑通3.1 项目分层与依赖选择后端用 Spring Boot 2.7 MyBatis-Plus数据库 MySQL 8前端可以先用 Thymeleaf 或 Vue 都行这里只讲后端。分层是 controller → service → mapperDTO 和 Entity 分开。依赖里必须有的是spring-boot-starter-web、mybatis-plus-boot-starter、mysql-connector-java、lombok。如果要做登录鉴权加spring-boot-starter-security或简单的 JWT 拦截器。选 MyBatis-Plus 而不是 JPA 的理由资产查询条件多按分类、状态、部门、使用人组合筛选MyBatis 的动态 SQL 写起来更直接而且分页插件成熟。JPA 在这种多条件动态查询场景下容易写出 N1 问题。3.2 资产领用的完整代码链路领用是资产第一次分配到人涉及三件事校验资产是否闲置、更新主表状态和使用人、写流转记录。这三步必须在一个事务里。Service public class AssetService { Autowired private AssetMapper assetMapper; Autowired private AssetTransferMapper transferMapper; Transactional(rollbackFor Exception.class) public void assign(Long assetId, Long employeeId, Long deptId, String operator) { // 1. 查资产带乐观锁版本号 Asset asset assetMapper.selectById(assetId); if (asset null) { throw new BizException(资产不存在); } // 2. 只有闲置状态才能领用 if (asset.getStatus() ! AssetStatus.IDLE.getCode()) { throw new BizException(资产当前状态不可领用 asset.getStatus()); } // 3. 更新主表where 条件带 version防止并发 int rows assetMapper.updateForAssign(assetId, employeeId, deptId, asset.getVersion()); if (rows 0) { throw new BizException(资产已被他人操作请刷新重试); } // 4. 写流转记录 AssetTransfer record new AssetTransfer(); record.setAssetId(assetId); record.setFromHolder(null); record.setToHolder(employeeId); record.setAction(TransferAction.ASSIGN.getCode()); record.setOperator(operator); transferMapper.insert(record); } }对应的 Mapper XMLupdate idupdateForAssign UPDATE asset SET holder_id #{employeeId}, dept_id #{deptId}, status 1, version version 1 WHERE id #{assetId} AND version #{version} AND status 0 /update参数说明version是乐观锁WHERE version #{version}保证只有拿到最新版本的人才能更新成功status 0是双重保险防止状态在查询和更新之间被改掉。逻辑说明如果rows 0说明有人抢先改了这条记录事务回滚前端提示重试。这是处理并发领用的标准做法比悲观锁SELECT ... FOR UPDATE轻量适合资产这种低冲突场景。3.3 转交和报废的差异处理转交和领用的区别是转交的from_holder不为空且资产状态保持「在用」。报废则要把状态改成「报废」同时清空holder_id并且报废后不能再被领用。Transactional(rollbackFor Exception.class) public void transfer(Long assetId, Long toEmployeeId, Long toDeptId, String operator) { Asset asset assetMapper.selectById(assetId); if (asset.getStatus() ! AssetStatus.IN_USE.getCode()) { throw new BizException(只有在用资产可以转交); } Long fromHolder asset.getHolderId(); int rows assetMapper.updateForTransfer(assetId, toEmployeeId, toDeptId, asset.getVersion()); if (rows 0) throw new BizException(操作冲突请重试); AssetTransfer record new AssetTransfer(); record.setAssetId(assetId); record.setFromHolder(fromHolder); record.setToHolder(toEmployeeId); record.setAction(TransferAction.TRANSFER.getCode()); record.setOperator(operator); transferMapper.insert(record); }逻辑说明转交必须记录from_holder否则历史轨迹断了。报废时to_holder设为 nullaction设为 4主表状态改为 3。报废是不可逆操作建议在 service 层加一个「报废前检查是否有未完成的维修单」的校验。3.4 分页查询与多条件筛选资产列表页通常要支持按分类、状态、部门、关键字资产编号或名称组合查询。用 MyBatis-Plus 的LambdaQueryWrapper可以写得很干净public IPageAssetVO pageQuery(AssetQuery query, int page, int size) { LambdaQueryWrapperAsset wrapper new LambdaQueryWrapper(); wrapper.eq(query.getCategoryId() ! null, Asset::getCategoryId, query.getCategoryId()) .eq(query.getStatus() ! null, Asset::getStatus, query.getStatus()) .eq(query.getDeptId() ! null, Asset::getDeptId, query.getDeptId()) .and(StringUtils.hasText(query.getKeyword()), w - w.like(Asset::getAssetNo, query.getKeyword()) .or().like(Asset::getName, query.getKeyword())) .orderByDesc(Asset::getCreateTime); return assetMapper.selectPage(new Page(page, size), wrapper); }参数说明eq的第一个参数是条件布尔值为 false 时该条件不拼进 SQL这是 MyBatis-Plus 处理动态条件的惯用法and(...)包住 or 条件避免 or 破坏前面的 and 逻辑。逻辑说明关键字查询用like会导致全表扫描资产量超过十万时要在asset_no和name上建索引或者引入 Elasticsearch。中小企业资产量一般几千到几万MySQL 索引足够。4. 避坑与排查资产系统上线后最容易翻车的五个地方4.1 并发领用导致同一台设备分给两个人现象两个管理员同时给同一台笔记本做领用结果流转记录里出现两条领用记录主表使用人是后提交的那个但前一个管理员以为成功了。原因查询和更新之间没有锁两个请求都读到「闲置」状态都执行了更新。解决更新语句的WHERE里必须带version和status更新影响行数为 0 就抛异常回滚。这是乐观锁的标准用法成本低效果确定。4.2 折旧金额对不上财务账现象系统算的累计折旧和财务手工算的差几块钱。原因一是用了 FLOAT 存金额二是每月折旧额四舍五入的时机不一致三是购入当月是否计提折旧的规则没统一。解决金额一律用 DECIMAL月折旧额在写入折旧表时就定死后续不再重算购入当月是否计提要在需求阶段和财务确认通常做法是「当月购入下月计提」。4.3 资产编号重复或断号现象批量导入时出现重复资产编号或者编号跳号。原因用数据库自增 ID 当资产编号或者用「前缀日期随机数」生成并发时碰撞。解决资产编号用独立的序列表或者用「分类前缀 年月 4位流水号」流水号从 Redis 的INCR取保证原子性。数据库层面加唯一索引兜底。4.4 部门树形结构查询慢现象查某个部门及其所有子部门的资产SQL 写了递归数据量一大就慢。原因每次递归查数据库N 次查询。解决部门表加path字段存「/1/3/7/」这样的路径查子部门用WHERE path LIKE /1/3/%一次查询搞定。部门变动不频繁维护 path 的成本可以接受。4.5 报废资产还能被领用现象已经报废的资产在领用下拉框里还能选到。原因前端下拉框查询没过滤状态或者后端领用接口没校验状态。解决前端下拉框只查status 0的资产后端领用接口必须再校验一次状态不能信任前端。这是「前端校验是体验后端校验是安全」的典型场景。5. 把资产系统做成能长期维护的样子三个进阶习惯第一个习惯是给所有状态变更留痕。资产系统最怕的是「谁改的、什么时候改的、改成什么」查不到。除了asset_transfer表建议再加一张operation_log表记录所有增删改操作的操作人、IP、时间、变更前后值。这张表不用来展示只在出问题时查。实现上可以用 AOP 切面在 service 方法上加注解自动记录不用每个方法手写。第二个习惯是把折旧和报表做成可重算的。折旧任务如果某天跑失败了要能补跑。做法是折旧表按(asset_id, period)唯一补跑时覆盖写入。报表查询不要直接读主表的累计折旧字段而是从折旧明细表汇总这样任何一天的报表都能重新生成。第三个习惯是给资产编号加校验位。纯流水号容易输错加一位校验位比如模 11 校验可以在手工录入时立刻发现错误。这个技巧在设备标签打印场景特别有用扫码枪扫出来的编号如果校验不过说明标签贴错了。// 资产编号校验位计算前12位加权求和后取模 public static char calcCheckDigit(String base) { int[] weight {3, 7, 9, 11, 13, 17, 19, 23, 29, 31, 37, 41}; int sum 0; for (int i 0; i base.length(); i) { sum (base.charAt(i) - 0) * weight[i]; } int mod sum % 11; return mod 10 ? X : (char) (0 mod); }参数说明weight数组是质数序列目的是让不同位置的数字对校验结果影响不同模 11 是常见选择因为 11 是质数分布均匀。逻辑说明生成资产编号时前 12 位是业务码第 13 位是校验位。录入时重新算一遍校验位不一致就提示编号错误。这个做法在 ISBN、身份证号里都有成本极低收益明显。我自己做这类系统最大的教训是一开始总想把功能做全结果数据模型没定好后面每加一个功能就要改表。后来学乖了先把资产、分类、部门、员工、流转这五个实体的关系画清楚再动手写代码返工少很多。希望帮到你。本文还有配套的精品资源点击获取
返回列表