feat: 重构公告及消息,公告支持系统消息推送提醒、定时发布、置顶、记录读取状态

This commit is contained in:
2025-05-20 22:35:31 +08:00
parent e2deb99b78
commit 0f3e94f32f
47 changed files with 1288 additions and 563 deletions

View File

@@ -12,3 +12,8 @@ VALUES ('admin', '465c194afb65670f38322df087f0a9bb225cc257e43eb4ac5a0c98ef5b3173
-- 默认分组continew-admin
INSERT INTO `sj_group_config` (`id`, `namespace_id`, `group_name`, `description`, `token`, `group_status`, `version`, `group_partition`, `id_generator_mode`, `init_scene`, `create_dt`, `update_dt`)
VALUES (1, '764d604ec6fc45f68cd92514c40e9e1a', 'continew-admin', '默认分组', 'SJ_Wyz3dmsdbDOkDujOTSSoBjGQP1BMsVnj', 1, 1, 0, 2, 1, NOW(), NOW());
-- 默认任务NoticePublishJob
INSERT INTO `sj_job`
(`namespace_id`, `group_name`, `job_name`, `args_type`, `next_trigger_at`, `job_status`, `task_type`, `route_key`, `executor_type`, `executor_info`, `trigger_type`, `trigger_interval`, `block_strategy`, `executor_timeout`, `max_retry_times`, `parallel_num`, `retry_interval`, `bucket_index`, `resident`, `notify_ids`, `owner_id`, `description`, `ext_attrs`, `deleted`, `create_dt`, `update_dt`)
VALUES ('764d604ec6fc45f68cd92514c40e9e1a', 'continew-admin', '公告发布', 1, 1747546500000, 1, 1, 4, 1, 'NoticePublishJob', 3, '0 * * * * ?', 1, 60, 3, 1, 1, 27, 0, '', NULL, '定时发布公告', '', 0, NOW(), NOW());

View File

@@ -12,3 +12,8 @@ VALUES ('admin', '465c194afb65670f38322df087f0a9bb225cc257e43eb4ac5a0c98ef5b3173
-- 默认分组continew-admin
INSERT INTO sj_group_config (id, namespace_id, group_name, description, token, group_status, version, group_partition, id_generator_mode, init_scene, create_dt, update_dt)
VALUES (1, '764d604ec6fc45f68cd92514c40e9e1a', 'continew-admin', '默认分组', 'SJ_Wyz3dmsdbDOkDujOTSSoBjGQP1BMsVnj', 1, 1, 0, 2, 1, NOW(), NOW());
-- 默认任务NoticePublishJob
INSERT INTO sj_job
(namespace_id, group_name, job_name, args_type, next_trigger_at, job_status, task_type, route_key, executor_type, executor_info, trigger_type, trigger_interval, block_strategy, executor_timeout, max_retry_times, parallel_num, retry_interval, bucket_index, resident, notify_ids, owner_id, description, ext_attrs, deleted, create_dt, update_dt)
VALUES ('764d604ec6fc45f68cd92514c40e9e1a', 'continew-admin', '公告发布', 1, 1747546500000, 1, 1, 4, 1, 'NoticePublishJob', 3, '0 * * * * ?', 1, 60, 3, 1, 1, 27, 0, '', NULL, '定时发布公告', '', 0, NOW(), NOW());

View File

@@ -24,7 +24,6 @@ import cn.hutool.core.util.RandomUtil;
import cn.hutool.core.util.ReUtil;
import cn.hutool.json.JSONUtil;
import com.xkcoding.justauth.autoconfigure.JustAuthProperties;
import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
import me.zhyd.oauth.AuthRequestBuilder;
@@ -54,11 +53,9 @@ import top.continew.admin.system.service.UserSocialService;
import top.continew.starter.core.autoconfigure.project.ProjectProperties;
import top.continew.starter.core.exception.BadRequestException;
import top.continew.starter.core.validation.ValidationUtils;
import top.continew.starter.messaging.websocket.util.WebSocketUtils;
import java.time.LocalDateTime;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
/**
@@ -168,15 +165,10 @@ public class SocialLoginHandler extends AbstractLoginHandler<SocialLoginReq> {
* @param user 用户信息
*/
private void sendSecurityMsg(UserDO user) {
MessageReq req = new MessageReq();
MessageTemplateEnum socialRegister = MessageTemplateEnum.SOCIAL_REGISTER;
req.setTitle(socialRegister.getTitle().formatted(projectProperties.getName()));
req.setContent(socialRegister.getContent().formatted(user.getNickname()));
req.setType(MessageTypeEnum.SECURITY);
messageService.add(req, CollUtil.toList(user.getId()));
List<String> tokenList = StpUtil.getTokenValueListByLoginId(user.getId());
for (String token : tokenList) {
WebSocketUtils.sendMessage(token, "1");
}
MessageTemplateEnum template = MessageTemplateEnum.SOCIAL_REGISTER;
MessageReq req = new MessageReq(MessageTypeEnum.SECURITY);
req.setTitle(template.getTitle().formatted(projectProperties.getName()));
req.setContent(template.getContent().formatted(user.getNickname()));
messageService.add(req, CollUtil.toList(user.getId().toString()));
}
}

View File

@@ -32,8 +32,14 @@ public enum MessageTemplateEnum {
/**
* 第三方登录
*/
SOCIAL_REGISTER("欢迎注册 %s", "尊敬的 %s欢迎注册使用请及时配置您的密码。");
SOCIAL_REGISTER("欢迎注册 %s", "尊敬的 %s欢迎注册使用请及时配置您的密码。", "/user/profile"),
/**
* 公告发布
*/
NOTICE_PUBLISH("您有一条新的公告", "公告《%s》已发布请及时查看。", "/user/notice?id=%s");
private final String title;
private final String content;
private final String path;
}

View File

@@ -31,10 +31,15 @@ import top.continew.starter.core.enums.BaseEnum;
@RequiredArgsConstructor
public enum MessageTypeEnum implements BaseEnum<Integer> {
/**
* 系统消息
*/
SYSTEM(1, "系统消息", UiConstants.COLOR_PRIMARY),
/**
* 安全消息
*/
SECURITY(1, "安全消息", UiConstants.COLOR_PRIMARY),;
SECURITY(2, "安全消息", UiConstants.COLOR_WARNING),;
private final Integer value;
private final String description;

View File

@@ -0,0 +1,40 @@
/*
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package top.continew.admin.system.enums;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import top.continew.starter.core.enums.BaseEnum;
/**
* 公告通知方式枚举
*
* @author Charles7c
* @since 2025/5/8 21:18
*/
@Getter
@RequiredArgsConstructor
public enum NoticeMethodEnum implements BaseEnum<Integer> {
/**
* 系统消息
*/
SYSTEM_MESSAGE(1, "系统消息"),;
private final Integer value;
private final String description;
}

View File

@@ -21,8 +21,6 @@ import lombok.RequiredArgsConstructor;
import top.continew.admin.common.constant.UiConstants;
import top.continew.starter.core.enums.BaseEnum;
import java.time.LocalDateTime;
/**
* 公告状态枚举
*
@@ -33,40 +31,22 @@ import java.time.LocalDateTime;
@RequiredArgsConstructor
public enum NoticeStatusEnum implements BaseEnum<Integer> {
/**
* 草稿
*/
DRAFT(1, "草稿", UiConstants.COLOR_WARNING),
/**
* 待发布
*/
PENDING_RELEASE(1, "待发布", UiConstants.COLOR_PRIMARY),
PENDING(2, "待发布", UiConstants.COLOR_PRIMARY),
/**
* 已发布
*/
PUBLISHED(2, "已发布", UiConstants.COLOR_SUCCESS),
/**
* 已过期
*/
EXPIRED(3, "已过期", UiConstants.COLOR_ERROR),;
PUBLISHED(3, "已发布", UiConstants.COLOR_SUCCESS),;
private final Integer value;
private final String description;
private final String color;
/**
* 获取公告状态
*
* @param effectiveTime 生效时间
* @param terminateTime 终止时间
* @return 公告状态
*/
public static NoticeStatusEnum getStatus(LocalDateTime effectiveTime, LocalDateTime terminateTime) {
LocalDateTime now = LocalDateTime.now();
if (effectiveTime != null && effectiveTime.isAfter(now)) {
return PENDING_RELEASE;
}
if (terminateTime != null && terminateTime.isBefore(now)) {
return EXPIRED;
}
return PUBLISHED;
}
}

View File

@@ -16,24 +16,15 @@
package top.continew.admin.system.mapper;
import org.apache.ibatis.annotations.Param;
import top.continew.admin.system.model.entity.MessageUserDO;
import top.continew.admin.system.model.entity.MessageLogDO;
import top.continew.starter.data.mp.base.BaseMapper;
/**
* 消息和用户 Mapper
* 消息日志 Mapper
*
* @author Bull-BCLS
* @author Charles7c
* @since 2023/10/15 20:25
*/
public interface MessageUserMapper extends BaseMapper<MessageUserDO> {
/**
* 根据用户 ID 和消息类型查询未读消息数量
*
* @param userId 用户 ID
* @param type 消息类型
* @return 未读消息信息
*/
Long selectUnreadCountByUserIdAndType(@Param("userId") Long userId, @Param("type") Integer type);
public interface MessageLogMapper extends BaseMapper<MessageLogDO> {
}

View File

@@ -16,14 +16,16 @@
package top.continew.admin.system.mapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.toolkit.Constants;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import org.apache.ibatis.annotations.Param;
import top.continew.admin.system.model.entity.MessageDO;
import top.continew.admin.system.model.query.MessageQuery;
import top.continew.admin.system.model.resp.message.MessageResp;
import top.continew.starter.data.mp.base.BaseMapper;
import java.util.List;
/**
* 消息 Mapper
*
@@ -33,12 +35,28 @@ import top.continew.starter.data.mp.base.BaseMapper;
public interface MessageMapper extends BaseMapper<MessageDO> {
/**
* 分页查询列表
* 分页查询消息列表
*
* @param page 分页查询条件
* @param queryWrapper 查询条件
* @return 分页信息
* @param page 分页参数
* @param query 查询条件
* @return 消息列表
*/
IPage<MessageResp> selectPageByUserId(@Param("page") IPage<Object> page,
@Param(Constants.WRAPPER) QueryWrapper<MessageDO> queryWrapper);
IPage<MessageResp> selectMessagePage(@Param("page") Page<MessageDO> page, @Param("query") MessageQuery query);
/**
* 查询未读消息列表
*
* @param userId 用户 ID
* @return 消息列表
*/
List<MessageDO> selectUnreadListByUserId(@Param("userId") Long userId);
/**
* 查询未读消息数量
*
* @param userId 用户 ID
* @param type 消息类型
* @return 未读消息数量
*/
Long selectUnreadCountByUserIdAndType(@Param("userId") Long userId, @Param("type") Integer type);
}

View File

@@ -0,0 +1,29 @@
/*
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package top.continew.admin.system.mapper;
import top.continew.admin.system.model.entity.NoticeLogDO;
import top.continew.starter.data.mp.base.BaseMapper;
/**
* 公告日志 Mapper
*
* @author Charles7c
* @since 2025/5/18 19:17
*/
public interface NoticeLogMapper extends BaseMapper<NoticeLogDO> {
}

View File

@@ -21,8 +21,8 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import org.apache.ibatis.annotations.Param;
import top.continew.admin.system.model.entity.NoticeDO;
import top.continew.admin.system.model.query.NoticeQuery;
import top.continew.admin.system.model.resp.NoticeDetailResp;
import top.continew.admin.system.model.resp.dashboard.DashboardNoticeResp;
import top.continew.admin.system.model.resp.notice.NoticeResp;
import top.continew.starter.data.mp.base.BaseMapper;
import java.util.List;
@@ -35,14 +35,6 @@ import java.util.List;
*/
public interface NoticeMapper extends BaseMapper<NoticeDO> {
/**
* 查询仪表盘公告列表
*
* @param userId 用户 ID
* @return 仪表盘公告列表
*/
List<DashboardNoticeResp> selectDashboardList(@Param("userId") Long userId);
/**
* 分页查询公告列表
*
@@ -50,5 +42,21 @@ public interface NoticeMapper extends BaseMapper<NoticeDO> {
* @param query 查询条件
* @return 公告列表
*/
IPage<NoticeDetailResp> selectNoticePage(@Param("page") Page<NoticeDO> page, @Param("query") NoticeQuery query);
IPage<NoticeResp> selectNoticePage(@Param("page") Page<NoticeDO> page, @Param("query") NoticeQuery query);
/**
* 查询未读公告数量
*
* @param userId 用户 ID
* @return 未读公告数量
*/
Long selectUnreadCountByUserId(@Param("userId") Long userId);
/**
* 查询仪表盘公告列表
*
* @param userId 用户 ID
* @return 仪表盘公告列表
*/
List<DashboardNoticeResp> selectDashboardList(@Param("userId") Long userId);
}

View File

@@ -20,12 +20,15 @@ import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler;
import lombok.Data;
import top.continew.admin.system.enums.MessageTypeEnum;
import top.continew.admin.system.enums.NoticeScopeEnum;
import java.io.Serial;
import java.io.Serializable;
import java.time.LocalDateTime;
import java.util.List;
/**
* 消息实体
@@ -57,15 +60,25 @@ public class MessageDO implements Serializable {
private String content;
/**
* 类型1系统消息
* 类型
*/
private MessageTypeEnum type;
/**
* 创建人
* 跳转路径
*/
@TableField(fill = FieldFill.INSERT)
private Long createUser;
private String path;
/**
* 通知范围
*/
private NoticeScopeEnum scope;
/**
* 通知用户
*/
@TableField(typeHandler = JacksonTypeHandler.class)
private List<String> users;
/**
* 创建时间

View File

@@ -18,20 +18,23 @@ package top.continew.admin.system.model.entity;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serial;
import java.io.Serializable;
import java.time.LocalDateTime;
/**
* 消息和用户关联实体
* 消息日志实体
*
* @author Charles7c
* @author Bull-BCLS
* @since 2023/10/15 20:25
*/
@Data
@TableName("sys_message_user")
public class MessageUserDO implements Serializable {
@NoArgsConstructor
@TableName("sys_message_log")
public class MessageLogDO implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
@@ -46,13 +49,14 @@ public class MessageUserDO implements Serializable {
*/
private Long userId;
/**
* 是否已读
*/
private Boolean isRead;
/**
* 读取时间
*/
private LocalDateTime readTime;
public MessageLogDO(Long messageId, Long userId, LocalDateTime readTime) {
this.messageId = messageId;
this.userId = userId;
this.readTime = readTime;
}
}

View File

@@ -22,6 +22,7 @@ import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler;
import lombok.Data;
import top.continew.admin.common.model.entity.BaseDO;
import top.continew.admin.system.enums.NoticeScopeEnum;
import top.continew.admin.system.enums.NoticeStatusEnum;
import java.io.Serial;
import java.time.LocalDateTime;
@@ -51,20 +52,10 @@ public class NoticeDO extends BaseDO {
private String content;
/**
* 类型
* 分类(取值于字典 notice_type
*/
private String type;
/**
* 生效时间
*/
private LocalDateTime effectiveTime;
/**
* 终止时间
*/
private LocalDateTime terminateTime;
/**
* 通知范围
*/
@@ -75,4 +66,30 @@ public class NoticeDO extends BaseDO {
*/
@TableField(typeHandler = JacksonTypeHandler.class)
private List<String> noticeUsers;
/**
* 通知方式
*/
@TableField(typeHandler = JacksonTypeHandler.class)
private List<Integer> noticeMethods;
/**
* 是否定时
*/
private Boolean isTiming;
/**
* 发布时间
*/
private LocalDateTime publishTime;
/**
* 是否置顶
*/
private Boolean isTop;
/**
* 状态
*/
private NoticeStatusEnum status;
}

View File

@@ -0,0 +1,61 @@
/*
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package top.continew.admin.system.model.entity;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serial;
import java.io.Serializable;
import java.time.LocalDateTime;
/**
* 公告日志实体
*
* @author Charles7c
* @since 2025/5/18 19:16
*/
@Data
@NoArgsConstructor
@TableName("sys_notice_log")
public class NoticeLogDO implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/**
* 公告 ID
*/
private Long noticeId;
/**
* 用户 ID
*/
private Long userId;
/**
* 读取时间
*/
private LocalDateTime readTime;
public NoticeLogDO(Long noticeId, Long userId, LocalDateTime readTime) {
this.noticeId = noticeId;
this.userId = userId;
this.readTime = readTime;
}
}

View File

@@ -18,10 +18,6 @@ package top.continew.admin.system.model.query;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import top.continew.admin.system.enums.MessageTypeEnum;
import top.continew.starter.data.core.annotation.Query;
import top.continew.starter.data.core.annotation.QueryIgnore;
import top.continew.starter.data.core.enums.QueryType;
import java.io.Serial;
import java.io.Serializable;
@@ -49,26 +45,23 @@ public class MessageQuery implements Serializable {
* 标题
*/
@Schema(description = "标题", example = "欢迎注册 xxx")
@Query(type = QueryType.LIKE)
private String title;
/**
* 类型
*/
@Schema(description = "类型", example = "1")
private MessageTypeEnum type;
private Integer type;
/**
* 是否已读
*/
@Schema(description = "是否已读", example = "true")
@QueryIgnore
private Boolean isRead;
/**
* 用户 ID
*/
@Schema(hidden = true)
@QueryIgnore
private Long userId;
}

View File

@@ -16,11 +16,8 @@
package top.continew.admin.system.model.query;
import com.fasterxml.jackson.annotation.JsonIgnore;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import top.continew.starter.data.core.annotation.Query;
import top.continew.starter.data.core.enums.QueryType;
import java.io.Serial;
import java.io.Serializable;
@@ -42,18 +39,17 @@ public class NoticeQuery implements Serializable {
* 标题
*/
@Schema(description = "标题", example = "这是公告标题")
@Query(type = QueryType.LIKE)
private String title;
/**
* 类型
* 分类(取值于字典 notice_type
*/
@Schema(description = "类型", example = "1")
@Schema(description = "分类(取值于字典 notice_type", example = "1")
private String type;
/**
* 用户 ID
*/
@JsonIgnore
@Schema(hidden = true)
private Long userId;
}

View File

@@ -20,6 +20,7 @@ import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.hibernate.validator.constraints.Length;
import top.continew.admin.system.enums.MessageTypeEnum;
@@ -33,6 +34,7 @@ import java.io.Serializable;
* @since 2023/10/15 19:05
*/
@Data
@NoArgsConstructor
@Schema(description = "消息创建请求参数")
public class MessageReq implements Serializable {
@@ -58,7 +60,17 @@ public class MessageReq implements Serializable {
/**
* 类型
*/
@Schema(description = "类型1系统消息", example = "1")
@Schema(description = "类型", example = "1")
@NotNull(message = "类型无效")
private MessageTypeEnum type;
/**
* 跳转路径
*/
@Schema(description = "跳转路径", example = "/user/profile")
private String path;
public MessageReq(MessageTypeEnum type) {
this.type = type;
}
}

View File

@@ -17,12 +17,12 @@
package top.continew.admin.system.model.req;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.Future;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import org.hibernate.validator.constraints.Length;
import top.continew.admin.system.enums.NoticeScopeEnum;
import top.continew.admin.system.enums.NoticeStatusEnum;
import java.io.Serial;
import java.io.Serializable;
@@ -58,26 +58,13 @@ public class NoticeReq implements Serializable {
private String content;
/**
* 类(取值于字典 notice_type
* 类(取值于字典 notice_type
*/
@Schema(description = "(取值于字典 notice_type", example = "1")
@NotBlank(message = "不能为空")
@Length(max = 30, message = "长度不能超过 {max} 个字符")
@Schema(description = "类(取值于字典 notice_type", example = "1")
@NotBlank(message = "类不能为空")
@Length(max = 30, message = "类长度不能超过 {max} 个字符")
private String type;
/**
* 生效时间
*/
@Schema(description = "生效时间", example = "2023-08-08 00:00:00", type = "string")
private LocalDateTime effectiveTime;
/**
* 终止时间
*/
@Schema(description = "终止时间", example = "2023-08-08 23:59:59", type = "string")
@Future(message = "终止时间必须是未来时间")
private LocalDateTime terminateTime;
/**
* 通知范围
*/
@@ -86,8 +73,38 @@ public class NoticeReq implements Serializable {
private NoticeScopeEnum noticeScope;
/**
* 指定用户
* 通知用户
*/
@Schema(description = "指定用户", example = "[1,2,3]")
@Schema(description = "通知用户", example = "[1,2,3]")
private List<String> noticeUsers;
/**
* 通知方式
*/
@Schema(description = "通知方式", example = "[1,2]")
private List<Integer> noticeMethods;
/**
* 是否定时
*/
@Schema(description = "是否定时", example = "false")
private Boolean isTiming;
/**
* 发布时间
*/
@Schema(description = "发布时间", example = "2023-08-08 00:00:00", type = "string")
private LocalDateTime publishTime;
/**
* 是否置顶
*/
@Schema(description = "是否置顶", example = "false")
private Boolean isTop;
/**
* 状态
*/
@Schema(description = "状态", example = "3")
private NoticeStatusEnum status;
}

View File

@@ -52,4 +52,10 @@ public class DashboardNoticeResp implements Serializable {
*/
@Schema(description = "类型(取值于字典 notice_type", example = "1")
private String type;
/**
* 是否置顶
*/
@Schema(description = "是否置顶", example = "false")
private Boolean isTop;
}

View File

@@ -16,11 +16,8 @@
package top.continew.admin.system.model.resp.message;
import cn.crane4j.annotation.Assemble;
import com.fasterxml.jackson.annotation.JsonIgnore;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import top.continew.admin.common.constant.ContainerConstants;
import top.continew.admin.system.enums.MessageTypeEnum;
import java.io.Serial;
@@ -61,9 +58,15 @@ public class MessageResp implements Serializable {
/**
* 类型
*/
@Schema(description = "类型1系统消息", example = "1")
@Schema(description = "类型", example = "1")
private MessageTypeEnum type;
/**
* 跳转路径
*/
@Schema(description = "跳转路径", example = "/user/profile")
private String path;
/**
* 是否已读
*/
@@ -76,19 +79,6 @@ public class MessageResp implements Serializable {
@Schema(description = "读取时间", example = "2023-08-08 23:59:59", type = "string")
private LocalDateTime readTime;
/**
* 创建人
*/
@JsonIgnore
@Assemble(prop = ":createUserString", container = ContainerConstants.USER_NICKNAME)
private Long createUser;
/**
* 创建人
*/
@Schema(description = "创建人", example = "超级管理员")
private String createUserString;
/**
* 创建时间
*/

View File

@@ -39,7 +39,7 @@ public class MessageTypeUnreadResp implements Serializable {
/**
* 类型
*/
@Schema(description = "类型1系统消息", example = "1")
@Schema(description = "类型", example = "1")
private MessageTypeEnum type;
/**

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
package top.continew.admin.system.model.resp;
package top.continew.admin.system.model.resp.notice;
import com.alibaba.excel.annotation.ExcelIgnoreUnannotated;
import com.alibaba.excel.annotation.ExcelProperty;
@@ -52,6 +52,14 @@ public class NoticeDetailResp extends BaseDetailResp {
@ExcelProperty(value = "标题", order = 2)
private String title;
/**
* 分类取值于字典 notice_type
*/
@Schema(description = "分类(取值于字典 notice_type", example = "1")
@ExcelProperty(value = "分类", converter = ExcelDictConverter.class, order = 3)
@DictExcelProperty("notice_type")
private String type;
/**
* 内容
*/
@@ -59,47 +67,49 @@ public class NoticeDetailResp extends BaseDetailResp {
private String content;
/**
* 类型取值于字典 notice_type
* 通知范围
*/
@Schema(description = "类型(取值于字典 notice_type", example = "1")
@ExcelProperty(value = "类型", converter = ExcelDictConverter.class, order = 3)
@DictExcelProperty("notice_type")
private String type;
@Schema(description = "通知范围", example = "2")
@ExcelProperty(value = "通知范围", converter = ExcelBaseEnumConverter.class, order = 4)
private NoticeScopeEnum noticeScope;
/**
* 通知用户
*/
@Schema(description = "通知用户", example = "[1,2,3]")
private List<String> noticeUsers;
/**
* 通知方式
*/
@Schema(description = "通知方式", example = "[1,2]")
private List<Integer> noticeMethods;
/**
* 是否定时
*/
@Schema(description = "是否定时", example = "false")
@ExcelProperty(value = "是否定时", order = 5)
private Boolean isTiming;
/**
* 发布时间
*/
@Schema(description = "发布时间", example = "2023-08-08 00:00:00", type = "string")
@ExcelProperty(value = "发布时间", order = 6)
private LocalDateTime publishTime;
/**
* 是否置顶
*/
@Schema(description = "是否置顶", example = "false")
@ExcelProperty(value = "是否置顶", order = 7)
private Boolean isTop;
/**
* 状态
*/
@Schema(description = "状态", example = "1")
@ExcelProperty(value = "状态", converter = ExcelBaseEnumConverter.class, order = 4)
@Schema(description = "状态", example = "3")
@ExcelProperty(value = "状态", converter = ExcelBaseEnumConverter.class, order = 8)
private NoticeStatusEnum status;
/**
* 生效时间
*/
@Schema(description = "生效时间", example = "2023-08-08 00:00:00", type = "string")
@ExcelProperty(value = "生效时间", order = 5)
private LocalDateTime effectiveTime;
/**
* 终止时间
*/
@Schema(description = "终止时间", example = "2023-08-08 23:59:59", type = "string")
@ExcelProperty(value = "终止时间", order = 6)
private LocalDateTime terminateTime;
/**
* 通知范围
*/
@Schema(description = "通知范围", example = "2")
private NoticeScopeEnum noticeScope;
/**
* 指定用户
*/
@Schema(description = "指定用户", example = "[1,2,3]")
private List<String> noticeUsers;
public NoticeStatusEnum getStatus() {
return NoticeStatusEnum.getStatus(effectiveTime, terminateTime);
}
}

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
package top.continew.admin.system.model.resp;
package top.continew.admin.system.model.resp.notice;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@@ -46,33 +46,11 @@ public class NoticeResp extends BaseResp {
private String title;
/**
* 取值于字典 notice_type
* 取值于字典 notice_type
*/
@Schema(description = "(取值于字典 notice_type", example = "1")
@Schema(description = "类(取值于字典 notice_type", example = "1")
private String type;
/**
* 生效时间
*/
@Schema(description = "生效时间", example = "2023-08-08 00:00:00", type = "string")
private LocalDateTime effectiveTime;
/**
* 终止时间
*/
@Schema(description = "终止时间", example = "2023-08-08 23:59:59", type = "string")
private LocalDateTime terminateTime;
/**
* 状态
*
* @return 公告状态
*/
@Schema(description = "状态", example = "1")
public NoticeStatusEnum getStatus() {
return NoticeStatusEnum.getStatus(effectiveTime, terminateTime);
}
/**
* 通知范围
*/
@@ -80,8 +58,38 @@ public class NoticeResp extends BaseResp {
private NoticeScopeEnum noticeScope;
/**
* 指定用户
* 通知方式
*/
@Schema(description = "指定用户", example = "[1,2,3]")
private List<String> noticeUsers;
@Schema(description = "通知方式", example = "[1,2]")
private List<Integer> noticeMethods;
/**
* 是否定时
*/
@Schema(description = "是否定时", example = "false")
private Boolean isTiming;
/**
* 发布时间
*/
@Schema(description = "发布时间", example = "2023-08-08 00:00:00", type = "string")
private LocalDateTime publishTime;
/**
* 是否置顶
*/
@Schema(description = "是否置顶", example = "false")
private Boolean isTop;
/**
* 状态
*/
@Schema(description = "状态", example = "3")
private NoticeStatusEnum status;
/**
* 是否已读
*/
@Schema(description = "是否已读", example = "false")
private Boolean isRead;
}

View File

@@ -0,0 +1,49 @@
/*
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package top.continew.admin.system.model.resp.notice;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serial;
import java.io.Serializable;
/**
* 未读公告响应参数
*
* @author Charles7c
* @since 2025/5/20 22:00
*/
@Data
@NoArgsConstructor
@Schema(description = "未读公告响应参数")
public class NoticeUnreadResp implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/**
* 未读公告数量
*/
@Schema(description = "未读公告数量", example = "1")
private Long total;
public NoticeUnreadResp(Long total) {
this.total = total;
}
}

View File

@@ -16,41 +16,32 @@
package top.continew.admin.system.service;
import top.continew.admin.system.model.resp.message.MessageUnreadResp;
import java.util.List;
/**
* 消息和用户关联业务接口
* 消息日志业务接口
*
* @author Bull-BCLS
* @author Charles7c
* @since 2023/10/15 19:05
*/
public interface MessageUserService {
/**
* 根据用户 ID 查询未读消息数量
*
* @param userId 用户 ID
* @param isDetail 是否查询详情
* @return 未读消息信息
*/
MessageUnreadResp countUnreadMessageByUserId(Long userId, Boolean isDetail);
public interface MessageLogService {
/**
* 新增
*
* @param messageId 消息 ID
* @param userIdList 用户 ID 列表
* @param userIds 用户 ID 列表
* @param messageId 消息 ID
*/
void add(Long messageId, List<Long> userIdList);
void addWithMessageId(List<Long> userIds, Long messageId);
/**
* 将消息标记已读
* 新增
*
* @param ids 消息ID为空则将所有消息标记已读
* @param messageIds 消息 ID 列表
* @param userId 用户 ID
*/
void readMessage(List<Long> ids);
void addWithUserId(List<Long> messageIds, Long userId);
/**
* 根据消息 ID 删除

View File

@@ -19,6 +19,7 @@ package top.continew.admin.system.service;
import top.continew.admin.system.model.query.MessageQuery;
import top.continew.admin.system.model.req.MessageReq;
import top.continew.admin.system.model.resp.message.MessageResp;
import top.continew.admin.system.model.resp.message.MessageUnreadResp;
import top.continew.starter.extension.crud.model.query.PageQuery;
import top.continew.starter.extension.crud.model.resp.PageResp;
@@ -28,6 +29,7 @@ import java.util.List;
* 消息业务接口
*
* @author Bull-BCLS
* @author Charles7c
* @since 2023/10/15 19:05
*/
public interface MessageService {
@@ -41,13 +43,30 @@ public interface MessageService {
*/
PageResp<MessageResp> page(MessageQuery query, PageQuery pageQuery);
/**
* 将消息标记已读
*
* @param ids 消息ID为空则将所有消息标记已读
* @param userId 用户ID
*/
void readMessage(List<Long> ids, Long userId);
/**
* 查询未读消息数量
*
* @param userId 用户 ID
* @param isDetail 是否查询详情
* @return 未读消息数量
*/
MessageUnreadResp countUnreadByUserId(Long userId, Boolean isDetail);
/**
* 新增
*
* @param req 请求参数
* @param userIdList 接收人列表
*/
void add(MessageReq req, List<Long> userIdList);
void add(MessageReq req, List<String> userIdList);
/**
* 删除

View File

@@ -0,0 +1,52 @@
/*
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package top.continew.admin.system.service;
import java.util.List;
/**
* 公告日志业务接口
*
* @author Charles7c
* @since 2025/5/18 19:12
*/
public interface NoticeLogService {
/**
* 新增
*
* @param userIds 用户 ID 列表
* @param noticeId 公告 ID
* @return 是否新增成功true成功false无变更/失败)
*/
boolean add(List<Long> userIds, Long noticeId);
/**
* 根据公告 ID 列表删除
*
* @param noticeIds 公告 ID 列表
*/
void deleteByNoticeIds(List<Long> noticeIds);
/**
* 根据公告 ID 查询用户 ID 列表
*
* @param noticeId 公告 ID
* @return 用户 ID 列表
*/
List<Long> listUserIdByNoticeId(Long noticeId);
}

View File

@@ -19,9 +19,10 @@ package top.continew.admin.system.service;
import top.continew.admin.system.model.entity.NoticeDO;
import top.continew.admin.system.model.query.NoticeQuery;
import top.continew.admin.system.model.req.NoticeReq;
import top.continew.admin.system.model.resp.NoticeDetailResp;
import top.continew.admin.system.model.resp.NoticeResp;
import top.continew.admin.system.model.resp.dashboard.DashboardNoticeResp;
import top.continew.admin.system.model.resp.notice.NoticeDetailResp;
import top.continew.admin.system.model.resp.notice.NoticeResp;
import top.continew.admin.system.model.resp.notice.NoticeUnreadResp;
import top.continew.starter.data.mp.service.IService;
import top.continew.starter.extension.crud.service.BaseService;
@@ -35,6 +36,29 @@ import java.util.List;
*/
public interface NoticeService extends BaseService<NoticeResp, NoticeDetailResp, NoticeQuery, NoticeReq>, IService<NoticeDO> {
/**
* 发布公告
*
* @param notice 公告信息
*/
void publish(NoticeDO notice);
/**
* 查询未读公告数量
*
* @param userId 用户 ID
* @return 未读公告响应参数
*/
NoticeUnreadResp countUnreadByUserId(Long userId);
/**
* 阅读公告
*
* @param id 公告 ID
* @param userId 用户 ID
*/
void readNotice(Long id, Long userId);
/**
* 查询仪表盘公告列表
*

View File

@@ -0,0 +1,71 @@
/*
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package top.continew.admin.system.service.impl;
import cn.hutool.core.collection.CollUtil;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import top.continew.admin.system.mapper.MessageLogMapper;
import top.continew.admin.system.model.entity.MessageLogDO;
import top.continew.admin.system.service.MessageLogService;
import java.time.LocalDateTime;
import java.util.List;
/**
* 消息日志业务实现
*
* @author Bull-BCLS
* @author Charles7c
* @since 2023/10/15 19:05
*/
@Service
@RequiredArgsConstructor
public class MessageLogServiceImpl implements MessageLogService {
private final MessageLogMapper baseMapper;
@Override
public void addWithMessageId(List<Long> userIdList, Long messageId) {
if (CollUtil.isEmpty(userIdList)) {
return;
}
List<MessageLogDO> list = userIdList.stream()
.map(userId -> new MessageLogDO(userId, messageId, LocalDateTime.now()))
.toList();
baseMapper.insert(list);
}
@Override
public void addWithUserId(List<Long> messageIds, Long userId) {
if (CollUtil.isEmpty(messageIds)) {
return;
}
List<MessageLogDO> list = messageIds.stream()
.map(messageId -> new MessageLogDO(userId, messageId, LocalDateTime.now()))
.toList();
baseMapper.insert(list);
}
@Override
public void deleteByMessageIds(List<Long> messageIds) {
if (CollUtil.isEmpty(messageIds)) {
return;
}
baseMapper.lambdaUpdate().in(MessageLogDO::getMessageId, messageIds).remove();
}
}

View File

@@ -17,32 +17,37 @@
package top.continew.admin.system.service.impl;
import cn.crane4j.annotation.AutoOperate;
import cn.dev33.satoken.stp.StpUtil;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.collection.CollUtil;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import top.continew.admin.system.enums.MessageTypeEnum;
import top.continew.admin.system.enums.NoticeScopeEnum;
import top.continew.admin.system.mapper.MessageMapper;
import top.continew.admin.system.model.entity.MessageDO;
import top.continew.admin.system.model.query.MessageQuery;
import top.continew.admin.system.model.req.MessageReq;
import top.continew.admin.system.model.resp.message.MessageResp;
import top.continew.admin.system.model.resp.message.MessageTypeUnreadResp;
import top.continew.admin.system.model.resp.message.MessageUnreadResp;
import top.continew.admin.system.service.MessageLogService;
import top.continew.admin.system.service.MessageService;
import top.continew.admin.system.service.MessageUserService;
import top.continew.starter.core.validation.CheckUtils;
import top.continew.starter.data.mp.util.QueryWrapperHelper;
import top.continew.starter.extension.crud.model.query.PageQuery;
import top.continew.starter.extension.crud.model.resp.PageResp;
import top.continew.starter.messaging.websocket.util.WebSocketUtils;
import java.util.ArrayList;
import java.util.List;
/**
* 消息业务实现
*
* @author Bull-BCLS
* @author Charles7c
* @since 2023/10/15 19:05
*/
@Service
@@ -50,32 +55,71 @@ import java.util.List;
public class MessageServiceImpl implements MessageService {
private final MessageMapper baseMapper;
private final MessageUserService messageUserService;
private final MessageLogService messageLogService;
@Override
@AutoOperate(type = MessageResp.class, on = "list")
public PageResp<MessageResp> page(MessageQuery query, PageQuery pageQuery) {
QueryWrapper<MessageDO> queryWrapper = QueryWrapperHelper.build(query, pageQuery.getSort());
queryWrapper.apply(null != query.getUserId(), "t2.user_id={0}", query.getUserId())
.apply(null != query.getIsRead(), "t2.is_read={0}", query.getIsRead());
IPage<MessageResp> page = baseMapper.selectPageByUserId(new Page<>(pageQuery.getPage(), pageQuery
.getSize()), queryWrapper);
IPage<MessageResp> page = baseMapper.selectMessagePage(new Page<>(pageQuery.getPage(), pageQuery
.getSize()), query);
return PageResp.build(page);
}
@Override
public void readMessage(List<Long> ids, Long userId) {
if (CollUtil.isEmpty(ids)) {
// 查询当前用户的未读消息
List<MessageDO> list = baseMapper.selectUnreadListByUserId(userId);
ids = list.stream().map(MessageDO::getId).toList();
}
messageLogService.addWithMessageId(ids, userId);
}
@Override
public MessageUnreadResp countUnreadByUserId(Long userId, Boolean isDetail) {
MessageUnreadResp result = new MessageUnreadResp();
Long total = 0L;
if (Boolean.TRUE.equals(isDetail)) {
List<MessageTypeUnreadResp> detailList = new ArrayList<>();
for (MessageTypeEnum messageType : MessageTypeEnum.values()) {
MessageTypeUnreadResp resp = new MessageTypeUnreadResp();
resp.setType(messageType);
Long count = baseMapper.selectUnreadCountByUserIdAndType(userId, messageType.getValue());
resp.setCount(count);
detailList.add(resp);
total += count;
}
result.setDetails(detailList);
} else {
total = baseMapper.selectUnreadCountByUserIdAndType(userId, null);
}
result.setTotal(total);
return result;
}
@Override
@Transactional(rollbackFor = Exception.class)
public void add(MessageReq req, List<Long> userIdList) {
CheckUtils.throwIf(() -> CollUtil.isEmpty(userIdList), "消息接收人不能为空");
public void add(MessageReq req, List<String> userIdList) {
MessageDO message = BeanUtil.copyProperties(req, MessageDO.class);
message.setScope(CollUtil.isEmpty(userIdList) ? NoticeScopeEnum.ALL : NoticeScopeEnum.USER);
message.setUsers(userIdList);
baseMapper.insert(message);
messageUserService.add(message.getId(), userIdList);
// 发送消息给指定在线用户
if (CollUtil.isNotEmpty(userIdList)) {
userIdList.parallelStream().forEach(userId -> {
List<String> tokenList = StpUtil.getTokenValueListByLoginId(userId);
tokenList.parallelStream().forEach(token -> WebSocketUtils.sendMessage(token, "1"));
});
return;
}
// 发送消息给所有在线用户
// TODO WebSocketUtils.sendMessage("1");
}
@Override
@Transactional(rollbackFor = Exception.class)
public void delete(List<Long> ids) {
baseMapper.deleteByIds(ids);
messageUserService.deleteByMessageIds(ids);
messageLogService.deleteByMessageIds(ids);
}
}

View File

@@ -1,98 +0,0 @@
/*
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package top.continew.admin.system.service.impl;
import cn.hutool.core.collection.CollUtil;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import top.continew.admin.system.enums.MessageTypeEnum;
import top.continew.admin.system.mapper.MessageUserMapper;
import top.continew.admin.system.model.entity.MessageUserDO;
import top.continew.admin.system.model.resp.message.MessageTypeUnreadResp;
import top.continew.admin.system.model.resp.message.MessageUnreadResp;
import top.continew.admin.system.service.MessageUserService;
import top.continew.starter.core.validation.CheckUtils;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
/**
* 消息和用户关联业务实现
*
* @author Bull-BCLS
* @since 2023/10/15 19:05
*/
@Service
@RequiredArgsConstructor
public class MessageUserServiceImpl implements MessageUserService {
private final MessageUserMapper baseMapper;
@Override
public MessageUnreadResp countUnreadMessageByUserId(Long userId, Boolean isDetail) {
MessageUnreadResp result = new MessageUnreadResp();
Long total = 0L;
if (Boolean.TRUE.equals(isDetail)) {
List<MessageTypeUnreadResp> detailList = new ArrayList<>();
for (MessageTypeEnum messageType : MessageTypeEnum.values()) {
MessageTypeUnreadResp resp = new MessageTypeUnreadResp();
resp.setType(messageType);
Long count = baseMapper.selectUnreadCountByUserIdAndType(userId, messageType.getValue());
resp.setCount(count);
detailList.add(resp);
total += count;
}
result.setDetails(detailList);
} else {
total = baseMapper.selectUnreadCountByUserIdAndType(userId, null);
}
result.setTotal(total);
return result;
}
@Override
public void add(Long messageId, List<Long> userIdList) {
CheckUtils.throwIfEmpty(userIdList, "消息接收人不能为空");
List<MessageUserDO> messageUserList = userIdList.stream().map(userId -> {
MessageUserDO messageUser = new MessageUserDO();
messageUser.setUserId(userId);
messageUser.setMessageId(messageId);
messageUser.setIsRead(false);
return messageUser;
}).toList();
baseMapper.insert(messageUserList);
}
@Override
public void readMessage(List<Long> ids) {
baseMapper.lambdaUpdate()
.set(MessageUserDO::getIsRead, true)
.set(MessageUserDO::getReadTime, LocalDateTime.now())
.eq(MessageUserDO::getIsRead, false)
.in(CollUtil.isNotEmpty(ids), MessageUserDO::getMessageId, ids)
.update();
}
@Override
public void deleteByMessageIds(List<Long> messageIds) {
if (CollUtil.isEmpty(messageIds)) {
return;
}
baseMapper.lambdaUpdate().in(MessageUserDO::getMessageId, messageIds).remove();
}
}

View File

@@ -0,0 +1,82 @@
/*
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package top.continew.admin.system.service.impl;
import cn.hutool.core.collection.CollUtil;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import top.continew.admin.system.mapper.NoticeLogMapper;
import top.continew.admin.system.model.entity.NoticeLogDO;
import top.continew.admin.system.service.NoticeLogService;
import java.time.LocalDateTime;
import java.util.Collection;
import java.util.List;
/**
* 公告日志业务实现
*
* @author Charles7c
* @since 2025/5/18 19:15
*/
@Service
@RequiredArgsConstructor
public class NoticeLogServiceImpl implements NoticeLogService {
private final NoticeLogMapper baseMapper;
@Override
@Transactional(rollbackFor = Exception.class)
public boolean add(List<Long> userIds, Long noticeId) {
// 检查是否有变更
List<Long> oldUserIdList = baseMapper.lambdaQuery()
.select(NoticeLogDO::getUserId)
.eq(NoticeLogDO::getNoticeId, noticeId)
.list()
.stream()
.map(NoticeLogDO::getUserId)
.toList();
Collection<Long> subtract = CollUtil.subtract(userIds, oldUserIdList);
if (CollUtil.isEmpty(subtract)) {
return false;
}
// 新增没有关联的
LocalDateTime now = LocalDateTime.now();
List<NoticeLogDO> list = subtract.stream().map(userId -> new NoticeLogDO(noticeId, userId, now)).toList();
return baseMapper.insertBatch(list);
}
@Override
public void deleteByNoticeIds(List<Long> noticeIds) {
if (CollUtil.isEmpty(noticeIds)) {
return;
}
baseMapper.lambdaUpdate().in(NoticeLogDO::getNoticeId, noticeIds).remove();
}
@Override
public List<Long> listUserIdByNoticeId(Long noticeId) {
return baseMapper.lambdaQuery()
.select(NoticeLogDO::getUserId)
.eq(NoticeLogDO::getNoticeId, noticeId)
.list()
.stream()
.map(NoticeLogDO::getUserId)
.toList();
}
}

View File

@@ -16,23 +16,32 @@
package top.continew.admin.system.service.impl;
import cn.hutool.core.collection.CollUtil;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import top.continew.admin.common.context.UserContextHolder;
import top.continew.admin.system.enums.*;
import top.continew.admin.system.mapper.NoticeMapper;
import top.continew.admin.system.model.entity.NoticeDO;
import top.continew.admin.system.model.query.NoticeQuery;
import top.continew.admin.system.model.req.MessageReq;
import top.continew.admin.system.model.req.NoticeReq;
import top.continew.admin.system.model.resp.NoticeDetailResp;
import top.continew.admin.system.model.resp.NoticeResp;
import top.continew.admin.system.model.resp.dashboard.DashboardNoticeResp;
import top.continew.admin.system.model.resp.notice.NoticeDetailResp;
import top.continew.admin.system.model.resp.notice.NoticeResp;
import top.continew.admin.system.model.resp.notice.NoticeUnreadResp;
import top.continew.admin.system.service.MessageService;
import top.continew.admin.system.service.NoticeLogService;
import top.continew.admin.system.service.NoticeService;
import top.continew.starter.core.validation.CheckUtils;
import top.continew.starter.core.validation.ValidationUtils;
import top.continew.starter.extension.crud.model.query.PageQuery;
import top.continew.starter.extension.crud.model.resp.PageResp;
import top.continew.starter.extension.crud.service.BaseServiceImpl;
import java.time.LocalDateTime;
import java.util.List;
/**
@@ -45,15 +54,130 @@ import java.util.List;
@RequiredArgsConstructor
public class NoticeServiceImpl extends BaseServiceImpl<NoticeMapper, NoticeDO, NoticeResp, NoticeDetailResp, NoticeQuery, NoticeReq> implements NoticeService {
private final NoticeLogService noticeLogService;
private final MessageService messageService;
@Override
public PageResp<NoticeResp> page(NoticeQuery query, PageQuery pageQuery) {
IPage<NoticeDetailResp> page = baseMapper.selectNoticePage(new Page<>(pageQuery.getPage(), pageQuery
IPage<NoticeResp> page = baseMapper.selectNoticePage(new Page<>(pageQuery.getPage(), pageQuery
.getSize()), query);
PageResp<NoticeResp> pageResp = PageResp.build(page, super.getListClass());
PageResp<NoticeResp> pageResp = PageResp.build(page);
pageResp.getList().forEach(this::fill);
return pageResp;
}
@Override
public void beforeCreate(NoticeReq req) {
// 校验定时发布
if (Boolean.TRUE.equals(req.getIsTiming())) {
ValidationUtils.throwIf(req.getPublishTime() == null, "定时发布时间不能为空");
ValidationUtils.throwIf(req.getPublishTime().isBefore(LocalDateTime.now()), "定时发布时间不能早于当前时间");
}
if (!NoticeStatusEnum.DRAFT.equals(req.getStatus())) {
if (Boolean.TRUE.equals(req.getIsTiming())) {
// 待发布
req.setStatus(NoticeStatusEnum.PENDING);
} else {
// 已发布
req.setStatus(NoticeStatusEnum.PUBLISHED);
req.setPublishTime(LocalDateTime.now());
}
}
}
@Override
public void afterCreate(NoticeReq req, NoticeDO entity) {
// 发送消息
if (NoticeStatusEnum.PUBLISHED.equals(entity.getStatus())) {
this.publish(entity);
}
}
@Override
public void beforeUpdate(NoticeReq req, Long id) {
NoticeDO oldNotice = super.getById(id);
switch (oldNotice.getStatus()) {
case PUBLISHED -> {
CheckUtils.throwIfNotEqual(req.getStatus(), oldNotice.getStatus(), "公告已发布,不允许修改状态");
CheckUtils.throwIfNotEqual(req.getIsTiming(), oldNotice.getIsTiming(), "公告已发布,不允许修改定时发布信息");
CheckUtils.throwIfNotEqual(req.getNoticeScope(), oldNotice.getNoticeScope(), "公告已发布,不允许修改通知范围");
if (NoticeScopeEnum.USER.equals(oldNotice.getNoticeScope())) {
CheckUtils.throwIfNotEmpty(CollUtil.disjunction(req.getNoticeUsers(), oldNotice
.getNoticeUsers()), "公告已发布,不允许修改通知用户");
}
CheckUtils.throwIf(!CollUtil.isEqualList(req.getNoticeMethods(), oldNotice
.getNoticeMethods()), "公告已发布,不允许修改通知方式");
// 修正定时发布信息
if (Boolean.TRUE.equals(oldNotice.getIsTiming())) {
CheckUtils.throwIfNotEqual(req.getPublishTime(), oldNotice.getPublishTime(), "公告已发布,不允许修改定时发布信息");
}
req.setPublishTime(oldNotice.getPublishTime());
}
case DRAFT, PENDING -> {
// 校验定时发布
if (Boolean.TRUE.equals(req.getIsTiming())) {
ValidationUtils.throwIf(req.getPublishTime() == null, "定时发布时间不能为空");
ValidationUtils.throwIf(req.getPublishTime().isBefore(LocalDateTime.now()), "定时发布时间不能早于当前时间");
}
// 已发布
if (NoticeStatusEnum.PUBLISHED.equals(req.getStatus())) {
if (Boolean.TRUE.equals(req.getIsTiming())) {
// 待发布
req.setStatus(NoticeStatusEnum.PENDING);
} else {
// 已发布
req.setStatus(NoticeStatusEnum.PUBLISHED);
req.setPublishTime(LocalDateTime.now());
}
}
}
default -> throw new IllegalArgumentException("状态无效");
}
}
@Override
public void afterUpdate(NoticeReq req, NoticeDO entity) {
// 重置定时发布时间
if (!NoticeStatusEnum.PUBLISHED.equals(entity.getStatus()) && Boolean.FALSE.equals(entity
.getIsTiming()) && entity.getPublishTime() != null) {
baseMapper.lambdaUpdate().set(NoticeDO::getPublishTime, null).eq(NoticeDO::getId, entity.getId()).update();
}
// 发送消息
if (Boolean.FALSE.equals(entity.getIsTiming()) && NoticeStatusEnum.PUBLISHED.equals(entity.getStatus())) {
this.publish(entity);
}
}
@Override
public void afterDelete(List<Long> ids) {
// 删除公告日志
noticeLogService.deleteByNoticeIds(ids);
}
@Override
public void publish(NoticeDO notice) {
List<Integer> noticeMethods = notice.getNoticeMethods();
if (CollUtil.isNotEmpty(noticeMethods) && noticeMethods.contains(NoticeMethodEnum.SYSTEM_MESSAGE.getValue())) {
MessageTemplateEnum template = MessageTemplateEnum.NOTICE_PUBLISH;
MessageReq req = new MessageReq(MessageTypeEnum.SYSTEM);
req.setTitle(template.getTitle());
req.setContent(template.getContent().formatted(notice.getTitle()));
req.setPath(template.getPath().formatted(notice.getId()));
// 新增消息
messageService.add(req, notice.getNoticeUsers());
}
}
@Override
public NoticeUnreadResp countUnreadByUserId(Long userId) {
return new NoticeUnreadResp(baseMapper.selectUnreadCountByUserId(userId));
}
@Override
public void readNotice(Long id, Long userId) {
noticeLogService.add(List.of(userId), id);
}
@Override
public List<DashboardNoticeResp> listDashboard() {
Long userId = UserContextHolder.getUserId();

View File

@@ -0,0 +1,4 @@
<?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="top.continew.admin.system.mapper.MessageLogMapper">
</mapper>

View File

@@ -1,14 +1,56 @@
<?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="top.continew.admin.system.mapper.MessageMapper">
<select id="selectPageByUserId" resultType="top.continew.admin.system.model.resp.message.MessageResp">
<select id="selectMessagePage" resultType="top.continew.admin.system.model.resp.message.MessageResp">
SELECT
t1.*,
t2.user_id,
t2.is_read,
t2.read_time
t1.id,
t1.title,
t1.content,
t1.type,
t1.path,
t1.scope,
t1.users,
t1.create_time,
t2.read_time IS NOT NULL AS isRead,
t2.read_time AS readTime
FROM sys_message AS t1
LEFT JOIN sys_message_user AS t2 ON t2.message_id = t1.id
${ew.getCustomSqlSegment}
LEFT JOIN sys_message_log AS t2 ON t2.message_id = t1.id
<where>
<if test="query.userId != null">
(t1.scope = 1 OR (t1.scope = 2 AND JSON_EXTRACT(t1.users, "$[0]") = CAST(#{query.userId} AS CHAR)))
</if>
<if test="query.title != null and query.title != ''">
AND t1.title LIKE CONCAT('%', #{query.title}, '%')
</if>
<if test="query.type != null and query.type != ''">
AND t1.type = #{query.type}
</if>
<if test="query.isRead != null">
AND t2.read_time IS <if test="query.isRead">NOT</if> NULL
</if>
</where>
ORDER BY t1.create_time DESC
</select>
<select id="selectUnreadListByUserId" resultType="top.continew.admin.system.model.entity.MessageDO">
SELECT
t1.*
FROM sys_message AS t1
LEFT JOIN sys_message_log AS t2 ON t2.message_id = t1.id
WHERE (t1.scope = 1 OR (t1.scope = 2 AND JSON_CONTAINS(t1.users, CONCAT('"', #{userId}, '"'))))
AND t2.read_time IS NULL
</select>
<select id="selectUnreadCountByUserIdAndType" resultType="java.lang.Long">
SELECT
COUNT(1)
FROM sys_message AS t1
LEFT JOIN sys_message_log AS t2 ON t2.message_id = t1.id
WHERE (t1.scope = 1 OR (t1.scope = 2 AND JSON_CONTAINS(t1.users, CONCAT('"', #{userId}, '"'))))
AND t2.read_time IS NULL
<if test="type != null">
AND t1.type = #{type}
</if>
</select>
</mapper>

View File

@@ -1,14 +0,0 @@
<?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="top.continew.admin.system.mapper.MessageUserMapper">
<select id="selectUnreadCountByUserIdAndType" resultType="Long">
SELECT
COUNT(t1.message_id)
FROM sys_message_user AS t1
LEFT JOIN sys_message AS t2 ON t2.id = t1.message_id
WHERE t1.user_id = #{userId} AND t1.is_read = false
<if test="type != null">
AND t2.type = #{type}
</if>
</select>
</mapper>

View File

@@ -2,28 +2,29 @@
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="top.continew.admin.system.mapper.NoticeMapper">
<select id="selectDashboardList"
resultType="top.continew.admin.system.model.resp.dashboard.DashboardNoticeResp">
SELECT
id, title, type
FROM sys_notice
WHERE (effective_time IS NULL OR NOW() > effective_time)
AND (terminate_time IS NULL OR terminate_time > NOW())
<if test="userId != null">
AND (notice_scope = 1 OR (notice_scope = 2 AND JSON_CONTAINS(notice_users, CONCAT('"', #{userId}, '"'))))
</if>
ORDER BY sort ASC, effective_time DESC
LIMIT 5
</select>
<resultMap id="notice" type="top.continew.admin.system.model.resp.notice.NoticeResp">
<id property="id" column="id" />
<result property="noticeMethods" column="notice_methods" typeHandler="com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler" />
</resultMap>
<select id="selectNoticePage" resultType="top.continew.admin.system.model.resp.NoticeDetailResp">
SELECT *
<select id="selectNoticePage" resultMap="notice">
SELECT
t1.id,
t1.title,
t1.type,
t1.notice_scope,
t1.notice_methods,
t1.is_timing,
t1.publish_time,
t1.is_top,
t1.status,
t1.create_user,
t2.read_time IS NOT NULL AS isRead
FROM sys_notice AS t1
LEFT JOIN sys_notice_log AS t2 ON t2.notice_id = t1.id
<where>
<if test="query.userId != null">
t1.notice_scope = 1 OR (t1.notice_scope = 2 AND JSON_EXTRACT(t1.notice_users, "$[0]") = CAST(#{query.userId} AS CHAR))
AND (t1.effective_time IS NULL OR NOW() > t1.effective_time)
AND (t1.terminate_time IS NULL OR t1.terminate_time > NOW())
(t1.notice_scope = 1 OR (t1.notice_scope = 2 AND JSON_EXTRACT(t1.notice_users, "$[0]") = CAST(#{query.userId} AS CHAR)))
</if>
<if test="query.title != null and query.title != ''">
AND t1.title LIKE CONCAT('%', #{query.title}, '%')
@@ -33,10 +34,32 @@
</if>
</where>
<if test="query.userId != null">
ORDER BY t1.sort ASC, t1.effective_time DESC
ORDER BY t1.is_top DESC, t1.publish_time DESC
</if>
<if test="query.userId == null">
ORDER BY t1.create_time DESC
</if>
</select>
<select id="selectUnreadCountByUserId" resultType="java.lang.Long">
SELECT
COUNT(1)
FROM sys_notice AS t1
LEFT JOIN sys_notice_log AS t2 ON t2.notice_id = t1.id
WHERE (t1.notice_scope = 1 OR (t1.notice_scope = 2 AND JSON_CONTAINS(t1.notice_users, CONCAT('"', #{userId}, '"'))))
AND t2.read_time IS NULL
</select>
<select id="selectDashboardList"
resultType="top.continew.admin.system.model.resp.dashboard.DashboardNoticeResp">
SELECT
id, title, type, is_top
FROM sys_notice
WHERE status = 3
<if test="userId != null">
AND (notice_scope = 1 OR (notice_scope = 2 AND JSON_CONTAINS(notice_users, CONCAT('"', #{userId}, '"'))))
</if>
ORDER BY is_top DESC, publish_time DESC
LIMIT 5
</select>
</mapper>

View File

@@ -1,84 +0,0 @@
/*
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package top.continew.admin.controller.system;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.enums.ParameterIn;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import top.continew.admin.common.context.UserContextHolder;
import top.continew.admin.system.model.query.MessageQuery;
import top.continew.admin.system.model.resp.message.MessageResp;
import top.continew.admin.system.model.resp.message.MessageUnreadResp;
import top.continew.admin.system.service.MessageService;
import top.continew.admin.system.service.MessageUserService;
import top.continew.starter.extension.crud.model.query.PageQuery;
import top.continew.starter.extension.crud.model.req.IdsReq;
import top.continew.starter.extension.crud.model.resp.PageResp;
import top.continew.starter.log.annotation.Log;
/**
* 消息管理 API
*
* @author Bull-BCLS
* @since 2023/10/15 19:05
*/
@Tag(name = "消息管理 API")
@RestController
@RequiredArgsConstructor
@RequestMapping("/system/message")
public class MessageController {
private final MessageService baseService;
private final MessageUserService messageUserService;
@Operation(summary = "分页查询列表", description = "分页查询列表")
@GetMapping
public PageResp<MessageResp> page(MessageQuery query, @Validated PageQuery pageQuery) {
query.setUserId(UserContextHolder.getUserId());
return baseService.page(query, pageQuery);
}
@Operation(summary = "删除数据", description = "删除数据")
@DeleteMapping
public void delete(@Validated @RequestBody IdsReq req) {
baseService.delete(req.getIds());
}
@Operation(summary = "标记已读", description = "将消息标记为已读状态")
@PatchMapping("/read")
public void read(@Validated @RequestBody IdsReq req) {
messageUserService.readMessage(req.getIds());
}
@Operation(summary = "全部已读", description = "将所有消息标记为已读状态")
@PatchMapping("/readAll")
public void readAll() {
messageUserService.readMessage(null);
}
@Log(ignore = true)
@Operation(summary = "查询未读消息数量", description = "查询当前用户的未读消息数量")
@Parameter(name = "isDetail", description = "是否查询详情", example = "true", in = ParameterIn.QUERY)
@GetMapping("/unread")
public MessageUnreadResp countUnread(@RequestParam(required = false) Boolean detail) {
return messageUserService.countUnreadMessageByUserId(UserContextHolder.getUserId(), detail);
}
}

View File

@@ -16,14 +16,16 @@
package top.continew.admin.controller.system;
import cn.hutool.core.collection.CollUtil;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.web.bind.annotation.RestController;
import top.continew.admin.common.controller.BaseController;
import top.continew.admin.system.enums.NoticeMethodEnum;
import top.continew.admin.system.enums.NoticeScopeEnum;
import top.continew.admin.system.model.query.NoticeQuery;
import top.continew.admin.system.model.req.NoticeReq;
import top.continew.admin.system.model.resp.NoticeDetailResp;
import top.continew.admin.system.model.resp.NoticeResp;
import top.continew.admin.system.model.resp.notice.NoticeDetailResp;
import top.continew.admin.system.model.resp.notice.NoticeResp;
import top.continew.admin.system.service.NoticeService;
import top.continew.starter.core.validation.ValidationUtils;
import top.continew.starter.extension.crud.annotation.CrudApi;
@@ -31,7 +33,8 @@ import top.continew.starter.extension.crud.annotation.CrudRequestMapping;
import top.continew.starter.extension.crud.enums.Api;
import java.lang.reflect.Method;
import java.time.LocalDateTime;
import java.util.Arrays;
import java.util.List;
/**
* 公告管理 API
@@ -52,15 +55,18 @@ public class NoticeController extends BaseController<NoticeService, NoticeResp,
return;
}
NoticeReq req = (NoticeReq)args[0];
// 校验生效时间
LocalDateTime effectiveTime = req.getEffectiveTime();
LocalDateTime terminateTime = req.getTerminateTime();
if (null != effectiveTime && null != terminateTime) {
ValidationUtils.throwIf(terminateTime.isBefore(effectiveTime), "终止时间必须晚于生效时间");
}
// 校验通知范围
if (NoticeScopeEnum.USER.equals(req.getNoticeScope())) {
ValidationUtils.throwIfEmpty(req.getNoticeUsers(), "通知用户不能为空");
}
// 校验通知方式
List<Integer> noticeMethods = req.getNoticeMethods();
if (CollUtil.isNotEmpty(noticeMethods)) {
List<Integer> validMethods = Arrays.stream(NoticeMethodEnum.values())
.map(NoticeMethodEnum::getValue)
.toList();
noticeMethods.forEach(method -> ValidationUtils.throwIf(!validMethods
.contains(method), "通知方式 [{}] 不正确", method));
}
}
}

View File

@@ -22,19 +22,24 @@ import io.swagger.v3.oas.annotations.enums.ParameterIn;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.*;
import top.continew.admin.common.context.UserContextHolder;
import top.continew.admin.system.enums.NoticeScopeEnum;
import top.continew.admin.system.model.query.MessageQuery;
import top.continew.admin.system.model.query.NoticeQuery;
import top.continew.admin.system.model.resp.NoticeDetailResp;
import top.continew.admin.system.model.resp.NoticeResp;
import top.continew.admin.system.model.resp.message.MessageResp;
import top.continew.admin.system.model.resp.message.MessageUnreadResp;
import top.continew.admin.system.model.resp.notice.NoticeDetailResp;
import top.continew.admin.system.model.resp.notice.NoticeResp;
import top.continew.admin.system.model.resp.notice.NoticeUnreadResp;
import top.continew.admin.system.service.MessageService;
import top.continew.admin.system.service.NoticeService;
import top.continew.starter.core.validation.CheckUtils;
import top.continew.starter.extension.crud.model.query.PageQuery;
import top.continew.starter.extension.crud.model.req.IdsReq;
import top.continew.starter.extension.crud.model.resp.BasePageResp;
import top.continew.starter.extension.crud.model.resp.PageResp;
import top.continew.starter.log.annotation.Log;
/**
* 个人消息 API
@@ -50,6 +55,40 @@ import top.continew.starter.extension.crud.model.resp.BasePageResp;
public class UserMessageController {
private final NoticeService noticeService;
private final MessageService messageService;
@Operation(summary = "分页查询消息列表", description = "分页查询消息列表")
@GetMapping
public PageResp<MessageResp> page(MessageQuery query, @Validated PageQuery pageQuery) {
query.setUserId(UserContextHolder.getUserId());
return messageService.page(query, pageQuery);
}
@Operation(summary = "删除消息", description = "删除消息")
@DeleteMapping
public void delete(@Validated @RequestBody IdsReq req) {
messageService.delete(req.getIds());
}
@Operation(summary = "消息标记为已读", description = "将消息标记为已读状态")
@PatchMapping("/read")
public void read(@Validated @RequestBody IdsReq req) {
messageService.readMessage(req.getIds(), UserContextHolder.getUserId());
}
@Operation(summary = "消息全部已读", description = "将所有消息标记为已读状态")
@PatchMapping("/readAll")
public void readAll() {
messageService.readMessage(null, UserContextHolder.getUserId());
}
@Log(ignore = true)
@Operation(summary = "查询未读消息数量", description = "查询当前用户的未读消息数量")
@Parameter(name = "isDetail", description = "是否查询详情", example = "true", in = ParameterIn.QUERY)
@GetMapping("/unread")
public MessageUnreadResp countUnreadMessage(@RequestParam(required = false) Boolean detail) {
return messageService.countUnreadByUserId(UserContextHolder.getUserId(), detail);
}
@Operation(summary = "分页查询公告列表", description = "分页查询公告列表")
@GetMapping("/notice")
@@ -66,6 +105,14 @@ public class UserMessageController {
CheckUtils.throwIf(detail == null || (NoticeScopeEnum.USER.equals(detail.getNoticeScope()) && !detail
.getNoticeUsers()
.contains(UserContextHolder.getUserId().toString())), "公告不存在或无权限访问");
noticeService.readNotice(id, UserContextHolder.getUserId());
return detail;
}
@Log(ignore = true)
@Operation(summary = "查询未读公告数量", description = "查询当前用户的未读公告数量")
@GetMapping("/notice/unread")
public NoticeUnreadResp countUnreadNotice() {
return noticeService.countUnreadByUserId(UserContextHolder.getUserId());
}
}

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
package top.continew.admin.controller.schedule;
package top.continew.admin.job;
import cn.hutool.core.util.StrUtil;
import com.aizuda.snailjob.client.job.core.annotation.JobExecutor;
@@ -53,8 +53,9 @@ public class DemoEnvironmentJob {
private final DictMapper dictMapper;
private final StorageMapper storageMapper;
private final NoticeMapper noticeMapper;
private final NoticeLogMapper noticeLogMapper;
private final MessageMapper messageMapper;
private final MessageUserMapper messageUserMapper;
private final MessageLogMapper messageLogMapper;
private final UserMapper userMapper;
private final UserRoleMapper userRoleMapper;
private final UserSocialMapper userSocialMapper;
@@ -109,7 +110,8 @@ public class DemoEnvironmentJob {
InterceptorIgnoreHelper.handle(IgnoreStrategy.builder().blockAttack(true).build());
SnailJobLog.REMOTE.info("演示环境待清理数据项检测完成,开始执行清理。");
// 清理关联数据
messageUserMapper.lambdaUpdate().gt(MessageUserDO::getMessageId, MESSAGE_FLAG).remove();
noticeLogMapper.lambdaUpdate().gt(NoticeLogDO::getNoticeId, DELETE_FLAG).remove();
messageLogMapper.lambdaUpdate().gt(MessageLogDO::getMessageId, MESSAGE_FLAG).remove();
userRoleMapper.lambdaUpdate().notIn(UserRoleDO::getRoleId, ROLE_FLAG).remove();
userRoleMapper.lambdaUpdate().notIn(UserRoleDO::getUserId, USER_FLAG).remove();
roleDeptMapper.lambdaUpdate().notIn(RoleDeptDO::getRoleId, ROLE_FLAG).remove();

View File

@@ -0,0 +1,112 @@
/*
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package top.continew.admin.job;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.extra.spring.SpringUtil;
import com.aizuda.snailjob.client.job.core.annotation.JobExecutor;
import com.aizuda.snailjob.common.log.SnailJobLog;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
import top.continew.admin.schedule.annotation.ConditionalOnEnabledScheduleJob;
import top.continew.admin.system.enums.NoticeMethodEnum;
import top.continew.admin.system.enums.NoticeStatusEnum;
import top.continew.admin.system.mapper.NoticeMapper;
import top.continew.admin.system.model.entity.NoticeDO;
import top.continew.admin.system.service.NoticeService;
import top.continew.starter.core.constant.PropertiesConstants;
import java.time.LocalDateTime;
import java.util.List;
/**
* 公告发布任务
*
* @author Charles7c
* @since 2025/5/11 22:19
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class NoticePublishJob {
/**
* 定时发布公告(未启用 Snail Job 则使用它)
*/
@Component
@ConditionalOnProperty(prefix = "snail-job", name = PropertiesConstants.ENABLED, havingValue = "false")
public static class Scheduler {
@Scheduled(cron = "0 * * * * ?")
@Transactional(rollbackFor = Exception.class)
public void publishNoticeWithSchedule() {
log.info("定时任务 [公告发布] 开始执行。");
publishNotice();
log.info("定时任务 [公告发布] 执行结束。");
}
}
/**
* 定时发布公告(启用 Snail Job 时)
*/
@Component
@ConditionalOnEnabledScheduleJob
public static class ScheduleJob {
@JobExecutor(name = "NoticePublishJob")
@Transactional(rollbackFor = Exception.class)
public void publishNoticeWithScheduleJob() {
SnailJobLog.REMOTE.info("定时任务 [公告发布] 开始执行。");
publishNotice();
SnailJobLog.REMOTE.info("定时任务 [公告发布] 执行结束。");
}
}
/**
* 发布公告
*/
private static void publishNotice() {
NoticeMapper noticeMapper = SpringUtil.getBean(NoticeMapper.class);
// 查询待发布公告
List<NoticeDO> list = noticeMapper.lambdaQuery()
.eq(NoticeDO::getStatus, NoticeStatusEnum.PENDING)
.le(NoticeDO::getPublishTime, LocalDateTime.now())
.list();
if (CollUtil.isEmpty(list)) {
return;
}
// 筛选需要发送消息的公告并发送
List<NoticeDO> needSendMessageList = list.stream()
.filter(notice -> CollUtil.isNotEmpty(notice.getNoticeMethods()))
.filter(notice -> notice.getNoticeMethods().contains(NoticeMethodEnum.SYSTEM_MESSAGE.getValue()))
.toList();
if (CollUtil.isNotEmpty(needSendMessageList)) {
// 发送消息
NoticeService noticeService = SpringUtil.getBean(NoticeService.class);
needSendMessageList.parallelStream().forEach(noticeService::publish);
}
// 更新状态
noticeMapper.lambdaUpdate()
.set(NoticeDO::getStatus, NoticeStatusEnum.PUBLISHED)
.in(NoticeDO::getId, list.stream().map(NoticeDO::getId).toList())
.update();
}
}

View File

@@ -46,10 +46,11 @@ VALUES
(1090, '通知公告', 1000, 2, '/system/notice', 'SystemNotice', 'system/notice/index', NULL, 'notification', b'0', b'0', b'0', NULL, 5, 1, 1, NOW()),
(1091, '列表', 1090, 3, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, 'system:notice:list', 1, 1, 1, NOW()),
(1092, '公告详情', 1090, 2, '/system/notice/detail', 'SystemNoticeDetail', 'system/notice/detail/index', NULL, NULL, b'0', b'0', b'1', 'system:notice:get', 2, 1, 1, NOW()),
(1093, '发布公告', 1090, 2, '/system/notice/add', 'SystemNoticeAdd', 'system/notice/add/index', NULL, NULL, b'0', b'0', b'1', 'system:notice:create', 3, 1, 1, NOW()),
(1094, '修改', 1090, 3, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, 'system:notice:update', 4, 1, 1, NOW()),
(1095, '删除', 1090, 3, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, 'system:notice:delete', 5, 1, 1, NOW()),
(1092, '详情', 1090, 3, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, 'system:notice:get', 2, 1, 1, NOW()),
(1093, '查看公告', 1090, 2, '/system/notice/view', 'SystemNoticeView', 'system/notice/view/index', NULL, NULL, b'0', b'0', b'1', 'system:notice:view', 3, 1, 1, NOW()),
(1094, '发布公告', 1090, 2, '/system/notice/add', 'SystemNoticeAdd', 'system/notice/add/index', NULL, NULL, b'0', b'0', b'1', 'system:notice:create', 4, 1, 1, NOW()),
(1095, '修改', 1090, 3, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, 'system:notice:update', 5, 1, 1, NOW()),
(1096, '删除', 1090, 3, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, 'system:notice:delete', 6, 1, 1, NOW()),
(1110, '文件管理', 1000, 2, '/system/file', 'SystemFile', 'system/file/index', NULL, 'file', b'0', b'0', b'0', NULL, 6, 1, 1, NOW()),
(1111, '列表', 1110, 3, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, 'system:file:list', 1, 1, 1, NOW()),
@@ -215,24 +216,21 @@ VALUES
INSERT INTO `sys_dict`
(`id`, `name`, `code`, `description`, `is_system`, `create_user`, `create_time`)
VALUES
(1, '公告类', 'notice_type', NULL, b'1', 1, NOW()),
(2, '消息类型', 'message_type', NULL, b'1', 1, NOW()),
(3, '客户端类型', 'client_type', NULL, b'1', 1, NOW()),
(4, '短信厂商', 'sms_supplier', NULL, b'1', 1, NOW());
(1, '公告', 'notice_type', NULL, b'1', 1, NOW()),
(2, '客户端类型', 'client_type', NULL, b'1', 1, NOW()),
(3, '短信厂商', 'sms_supplier', NULL, b'1', 1, NOW());
INSERT INTO `sys_dict_item`
(`id`, `label`, `value`, `color`, `sort`, `description`, `status`, `dict_id`, `create_user`, `create_time`)
VALUES
(1, '通知', '1', 'primary', 1, NULL, 1, 1, 1, NOW()),
(2, '活动', '2', 'success', 2, NULL, 1, 1, 1, NOW()),
(3, '安全消息', '1', 'warning', 1, NULL, 1, 2, 1, NOW()),
(4, '活动消息', '2', 'success', 2, NULL, 1, 2, 1, NOW()),
(5, '桌面端', 'PC', 'primary', 1, NULL, 1, 3, 1, NOW()),
(6, '安卓', 'ANDROID', 'success', 2, NULL, 1, 3, 1, NOW()),
(7, '小程序', 'XCX', 'warning', 3, NULL, 1, 3, 1, NOW()),
(8, '阿里', 'alibaba', 'warning', 1, NULL, 1, 4, 1, NOW()),
(9, '腾讯云', 'tencent', 'primary', 2, NULL, 1, 4, 1, NOW()),
(10, '容联云', 'cloopen', 'success', 3, NULL, 1, 4, 1, NOW());
(1, '产品新闻', '1', 'primary', 1, NULL, 1, 1, 1, NOW()),
(2, '企业动态', '2', 'success', 2, NULL, 1, 1, 1, NOW()),
(3, '桌面端', 'PC', 'primary', 1, NULL, 1, 2, 1, NOW()),
(4, '安卓', 'ANDROID', 'success', 2, NULL, 1, 2, 1, NOW()),
(5, '小程序', 'XCX', 'warning', 3, NULL, 1, 2, 1, NOW()),
(6, '阿里云', 'alibaba', 'warning', 1, NULL, 1, 3, 1, NOW()),
(7, '腾讯云', 'tencent', 'primary', 2, NULL, 1, 3, 1, NOW()),
(8, '容联', 'cloopen', 'success', 3, NULL, 1, 3, 1, NOW());
-- 初始化默认用户和角色关联数据
INSERT INTO `sys_user_role`

View File

@@ -218,31 +218,34 @@ CREATE TABLE IF NOT EXISTS `sys_log` (
CREATE TABLE IF NOT EXISTS `sys_message` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'ID',
`title` varchar(50) NOT NULL COMMENT '标题',
`content` varchar(255) DEFAULT NULL COMMENT '内容',
`type` tinyint(1) UNSIGNED NOT NULL DEFAULT 1 COMMENT '类型1系统消息',
`create_user` bigint(20) DEFAULT NULL COMMENT '创建人',
`content` text DEFAULT NULL COMMENT '内容',
`type` tinyint(1) UNSIGNED NOT NULL DEFAULT 1 COMMENT '类型1系统消息2安全消息',
`path` varchar(255) DEFAULT NULL COMMENT '跳转路径',
`scope` tinyint(1) UNSIGNED NOT NULL DEFAULT 1 COMMENT '通知范围1所有人2指定用户',
`users` json DEFAULT NULL COMMENT '通知用户',
`create_time` datetime NOT NULL COMMENT '创建时间',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='消息表';
CREATE TABLE IF NOT EXISTS `sys_message_user` (
`message_id` bigint(20) NOT NULL COMMENT '消息ID',
`user_id` bigint(11) NOT NULL COMMENT '用户ID',
`is_read` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否已',
`read_time` datetime DEFAULT NULL COMMENT '读取时间',
CREATE TABLE IF NOT EXISTS `sys_message_log` (
`message_id` bigint(20) NOT NULL COMMENT '消息ID',
`user_id` bigint(20) NOT NULL COMMENT '用户ID',
`read_time` datetime DEFAULT NULL COMMENT '取时间',
PRIMARY KEY (`message_id`, `user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='消息和用户关联';
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='消息日志';
CREATE TABLE IF NOT EXISTS `sys_notice` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'ID',
`title` varchar(150) NOT NULL COMMENT '标题',
`content` mediumtext NOT NULL COMMENT '内容',
`type` varchar(30) NOT NULL COMMENT '',
`effective_time` datetime DEFAULT NULL COMMENT '生效时间',
`terminate_time` datetime DEFAULT NULL COMMENT '终止时间',
`type` varchar(30) NOT NULL COMMENT '',
`notice_scope` tinyint(1) UNSIGNED NOT NULL DEFAULT 1 COMMENT '通知范围1所有人2指定用户',
`notice_users` json DEFAULT NULL COMMENT '通知用户',
`sort` int NOT NULL DEFAULT 999 COMMENT '排序',
`notice_methods` json DEFAULT NULL COMMENT '通知方式1登录弹窗2系统消息',
`is_timing` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否定时',
`publish_time` datetime DEFAULT NULL COMMENT '发布时间',
`is_top` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否置顶',
`status` tinyint(1) UNSIGNED NOT NULL DEFAULT 1 COMMENT '状态1草稿2待发布3已发布',
`create_user` bigint(20) NOT NULL COMMENT '创建人',
`create_time` datetime NOT NULL COMMENT '创建时间',
`update_user` bigint(20) DEFAULT NULL COMMENT '修改人',
@@ -252,6 +255,13 @@ CREATE TABLE IF NOT EXISTS `sys_notice` (
INDEX `idx_update_user`(`update_user`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='公告表';
CREATE TABLE IF NOT EXISTS `sys_notice_log` (
`notice_id` bigint(20) NOT NULL COMMENT '公告ID',
`user_id` bigint(20) NOT NULL COMMENT '用户ID',
`read_time` datetime DEFAULT NULL COMMENT '读取时间',
PRIMARY KEY (`notice_id`, `user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='公告日志表';
CREATE TABLE IF NOT EXISTS `sys_storage` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'ID',
`name` varchar(100) NOT NULL COMMENT '名称',

View File

@@ -46,10 +46,11 @@ VALUES
(1090, '通知公告', 1000, 2, '/system/notice', 'SystemNotice', 'system/notice/index', NULL, 'notification', false, false, false, NULL, 5, 1, 1, NOW()),
(1091, '列表', 1090, 3, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, 'system:notice:list', 1, 1, 1, NOW()),
(1092, '公告详情', 1090, 2, '/system/notice/detail', 'SystemNoticeDetail', 'system/notice/detail/index', NULL, NULL, false, false, true, 'system:notice:get', 2, 1, 1, NOW()),
(1093, '发布公告', 1090, 2, '/system/notice/add', 'SystemNoticeAdd', 'system/notice/add/index', NULL, NULL, false, false, true, 'system:notice:create', 3, 1, 1, NOW()),
(1094, '修改', 1090, 3, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, 'system:notice:update', 4, 1, 1, NOW()),
(1095, '删除', 1090, 3, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, 'system:notice:delete', 5, 1, 1, NOW()),
(1092, '详情', 1090, 3, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, 'system:notice:get', 2, 1, 1, NOW()),
(1093, '查看公告', 1090, 2, '/system/notice/view', 'SystemNoticeView', 'system/notice/view/index', NULL, NULL, false, false, true, 'system:notice:view', 3, 1, 1, NOW()),
(1094, '发布公告', 1090, 2, '/system/notice/add', 'SystemNoticeAdd', 'system/notice/add/index', NULL, NULL, false, false, true, 'system:notice:create', 4, 1, 1, NOW()),
(1095, '修改', 1090, 3, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, 'system:notice:update', 5, 1, 1, NOW()),
(1096, '删除', 1090, 3, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, 'system:notice:delete', 6, 1, 1, NOW()),
(1110, '文件管理', 1000, 2, '/system/file', 'SystemFile', 'system/file/index', NULL, 'file', false, false, false, NULL, 6, 1, 1, NOW()),
(1111, '列表', 1110, 3, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, 'system:file:list', 1, 1, 1, NOW()),
@@ -215,24 +216,21 @@ VALUES
INSERT INTO "sys_dict"
("id", "name", "code", "description", "is_system", "create_user", "create_time")
VALUES
(1, '公告类', 'notice_type', NULL, true, 1, NOW()),
(2, '消息类型', 'message_type', NULL, true, 1, NOW()),
(3, '客户端类型', 'client_type', NULL, true, 1, NOW()),
(4, '短信厂商', 'sms_supplier', NULL, true, 1, NOW());
(1, '公告', 'notice_type', NULL, true, 1, NOW()),
(2, '客户端类型', 'client_type', NULL, true, 1, NOW()),
(3, '短信厂商', 'sms_supplier', NULL, true, 1, NOW());
INSERT INTO "sys_dict_item"
("id", "label", "value", "color", "sort", "description", "status", "dict_id", "create_user", "create_time")
VALUES
(1, '通知', '1', 'primary', 1, NULL, 1, 1, 1, NOW()),
(2, '活动', '2', 'success', 2, NULL, 1, 1, 1, NOW()),
(3, '安全消息', '1', 'warning', 1, NULL, 1, 2, 1, NOW()),
(4, '活动消息', '2', 'success', 2, NULL, 1, 2, 1, NOW()),
(5, '桌面端', 'PC', 'primary', 1, NULL, 1, 3, 1, NOW()),
(6, '安卓', 'ANDROID', 'success', 2, NULL, 1, 3, 1, NOW()),
(7, '小程序', 'XCX', 'warning', 3, NULL, 1, 3, 1, NOW()),
(8, '阿里', 'alibaba', 'warning', 1, NULL, 1, 4, 1, NOW()),
(9, '腾讯云', 'tencent', 'primary', 2, NULL, 1, 4, 1, NOW()),
(10, '容联云', 'cloopen', 'success', 3, NULL, 1, 4, 1, NOW());
(1, '产品新闻', '1', 'primary', 1, NULL, 1, 1, 1, NOW()),
(2, '企业动态', '2', 'success', 2, NULL, 1, 1, 1, NOW()),
(3, '桌面端', 'PC', 'primary', 1, NULL, 1, 2, 1, NOW()),
(4, '安卓', 'ANDROID', 'success', 2, NULL, 1, 2, 1, NOW()),
(5, '小程序', 'XCX', 'warning', 3, NULL, 1, 2, 1, NOW()),
(6, '阿里云', 'alibaba', 'warning', 1, NULL, 1, 3, 1, NOW()),
(7, '腾讯云', 'tencent', 'primary', 2, NULL, 1, 3, 1, NOW()),
(8, '容联', 'cloopen', 'success', 3, NULL, 1, 3, 1, NOW());
-- 初始化默认用户和角色关联数据
INSERT INTO "sys_user_role"

View File

@@ -360,43 +360,47 @@ COMMENT ON TABLE "sys_log" IS '系统日志表';
CREATE TABLE IF NOT EXISTS "sys_message" (
"id" int8 NOT NULL,
"title" varchar(50) NOT NULL,
"content" varchar(255) DEFAULT NULL,
"content" text DEFAULT NULL,
"type" int2 NOT NULL DEFAULT 1,
"create_user" int8 DEFAULT NULL,
"path" varchar(255) DEFAULT NULL,
"scope" int2 NOT NULL DEFAULT 1,
"users" json DEFAULT NULL,
"create_time" timestamp NOT NULL,
PRIMARY KEY ("id")
);
COMMENT ON COLUMN "sys_message"."id" IS 'ID';
COMMENT ON COLUMN "sys_message"."title" IS '标题';
COMMENT ON COLUMN "sys_message"."content" IS '内容';
COMMENT ON COLUMN "sys_message"."type" IS '类型1系统消息';
COMMENT ON COLUMN "sys_message"."create_user" IS '创建人';
COMMENT ON COLUMN "sys_message"."type" IS '类型1系统消息2安全消息';
COMMENT ON COLUMN "sys_message"."path" IS '跳转路径';
COMMENT ON COLUMN "sys_message"."scope" IS '通知范围1所有人2指定用户';
COMMENT ON COLUMN "sys_message"."users" IS '通知用户';
COMMENT ON COLUMN "sys_message"."create_time" IS '创建时间';
COMMENT ON TABLE "sys_message" IS '消息表';
CREATE TABLE IF NOT EXISTS "sys_message_user" (
CREATE TABLE IF NOT EXISTS "sys_message_log" (
"message_id" int8 NOT NULL,
"user_id" int8 NOT NULL,
"is_read" bool NOT NULL DEFAULT false,
"read_time" timestamp DEFAULT NULL,
PRIMARY KEY ("message_id", "user_id")
);
COMMENT ON COLUMN "sys_message_user"."message_id" IS '消息ID';
COMMENT ON COLUMN "sys_message_user"."user_id" IS '用户ID';
COMMENT ON COLUMN "sys_message_user"."is_read" IS '是否已';
COMMENT ON COLUMN "sys_message_user"."read_time" IS '读取时间';
COMMENT ON TABLE "sys_message_user" IS '消息和用户关联表';
COMMENT ON COLUMN "sys_message_log"."message_id" IS '消息ID';
COMMENT ON COLUMN "sys_message_log"."user_id" IS '用户ID';
COMMENT ON COLUMN "sys_message_log"."read_time" IS '取时间';
COMMENT ON TABLE "sys_message_log" IS '消息日志表';
CREATE TABLE IF NOT EXISTS "sys_notice" (
"id" int8 NOT NULL,
"title" varchar(150) NOT NULL,
"content" text NOT NULL,
"type" varchar(30) NOT NULL,
"effective_time" timestamp DEFAULT NULL,
"terminate_time" timestamp DEFAULT NULL,
"notice_scope" int2 NOT NULL DEFAULT 1,
"notice_users" json DEFAULT NULL,
"sort" int4 NOT NULL DEFAULT 999,
"notice_methods" json DEFAULT NULL,
"is_timing" bool NOT NULL DEFAULT false,
"publish_time" timestamp DEFAULT NULL,
"is_top" bool NOT NULL DEFAULT false,
"status" int2 NOT NULL DEFAULT 1,
"create_user" int8 NOT NULL,
"create_time" timestamp NOT NULL,
"update_user" int8 DEFAULT NULL,
@@ -408,18 +412,31 @@ CREATE INDEX "idx_notice_update_user" ON "sys_notice" ("update_user");
COMMENT ON COLUMN "sys_notice"."id" IS 'ID';
COMMENT ON COLUMN "sys_notice"."title" IS '标题';
COMMENT ON COLUMN "sys_notice"."content" IS '内容';
COMMENT ON COLUMN "sys_notice"."type" IS '';
COMMENT ON COLUMN "sys_notice"."effective_time" IS '生效时间';
COMMENT ON COLUMN "sys_notice"."terminate_time" IS '终止时间';
COMMENT ON COLUMN "sys_notice"."type" IS '';
COMMENT ON COLUMN "sys_notice"."notice_scope" IS '通知范围1所有人2指定用户';
COMMENT ON COLUMN "sys_notice"."notice_users" IS '通知用户';
COMMENT ON COLUMN "sys_notice"."sort" IS '排序';
COMMENT ON COLUMN "sys_notice"."notice_methods" IS '通知方式1登录弹窗2系统消息';
COMMENT ON COLUMN "sys_notice"."is_timing" IS '是否定时';
COMMENT ON COLUMN "sys_notice"."publish_time" IS '发布时间';
COMMENT ON COLUMN "sys_notice"."is_top" IS '是否置顶';
COMMENT ON COLUMN "sys_notice"."status" IS '状态1草稿2待发布3已发布';
COMMENT ON COLUMN "sys_notice"."create_user" IS '创建人';
COMMENT ON COLUMN "sys_notice"."create_time" IS '创建时间';
COMMENT ON COLUMN "sys_notice"."update_user" IS '修改人';
COMMENT ON COLUMN "sys_notice"."update_time" IS '修改时间';
COMMENT ON TABLE "sys_notice" IS '公告表';
CREATE TABLE IF NOT EXISTS "sys_notice_log" (
"notice_id" int8 NOT NULL,
"user_id" int8 NOT NULL,
"read_time" timestamp DEFAULT NULL,
PRIMARY KEY ("notice_id", "user_id")
);
COMMENT ON COLUMN "sys_notice_log"."notice_id" IS '消息ID';
COMMENT ON COLUMN "sys_notice_log"."user_id" IS '用户ID';
COMMENT ON COLUMN "sys_notice_log"."read_time" IS '读取时间';
COMMENT ON TABLE "sys_notice_log" IS '公告日志表';
CREATE TABLE IF NOT EXISTS "sys_storage" (
"id" int8 NOT NULL,
"name" varchar(100) NOT NULL,