一、概述
后端实现了物资预约借用平台后端的全部基本功能和大部分拓展功能,包括使用统一认证账号登录、管理员端的物品管理、用户管理,用户端的预约、借用、归还、评论等。
二、开发历程
1. 初始化
项目框架参照了学线纳新系统,Spring Boot 项目整合 Spring Data JPA,使用 MySQL 和 Redis 作为数据库,图床采用阿里云 OSS。
2. 设计数据库架构
设计数据库架构是这个项目面临的第一个挑战。
分析项目需求,可得到必要的实体类及对应的数据库表有:房间、用户、物品、(预约或借用)记录、评论、(用户和房间管理)私信。根据实际需要,我们设计了每个表中的字段。
项目的核心功能之一是预约和借用,我们以预约和借用记录为例,分析数据库的部分架构。
预约借用操作的主体是用户,客体是物品,操作是在房间内进行的,操作记录存储在 record 表中,表中需包含预约时间、借用时间、是否已归还,归还证明图片等字段。record 和 item, room ,user 表都是一对多关联。这样的架构,使得对指定物品、房间、用户的所有记录查询都很方便。
另一个在设计时颇费苦心的,是用户和房间的多对多关系。一个房间都很多用户,一个用户可以加入多个房间,并且用户在房间中承担着不同的职能(一级管理员、二级管理员、普通用户),且用户在房间中有着违约次数统计、是否被禁用状态。为实现用户和房间之间的复杂联系,我们采取在中间表中增加字段的方式。
这样,用户在房间中的状态就能在中间表中清晰表示。
值得一提的是,room_user 表采用联合主键设计,将 room 表和 user 表的主键 作为联合主键,保证唯一性的同时也方便查询。
定好数据库的大体框架后,根据实际开发中的业务需求,我们对数据库架构又进行了修改和完善。
最终的数据库架构如图:
3. 代码编写
遇到的问题
有了数据库结构之后,在代码编写部分,我们遇到了一些问题。
在设计实体类时,为方便从一端直接访问多端,我们在一对多的父类中设计了一个容器类字段(Set 或 List)存储子对象的集合。但是子类中也含有映射父类对象的集合。这会导致两个问题:
- 生成对象时,hashCode 方法的无限循环调用
- 将对象转化为 JSON 返回给前端时,生成 JSON 时一层套一层,无限嵌套
第一个问题是因为 使用了 lombok 的 @Data 之后,类中的所有字段都会用来计算哈希值,这样不仅计算复杂,更严重的问题是 hashCode 方法的循环调用。为解决这个问题,我们使用 @EqualsAndHashCode 注解,并注明 onlyExplicitlyIncluded = true 。
以 Room 实体类的部分代码为例:
@Entity
@Table(name = "room", schema = "material_management_system")
@Data
@EqualsAndHashCode(onlyExplicitlyIncluded = true)
@AllArgsConstructor
@NoArgsConstructor
public class Room {
@EqualsAndHashCode.Include
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Id
@Column(name = "room_id")
private Integer roomId;
@EqualsAndHashCode.Include
@Basic
@Column(name = "name", unique = true)
@Size(max = 25)
private String name;
@Basic
@Column(name = "introduction")
@Size(max = 200)
private String introduction;
@OneToMany(mappedBy = "room", fetch = FetchType.LAZY, cascade = CascadeType.ALL)
private Set<RoomUserMapping> users = new HashSet<>();
这样,只有带 @EqualsAndHashCode.Include 注解的字段才会进行哈希值的计算,避免了循环调用的问题。
对于返回 JSON 无限嵌套的问题,解决方法是不直接向前端返回实体类的对象,而是将需要返回的字段包装在 map 里,再返回 map。比如获取指定房间内物品列表的方法中,并不将 item 直接放进 列表中,而是将指定字段放入。
public DataResponse getItemList(Integer roomId, String token) {
// 省略部分代码
List<Map<String, Object>> data = new ArrayList<>();
List<Item> items = itemRepository.findByRoomId(roomId);
for (Item item : items) {
Map<String, Object> map = new HashMap<>();
map.put("itemId", item.getItemId());
map.put("name", item.getName());
map.put("intro", item.getIntro());
map.put("category", item.getCategory());
map.put("img", item.getImg());
map.put("contributor", item.getContributor().getName());
map.put("addedTime", item.getIntro());
map.put("isBorrowed", item.getIsBorrowed());
map.put("isReserved", item.getIsReserved());
data.add(map);
}
return vo.data(data);
违约判定
在物资预约借用平台中,违约判定是很关键的功能。我们认为,用户逾期未还物品应当由系统自动判定。这个功能并不需要精确到小时的时效性,因此,我们的设计方案是,每天凌晨一点自动执行违约判定程序,只要有一次物品到期未归还,用户就会在该房间内被禁止借用,不能进行预约或借用操作。只有联系管理员将封禁状态解除,用户才能正常借用。
部署图床
(之后写)
三、功能展示
我们提供了41个接口,包含物资预约借用平台的几乎全部功能,并编写了规范的接口文档。
接口文档链接如下:
https://apifox.com/apidoc/shared-ca4f7f21-468c-4703-bfb2-c2b31b17561f?pwd=sduonline
四、反思与总结
回顾整个项目,我们有以下几点不足:
- 与其他部门沟通不充分,对接工作不完善
- 对项目合作流程了解不够(个人原因),前期项目进度推动不力
- 架构仍有优化空间
对我个人而言,我在这次寒假实训中熟悉了 Spring Boot 和 JPA 的使用,为 Java 课设和日后的合作项目积累了经验。
希望我的个人经历能为后人带来一些微不足道的启发。