refactor(project): 重构权限常量定义并移除需求进度聚合功能

- 将产品和项目查询权限码统一提取到常量类中
- 移除需求进度聚合计算的相关实现代码
- 更新权限验证注解使用新的常量定义
- 清理相关的单元测试代码
- 更新错误码注释说明
This commit is contained in:
2026-06-11 09:17:21 +08:00
parent 10b7ccdeb0
commit 79591e66be
30 changed files with 1598 additions and 41 deletions

View File

@@ -0,0 +1,45 @@
package com.njcn.rdms.module.system.api.notify;
import com.njcn.rdms.framework.common.pojo.CommonResult;
import com.njcn.rdms.module.system.api.notify.dto.NotifySingleSendReqDTO;
import com.njcn.rdms.module.system.enums.ApiConstants;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import java.util.Map;
/**
* 站内信发送 API跨模块统一入口契约
*
* <p>业务方(如 project经此发送站内信不依赖 system-boot 内部发送实现。
* userType 在能力层固定 ADMIN不暴露给业务方。</p>
*
* @author hongawen
*/
@FeignClient(name = ApiConstants.NAME)
@Tag(name = "RPC 服务 - 站内信发送") // 对 NotifySendService 的封装,供其它模块统一入口调用
public interface NotifyMessageSendApi {
String PREFIX = ApiConstants.PREFIX + "/notify-message-send";
@PostMapping(PREFIX + "/send-single")
@Operation(summary = "发送单条站内信")
CommonResult<Long> sendSingleNotify(@Valid @RequestBody NotifySingleSendReqDTO reqDTO);
/**
* 便捷方法发送单条站内信给管理后台用户userType 固定 ADMIN由实现层处理
*
* @param userId 接收用户编号
* @param templateCode 模板编码(场景标识,发送前模板必须已配置)
* @param templateParams 模板参数
*/
default void sendSingleNotifyToAdmin(Long userId, String templateCode, Map<String, Object> templateParams) {
sendSingleNotify(new NotifySingleSendReqDTO().setUserId(userId)
.setTemplateCode(templateCode).setTemplateParams(templateParams)).checkError();
}
}

View File

@@ -0,0 +1,31 @@
package com.njcn.rdms.module.system.api.notify.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import lombok.experimental.Accessors;
import java.util.Map;
/**
* 发送单条站内信 Request DTO跨模块统一入口入参
*
* @author hongawen
*/
@Schema(description = "RPC 服务 - 发送单条站内信 Request DTO")
@Data
@Accessors(chain = true)
public class NotifySingleSendReqDTO {
@Schema(description = "接收用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
@NotNull(message = "接收用户编号不能为空")
private Long userId;
@Schema(description = "站内信模板编码", requiredMode = Schema.RequiredMode.REQUIRED, example = "task_assigned")
@NotNull(message = "模板编码不能为空")
private String templateCode;
@Schema(description = "模板参数(占位符 -> 值)", example = "{\"taskName\":\"联调\"}")
private Map<String, Object> templateParams;
}

View File

@@ -0,0 +1,33 @@
package com.njcn.rdms.module.system.api.notify;
import com.njcn.rdms.framework.common.pojo.CommonResult;
import com.njcn.rdms.module.system.api.notify.dto.NotifySingleSendReqDTO;
import com.njcn.rdms.module.system.service.notify.NotifySendService;
import io.swagger.v3.oas.annotations.Hidden;
import jakarta.annotation.Resource;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.RestController;
import static com.njcn.rdms.framework.common.pojo.CommonResult.success;
/**
* {@link NotifyMessageSendApi} 的实现:委托现有 {@link NotifySendService},不重写发送逻辑。
*
* @author hongawen
*/
@RestController // 提供 RESTful API 接口,给 Feign 调用
@Validated
@Hidden
public class NotifyMessageSendApiImpl implements NotifyMessageSendApi {
@Resource
private NotifySendService notifySendService;
@Override
public CommonResult<Long> sendSingleNotify(NotifySingleSendReqDTO reqDTO) {
Long logId = notifySendService.sendSingleNotifyToAdmin(
reqDTO.getUserId(), reqDTO.getTemplateCode(), reqDTO.getTemplateParams());
return success(logId);
}
}

View File

@@ -99,4 +99,13 @@ public class NoticeController {
return success(true);
}
@GetMapping("/recent")
@Operation(summary = "获取最近公告", description = "工作台首页用,登录即可访问,仅返回正常状态公告")
public CommonResult<List<NoticeRespVO>> getRecentNotices(
@RequestParam(name = "size", defaultValue = "3") Integer size,
@RequestParam(name = "type", required = false) Integer type) {
List<NoticeDO> list = noticeService.getRecentNotices(size, type);
return success(BeanUtils.toBean(list, NoticeRespVO.class));
}
}

View File

@@ -20,6 +20,9 @@ public class NotifyMessageMyPageReqVO extends PageParam {
@Schema(description = "是否已读", example = "true")
private Boolean readStatus;
@Schema(description = "关键字,对消息正文模糊匹配;不传或空串 = 不过滤", example = "指派")
private String keyword;
@Schema(description = "创建时间")
@DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
private LocalDateTime[] createTime;

View File

@@ -17,4 +17,17 @@ public interface NoticeMapper extends BaseMapperX<NoticeDO> {
.orderByDesc(NoticeDO::getId));
}
/**
* 查询最近 N 条公告:仅 status 正常CommonStatusEnum.ENABLE=0可按 type 过滤,按 id 倒序取 size 条。
* type 为 null 时不过滤类型type 动态透传,不做代码侧白名单校验。
*/
default java.util.List<NoticeDO> selectRecentList(Integer size, Integer type) {
int limit = (size == null || size <= 0) ? 3 : size;
return selectList(new LambdaQueryWrapperX<NoticeDO>()
.eq(NoticeDO::getStatus, com.njcn.rdms.framework.common.enums.CommonStatusEnum.ENABLE.getStatus())
.eqIfPresent(NoticeDO::getType, type)
.orderByDesc(NoticeDO::getId)
.last("LIMIT " + limit));
}
}

View File

@@ -29,9 +29,12 @@ public interface NotifyMessageMapper extends BaseMapperX<NotifyMessageDO> {
default PageResult<NotifyMessageDO> selectPage(NotifyMessageMyPageReqVO reqVO, Long userId, Integer userType) {
return selectPage(reqVO, new LambdaQueryWrapperX<NotifyMessageDO>()
.eqIfPresent(NotifyMessageDO::getReadStatus, reqVO.getReadStatus())
// 关键字对最终渲染正文模糊匹配;空串/不传由 likeIfPresent 跳过,不影响存量调用
.likeIfPresent(NotifyMessageDO::getTemplateContent, reqVO.getKeyword())
.betweenIfPresent(NotifyMessageDO::getCreateTime, reqVO.getCreateTime())
.eq(NotifyMessageDO::getUserId, userId)
.eq(NotifyMessageDO::getUserType, userType)
// 雪花 id 按时间单调递增id 倒序 = 收到时间倒序且排序唯一稳定(与前端分页口径约定一致,勿改为按 read_status/read_time 排序)
.orderByDesc(NotifyMessageDO::getId));
}

View File

@@ -57,4 +57,13 @@ public interface NoticeService {
*/
NoticeDO getNotice(Long id);
/**
* 获取最近 N 条公告(工作台首页用):仅正常状态,可按 type 过滤。
*
* @param size 条数,默认 3
* @param type 公告类型({@link com.njcn.rdms.module.system.enums.notice.NoticeTypeEnum}null 表示不过滤
* @return 公告列表
*/
List<NoticeDO> getRecentNotices(Integer size, Integer type);
}

View File

@@ -65,6 +65,12 @@ public class NoticeServiceImpl implements NoticeService {
return noticeMapper.selectById(id);
}
@Override
public List<NoticeDO> getRecentNotices(Integer size, Integer type) {
int limit = (size == null || size <= 0) ? 3 : size;
return noticeMapper.selectRecentList(limit, type);
}
@VisibleForTesting
public void validateNoticeExists(Long id) {
if (id == null) {

View File

@@ -0,0 +1,80 @@
package com.njcn.rdms.module.system.dal.mysql.notify;
import com.baomidou.mybatisplus.core.MybatisConfiguration;
import com.baomidou.mybatisplus.core.conditions.Wrapper;
import com.baomidou.mybatisplus.core.metadata.TableInfoHelper;
import com.njcn.rdms.framework.common.pojo.PageResult;
import com.njcn.rdms.module.system.controller.admin.notify.vo.message.NotifyMessageMyPageReqVO;
import com.njcn.rdms.module.system.dal.dataobject.notify.NotifyMessageDO;
import org.apache.ibatis.builder.MapperBuilderAssistant;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
import java.util.Locale;
import static org.junit.jupiter.api.Assertions.assertFalse;
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.doReturn;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
/**
* {@link NotifyMessageMapper} 的单元测试 —— 我的站内信分页keyword 检索 + 排序口径)。
*/
class NotifyMessageMapperTest {
@BeforeAll
static void initMyBatisPlusTableInfo() {
TableInfoHelper.initTableInfo(new MapperBuilderAssistant(new MybatisConfiguration(), ""), NotifyMessageDO.class);
}
@SuppressWarnings({"unchecked", "rawtypes"})
private String captureMyPageSqlSegment(NotifyMessageMyPageReqVO reqVO) {
NotifyMessageMapper mapper = mock(NotifyMessageMapper.class, invocation -> invocation.callRealMethod());
doReturn(PageResult.empty()).when(mapper).selectPage(eq(reqVO), any(Wrapper.class));
mapper.selectPage(reqVO, 1L, 2);
ArgumentCaptor<Wrapper<NotifyMessageDO>> wrapperCaptor = ArgumentCaptor.forClass(Wrapper.class);
verify(mapper).selectPage(eq(reqVO), wrapperCaptor.capture());
return wrapperCaptor.getValue().getSqlSegment().toLowerCase(Locale.ROOT);
}
@Test
void myPage_keywordShouldFilterTemplateContentByLike() {
NotifyMessageMyPageReqVO reqVO = new NotifyMessageMyPageReqVO();
reqVO.setKeyword("指派");
reqVO.setReadStatus(false);
String sqlSegment = captureMyPageSqlSegment(reqVO);
assertTrue(sqlSegment.contains("template_content like"));
assertTrue(sqlSegment.contains("read_status"));
assertTrue(sqlSegment.contains("user_id"));
assertTrue(sqlSegment.contains("user_type"));
}
@Test
void myPage_blankKeywordShouldNotAddLikeCondition() {
NotifyMessageMyPageReqVO noKeyword = new NotifyMessageMyPageReqVO();
assertFalse(captureMyPageSqlSegment(noKeyword).contains("like"));
NotifyMessageMyPageReqVO blankKeyword = new NotifyMessageMyPageReqVO();
blankKeyword.setKeyword("");
assertFalse(captureMyPageSqlSegment(blankKeyword).contains("like"));
}
@Test
void myPage_shouldOrderByIdDescOnly() {
String sqlSegment = captureMyPageSqlSegment(new NotifyMessageMyPageReqVO());
// 与前端约定的分页口径id 倒序(= 收到时间倒序、唯一稳定),不随读状态重排
assertTrue(sqlSegment.contains("order by id desc"));
assertFalse(sqlSegment.contains("read_status desc"));
assertFalse(sqlSegment.contains("read_time"));
}
}

View File

@@ -0,0 +1,38 @@
package com.njcn.rdms.module.system.service.notice;
import com.njcn.rdms.framework.test.core.ut.BaseMockitoUnitTest;
import com.njcn.rdms.module.system.dal.dataobject.notice.NoticeDO;
import com.njcn.rdms.module.system.dal.mysql.notice.NoticeMapper;
import org.junit.jupiter.api.Test;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import java.util.List;
import static java.util.Collections.singletonList;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.Mockito.when;
/**
* {@link NoticeServiceImpl} 的单元测试 —— 工作台最近公告查询。
*/
class NoticeServiceImplTest extends BaseMockitoUnitTest {
@InjectMocks
private NoticeServiceImpl noticeService;
@Mock
private NoticeMapper noticeMapper;
@Test
void testGetRecentNotices_delegatesToMapper() {
NoticeDO notice = new NoticeDO();
notice.setId(100L);
when(noticeMapper.selectRecentList(3, 2)).thenReturn(singletonList(notice));
List<NoticeDO> result = noticeService.getRecentNotices(3, 2);
assertEquals(1, result.size());
assertEquals(100L, result.get(0).getId());
}
}