refactor(asset): 优化借阅归还提醒服务事务处理

- 使用 TransactionTemplate 替代 @Transactional 注解进行事务管理
- 移除 SimpleDateFormat,改用 DateFormatUtils 处理日期格式化
- 在批量处理逾期借阅订单时确保每条记录的事务独立性
- 重构测试类以适配新的事务处理方式并添加事务模板模拟

feat(system): 增强通知查看权限控制

- 实现按用户过滤可查看通知的功能
- 添加通知接收者可见性验证SQL逻辑
- 优化批量标记通知为已读的权限检查性能
- 更新通知列表接口以应用查看权限过滤
- 调整通知管理权限检查逻辑以支持更细粒度控制

test(system): 迁移通知接收者服务单元测试

- 将原有的资产模块中的通知接收者测试迁移至系统模块
- 补充批处理可查看通知ID功能的单元测试用例
- 完善通知权限过滤相关的测试覆盖

chore(system): 更新系统模块依赖配置

- 添加Spring Boot测试依赖支持
- 配置Maven Surefire插件以提升测试执行能力
main
yangk 3 days ago
parent 1c029399c7
commit cbf881ea6e

@ -59,6 +59,12 @@ public class SysNoticeController extends BaseController
@ResponseBody
public TableDataInfo list(SysNotice notice)
{
SysUser currentUser = ShiroUtils.getSysUser();
Long userId = currentUser == null ? null : currentUser.getUserId();
if (!canManageNotice(userId))
{
notice.getParams().put("visibleUserId", userId == null ? -1L : userId);
}
startPage();
List<SysNotice> list = noticeService.selectNoticeList(notice);
return getDataTable(list);
@ -208,6 +214,12 @@ public class SysNoticeController extends BaseController
private boolean canManageNotice(Long userId)
{
return ShiroUtils.isAdmin(userId) || ShiroUtils.getSubject().isPermitted("system:notice:list");
if (userId == null)
{
return false;
}
return ShiroUtils.isAdmin(userId) || ShiroUtils.getSubject().isPermitted("system:notice:add")
|| ShiroUtils.getSubject().isPermitted("system:notice:edit")
|| ShiroUtils.getSubject().isPermitted("system:notice:remove");
}
}

@ -1,10 +1,10 @@
package com.ruoyi.asset.service.impl;
import java.text.SimpleDateFormat;
import java.util.List;
import org.apache.commons.lang3.time.DateFormatUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.support.TransactionTemplate;
import org.springframework.web.util.HtmlUtils;
import com.ruoyi.asset.constant.BorrowOrderStatus;
import com.ruoyi.asset.domain.AmsBorrowOrder;
@ -43,8 +43,10 @@ public class BorrowReturnReminderServiceImpl implements IBorrowReturnReminderSer
@Autowired
private ISysNoticeReceiverService noticeReceiverService;
@Autowired
private TransactionTemplate transactionTemplate;
@Override
@Transactional(rollbackFor = Exception.class)
public int remindOverdueBorrowOrders()
{
List<AmsBorrowOrder> candidates = amsBorrowOrderMapper.selectOverdueBorrowReturnReminderCandidates(
@ -61,7 +63,11 @@ public class BorrowReturnReminderServiceImpl implements IBorrowReturnReminderSer
{
continue;
}
createdCount += createReminder(order);
Integer created = transactionTemplate.execute(status -> createReminder(order));
if (created != null)
{
createdCount += created;
}
}
return createdCount;
}
@ -110,7 +116,7 @@ public class BorrowReturnReminderServiceImpl implements IBorrowReturnReminderSer
{
return "";
}
return new SimpleDateFormat("yyyy-MM-dd").format(order.getExpectedReturnDate());
return DateFormatUtils.format(order.getExpectedReturnDate(), "yyyy-MM-dd");
}
private String escape(String value)

@ -25,6 +25,8 @@ import org.mockito.ArgumentCaptor;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.transaction.support.TransactionCallback;
import org.springframework.transaction.support.TransactionTemplate;
@ExtendWith(MockitoExtension.class)
class BorrowReturnReminderServiceImplTest
@ -38,6 +40,9 @@ class BorrowReturnReminderServiceImplTest
@Mock
private ISysNoticeReceiverService noticeReceiverService;
@Mock
private TransactionTemplate transactionTemplate;
@InjectMocks
private BorrowReturnReminderServiceImpl service;
@ -45,6 +50,7 @@ class BorrowReturnReminderServiceImplTest
@Test
void remindShouldCreateDirectedNoticeForDueCandidate()
{
mockTransactionTemplate();
AmsBorrowOrder order = buildOrder();
when(amsBorrowOrderMapper.selectOverdueBorrowReturnReminderCandidates(
BorrowOrderStatus.BORROWING, BorrowOrderStatus.BORROWING))
@ -95,6 +101,7 @@ class BorrowReturnReminderServiceImplTest
@Test
void remindShouldFailFastWhenReceiverInsertFails()
{
mockTransactionTemplate();
AmsBorrowOrder order = buildOrder();
when(amsBorrowOrderMapper.selectOverdueBorrowReturnReminderCandidates(
BorrowOrderStatus.BORROWING, BorrowOrderStatus.BORROWING))
@ -113,6 +120,14 @@ class BorrowReturnReminderServiceImplTest
assertTrue(exception.getMessage().contains("接收人写入异常"));
}
private void mockTransactionTemplate()
{
when(transactionTemplate.execute(any())).thenAnswer(invocation -> {
TransactionCallback<?> callback = invocation.getArgument(0);
return callback.doInTransaction(null);
});
}
private AmsBorrowOrder buildOrder()
{
AmsBorrowOrder order = new AmsBorrowOrder();

@ -23,6 +23,22 @@
<artifactId>ruoyi-common</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.5.4</version>
</plugin>
</plugins>
</build>
</project>

@ -1,5 +1,6 @@
package com.ruoyi.system.mapper;
import java.util.List;
import org.apache.ibatis.annotations.Param;
import com.ruoyi.system.domain.SysNoticeReceiver;
@ -46,11 +47,13 @@ public interface SysNoticeReceiverMapper
public int countBySource(@Param("sourceType") String sourceType, @Param("sourceId") Long sourceId,
@Param("userId") Long userId);
public List<Long> selectViewableNoticeIds(@Param("noticeIds") Long[] noticeIds, @Param("userId") Long userId);
/**
*
*
* @param noticeIds ID
* @return
*/
public int deleteByNoticeIds(@Param("noticeIds") String[] noticeIds);
public int deleteByNoticeIds(@Param("noticeIds") Long[] noticeIds);
}

@ -1,5 +1,6 @@
package com.ruoyi.system.service;
import java.util.List;
import com.ruoyi.system.domain.SysNoticeReceiver;
/**
@ -28,6 +29,8 @@ public interface ISysNoticeReceiverService
*/
public boolean isSourceNotified(String sourceType, Long sourceId, Long userId);
public List<Long> filterViewableNoticeIds(Long[] noticeIds, Long userId);
/**
*
*

@ -2,7 +2,6 @@ package com.ruoyi.system.service.impl;
import java.util.List;
import java.util.Map;
import java.util.Arrays;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import com.ruoyi.common.core.text.Convert;
@ -70,14 +69,12 @@ public class SysNoticeReadServiceImpl implements ISysNoticeReadService
{
return;
}
Long[] readableNoticeIds = Arrays.stream(noticeIds)
.filter(noticeId -> noticeReceiverService.canViewNotice(noticeId, userId))
.toArray(Long[]::new);
if (readableNoticeIds.length == 0)
List<Long> readableNoticeIds = noticeReceiverService.filterViewableNoticeIds(noticeIds, userId);
if (readableNoticeIds.isEmpty())
{
return;
}
noticeReadMapper.insertNoticeReadBatch(userId, readableNoticeIds);
noticeReadMapper.insertNoticeReadBatch(userId, readableNoticeIds.toArray(new Long[0]));
}
/**

@ -1,5 +1,9 @@
package com.ruoyi.system.service.impl;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import com.ruoyi.common.core.text.Convert;
@ -43,6 +47,23 @@ public class SysNoticeReceiverServiceImpl implements ISysNoticeReceiverService
return noticeReceiverMapper.countBySource(sourceType, sourceId, userId) > 0;
}
@Override
public List<Long> filterViewableNoticeIds(Long[] noticeIds, Long userId)
{
if (noticeIds == null || noticeIds.length == 0 || userId == null)
{
return Collections.emptyList();
}
Long[] effectiveNoticeIds = Arrays.stream(noticeIds)
.filter(Objects::nonNull)
.toArray(Long[]::new);
if (effectiveNoticeIds.length == 0)
{
return Collections.emptyList();
}
return noticeReceiverMapper.selectViewableNoticeIds(effectiveNoticeIds, userId);
}
@Override
public int insertNoticeReceiver(SysNoticeReceiver noticeReceiver)
{
@ -56,7 +77,7 @@ public class SysNoticeReceiverServiceImpl implements ISysNoticeReceiverService
@Override
public void deleteByNoticeIds(String ids)
{
String[] noticeIds = Convert.toStrArray(ids);
Long[] noticeIds = Convert.toLongArray(ids);
if (noticeIds.length == 0)
{
return;

@ -21,6 +21,15 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
select notice_id, notice_title, notice_type, cast(notice_content as char) as notice_content, status, create_by, create_time, update_by, update_time, remark
from sys_notice
</sql>
<sql id="receiverVisibleFilter">
<if test="params.visibleUserId != null">
AND (
not exists (select 1 from sys_notice_receiver nr_all where nr_all.notice_id = sys_notice.notice_id)
or exists (select 1 from sys_notice_receiver nr_user where nr_user.notice_id = sys_notice.notice_id and nr_user.user_id = #{params.visibleUserId})
)
</if>
</sql>
<select id="selectNoticeById" parameterType="Long" resultMap="SysNoticeResult">
<include refid="selectNoticeVo"/>
@ -39,6 +48,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<if test="createBy != null and createBy != ''">
AND create_by like concat('%', #{createBy}, '%')
</if>
<include refid="receiverVisibleFilter"/>
</where>
order by notice_id desc
</select>

@ -17,14 +17,18 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
values (#{noticeId}, #{userId}, sysdate())
</insert>
<!-- 查询未读数量:正常状态公告 减去 当前用户已读数 -->
<select id="selectUnreadCount" resultType="int">
select count(*) from sys_notice n
where n.status = '0'
<sql id="receiverVisibleFilter">
and (
not exists (select 1 from sys_notice_receiver nr_all where nr_all.notice_id = n.notice_id)
or exists (select 1 from sys_notice_receiver nr_user where nr_user.notice_id = n.notice_id and nr_user.user_id = #{userId})
)
</sql>
<!-- 查询未读数量:正常状态公告 减去 当前用户已读数 -->
<select id="selectUnreadCount" resultType="int">
select count(*) from sys_notice n
where n.status = '0'
<include refid="receiverVisibleFilter"/>
and not exists (select 1 from sys_notice_read r where r.notice_id = n.notice_id and r.user_id = #{userId})
</select>
@ -47,10 +51,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
left join sys_notice_read r
on r.notice_id = n.notice_id and r.user_id = #{userId}
where n.status = '0'
and (
not exists (select 1 from sys_notice_receiver nr_all where nr_all.notice_id = n.notice_id)
or exists (select 1 from sys_notice_receiver nr_user where nr_user.notice_id = n.notice_id and nr_user.user_id = #{userId})
)
<include refid="receiverVisibleFilter"/>
order by n.notice_id desc
limit #{limit}
</select>

@ -39,6 +39,19 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
and user_id = #{userId}
</select>
<select id="selectViewableNoticeIds" resultType="Long">
select n.notice_id
from sys_notice n
where n.notice_id in
<foreach collection="noticeIds" item="noticeId" open="(" separator="," close=")">
#{noticeId}
</foreach>
and (
not exists (select 1 from sys_notice_receiver nr_all where nr_all.notice_id = n.notice_id)
or exists (select 1 from sys_notice_receiver nr_user where nr_user.notice_id = n.notice_id and nr_user.user_id = #{userId})
)
</select>
<delete id="deleteByNoticeIds">
delete from sys_notice_receiver where notice_id in
<foreach collection="noticeIds" item="noticeId" open="(" separator="," close=")">

@ -1,14 +1,18 @@
package com.ruoyi.asset.service.impl;
package com.ruoyi.system.service.impl;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertEquals;
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.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import java.util.Collections;
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;
@ -46,6 +50,21 @@ class SysNoticeReceiverServiceImplTest
assertFalse(service.canViewNotice(10L, 3L));
}
/** 批量标记已读前先用一条 SQL 过滤当前用户真正可见的通知,避免逐条查权限。 */
@Test
void filterViewableNoticeIdsShouldUseBatchMapper()
{
when(noticeReceiverMapper.selectViewableNoticeIds(any(Long[].class), eq(2L)))
.thenReturn(Collections.singletonList(10L));
assertEquals(Collections.singletonList(10L),
service.filterViewableNoticeIds(new Long[] { 10L, null, 11L }, 2L));
ArgumentCaptor<Long[]> noticeIdsCaptor = ArgumentCaptor.forClass(Long[].class);
verify(noticeReceiverMapper).selectViewableNoticeIds(noticeIdsCaptor.capture(), eq(2L));
assertArrayEquals(new Long[] { 10L, 11L }, noticeIdsCaptor.getValue());
}
/** 业务来源唯一键用于判断同一借用单是否已经提醒过该借用人。 */
@Test
void isSourceNotifiedShouldUseSourceAndUser()
Loading…
Cancel
Save