简介:这份固定资产管理系统源码是一套基于C#的完整企业级应用实例,主要面向.NET开发者、软件学习者及需要二次开发的技术人员。系统覆盖资产卡片管理、分类维护、条码打印等核心业务,可帮助用户理解从WinForms界面设计到ADO.NET数据库交互的全流程实现思路。资源包共191个文件,以39个cs源文件、33个resx界面资源、33个resources资源文件为主,配合25个dll依赖、3个exe可执行程序及SQL Server数据库备份(.bak)等,压缩后仅1.89MB,结构紧凑,便于按项目文件直接打开与研读。目前已有1319人学习下载。通过源码可掌握资产数据表的增删改查操作、条码图像生成与打印调用、用户权限控制及异常日志处理等关键编码方法,同时项目内的.sln与.csproj工程文件、配置文件也还原了原始开发环境,适合用来对照练习或作为毕业设计、课程项目的参考基础。
1. 固定资产管理系统源码:先把资产台账从Excel里救出来
做设备管理的人都有过这种经历:年底盘点,账上写着 43 台笔记本,仓库实际只有 39 台,还有 4 台在某个离职同事的工位上没登记。用 Excel 管固定资产,最大的问题不是不会用表格,而是表格只有一个人维护,领用人、存放地点、折旧状态全是文字描述,时间一长就变成一笔糊涂账。这份固定资产管理系统源码,解决的就是这个“账实不符”的问题——把资产卡片、领用归还、折旧计算和盘点报表全部放进数据库,谁在用什么、设备还剩多少价值,打开系统一眼就能看清。源码是 Java Web 方向的完整工程,SSM 那一套经典架构,适合正在做 Java 课程设计的学生,也适合小公司、学校科室想直接部署使用的运维或开发人员。
2. 从表结构到三层架构:这套源码的骨架是这么搭的
2.1 数据库五张核心表怎么设计
打开源码里的 SQL 脚本,第一件事先看表结构。固定资产管理系统说复杂也复杂,说简单也简单——核心就五张表:用户表、资产分类表、资产信息表、领用记录表、折旧记录表。我拆过不少这类课程设计源码,表少的三张,多的十几张,这套取了个中间值,刚好覆盖完整业务流,又不会让新手看得头晕。
资产信息表(asset)是最核心的一张,字段设计有讲究。资产编号用 varchar(32),存的是像“ZC-2024-001”这样的手工编号,而不是自增主键,因为固定资产编号在企业里通常有编码规则,不能交给数据库自动生成。资产名称、规格型号、单价、购置日期是必填项,还有一个 status 字段用来标记当前状态,0 在库、1 已领用、2 维修中、3 已报废。为什么单独拿出来说?因为很多新手写管理系统,喜欢用字符串直接存“在用”“闲置”,后来做统计报表的时候就傻眼了——中文字符串没法高效分组,写 SQL 时还要到处 LIKE,用数字字典反而简单。
领用记录表(asset_record)记录每一次资产的出入流水,外键关联资产 ID 和用户 ID。这里有个容易忽略的细节:原始表设计里 record_type 字段用 1 表示领用、2 表示归还,而不是分别建两张表。这样后期做资产轨迹查询时,一条 SQL 就能把一个资产的完整生命周期拉出来,不用 JOIN 两次。
2.2 Mapper 层和 Service 层的边界
这三层架构的源码包名通常是 controller、service、mapper、entity 四个包。新手最容易困惑的是 Service 层到底该写什么。看这份源码里的资产领用接口,逻辑是这样拆的:Controller 只负责接参数和返回 JSON,Service 层做两件事——校验资产状态、写入领用记录并更新资产表状态。为什么校验逻辑不写在 Mapper 里?因为 Mapper 的原则是单表操作,跨表的业务规则必须上浮到 Service 层。比如领用时查一下资产当前是不是“在库”,这需要先按 ID 查资产表,再判断,然后做 UPDATE,两个 SQL 分属不同表,只能在 Service 层编排。
@Service public class AssetService { @Autowired private AssetMapper assetMapper; @Autowired private AssetRecordMapper recordMapper; @Transactional public Result borrowAsset(Integer assetId, Integer userId) { // 1. 查询资产当前状态 Asset asset = assetMapper.selectById(assetId); if (asset == null) { return Result.error("资产不存在"); } if (asset.getStatus() != 0) { return Result.error("资产当前不可领用,状态:" + asset.getStatus()); } // 2. 写入领用记录 AssetRecord record = new AssetRecord(); record.setAssetId(assetId); record.setUserId(userId); record.setRecordType(1); record.setCreateTime(new Date()); recordMapper.insert(record); // 3. 更新资产状态为已领用 asset.setStatus(1); assetMapper.updateById(asset); return Result.success(); } }这段代码里最关键的是@Transactional注解。两步写操作——插入领用记录和更新资产状态——必须放在同一个事务里,否则会出现“记录有了但状态没改”的数据不一致情况。参数说明:assetId是资产的唯一 ID,userId是领用人的用户 ID,传参用包装类 Integer 而不是基本类型 int,避免前端传空时自动拆箱抛 NPE。Result是统一返回体,里面包含 code、msg、data 三个字段,前端拿到 code 为 200 才认为请求成功。
3. 资产登记与领用归还:把日常工作流走通
3.1 资产卡片登记接口
资产登记是系统最基础的功能,对应的前端页面是资产添加表单,后端接口是POST /asset/add。表单字段有资产编号、名称、分类 ID、单价、购置日期、存放地点、使用部门、备注。这里有一个容易写歪的地方:分类 ID 前端传过来是字符串,后端必须转成 Integer,否则 MyBatis 会在类型转换时报错。我在源码里看到 Controller 层的写法是这样的:
@PostMapping("/add") public Result addAsset(@RequestBody Asset asset) { if (StringUtils.isBlank(asset.getAssetNo())) { return Result.error("资产编号不能为空"); } // 校验资产编号是否重复 Asset exist = assetMapper.selectByAssetNo(asset.getAssetNo()); if (exist != null) { return Result.error("资产编号已存在"); } asset.setStatus(0); asset.setCreateTime(new Date()); assetMapper.insert(asset); return Result.success(); }关于资产编号去重,很多系统只在数据库建了唯一索引,后端不复用查。这样做前端体验不好——用户填完一整张表单,点提交才提示“编号重复”,又得重新改。我一般会在后端加一次selectByAssetNo预校验,双重保险。需要注意的是@RequestBody注解表示前端传的是 JSON 格式数据,如果前端用的是普通表单提交(application/x-www-form-urlencoded),这个注解会直接报 415 错误,对应的前端ajax请求必须设置contentType: 'application/json'。
3.2 领用与归还的状态机
资产状态流转是这类业务系统的命门。这套源码的状态机是:在库(0) → 已领用(1) → 在库(0),以及 在库(0) → 维修中(2) → 在库(0),已领用(1) → 已报废(3)。领用只有“在库”能跳到“已领用”,归还只有“已领用”能回到“在库”。可以让别的流转方式也跑通,比如直接把“维修中”的资产报废,但业务上说要先修、修不好再走报废审批,不能跳过过程。
实际操作中我把这段状态机写在一个独立的AssetStatusChecker类里,不在 Service 层里堆if:
public class AssetStatusChecker { private static final Map<Integer, List<Integer>> ALLOWED_TRANSITIONS = new HashMap<>(); static { ALLOWED_TRANSITIONS.put(0, Arrays.asList(1, 2)); // 在库可领用、可送修 ALLOWED_TRANSITIONS.put(1, Arrays.asList(0, 3)); // 已领用可归还、可报废 ALLOWED_TRANSITIONS.put(2, Arrays.asList(0)); // 维修中只能回到在库 } public static boolean canTransition(int currentStatus, int targetStatus) { List<Integer> allowed = ALLOWED_TRANSITIONS.get(currentStatus); return allowed != null && allowed.contains(targetStatus); } }为什么用Map+List而不是一堆if-else?状态一多,if-else就会嵌套成金字塔,每个状态跳转都散落在不同方法里。用这张表一查,规则的增删只需要改static块里的配置,代码结构更接近业务规则本身。参数说明:currentStatus是当前状态值,targetStatus是目标状态值,返回布尔值。每次领用、归还、报废操作前调用canTransition做前置判断,不通过就直接返回“当前状态不允许该操作”。
4. 折旧计算与Excel报表导出:月底算账不再靠手按计算器
4.1 平均年限法与双倍余额递减法的实现
固定资产管理里最让财务头疼的是折旧。这套源码内置了两种折旧算法:平均年限法和双倍余额递减法。平均年限法最简单——每月折旧额 = (原值 - 残值) / 使用月数。双倍余额递减法前期折旧快,是加速折旧的一种方式。源码里用策略模式实现,DepreciationStrategy接口有两个实现类,Service 层根据资产分类传入的策略类型动态选择:
public interface DepreciationStrategy { BigDecimal calculateMonthlyDepreciation(BigDecimal originalValue, BigDecimal residualValue, int usefulLifeMonths, int monthsUsed); }平均年限法的实现就很直白:
public class StraightLineStrategy implements DepreciationStrategy { @Override public BigDecimal calculateMonthlyDepreciation(BigDecimal originalValue, BigDecimal residualValue, int usefulLifeMonths, int monthsUsed) { if (usefulLifeMonths <= 0) { throw new IllegalArgumentException("使用月数必须大于0"); } // 月折旧额 = (原值 - 残值) / 预计使用月数 BigDecimal depreciableAmount = originalValue.subtract(residualValue); return depreciableAmount.divide(BigDecimal.valueOf(usefulLifeMonths), 2, RoundingMode.HALF_UP); } }RoundingMode.HALF_UP是四舍五入,保留两位小数。这里提醒一句:涉及金额计算的场景,double和float都不要用,精度会丢。源码里坚持用BigDecimal,在构造函数中传入字符串而不是浮点数更安全。折旧计算的结果会按月写入折旧记录表,方便年底汇总。如果单位资产量大,推荐用一个定时任务(比如每天凌晨跑一次),把所有资产的折旧批量算一遍,而不是用户点开报表时才实时算。源码里没有现成的定时任务,我一般会建议用 Spring 的@Scheduled注解补上,调用现成的折旧 Service 方法就行。
4.2 用POI导出资产盘点表
系统里的资产列表页面提供了“导出 Excel”按钮,实现方式是 Apache POI。这类功能在管理系统中几乎是标配,但新手容易在单元格格式上翻车。举例:资产单价是 BigDecimal,直接setCellValue(price)没问题,但如果不设置单元格格式,Excel 里显示成科学计数法就很闹心。修正方法是设置数据格式:
public void exportAssetList(List<Asset> assetList, HttpServletResponse response) throws IOException { XSSFWorkbook workbook = new XSSFWorkbook(); Sheet sheet = workbook.createSheet("资产盘点表"); String[] headers = {"资产编号", "资产名称", "单价", "购置日期", "状态"}; Row headerRow = sheet.createRow(0); for (int i = 0; i < headers.length; i++) { headerRow.createCell(i).setCellValue(headers[i]); } // 单元格数字格式:两位小数,避免科学计数法 CellStyle numberStyle = workbook.createCellStyle(); numberStyle.setDataFormat(workbook.createDataFormat().getFormat("0.00")); for (int i = 0; i < assetList.size(); i++) { Row row = sheet.createRow(i + 1); Asset asset = assetList.get(i); row.createCell(0).setCellValue(asset.getAssetNo()); row.createCell(1).setCellValue(asset.getAssetName()); Cell priceCell = row.createCell(2); priceCell.setCellValue(asset.getPrice().doubleValue()); priceCell.setCellStyle(numberStyle); row.createCell(3).setCellValue(asset.getPurchaseDate().toString()); row.createCell(4).setCellValue(asset.getStatus()); } response.setContentType("application/vnd.ms-excel"); response.setHeader("Content-Disposition", "attachment;filename=asset_list.xlsx"); workbook.write(response.getOutputStream()); }这个导出方法的编码陷阱在response.setHeader("Content-Disposition", ...)那里——文件名如果是中文,直接写在英文后面会乱码,必须用URLEncoder.encode转义后再拼接到 header 里。正确的写法是filename*=UTF-8''%E5%9B%BA%E5%AE%9A%E8%B5%84%E4%BA%A7.xlsx这种 RFC 5987 格式。另外 POI 的XSSFWorkbook对应 .xlsx 文件,HSSFWorkbook对应 .xls,数据量超过 65536 行时必须用前者,别在导大数据时用了老 API 导致 Excel 打不开。
5. 避坑指南:我在这套源码上踩过的四个坑
5.1 日期字段总是差8小时
现象:资产列表页面显示购置日期是 2024-05-01,数据库里存的也是 2024-05-01,但通过接口查出来返回给前端变成了 2024-04-30 16:00:00。
原因:JDBC 连接串里没带serverTimezone参数,MySQL 默认使用服务器时区,而本机是 CST(中国标准时间,UTC+8),驱动误判成了 UTC。Java 里new Date()拿到的是带时区的时间戳,存进 MySQL datetime 字段后,读出时被减去 8 小时。
解决:在 JDBC 连接串末尾加serverTimezone=Asia/Shanghai,或者改成serverTimezone=GMT%2B8。如果是 Docker 里跑的 MySQL,还要检查容器本身的时区,docker exec -it mysql容器 date看一下是不是 UTC,是的话用-v /etc/localtime:/etc/localtime:ro重新挂载宿主机时区。
5.2 MyBatis 查询结果全是 null
现象:资产列表接口能查到数据条数,但每条数据的字段全是 null,前端表格空白。
原因:数据库表字段是下划线命名(asset_no、asset_name),实体类是驼峰命名(assetNo、assetName),MyBatis 的resultType自动映射时,默认不开启驼峰转换,下划线字段名匹配不上驼峰属性。
解决:在application.yml或mybatis-config.xml里加一行配置map-underscore-to-camel-case: true。加了之后 MyBatis 会把asset_no自动映射到assetNo。如果用了resultMap,这条路不通用,必须手动写column="asset_no" property="assetNo"映射。
5.3 Layui 表格接口格式对不上
现象:前端用的是 Layui 的table.render,死活渲染不出数据,浏览器 F12 看到接口返回{"code":200,"msg":"success","data":[...]},前端表格就是显示“暂无数据”。
原因:Layui 表格对接口返回格式有强制约定——必须返回{"code":0,"msg":"","count":总条数,"data":[...]},且code必须是 0 而不是 200,额外必须提供count字段做分页。后端返回的code=200被 Layui 判定为接口异常。
解决:在后端写一个专门给 Layui 表格用的统一返回体,code写死为 0,加上count字段。这属于“前端框架强势绑定期望”的情况,后端要么改返回结构,要么改前端表格的parseData回调去适配,二选一,不要两边各改一半。
5.4 导出文件名中文乱码
现象:导出 Excel 没问题,但下载下来的文件名全是%E5%9B%BA%E5%AE%9A%E8%B5%84%E4%BA%A7.xlsx或者干脆变成一个陌生乱码字符串。
原因:Content-Disposition头里的文件名直接用了中文字符串,HTTP 协议头只支持 ASCII,中文必须百分号编码。不同浏览器对裸中文的容错不一致,Chrome 能解析,老版 Edge 就拉闸。
解决:在 Java 里用URLEncoder.encode("固定资产盘点表.xlsx", "UTF-8"),然后把空格替换成%20,最终拼进 header。更好的兼容做法是同时写filename(ASCII 兜底名)和filename*(RFC 5987 编码名)两个值,这样老浏览器和新浏览器都能识别。
6. 上线前的最后一道工序:部署与安全加固
6.1 用IDEA打包部署到Linux服务器
源码拿到手先在本地跑通,再部署到服务器。我习惯用 Maven 的package命令打包成 WAR 包(或 JAR 包),然后丢到 Tomcat 的 webapps 目录。打包前检查三件事:JDK 版本和 pom.xml 里maven.compiler.source一致;数据库连接配置改成服务器地址;logging.level把 MyBatis 的 SQL 日志打开,方便线上排查。
# 在项目根目录执行打包 mvn clean package -DskipTests # 查看打包产物 ls -lh target/*.war # 部署到 Tomcat cp target/asset-manage.war /usr/local/tomcat/webapps/启动后第一件事不是直接访问页面,而是看日志。catalina.out里如果出现Table 'asset_manage.asset' doesn't exist,说明 SQL 建表脚本没执行,不是程序问题。数据库初始化脚本一般在源码的sql/目录下,手动导入一次:
mysql -u root -p asset_manage < /usr/local/asset_manage.sql6.2 登录接口的安全加固细节
这套源码里的登录逻辑是账号密码比对,密码以 MD5 形式存库。说实话这种课程设计水平的加密放在生产环境是不够的,MD5 撞库成本太低。我接手类似项目后,一般会做三处加固。第一处是密码加盐——每个用户生成一个随机盐值,密码存储值为MD5(盐 + 密码),盐值单独存在用户表里,就算数据库泄漏,攻击者拿到的是加盐摘要,没法直接反查出明文。第二处是登录接口加验证码校验,防止脚本暴力尝试密码。第三处是登录成功后在 Session 里写入用户 ID 和角色,Controller 层通过拦截器校验未登录用户强制跳转登录页。
// 加盐密码校验示例 public boolean checkPassword(String inputPassword, User user) { String hashed = md5(user.getSalt() + inputPassword); return hashed.equals(user.getPassword()); }资源本身只要能跑通,就算完成了它的使命;上线到真实业务环境时,安全这块需要自己动手补上。另外部署完一定要验证一遍核心链路:走一遍“新增资产 → 领用 → 归还 → 查看折旧报表 → 导出 Excel”,五步全走通,这套系统才算真正落地。
我把这个验证流程养成了习惯,因为交付过太多套源码,真正常见的翻车点从来不在新增功能上,而在状态流转和导出这类边角处。从那以后我每次接手一套源码,都会先把这个全链路走一遍再谈部署,希望帮到你——拿到这套固定资产管理系统源码后,别急着改功能,先把基础流程跑通,再按你的业务加规则。
本文还有配套的精品资源,点击获取