在企业资产管理领域,固定资产的租借流转是日常运营中频繁且关键的环节。传统依赖纸质单据或零散电子表格的管理方式,普遍存在信息更新滞后、流程不透明、状态追踪困难等问题,导致资产利用率低下,管理成本高昂。针对这一痛点,本系统采用经典的SSH(Struts2 + Spring + Hibernate)技术栈,构建了一套全流程、数字化的企业固定资产租借管理解决方案,旨在实现资产从入库到报废的全生命周期精细化管理。
系统采用典型的三层架构设计,实现了表现层、业务逻辑层和数据持久层的清晰分离。表现层由Struts2框架负责,通过其强大的拦截器机制和标签库,处理用户请求、参数绑定和视图渲染。业务逻辑层由Spring框架的IoC(控制反转)容器统一管理,所有Service组件以接口形式定义,并由具体实现类完成业务规则的封装,同时利用Spring的声明式事务管理确保数据操作的一致性。数据持久层则基于Hibernate ORM框架,通过对象-关系映射将Java实体类与数据库表关联,极大简化了数据库的CRUD(增删改查)操作,并提供了跨数据库的兼容性。这种分层架构确保了系统的高内聚、低耦合,为后续功能扩展和维护奠定了坚实基础。
数据库设计的核心考量
系统的数据模型围绕资产实体、租借流程和用户权限三大核心构建。数据库共设计了9张表,以下详细分析其中几个关键表的结构与设计亮点。
1. 设备信息表 (device)
设备信息表是整个系统的核心数据载体,记录了所有固定资产的静态属性和动态状态。其DDL定义如下:
CREATE TABLE `device` (
`device_id` int(11) NOT NULL AUTO_INCREMENT,
`device_name` varchar(100) NOT NULL COMMENT '设备名称',
`device_type` varchar(50) NOT NULL COMMENT '设备类型',
`manufacturer_id` int(11) NOT NULL COMMENT '制造商ID',
`model` varchar(50) DEFAULT NULL COMMENT '型号',
`serial_number` varchar(100) UNIQUE COMMENT '序列号',
`purchase_date` date NOT NULL COMMENT '购入日期',
`price` decimal(10,2) NOT NULL COMMENT '价格',
`status` enum('闲置','租借中','维修中','已报废') NOT NULL DEFAULT '闲置' COMMENT '设备状态',
`current_holder` int(11) DEFAULT NULL COMMENT '当前持有人(用户ID)',
`location` varchar(200) DEFAULT NULL COMMENT '存放位置',
`last_maintenance_date` date DEFAULT NULL COMMENT '上次保养日期',
`next_maintenance_date` date DEFAULT NULL COMMENT '下次保养日期',
`notes` text COMMENT '备注',
PRIMARY KEY (`device_id`),
KEY `fk_device_manufacturer` (`manufacturer_id`),
KEY `idx_status` (`status`),
KEY `idx_holder` (`current_holder`),
CONSTRAINT `fk_device_manufacturer` FOREIGN KEY (`manufacturer_id`) REFERENCES `manufacturer` (`id`),
CONSTRAINT `fk_device_user` FOREIGN KEY (`current_holder`) REFERENCES `user` (`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='设备信息表';
设计亮点分析:
- 状态机设计:
status字段使用ENUM类型严格限定设备可能处于的几种状态(闲置、租借中、维修中、已报废)。这种设计确保了状态数据的有效性和一致性,业务逻辑可以基于明确的状态进行流转控制。例如,只有当设备状态为“闲置”时,才能被申请租借。 - 唯一性约束与索引优化:
serial_number(序列号)字段设置了唯一约束,保证了每件资产在系统中的唯一标识,防止重复录入。同时,为status和current_holder等高频查询条件建立了索引(idx_status,idx_holder),显著提升了根据状态筛选设备或查询用户持有设备等操作的查询效率。 - 外键关联与数据完整性:通过外键约束关联制造商表(
manufacturer)和用户表(user),确保了关联数据的参照完整性。当删除一个制造商或用户时,数据库会阻止或级联操作,防止出现“幽灵”数据。
2. 租借记录表 (rental_record)
租借记录表是业务流程的核心,它完整记录了每一次租借活动的生命周期。
CREATE TABLE `rental_record` (
`record_id` int(11) NOT NULL AUTO_INCREMENT,
`device_id` int(11) NOT NULL COMMENT '设备ID',
`borrower_id` int(11) NOT NULL COMMENT '借用人ID',
`approver_id` int(11) DEFAULT NULL COMMENT '审批人ID',
`apply_time` datetime NOT NULL COMMENT '申请时间',
`planned_return_time` datetime NOT NULL COMMENT '计划归还时间',
`actual_return_time` datetime DEFAULT NULL COMMENT '实际归还时间',
`rental_status` enum('待审批','已批准','已拒绝','租借中','已完成','超期归还') NOT NULL DEFAULT '待审批' COMMENT '租借状态',
`approval_time` datetime DEFAULT NULL COMMENT '审批时间',
`approval_notes` text COMMENT '审批意见',
`rental_fee` decimal(8,2) DEFAULT NULL COMMENT '租借费用',
PRIMARY KEY (`record_id`),
KEY `fk_record_device` (`device_id`),
KEY `fk_record_borrower` (`borrower_id`),
KEY `fk_record_approver` (`approver_id`),
KEY `idx_apply_time` (`apply_time`),
CONSTRAINT `fk_record_approver` FOREIGN KEY (`approver_id`) REFERENCES `user` (`user_id`),
CONSTRAINT `fk_record_borrower` FOREIGN KEY (`borrower_id`) REFERENCES `user` (`user_id`),
CONSTRAINT `fk_record_device` FOREIGN KEY (`device_id`) REFERENCES `device` (`device_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='租借记录表';
设计亮点分析:
- 完备的时间戳记录:该表包含了
apply_time(申请时间)、approval_time(审批时间)、planned_return_time(计划归还时间)和actual_return_time(实际归还时间)。这些时间点清晰地勾勒出一次租借业务的完整时间线,为流程监控、效率分析和超期计费提供了精确的数据支撑。 - 灵活的状态追踪:
rental_status字段的ENUM类型定义了租借流程的各个节点状态。从“待审批”到“已完成”或“超期归还”,系统通过更新此状态来驱动流程前进,并与设备表的status字段联动(如审批通过后,设备状态同步更新为“租借中”)。 - 多角色关联:通过
borrower_id(借用人)和approver_id(审批人)两个外键,将租借行为与具体的用户关联起来,明确了责任主体,便于追溯和统计。
核心功能实现与代码解析
1. 资产信息管理与状态维护
资产信息管理是系统的基础。管理员可以通过界面进行设备的录入、查询、修改和状态更新。下图展示了设备管理的主界面,管理员可以清晰地查看所有设备的列表、状态和基本信息。

对应的后端实现中,DeviceService接口定义了核心的业务方法,由其实现类DeviceServiceImpl通过Spring注入的DeviceDAO完成数据操作。
Service层接口与实现:
// DeviceService.java
public interface DeviceService {
// 根据ID查找设备
Device findDeviceById(Integer deviceId);
// 分页查询设备列表,可带条件
Page<Device> findDevicesByCriteria(DeviceQueryCriteria criteria, Pageable pageable);
// 新增或更新设备信息
void saveOrUpdateDevice(Device device);
// 更新设备状态
void updateDeviceStatus(Integer deviceId, DeviceStatus newStatus);
// 根据类型统计设备数量
Map<String, Long> countDevicesByType();
}
// DeviceServiceImpl.java
@Service
@Transactional
public class DeviceServiceImpl implements DeviceService {
@Autowired
private DeviceDAO deviceDAO;
@Override
public Device findDeviceById(Integer deviceId) {
return deviceDAO.findById(deviceId);
}
@Override
@Transactional(readOnly = true)
public Page<Device> findDevicesByCriteria(DeviceQueryCriteria criteria, Pageable pageable) {
// 构建Hibernate查询,根据条件(如设备名称、状态、类型)过滤
DetachedCriteria dc = DetachedCriteria.forClass(Device.class);
if (StringUtils.isNotBlank(criteria.getDeviceName())) {
dc.add(Restrictions.like("deviceName", "%" + criteria.getDeviceName() + "%"));
}
if (criteria.getStatus() != null) {
dc.add(Restrictions.eq("status", criteria.getStatus()));
}
// ... 其他条件
return deviceDAO.findByCriteria(dc, pageable);
}
@Override
public void updateDeviceStatus(Integer deviceId, DeviceStatus newStatus) {
Device device = deviceDAO.findById(deviceId);
if (device != null) {
device.setStatus(newStatus);
deviceDAO.saveOrUpdate(device); // Hibernate的saveOrUpdate方法
}
}
}
Struts2 Action处理请求:
// DeviceAction.java
public class DeviceAction extends ActionSupport {
private Device device;
private Integer deviceId;
private List<Device> deviceList;
private DeviceService deviceService;
// 依赖注入
public void setDeviceService(DeviceService deviceService) {
this.deviceService = deviceService;
}
// 查询设备列表
public String list() {
DeviceQueryCriteria criteria = new DeviceQueryCriteria();
// ... 从请求参数组装查询条件
Pageable pageable = new PageRequest(0, 20); // 分页参数
Page<Device> page = deviceService.findDevicesByCriteria(criteria, pageable);
this.deviceList = page.getContent();
return SUCCESS;
}
// 更新设备状态
public String updateStatus() {
try {
DeviceStatus newStatus = DeviceStatus.valueOf(request.getParameter("status"));
deviceService.updateDeviceStatus(deviceId, newStatus);
addActionMessage("设备状态更新成功!");
} catch (Exception e) {
addActionError("状态更新失败: " + e.getMessage());
}
return SUCCESS;
}
// getters and setters ...
}
2. 租借申请与审批流程
租借流程是系统的核心业务。员工用户登录后,可以查看可租借的资产并发起申请。管理员则负责审批这些申请。下图分别展示了用户的申请页面和管理员的待审批列表。


租借业务涉及多个实体状态的变化,需要在高阶Service中实现事务性操作。
租借服务实现:
// RentalService.java
@Service
@Transactional
public class RentalService {
@Autowired
private RentalRecordDAO rentalRecordDAO;
@Autowired
private DeviceService deviceService;
@Autowired
private UserService userService;
/**
* 用户申请租借设备
*/
public RentalRecord applyForRental(Integer deviceId, Integer borrowerId, Date plannedReturnTime) throws BusinessException {
// 1. 检查设备是否存在且状态为闲置
Device device = deviceService.findDeviceById(deviceId);
if (device == null) {
throw new BusinessException("设备不存在");
}
if (device.getStatus() != DeviceStatus.IDLE) {
throw new BusinessException("该设备当前不可租借,状态为: " + device.getStatus());
}
// 2. 创建租借记录,状态为“待审批”
RentalRecord record = new RentalRecord();
record.setDevice(device);
record.setBorrower(userService.findUserById(borrowerId));
record.setApplyTime(new Date());
record.setPlannedReturnTime(plannedReturnTime);
record.setRentalStatus(RentalStatus.PENDING_APPROVAL);
rentalRecordDAO.save(record);
return record;
}
/**
* 管理员审批租借申请
*/
public void approveRental(Integer recordId, Integer approverId, String notes, boolean approved) {
RentalRecord record = rentalRecordDAO.findById(recordId);
if (record.getRentalStatus() != RentalStatus.PENDING_APPROVAL) {
throw new BusinessException("该申请已处理,无法重复审批");
}
record.setApprover(userService.findUserById(approverId));
record.setApprovalTime(new Date());
record.setApprovalNotes(notes);
if (approved) {
record.setRentalStatus(RentalStatus.APPROVED);
// 关键:同步更新设备状态为“租借中”
deviceService.updateDeviceStatus(record.getDevice().getDeviceId(), DeviceStatus.RENTED);
// 更新设备当前持有人
record.getDevice().setCurrentHolder(record.getBorrower());
} else {
record.setRentalStatus(RentalStatus.REJECTED);
}
rentalRecordDAO.saveOrUpdate(record);
}
}
上述approveRental方法使用了Spring的声明式事务管理(@Transactional)。如果在该方法执行过程中,更新设备状态或保存租借记录任何一步失败,整个事务将会回滚,确保数据的一致性。
3. 资产归还与状态更新
资产归还是租借流程的终点。系统需要处理正常归还和超期归还的情况,并可能涉及费用计算。下图展示了设备归还管理的界面。

归还功能的Service层实现:
// RentalService.java (续)
/**
* 处理设备归还
*/
public void processReturn(Integer recordId, String returnNotes) {
RentalRecord record = rentalRecordDAO.findById(recordId);
if (record.getRentalStatus() != RentalStatus.APPROVED && record.getRentalStatus() != RentalStatus.RENTING) {
throw new BusinessException("无效的租借记录状态,无法归还");
}
Date actualReturnTime = new Date();
record.setActualReturnTime(actualReturnTime);
record.setReturnNotes(returnNotes);
// 判断是否超期
if (actualReturnTime.after(record.getPlannedReturnTime())) {
record.setRentalStatus(RentalStatus.OVERDUE_RETURNED);
// 计算超期费用 (此处为简化逻辑)
long overdueDays = (actualReturnTime.getTime() - record.getPlannedReturnTime().getTime()) / (1000 * 60 * 60 * 24);
BigDecimal fee = calculateOverdueFee(overdueDays);
record.setRentalFee(fee);
} else {
record.setRentalStatus(RentalStatus.COMPLETED);
}
// 关键:将设备状态重置为“闲置”,并清空持有人
Device device = record.getDevice();
device.setStatus(DeviceStatus.IDLE);
device.setCurrentHolder(null);
deviceService.saveOrUpdateDevice(device); // 更新设备
rentalRecordDAO.saveOrUpdate(record); // 更新租借记录
}
private BigDecimal calculateOverdueFee(long days) {
// 简单的按日计费规则,实际中可能更复杂
return BigDecimal.valueOf(days).multiply(new BigDecimal("50.00"));
}
4. 数据统计与报表生成
系统还提供了数据统计功能,帮助管理人员宏观掌握资产使用情况。下图展示了租借统计的界面。

统计功能通常涉及复杂的数据库查询,使用Hibernate的HQL(Hibernate Query Language)或原生SQL可以高效实现。
统计查询示例:
// StatisticsService.java
@Service
@Transactional(readOnly = true) // 统计查询一般为只读事务
public class StatisticsService {
@Autowired
private SessionFactory sessionFactory;
/**
* 统计各部门的租借次数
*/
public Map<String, Long> getRentalCountByDepartment() {
String hql = "SELECT u.department, COUNT(r) " +
"FROM RentalRecord r JOIN r.borrower u " +
"WHERE r.rentalStatus IN (:completedStatus, :overdueStatus) " +
"GROUP BY u.department";
Query query = sessionFactory.getCurrentSession().createQuery(hql);
query.setParameterList("completedStatus", Arrays.asList(RentalStatus.COMPLETED, RentalStatus.OVERDUE_RETURNED));
List<Object[]> resultList = query.list();
Map<String, Long> statMap = new HashMap<>();
for (Object[] result : resultList) {
statMap.put((String) result[0], (Long) result[1]);
}
return statMap;
}
/**
* 使用原生SQL查询设备利用率(租借天数/总天数)
*/
public List<DeviceUtilizationDTO> getDeviceUtilizationRate(Date startDate, Date endDate) {
String sql = "SELECT d.device_id, d.device_name, " +
"SUM(DATEDIFF(LEAST(IFNULL(r.actual_return_time, CURDATE()), ?2), GREATEST(r.apply_time, ?1))) as rented_days, " +
"DATEDIFF(?2, ?1) as total_days " +
"FROM device d " +
"LEFT JOIN rental_record r ON d.device_id = r.device_id AND r.rental_status IN ('COMPLETED', 'OVERDUE_RETURNED') " +
"AND (r.apply_time <= ?2 AND IFNULL(r.actual_return_time, CURDATE()) >= ?1) " +
"WHERE d.status != '已报废' "