diff --git a/.kiro/steering/项目结构说明.md b/.kiro/steering/项目结构说明.md index 71420e6..3855af6 100644 --- a/.kiro/steering/项目结构说明.md +++ b/.kiro/steering/项目结构说明.md @@ -26,7 +26,8 @@ offerpie/back-end │ │ ├─ JobIntentionController.java # 求职意向接口(查询与保存) │ │ ├─ JobController.java # 岗位接口(岗位列表查询、收藏、投递、不感兴趣、求职助手任务列表、AI岗位推荐) │ │ ├─ JobAgentConfigController.java # 求职助手配置接口(配置查询与保存) -│ │ └─ UserResumeController.java # 用户简历接口(简历列表、主表及5张子表的查询与保存、子表单条添加/编辑/删除、简历删除、设置默认简历) +│ │ ├─ UserResumeController.java # 用户简历接口(简历列表、主表及5张子表的查询与保存、子表单条添加/编辑/删除、简历删除、设置默认简历) +│ │ └─ MessageController.java # 站内信消息接口(消息列表、未读数、按类型未读数、标记已读) │ ├─ service/ │ │ ├─ LoginService.java # 登录业务逻辑(验证码校验、自动注册、JWT生成、Cookie设置) │ │ ├─ UserRegisterService.java # 用户注册服务(注册逻辑、邀请码生成与绑定) @@ -37,19 +38,22 @@ offerpie/back-end │ │ ├─ JobService.java # 岗位服务(岗位列表查询、匹配度计算编排、求职助手任务列表、AI岗位推荐) │ │ ├─ JobAgentConfigService.java # 求职助手配置服务(配置查询与保存) │ │ ├─ UserResumeService.java # 用户简历服务(简历列表、主表及5张子表的CRUD、子表单条添加/编辑/删除、设置默认简历) +│ │ ├─ MessageQueryService.java # 站内信查询服务(消息分页列表、未读数、按类型未读数) │ │ └─ WxPayNotifyMessageAbstractImpl.java # 微信支付回调实现 │ └─ pojo/ │ ├─ param/ │ │ ├─ userProfile/ # 个人资料入参(UserProfileParam、各子表Param) │ │ ├─ resume/ # 简历入参(ResumeParam、各子表Param、各子表UpdateParam、ResumeSubTableParam) │ │ ├─ job/ # 岗位相关入参(JobIntentionParam、JobQueryParam、JobAgentTaskQueryParam、JobAgentRecommendParam) -│ │ └─ jobAgent/ # 求职助手入参(JobAgentConfigParam) +│ │ ├─ jobAgent/ # 求职助手入参(JobAgentConfigParam) +│ │ └─ message/ # 站内信入参(MessageQueryParam) │ ├─ dto/ │ │ ├─ SmsLoginDto.java # 短信登录入参(mobileNumber + code + inviteCode) │ │ ├─ userProfile/ # 个人资料出参(UserProfileDto、各子表Dto) │ │ ├─ resume/ # 简历出参(ResumeDto、ResumeListItemDto、各子表Dto) │ │ ├─ job/ # 岗位相关出参(JobIntentionDto、JobDto、JobMatchScoreDto、JobAgentRecommendDto) -│ │ └─ jobAgent/ # 求职助手出参(JobAgentConfigDto) +│ │ ├─ jobAgent/ # 求职助手出参(JobAgentConfigDto) +│ │ └─ message/ # 站内信出参(MessageDto、MessageUnreadCountDto) │ └─ vo/ │ ├─ LoginVo.java # 登录返回(userId + nick) │ ├─ LanguageAbility.java # 语言能力对象(求职助手配置JSON字段) @@ -119,6 +123,8 @@ offerpie/back-end │ ├─ JobAgentConfigMapper.java # 求职助手配置Mapper │ ├─ JobAgentChatMessageMapper.java # 求职助手对话消息Mapper │ ├─ UserJobCustomizeResumeMapper.java # 用户岗位定制简历Mapper + │ ├─ MessageMapper.java # 站内信消息Mapper + │ ├─ MessageReadMapper.java # 消息已读状态Mapper │ └─ AppJobDataMapper.java # 爬虫岗位原始数据Mapper ├─ pojo/ │ ├─ po/ # 持久化实体 @@ -161,11 +167,13 @@ offerpie/back-end │ │ ├─ JobAgentConfig.java # 求职助手配置表(bg_job_agent_config) │ │ ├─ JobAgentChatMessage.java # 求职助手对话消息表(bg_job_agent_chat_message) │ │ ├─ UserJobCustomizeResume.java # 用户岗位定制简历表(bg_user_job_customize_resume) + │ │ ├─ Message.java # 站内信消息表(bg_message) + │ │ ├─ MessageRead.java # 消息已读状态表(bg_message_read) │ │ └─ AppJobData.java # 爬虫岗位原始数据表(app_job_data) │ └─ vo/ # ViewObject(OssUrlVo、DescriptionParagraph、JobListItemVo、UserHonorsVo 等) ├─ resources/mapper/ # MyBatis XML 映射文件 │ └─ JobMapper.xml # 岗位自定义SQL(selectJobPage) - └─ service/ # 业务 Service(OssService、SmsService、DictCacheService、JobCleanService、JobCleanTransactionService、CompanyCleanService、CompanyCleanTransactionService、UserProfileAnalyzeService、JobMatchService 等) + └─ service/ # 业务 Service(OssService、SmsService、DictCacheService、JobCleanService、JobCleanTransactionService、CompanyCleanService、CompanyCleanTransactionService、UserProfileAnalyzeService、JobMatchService、MessageService 等) ``` > **设计理念** – 业务实体和 Mapper 位于 `manager`,B 端和 C 端共享;C 端特有的注解、切面、权限服务、路由菜单服务位于 `client-api`,避免 B 端误用;`common` 提供统一的技术支撑。 @@ -174,7 +182,7 @@ offerpie/back-end |------|----------|-----------| | **client-api** | - 面向终端用户的 REST API
- 启动 Spring Boot 应用
- 短信验证码登录(含自动注册、邀请码绑定)
- **功能权限校验**:注解 + 切面 + 权限服务(校验、扣减、回退)
- **路由菜单**:获取用户有效菜单树
- **求职助手**:配置管理、AI岗位推荐、任务列表 | `ClientApplication`、`LoginController`、`RouteMenuController`、`JobController`、`JobAgentConfigController`、`UserResumeController`、`FuncPermission`、`FuncPermissionAspect`、`FuncPermissionService`、`RouteMenuService`、`JobService`、`JobAgentConfigService`、`UserRegisterService`、`RouteMenuVo` | | **common** | - **统一配置**:OSS、Redis、Security、WxPay、Sms、Async 等
- **跨层工具**:HTTP、IP、认证、验证码、Redis Server 等
- **全局拦截/切面**:日志、TraceId、黑名单、SQL 打印
- **统一异常/响应**:`GlobalExceptionAdvice`、`UnifiedResponse`
- **业务抽象**:邮件发送、微信支付(Native/JS/Transfer)、异步任务
- **公共 POJO**:登录令牌、防重放信息等 | `config/`, `tool/`, `interceptor/`, `aop/`, `exception/`, `email/`, `wxPay/`, `pojo/` | -| **manager** | - **业务实体**(`User`、`OssFile`、`UserInvite`、`RouteMenu`、`FuncPermission`、`UserRouteMenuStock`、`UserFuncPermissionStock`、`UserFuncUsageLog`、`ChinaRegionsCode`、`JobCategory`、`Company`、`Job`、`JobRegionRelation`、`Industry`、`SkillTag`、`UserJobFavorite`、`UserJobApplication`、`UserJobDislike`、`UserJobIntention`、`UserProfile`及5张子表、`UserProfileSkillTagRelation`、`UserResume`及5张子表、`ResumeDiagnosisReport`、`ResumeDiagnosisIssue`、`JobAgentConfig`、`UserJobCustomizeResume`、`AppJobData`)
- **MyBatis Mapper**(对应全部业务实体的 Mapper,含 `JobAgentConfigMapper`、`UserJobCustomizeResumeMapper`)
- **业务 API**:文件上传/下载、健康检查、地区/岗位分类/行业字典查询
- **业务逻辑**:OssService、SmsService、DictCacheService、JobCleanService、CompanyCleanService、UserProfileAnalyzeService、JobMatchService 等
- **既供 B 端 UI(待实现)使用,也供 C 端业务直接调用** | `controller/`, `mapper/`, `pojo/po/`, `pojo/vo/`, `service/`, `constant/` | +| **manager** | - **业务实体**(`User`、`OssFile`、`UserInvite`、`RouteMenu`、`FuncPermission`、`UserRouteMenuStock`、`UserFuncPermissionStock`、`UserFuncUsageLog`、`ChinaRegionsCode`、`JobCategory`、`Company`、`Job`、`JobRegionRelation`、`Industry`、`SkillTag`、`UserJobFavorite`、`UserJobApplication`、`UserJobDislike`、`UserJobIntention`、`UserProfile`及5张子表、`UserProfileSkillTagRelation`、`UserResume`及5张子表、`ResumeDiagnosisReport`、`ResumeDiagnosisIssue`、`JobAgentConfig`、`UserJobCustomizeResume`、`Message`、`MessageRead`、`AppJobData`)
- **MyBatis Mapper**(对应全部业务实体的 Mapper,含 `JobAgentConfigMapper`、`UserJobCustomizeResumeMapper`、`MessageMapper`、`MessageReadMapper`)
- **业务 API**:文件上传/下载、健康检查、地区/岗位分类/行业字典查询
- **业务逻辑**:OssService、SmsService、DictCacheService、JobCleanService、CompanyCleanService、UserProfileAnalyzeService、JobMatchService、MessageService 等
- **既供 B 端 UI(待实现)使用,也供 C 端业务直接调用** | `controller/`, `mapper/`, `pojo/po/`, `pojo/vo/`, `service/`, `constant/` | ## 3️⃣ 关键业务实体 | 实体 | 所属模块 | 作用概述 | @@ -222,6 +230,8 @@ offerpie/back-end | `JobAgentConfig` | manager | 求职助手配置表(bg_job_agent_config),一个用户一条记录,存储Agent模式、投递目标、网申常见问题预设答案(部门调剂、地点调剂、面试方式、语言能力、到岗时间、实习天数/时长)。 | | `JobAgentChatMessage` | manager | 求职助手对话消息表(bg_job_agent_chat_message),记录用户与求职助手的完整对话流,含4种消息类型(user/assistant/recommend/apply_progress),文本存content,JSON数据存extra由前端维护。 | | `UserJobCustomizeResume` | manager | 用户岗位定制简历表(bg_user_job_customize_resume),一个用户+一个岗位=一份定制简历,content字段存完整CustomizeResume JSON,唯一索引(user_id, job_id)。 | +| `Message` | manager | 站内信消息表(bg_message),统一存储系统消息(1)、运营消息(2)、订单消息(3),支持指定用户(targetType=1)和全员推送(targetType=2),可关联业务(bizType+bizId)。 | +| `MessageRead` | manager | 消息已读状态表(bg_message_read),记录用户对消息的已读状态,唯一索引(message_id, user_id)。 | ## 4️⃣ 权限体系设计 ### 整体架构 diff --git a/client-api/src/main/java/org/jiayunet/controller/MessageController.java b/client-api/src/main/java/org/jiayunet/controller/MessageController.java new file mode 100644 index 0000000..b6ef6c6 --- /dev/null +++ b/client-api/src/main/java/org/jiayunet/controller/MessageController.java @@ -0,0 +1,58 @@ +package org.jiayunet.controller; + +import lombok.AllArgsConstructor; +import org.jiayunet.pojo.PageResult; +import org.jiayunet.pojo.dto.message.MessageDto; +import org.jiayunet.pojo.dto.message.MessageUnreadCountDto; +import org.jiayunet.pojo.param.message.MessageQueryParam; +import org.jiayunet.service.MessageQueryService; +import org.jiayunet.service.MessageService; +import org.jiayunet.tool.UserSecurityTool; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +/** + * 站内信消息接口 + * + * @author zk + */ +@RestController +@RequestMapping("/message") +@AllArgsConstructor +public class MessageController { + + private final MessageQueryService messageQueryService; + private final MessageService messageService; + + /** + * 分页查询消息列表 + */ + @GetMapping("/list") + public PageResult listMessages(@Validated MessageQueryParam param) { + return messageQueryService.listMessages(param, UserSecurityTool.getUserId()); + } + + /** + * 查询未读消息数 + */ + @GetMapping("/unread-count") + public Long countUnread() { + return messageQueryService.countUnread(UserSecurityTool.getUserId()); + } + + /** + * 按类型查询未读消息数 + */ + @GetMapping("/unread-count-by-type") + public MessageUnreadCountDto countUnreadByType() { + return messageQueryService.countUnreadByType(UserSecurityTool.getUserId()); + } + + /** + * 标记消息已读 + */ + @PostMapping("/read/{messageId}") + public void markRead(@PathVariable Long messageId) { + messageService.markRead(messageId, UserSecurityTool.getUserId()); + } +} diff --git a/client-api/src/main/java/org/jiayunet/pojo/dto/message/MessageDto.java b/client-api/src/main/java/org/jiayunet/pojo/dto/message/MessageDto.java new file mode 100644 index 0000000..e05c0cd --- /dev/null +++ b/client-api/src/main/java/org/jiayunet/pojo/dto/message/MessageDto.java @@ -0,0 +1,37 @@ +package org.jiayunet.pojo.dto.message; + +import lombok.Data; + +import java.time.Instant; + +/** + * 消息列表出参 + * + * @author zk + */ +@Data +public class MessageDto { + + private Long id; + + /** 消息类型 1=系统消息 2=运营消息 3=订单消息 */ + private Integer type; + + /** 消息标题 */ + private String title; + + /** 消息内容 */ + private String content; + + /** 关联业务类型 */ + private String bizType; + + /** 关联业务ID */ + private Long bizId; + + /** 是否已读 */ + private Boolean read; + + /** 创建时间 */ + private Instant createTime; +} diff --git a/client-api/src/main/java/org/jiayunet/pojo/dto/message/MessageUnreadCountDto.java b/client-api/src/main/java/org/jiayunet/pojo/dto/message/MessageUnreadCountDto.java new file mode 100644 index 0000000..42c69e2 --- /dev/null +++ b/client-api/src/main/java/org/jiayunet/pojo/dto/message/MessageUnreadCountDto.java @@ -0,0 +1,24 @@ +package org.jiayunet.pojo.dto.message; + +import lombok.Data; + +/** + * 各类型消息未读数出参 + * + * @author zk + */ +@Data +public class MessageUnreadCountDto { + + /** 总未读数 */ + private Long total; + + /** 系统消息未读数 */ + private Long system; + + /** 运营消息未读数 */ + private Long operation; + + /** 订单消息未读数 */ + private Long order; +} diff --git a/client-api/src/main/java/org/jiayunet/pojo/param/message/MessageQueryParam.java b/client-api/src/main/java/org/jiayunet/pojo/param/message/MessageQueryParam.java new file mode 100644 index 0000000..e02ca34 --- /dev/null +++ b/client-api/src/main/java/org/jiayunet/pojo/param/message/MessageQueryParam.java @@ -0,0 +1,18 @@ +package org.jiayunet.pojo.param.message; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.jiayunet.pojo.PageParam; + +/** + * 消息分页查询入参 + * + * @author zk + */ +@Data +@EqualsAndHashCode(callSuper = true) +public class MessageQueryParam extends PageParam { + + /** 消息类型筛选,null=全部 */ + private Integer type; +} diff --git a/client-api/src/main/java/org/jiayunet/service/MessageQueryService.java b/client-api/src/main/java/org/jiayunet/service/MessageQueryService.java new file mode 100644 index 0000000..00def88 --- /dev/null +++ b/client-api/src/main/java/org/jiayunet/service/MessageQueryService.java @@ -0,0 +1,90 @@ +package org.jiayunet.service; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import org.jiayunet.mapper.MessageMapper; +import org.jiayunet.mapper.MessageReadMapper; +import org.jiayunet.pojo.PageResult; +import org.jiayunet.pojo.dto.message.MessageDto; +import org.jiayunet.pojo.dto.message.MessageUnreadCountDto; +import org.jiayunet.pojo.param.message.MessageQueryParam; +import org.jiayunet.pojo.po.Message; +import org.jiayunet.pojo.po.MessageRead; +import org.springframework.beans.BeanUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +/** + * C端消息查询服务 + *

提供用户消息分页列表和未读数查询

+ *

依赖:MessageMapper、MessageReadMapper

+ *

使用表:bg_message(查询消息)、bg_message_read(查询已读状态)

+ * + * @author zk + */ +@Service +public class MessageQueryService { + + @Autowired + private MessageMapper messageMapper; + + @Autowired + private MessageReadMapper messageReadMapper; + + /** + * 分页查询用户消息列表 + *

1. 查询用户可见消息(指定用户+全员)分页 2. 批量查已读状态 3. 组装DTO返回

+ */ + public PageResult listMessages(MessageQueryParam param, Long userId) { + Page page = messageMapper.selectPage(param.toPage(), new LambdaQueryWrapper() + .and(w -> w.eq(Message::getUserId, userId).or().eq(Message::getTargetType, 2)) + .eq(param.getType() != null, Message::getType, param.getType()).orderByDesc(Message::getCreateTime)); + + List messageIds = page.getRecords().stream().map(Message::getId).collect(Collectors.toList()); + + // 批量查已读状态 + Set readIds = messageIds.isEmpty() ? Set.of() : messageReadMapper.selectList(new LambdaQueryWrapper().eq(MessageRead::getUserId, userId).in(MessageRead::getMessageId, messageIds)).stream().map(MessageRead::getMessageId).collect(Collectors.toSet()); + + List dtoList = page.getRecords().stream().map(msg -> { + MessageDto dto = new MessageDto(); + BeanUtils.copyProperties(msg, dto); + dto.setRead(readIds.contains(msg.getId())); + return dto; + }).collect(Collectors.toList()); + + return new PageResult<>(page.getCurrent(), page.getSize(), page.getTotal(), dtoList); + } + + /** + * 查询未读消息数 + */ + public Long countUnread(Long userId) { + Long totalCount = messageMapper.selectCount(new LambdaQueryWrapper().and(w -> w.eq(Message::getUserId, userId).or().eq(Message::getTargetType, 2))); + Long readCount = messageReadMapper.selectCount(new LambdaQueryWrapper().eq(MessageRead::getUserId, userId)); + return Math.max(0, totalCount - readCount); + } + + /** + * 按类型查询未读消息数 + *

分别统计系统消息(1)、运营消息(2)、订单消息(3)的未读数量

+ */ + public MessageUnreadCountDto countUnreadByType(Long userId) { + Set readIds = messageReadMapper.selectList(new LambdaQueryWrapper().eq(MessageRead::getUserId, userId).select(MessageRead::getMessageId)).stream().map(MessageRead::getMessageId).collect(Collectors.toSet()); + + Map unreadMap = messageMapper.selectList(new LambdaQueryWrapper() + .and(w -> w.eq(Message::getUserId, userId).or().eq(Message::getTargetType, 2)).select(Message::getId, Message::getType)) + .stream().filter(msg -> !readIds.contains(msg.getId())).collect(Collectors.groupingBy(Message::getType, Collectors.counting())); + + MessageUnreadCountDto dto = new MessageUnreadCountDto(); + dto.setSystem(unreadMap.getOrDefault(1, 0L)); + dto.setOperation(unreadMap.getOrDefault(2, 0L)); + dto.setOrder(unreadMap.getOrDefault(3, 0L)); + dto.setTotal(dto.getSystem() + dto.getOperation() + dto.getOrder()); + return dto; + } +} diff --git a/manager/src/main/java/org/jiayunet/mapper/MessageMapper.java b/manager/src/main/java/org/jiayunet/mapper/MessageMapper.java new file mode 100644 index 0000000..ff0832b --- /dev/null +++ b/manager/src/main/java/org/jiayunet/mapper/MessageMapper.java @@ -0,0 +1,13 @@ +package org.jiayunet.mapper; + +import org.apache.ibatis.annotations.Mapper; +import org.jiayunet.pojo.po.Message; + +/** + * 站内信消息Mapper + * + * @author zk + */ +@Mapper +public interface MessageMapper extends CommonMapper { +} diff --git a/manager/src/main/java/org/jiayunet/mapper/MessageReadMapper.java b/manager/src/main/java/org/jiayunet/mapper/MessageReadMapper.java new file mode 100644 index 0000000..d95587d --- /dev/null +++ b/manager/src/main/java/org/jiayunet/mapper/MessageReadMapper.java @@ -0,0 +1,13 @@ +package org.jiayunet.mapper; + +import org.apache.ibatis.annotations.Mapper; +import org.jiayunet.pojo.po.MessageRead; + +/** + * 消息已读状态Mapper + * + * @author zk + */ +@Mapper +public interface MessageReadMapper extends CommonMapper { +} diff --git a/manager/src/main/java/org/jiayunet/pojo/po/Message.java b/manager/src/main/java/org/jiayunet/pojo/po/Message.java new file mode 100644 index 0000000..dce0428 --- /dev/null +++ b/manager/src/main/java/org/jiayunet/pojo/po/Message.java @@ -0,0 +1,46 @@ +package org.jiayunet.pojo.po; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; + +import java.time.Instant; + +/** + * 站内信消息表(bg_message) + *

统一存储系统消息、运营消息、订单消息

+ * + * @author zk + */ +@Data +@TableName("bg_message") +public class Message { + + @TableId(type = IdType.ASSIGN_ID) + private Long id; + + /** 消息类型 1=系统消息 2=运营消息 3=订单消息 */ + private Integer type; + + /** 目标类型 1=指定用户 2=全部用户 */ + private Integer targetType; + + /** 目标用户ID,targetType=1时必填,2时为NULL */ + private Long userId; + + /** 消息标题 */ + private String title; + + /** 消息内容 */ + private String content; + + /** 关联业务类型,如 order/resume_diagnose */ + private String bizType; + + /** 关联业务ID,配合 bizType 跳转 */ + private Long bizId; + + /** 创建时间 */ + private Instant createTime; +} diff --git a/manager/src/main/java/org/jiayunet/pojo/po/MessageRead.java b/manager/src/main/java/org/jiayunet/pojo/po/MessageRead.java new file mode 100644 index 0000000..9f82800 --- /dev/null +++ b/manager/src/main/java/org/jiayunet/pojo/po/MessageRead.java @@ -0,0 +1,31 @@ +package org.jiayunet.pojo.po; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; + +import java.time.Instant; + +/** + * 消息已读状态表(bg_message_read) + *

记录用户对消息的已读状态,用户读了才插入一条记录

+ * + * @author zk + */ +@Data +@TableName("bg_message_read") +public class MessageRead { + + @TableId(type = IdType.ASSIGN_ID) + private Long id; + + /** 消息ID,关联 bg_message.id */ + private Long messageId; + + /** 用户ID */ + private Long userId; + + /** 阅读时间 */ + private Instant readTime; +} diff --git a/manager/src/main/java/org/jiayunet/service/MessageService.java b/manager/src/main/java/org/jiayunet/service/MessageService.java new file mode 100644 index 0000000..c075568 --- /dev/null +++ b/manager/src/main/java/org/jiayunet/service/MessageService.java @@ -0,0 +1,76 @@ +package org.jiayunet.service; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import org.jiayunet.mapper.MessageMapper; +import org.jiayunet.mapper.MessageReadMapper; +import org.jiayunet.pojo.po.Message; +import org.jiayunet.pojo.po.MessageRead; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.Assert; + +import java.time.Instant; + +/** + * 站内信消息服务 + *

提供消息发送和已读标记能力,供 C 端和 B 端共用

+ *

使用表:bg_message(消息主表)、bg_message_read(已读状态表)

+ * + * @author zk + */ +@Service +public class MessageService { + + @Autowired + private MessageMapper messageMapper; + + @Autowired + private MessageReadMapper messageReadMapper; + + /** + * 发送消息(指定用户) + */ + @Transactional(rollbackFor = Exception.class) + public void sendToUser(Integer type, Long userId, String title, String content, String bizType, Long bizId) { + Assert.notNull(userId, "目标用户ID不能为空"); + Message msg = new Message(); + msg.setType(type); + msg.setTargetType(1); + msg.setUserId(userId); + msg.setTitle(title); + msg.setContent(content); + msg.setBizType(bizType); + msg.setBizId(bizId); + msg.setCreateTime(Instant.now()); + messageMapper.insert(msg); + } + + /** + * 发送消息(全部用户) + */ + @Transactional(rollbackFor = Exception.class) + public void sendToAll(Integer type, String title, String content) { + Message msg = new Message(); + msg.setType(type); + msg.setTargetType(2); + msg.setTitle(title); + msg.setContent(content); + msg.setCreateTime(Instant.now()); + messageMapper.insert(msg); + } + + /** + * 标记消息已读(幂等,重复调用不报错) + */ + @Transactional(rollbackFor = Exception.class) + public void markRead(Long messageId, Long userId) { + Long count = messageReadMapper.selectCount(new LambdaQueryWrapper().eq(MessageRead::getMessageId, messageId).eq(MessageRead::getUserId, userId)); + if (count > 0) return; + MessageRead read = new MessageRead(); + read.setMessageId(messageId); + read.setUserId(userId); + read.setReadTime(Instant.now()); + messageReadMapper.insert(read); + } +}