feat(asset): 添加借用到期归还提醒功能
- 新增AmsBorrowOrderMapper查询到期借用单据接口 - 实现BorrowReturnReminderService借用到期提醒服务 - 添加SysNoticeReceiver通知接收人相关实体和接口 - 集成系统通知公告模块支持定向消息推送 - 完善通知查看权限控制和防重复提醒机制 - 编写单元测试验证提醒逻辑和数据完整性main
parent
b45c3ab2bd
commit
1c029399c7
@ -0,0 +1,134 @@
|
||||
package com.ruoyi.asset.service.impl;
|
||||
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.List;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.web.util.HtmlUtils;
|
||||
import com.ruoyi.asset.constant.BorrowOrderStatus;
|
||||
import com.ruoyi.asset.domain.AmsBorrowOrder;
|
||||
import com.ruoyi.asset.mapper.AmsBorrowOrderMapper;
|
||||
import com.ruoyi.asset.service.IBorrowReturnReminderService;
|
||||
import com.ruoyi.common.exception.ServiceException;
|
||||
import com.ruoyi.system.domain.SysNotice;
|
||||
import com.ruoyi.system.domain.SysNoticeReceiver;
|
||||
import com.ruoyi.system.service.ISysNoticeReceiverService;
|
||||
import com.ruoyi.system.service.ISysNoticeService;
|
||||
|
||||
/**
|
||||
* 借用到期归还提醒 服务层实现
|
||||
*
|
||||
* @author Yangk
|
||||
*/
|
||||
@Service("borrowReturnReminderService")
|
||||
public class BorrowReturnReminderServiceImpl implements IBorrowReturnReminderService
|
||||
{
|
||||
private static final String SOURCE_TYPE_BORROW_OVERDUE = "BORROW_OVERDUE";
|
||||
|
||||
private static final int NOTICE_TITLE_MAX_LENGTH = 50;
|
||||
|
||||
private static final String NOTICE_TYPE_NOTICE = "1";
|
||||
|
||||
private static final String NOTICE_STATUS_NORMAL = "0";
|
||||
|
||||
private static final String SYSTEM_USER = "system";
|
||||
|
||||
@Autowired
|
||||
private AmsBorrowOrderMapper amsBorrowOrderMapper;
|
||||
|
||||
@Autowired
|
||||
private ISysNoticeService noticeService;
|
||||
|
||||
@Autowired
|
||||
private ISysNoticeReceiverService noticeReceiverService;
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public int remindOverdueBorrowOrders()
|
||||
{
|
||||
List<AmsBorrowOrder> candidates = amsBorrowOrderMapper.selectOverdueBorrowReturnReminderCandidates(
|
||||
BorrowOrderStatus.BORROWING, BorrowOrderStatus.BORROWING);
|
||||
int createdCount = 0;
|
||||
for (AmsBorrowOrder order : candidates)
|
||||
{
|
||||
if (order.getOrderId() == null || order.getBorrowUserId() == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (noticeReceiverService.isSourceNotified(SOURCE_TYPE_BORROW_OVERDUE, order.getOrderId(),
|
||||
order.getBorrowUserId()))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
createdCount += createReminder(order);
|
||||
}
|
||||
return createdCount;
|
||||
}
|
||||
|
||||
private int createReminder(AmsBorrowOrder order)
|
||||
{
|
||||
SysNotice notice = new SysNotice();
|
||||
notice.setNoticeTitle(truncate("借用资产到期归还提醒:" + defaultString(order.getBorrowNo()),
|
||||
NOTICE_TITLE_MAX_LENGTH));
|
||||
notice.setNoticeType(NOTICE_TYPE_NOTICE);
|
||||
notice.setStatus(NOTICE_STATUS_NORMAL);
|
||||
notice.setCreateBy(SYSTEM_USER);
|
||||
notice.setNoticeContent(buildNoticeContent(order));
|
||||
if (noticeService.insertNotice(notice) != 1 || notice.getNoticeId() == null)
|
||||
{
|
||||
throw new ServiceException("借用到期提醒生成失败,通知公告写入异常");
|
||||
}
|
||||
|
||||
SysNoticeReceiver receiver = new SysNoticeReceiver();
|
||||
receiver.setNoticeId(notice.getNoticeId());
|
||||
receiver.setUserId(order.getBorrowUserId());
|
||||
receiver.setSourceType(SOURCE_TYPE_BORROW_OVERDUE);
|
||||
receiver.setSourceId(order.getOrderId());
|
||||
// 业务来源唯一键兜底防重,避免每天定时任务重复提醒同一借用单。
|
||||
if (noticeReceiverService.insertNoticeReceiver(receiver) != 1)
|
||||
{
|
||||
throw new ServiceException("借用到期提醒生成失败,接收人写入异常");
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
private String buildNoticeContent(AmsBorrowOrder order)
|
||||
{
|
||||
String borrowNo = escape(order.getBorrowNo());
|
||||
String borrowUserName = escape(order.getBorrowUserName());
|
||||
String expectedReturnDate = formatDate(order);
|
||||
return "<p>借用单号:" + borrowNo + "</p>"
|
||||
+ "<p>借用人:" + borrowUserName + "</p>"
|
||||
+ "<p>预计归还日期:" + expectedReturnDate + "</p>"
|
||||
+ "<p>该借用单已到预计归还日期,请及时发起归还申请或联系管理员处理。</p>";
|
||||
}
|
||||
|
||||
private String formatDate(AmsBorrowOrder order)
|
||||
{
|
||||
if (order.getExpectedReturnDate() == null)
|
||||
{
|
||||
return "";
|
||||
}
|
||||
return new SimpleDateFormat("yyyy-MM-dd").format(order.getExpectedReturnDate());
|
||||
}
|
||||
|
||||
private String escape(String value)
|
||||
{
|
||||
return HtmlUtils.htmlEscape(defaultString(value));
|
||||
}
|
||||
|
||||
private String defaultString(String value)
|
||||
{
|
||||
return value == null ? "" : value;
|
||||
}
|
||||
|
||||
private String truncate(String value, int maxLength)
|
||||
{
|
||||
if (value == null || value.length() <= maxLength)
|
||||
{
|
||||
return value;
|
||||
}
|
||||
return value.substring(0, maxLength);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,75 @@
|
||||
package com.ruoyi.asset.service.impl;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import com.ruoyi.system.domain.SysNoticeReceiver;
|
||||
import com.ruoyi.system.mapper.SysNoticeReceiverMapper;
|
||||
import com.ruoyi.system.service.impl.SysNoticeReceiverServiceImpl;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class SysNoticeReceiverServiceImplTest
|
||||
{
|
||||
@Mock
|
||||
private SysNoticeReceiverMapper noticeReceiverMapper;
|
||||
|
||||
@InjectMocks
|
||||
private SysNoticeReceiverServiceImpl service;
|
||||
|
||||
/** 没有接收人记录的通知仍是全员公告。 */
|
||||
@Test
|
||||
void canViewNoticeShouldAllowGlobalNotice()
|
||||
{
|
||||
when(noticeReceiverMapper.countReceiverByNoticeId(10L)).thenReturn(0);
|
||||
|
||||
assertTrue(service.canViewNotice(10L, 2L));
|
||||
}
|
||||
|
||||
/** 存在接收人记录时,只有命中的用户可见。 */
|
||||
@Test
|
||||
void canViewNoticeShouldAllowOnlyMatchedReceiver()
|
||||
{
|
||||
when(noticeReceiverMapper.countReceiverByNoticeId(10L)).thenReturn(1);
|
||||
when(noticeReceiverMapper.countReceiverByNoticeIdAndUserId(10L, 2L)).thenReturn(1);
|
||||
when(noticeReceiverMapper.countReceiverByNoticeIdAndUserId(10L, 3L)).thenReturn(0);
|
||||
|
||||
assertTrue(service.canViewNotice(10L, 2L));
|
||||
assertFalse(service.canViewNotice(10L, 3L));
|
||||
}
|
||||
|
||||
/** 业务来源唯一键用于判断同一借用单是否已经提醒过该借用人。 */
|
||||
@Test
|
||||
void isSourceNotifiedShouldUseSourceAndUser()
|
||||
{
|
||||
when(noticeReceiverMapper.countBySource("BORROW_OVERDUE", 100L, 2L)).thenReturn(1);
|
||||
|
||||
assertTrue(service.isSourceNotified("BORROW_OVERDUE", 100L, 2L));
|
||||
assertFalse(service.isSourceNotified(null, 100L, 2L));
|
||||
}
|
||||
|
||||
/** 接收人创建时间由服务层兜底,避免调用方漏填导致审计字段为空。 */
|
||||
@Test
|
||||
void insertNoticeReceiverShouldFillCreateTime()
|
||||
{
|
||||
SysNoticeReceiver receiver = new SysNoticeReceiver();
|
||||
receiver.setNoticeId(10L);
|
||||
receiver.setUserId(2L);
|
||||
receiver.setSourceType("BORROW_OVERDUE");
|
||||
receiver.setSourceId(100L);
|
||||
|
||||
service.insertNoticeReceiver(receiver);
|
||||
|
||||
ArgumentCaptor<SysNoticeReceiver> receiverCaptor = ArgumentCaptor.forClass(SysNoticeReceiver.class);
|
||||
verify(noticeReceiverMapper).insertNoticeReceiver(receiverCaptor.capture());
|
||||
assertNotNull(receiverCaptor.getValue().getCreateTime());
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,109 @@
|
||||
package com.ruoyi.system.domain;
|
||||
|
||||
import java.util.Date;
|
||||
import org.apache.commons.lang3.builder.ToStringBuilder;
|
||||
import org.apache.commons.lang3.builder.ToStringStyle;
|
||||
import com.ruoyi.common.core.domain.BaseEntity;
|
||||
|
||||
/**
|
||||
* 通知公告接收人对象 sys_notice_receiver
|
||||
*
|
||||
* @author ruoyi
|
||||
*/
|
||||
public class SysNoticeReceiver extends BaseEntity
|
||||
{
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/** 接收人主键 */
|
||||
private Long receiverId;
|
||||
|
||||
/** 通知公告ID */
|
||||
private Long noticeId;
|
||||
|
||||
/** 接收用户ID */
|
||||
private Long userId;
|
||||
|
||||
/** 业务来源类型 */
|
||||
private String sourceType;
|
||||
|
||||
/** 业务来源ID */
|
||||
private Long sourceId;
|
||||
|
||||
/** 创建时间 */
|
||||
private Date createTime;
|
||||
|
||||
public Long getReceiverId()
|
||||
{
|
||||
return receiverId;
|
||||
}
|
||||
|
||||
public void setReceiverId(Long receiverId)
|
||||
{
|
||||
this.receiverId = receiverId;
|
||||
}
|
||||
|
||||
public Long getNoticeId()
|
||||
{
|
||||
return noticeId;
|
||||
}
|
||||
|
||||
public void setNoticeId(Long noticeId)
|
||||
{
|
||||
this.noticeId = noticeId;
|
||||
}
|
||||
|
||||
public Long getUserId()
|
||||
{
|
||||
return userId;
|
||||
}
|
||||
|
||||
public void setUserId(Long userId)
|
||||
{
|
||||
this.userId = userId;
|
||||
}
|
||||
|
||||
public String getSourceType()
|
||||
{
|
||||
return sourceType;
|
||||
}
|
||||
|
||||
public void setSourceType(String sourceType)
|
||||
{
|
||||
this.sourceType = sourceType;
|
||||
}
|
||||
|
||||
public Long getSourceId()
|
||||
{
|
||||
return sourceId;
|
||||
}
|
||||
|
||||
public void setSourceId(Long sourceId)
|
||||
{
|
||||
this.sourceId = sourceId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Date getCreateTime()
|
||||
{
|
||||
return createTime;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setCreateTime(Date createTime)
|
||||
{
|
||||
this.createTime = createTime;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString()
|
||||
{
|
||||
return new ToStringBuilder(this, ToStringStyle.MULTI_LINE_STYLE)
|
||||
.append("receiverId", getReceiverId())
|
||||
.append("noticeId", getNoticeId())
|
||||
.append("userId", getUserId())
|
||||
.append("sourceType", getSourceType())
|
||||
.append("sourceId", getSourceId())
|
||||
.append("createTime", getCreateTime())
|
||||
.toString();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,56 @@
|
||||
package com.ruoyi.system.mapper;
|
||||
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import com.ruoyi.system.domain.SysNoticeReceiver;
|
||||
|
||||
/**
|
||||
* 通知公告接收人 数据层
|
||||
*
|
||||
* @author ruoyi
|
||||
*/
|
||||
public interface SysNoticeReceiverMapper
|
||||
{
|
||||
/**
|
||||
* 新增通知公告接收人
|
||||
*
|
||||
* @param noticeReceiver 通知公告接收人
|
||||
* @return 结果
|
||||
*/
|
||||
public int insertNoticeReceiver(SysNoticeReceiver noticeReceiver);
|
||||
|
||||
/**
|
||||
* 查询某通知是否存在定向接收人
|
||||
*
|
||||
* @param noticeId 通知公告ID
|
||||
* @return 接收人数量
|
||||
*/
|
||||
public int countReceiverByNoticeId(@Param("noticeId") Long noticeId);
|
||||
|
||||
/**
|
||||
* 查询某用户是否为指定通知接收人
|
||||
*
|
||||
* @param noticeId 通知公告ID
|
||||
* @param userId 用户ID
|
||||
* @return 接收人数量
|
||||
*/
|
||||
public int countReceiverByNoticeIdAndUserId(@Param("noticeId") Long noticeId, @Param("userId") Long userId);
|
||||
|
||||
/**
|
||||
* 查询业务来源是否已给指定用户生成过通知
|
||||
*
|
||||
* @param sourceType 业务来源类型
|
||||
* @param sourceId 业务来源ID
|
||||
* @param userId 用户ID
|
||||
* @return 接收人数量
|
||||
*/
|
||||
public int countBySource(@Param("sourceType") String sourceType, @Param("sourceId") Long sourceId,
|
||||
@Param("userId") Long userId);
|
||||
|
||||
/**
|
||||
* 通知删除时清理接收人关系
|
||||
*
|
||||
* @param noticeIds 通知公告ID数组
|
||||
* @return 结果
|
||||
*/
|
||||
public int deleteByNoticeIds(@Param("noticeIds") String[] noticeIds);
|
||||
}
|
||||
@ -0,0 +1,66 @@
|
||||
package com.ruoyi.system.service.impl;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
import com.ruoyi.common.core.text.Convert;
|
||||
import com.ruoyi.common.utils.DateUtils;
|
||||
import com.ruoyi.system.domain.SysNoticeReceiver;
|
||||
import com.ruoyi.system.mapper.SysNoticeReceiverMapper;
|
||||
import com.ruoyi.system.service.ISysNoticeReceiverService;
|
||||
|
||||
/**
|
||||
* 通知公告接收人 服务层实现
|
||||
*
|
||||
* @author ruoyi
|
||||
*/
|
||||
@Service
|
||||
public class SysNoticeReceiverServiceImpl implements ISysNoticeReceiverService
|
||||
{
|
||||
@Autowired
|
||||
private SysNoticeReceiverMapper noticeReceiverMapper;
|
||||
|
||||
@Override
|
||||
public boolean canViewNotice(Long noticeId, Long userId)
|
||||
{
|
||||
if (noticeId == null || userId == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (noticeReceiverMapper.countReceiverByNoticeId(noticeId) == 0)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
return noticeReceiverMapper.countReceiverByNoticeIdAndUserId(noticeId, userId) > 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isSourceNotified(String sourceType, Long sourceId, Long userId)
|
||||
{
|
||||
if (sourceType == null || sourceId == null || userId == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
return noticeReceiverMapper.countBySource(sourceType, sourceId, userId) > 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int insertNoticeReceiver(SysNoticeReceiver noticeReceiver)
|
||||
{
|
||||
if (noticeReceiver.getCreateTime() == null)
|
||||
{
|
||||
noticeReceiver.setCreateTime(DateUtils.getNowDate());
|
||||
}
|
||||
return noticeReceiverMapper.insertNoticeReceiver(noticeReceiver);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deleteByNoticeIds(String ids)
|
||||
{
|
||||
String[] noticeIds = Convert.toStrArray(ids);
|
||||
if (noticeIds.length == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
noticeReceiverMapper.deleteByNoticeIds(noticeIds);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,49 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<!DOCTYPE mapper
|
||||
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||
<mapper namespace="com.ruoyi.system.mapper.SysNoticeReceiverMapper">
|
||||
|
||||
<resultMap type="SysNoticeReceiver" id="SysNoticeReceiverResult">
|
||||
<id property="receiverId" column="receiver_id" />
|
||||
<result property="noticeId" column="notice_id" />
|
||||
<result property="userId" column="user_id" />
|
||||
<result property="sourceType" column="source_type" />
|
||||
<result property="sourceId" column="source_id" />
|
||||
<result property="createTime" column="create_time" />
|
||||
</resultMap>
|
||||
|
||||
<insert id="insertNoticeReceiver" parameterType="SysNoticeReceiver" useGeneratedKeys="true" keyProperty="receiverId">
|
||||
insert into sys_notice_receiver (notice_id, user_id, source_type, source_id, create_time)
|
||||
values (#{noticeId}, #{userId}, #{sourceType}, #{sourceId}, #{createTime})
|
||||
</insert>
|
||||
|
||||
<select id="countReceiverByNoticeId" resultType="int">
|
||||
select count(1)
|
||||
from sys_notice_receiver
|
||||
where notice_id = #{noticeId}
|
||||
</select>
|
||||
|
||||
<select id="countReceiverByNoticeIdAndUserId" resultType="int">
|
||||
select count(1)
|
||||
from sys_notice_receiver
|
||||
where notice_id = #{noticeId}
|
||||
and user_id = #{userId}
|
||||
</select>
|
||||
|
||||
<select id="countBySource" resultType="int">
|
||||
select count(1)
|
||||
from sys_notice_receiver
|
||||
where source_type = #{sourceType}
|
||||
and source_id = #{sourceId}
|
||||
and user_id = #{userId}
|
||||
</select>
|
||||
|
||||
<delete id="deleteByNoticeIds">
|
||||
delete from sys_notice_receiver where notice_id in
|
||||
<foreach collection="noticeIds" item="noticeId" open="(" separator="," close=")">
|
||||
#{noticeId}
|
||||
</foreach>
|
||||
</delete>
|
||||
|
||||
</mapper>
|
||||
Loading…
Reference in New Issue