站内消息.

This commit is contained in:
lijiahang
2024-05-14 19:17:12 +08:00
parent 2fa3eb2251
commit 4fe6208d0e
19 changed files with 385 additions and 52 deletions

View File

@@ -12,6 +12,8 @@
`2024-05-15` `release`
* 🌈 新增 站内信模块
* 🔨 优化 执行命令日志跳转逻辑
* 🔨 修改 `exitStatus` 改为 `exitCode`
[如何升级](/update/v1.0.8.md)

View File

@@ -55,6 +55,10 @@ public interface FieldConst {
String COUNT = "count";
String DATE = "date";
String TIME = "time";
String LOCATION = "location";
String USER_AGENT = "userAgent";

View File

@@ -0,0 +1,53 @@
package com.orion.ops.module.asset.define.message;
import com.orion.ops.module.infra.define.SystemMessageDefine;
import com.orion.ops.module.infra.enums.MessageClassifyEnum;
import lombok.Getter;
/**
* 命令执行 系统消息定义
*
* @author Jiahang Li
* @version 1.0.0
* @since 2024/5/14 17:23
*/
@Getter
public enum ExecMessageDefine implements SystemMessageDefine {
/**
* 命令执行部分失败
*/
EXEC_FAILED(MessageClassifyEnum.NOTICE,
"命令执行失败",
"您在 <sb>${time}</sb> 执行的命令部分失败, 或者返回了非零的 exitCode。点击查看详情 <sb>#${id}</sb> >>>"),
;
ExecMessageDefine(MessageClassifyEnum classify, String title, String content) {
this.classify = classify;
this.type = this.name();
this.title = title;
this.content = content;
}
/**
* 消息分类
*/
private final MessageClassifyEnum classify;
/**
* 消息类型
*/
private final String type;
/**
* 标题
*/
private final String title;
/**
* 内容
*/
private final String content;
}

View File

@@ -0,0 +1,53 @@
package com.orion.ops.module.asset.define.message;
import com.orion.ops.module.infra.define.SystemMessageDefine;
import com.orion.ops.module.infra.enums.MessageClassifyEnum;
import lombok.Getter;
/**
* 上传任务 系统消息定义
*
* @author Jiahang Li
* @version 1.0.0
* @since 2024/5/14 17:23
*/
@Getter
public enum UploadMessageDefine implements SystemMessageDefine {
/**
* 上传任务部分失败
*/
UPLOAD_FAILED(MessageClassifyEnum.NOTICE,
"批量上传失败",
"您在 <sb>${time}</sb> 提交的上传任务中, 有部分主机文件上传失败。点击查看详情 <sb>#${id}</sb> >>>"),
;
UploadMessageDefine(MessageClassifyEnum classify, String title, String content) {
this.classify = classify;
this.type = this.name();
this.title = title;
this.content = content;
}
/**
* 消息分类
*/
private final MessageClassifyEnum classify;
/**
* 消息类型
*/
private final String type;
/**
* 标题
*/
private final String title;
/**
* 内容
*/
private final String content;
}

View File

@@ -22,7 +22,7 @@ import java.util.List;
@Schema(name = "ExecCommandDTO", description = "批量执行启动对象")
public class ExecCommandDTO {
@Schema(description = "hostId")
@Schema(description = "logId")
private Long logId;
@Schema(description = "用户id")

View File

@@ -67,6 +67,9 @@ public abstract class BaseExecCommandHandler implements IExecCommandHandler {
private CommandExecutor executor;
@Getter
private Integer exitCode;
private volatile boolean closed;
private volatile boolean interrupted;
@@ -228,6 +231,7 @@ public abstract class BaseExecCommandHandler implements IExecCommandHandler {
// 完成
updateRecord.setFinishTime(new Date());
updateRecord.setExitStatus(executor.getExitCode());
this.exitCode = executor.getExitCode();
} else if (ExecHostStatusEnum.FAILED.equals(status)) {
// 失败
updateRecord.setFinishTime(new Date());

View File

@@ -7,20 +7,29 @@ import com.orion.lang.utils.Booleans;
import com.orion.lang.utils.Threads;
import com.orion.lang.utils.collect.Lists;
import com.orion.lang.utils.io.Streams;
import com.orion.lang.utils.time.Dates;
import com.orion.net.host.ssh.ExitCode;
import com.orion.ops.framework.common.constant.ExtraFieldConst;
import com.orion.ops.module.asset.dao.ExecLogDAO;
import com.orion.ops.module.asset.define.AssetThreadPools;
import com.orion.ops.module.asset.define.config.AppExecLogConfig;
import com.orion.ops.module.asset.define.message.ExecMessageDefine;
import com.orion.ops.module.asset.entity.domain.ExecLogDO;
import com.orion.ops.module.asset.enums.ExecHostStatusEnum;
import com.orion.ops.module.asset.enums.ExecStatusEnum;
import com.orion.ops.module.asset.handler.host.exec.command.dto.ExecCommandDTO;
import com.orion.ops.module.asset.handler.host.exec.command.dto.ExecCommandHostDTO;
import com.orion.ops.module.asset.handler.host.exec.command.manager.ExecTaskManager;
import com.orion.ops.module.infra.api.SystemMessageApi;
import com.orion.ops.module.infra.entity.dto.message.SystemMessageDTO;
import com.orion.spring.SpringHolder;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* 命令执行任务
@@ -38,6 +47,8 @@ public class ExecTaskHandler implements IExecTaskHandler {
private static final AppExecLogConfig appExecLogConfig = SpringHolder.getBean(AppExecLogConfig.class);
private static final SystemMessageApi systemMessageApi = SpringHolder.getBean(SystemMessageApi.class);
private final ExecCommandDTO execCommand;
private TimeoutChecker<TimeoutEndpoint> timeoutChecker;
@@ -45,6 +56,8 @@ public class ExecTaskHandler implements IExecTaskHandler {
@Getter
private final List<IExecCommandHandler> handlers;
private Date startTime;
public ExecTaskHandler(ExecCommandDTO execCommand) {
this.execCommand = execCommand;
this.handlers = Lists.newList();
@@ -56,9 +69,9 @@ public class ExecTaskHandler implements IExecTaskHandler {
// 添加任务
execTaskManager.addTask(id, this);
log.info("ExecTaskHandler.run start id: {}", id);
// 更新状态
this.updateStatus(ExecStatusEnum.RUNNING);
try {
// 更新状态
this.updateStatus(ExecStatusEnum.RUNNING);
// 执行命令
this.runHostCommand();
// 更新状态-执行完成
@@ -69,10 +82,12 @@ public class ExecTaskHandler implements IExecTaskHandler {
this.updateStatus(ExecStatusEnum.FAILED);
log.error("ExecTaskHandler.run error id: {}", id, e);
} finally {
// 释放资源
Streams.close(this);
// 检查是否发送消息
this.checkSendMessage();
// 移除任务
execTaskManager.removeTask(id);
// 释放资源
this.close();
}
}
@@ -82,6 +97,13 @@ public class ExecTaskHandler implements IExecTaskHandler {
handlers.forEach(IExecCommandHandler::interrupt);
}
@Override
public void close() {
log.info("ExecTaskHandler-close id: {}", execCommand.getLogId());
Streams.close(timeoutChecker);
this.handlers.forEach(Streams::close);
}
/**
* 执行主机命令
*
@@ -139,6 +161,7 @@ public class ExecTaskHandler implements IExecTaskHandler {
update.setStatus(statusName);
if (ExecStatusEnum.RUNNING.equals(status)) {
// 执行中
this.startTime = new Date();
update.setStartTime(new Date());
} else if (ExecStatusEnum.COMPLETED.equals(status)) {
// 执行完成
@@ -151,11 +174,30 @@ public class ExecTaskHandler implements IExecTaskHandler {
log.info("ExecTaskHandler-updateStatus finish id: {}, effect: {}", id, effect);
}
@Override
public void close() {
log.info("ExecTaskHandler-close id: {}", execCommand.getLogId());
Streams.close(timeoutChecker);
this.handlers.forEach(Streams::close);
/**
* 检查是否发送消息
*/
private void checkSendMessage() {
// 检查是否执行失败/exitCode
boolean hasError = handlers.stream().anyMatch(s ->
ExecHostStatusEnum.FAILED.equals(s.getStatus())
|| ExecHostStatusEnum.TIMEOUT.equals(s.getStatus())
|| !ExitCode.isSuccess(s.getExitCode()));
if (!hasError) {
return;
}
// 参数
Map<String, Object> params = new HashMap<>();
params.put(ExtraFieldConst.ID, execCommand.getLogId());
params.put(ExtraFieldConst.TIME, Dates.format(this.startTime, Dates.MD_HM));
SystemMessageDTO message = SystemMessageDTO.builder()
.receiverId(execCommand.getUserId())
.receiverUsername(execCommand.getUsername())
.relKey(String.valueOf(execCommand.getLogId()))
.params(params)
.build();
// 发送
systemMessageApi.create(ExecMessageDefine.EXEC_FAILED, message);
}
}

View File

@@ -31,6 +31,13 @@ public interface IExecCommandHandler extends Runnable, SafeCloseable {
*/
ExecHostStatusEnum getStatus();
/**
* 获取退出码
*
* @return exit code
*/
Integer getExitCode();
/**
* 获取主机 id
*

View File

@@ -3,10 +3,13 @@ package com.orion.ops.module.asset.handler.host.upload.task;
import com.orion.lang.utils.Threads;
import com.orion.lang.utils.io.Files1;
import com.orion.lang.utils.io.Streams;
import com.orion.lang.utils.time.Dates;
import com.orion.ops.framework.common.constant.Const;
import com.orion.ops.framework.common.constant.ExtraFieldConst;
import com.orion.ops.module.asset.dao.UploadTaskDAO;
import com.orion.ops.module.asset.dao.UploadTaskFileDAO;
import com.orion.ops.module.asset.define.AssetThreadPools;
import com.orion.ops.module.asset.define.message.UploadMessageDefine;
import com.orion.ops.module.asset.entity.domain.UploadTaskDO;
import com.orion.ops.module.asset.entity.domain.UploadTaskFileDO;
import com.orion.ops.module.asset.enums.UploadTaskFileStatusEnum;
@@ -16,14 +19,13 @@ import com.orion.ops.module.asset.handler.host.upload.manager.FileUploadTaskMana
import com.orion.ops.module.asset.handler.host.upload.uploader.FileUploader;
import com.orion.ops.module.asset.handler.host.upload.uploader.IFileUploader;
import com.orion.ops.module.asset.service.UploadTaskService;
import com.orion.ops.module.infra.api.SystemMessageApi;
import com.orion.ops.module.infra.entity.dto.message.SystemMessageDTO;
import com.orion.spring.SpringHolder;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.*;
import java.util.stream.Collectors;
/**
@@ -42,6 +44,8 @@ public class FileUploadTask implements IFileUploadTask {
private static final UploadTaskService uploadTaskService = SpringHolder.getBean(UploadTaskService.class);
private static final SystemMessageApi systemMessageApi = SpringHolder.getBean(SystemMessageApi.class);
private static final FileUploadTaskManager fileUploadTaskManager = SpringHolder.getBean(FileUploadTaskManager.class);
private final Long id;
@@ -91,6 +95,8 @@ public class FileUploadTask implements IFileUploadTask {
} else {
this.updateStatus(UploadTaskStatusEnum.FINISHED);
}
// 检查是否发送消息
this.checkSendMessage();
// 移除任务
fileUploadTaskManager.removeTask(id);
// 释放资源
@@ -187,4 +193,33 @@ public class FileUploadTask implements IFileUploadTask {
uploadTaskDAO.updateById(update);
}
/**
* 检查是否发送消息
*/
private void checkSendMessage() {
if (canceled) {
return;
}
// 检查是否上传失败
boolean hasError = uploaderList.stream()
.map(IFileUploader::getFiles)
.flatMap(Collection::stream)
.anyMatch(s -> UploadTaskFileStatusEnum.FAILED.name().equals(s.getStatus()));
if (!hasError) {
return;
}
// 参数
Map<String, Object> params = new HashMap<>();
params.put(ExtraFieldConst.ID, record.getId());
params.put(ExtraFieldConst.TIME, Dates.format(record.getCreateTime(), Dates.MD_HM));
SystemMessageDTO message = SystemMessageDTO.builder()
.receiverId(record.getUserId())
.receiverUsername(record.getUsername())
.relKey(String.valueOf(record.getId()))
.params(params)
.build();
// 发送
systemMessageApi.create(UploadMessageDefine.UPLOAD_FAILED, message);
}
}

View File

@@ -1,6 +1,8 @@
package com.orion.ops.module.infra.api;
import com.orion.ops.module.infra.define.SystemMessageDefine;
import com.orion.ops.module.infra.entity.dto.message.SystemMessageCreateDTO;
import com.orion.ops.module.infra.entity.dto.message.SystemMessageDTO;
import com.orion.ops.module.infra.enums.MessageClassifyEnum;
/**
@@ -12,6 +14,15 @@ import com.orion.ops.module.infra.enums.MessageClassifyEnum;
*/
public interface SystemMessageApi {
/**
* 创建系统消息
*
* @param define define
* @param dto dto
* @return id
*/
Long create(SystemMessageDefine define, SystemMessageDTO dto);
/**
* 创建系统消息
*
@@ -19,6 +30,6 @@ public interface SystemMessageApi {
* @param dto dto
* @return id
*/
Long createSystemMessage(MessageClassifyEnum classify, SystemMessageCreateDTO dto);
Long create(MessageClassifyEnum classify, SystemMessageCreateDTO dto);
}

View File

@@ -0,0 +1,47 @@
package com.orion.ops.module.infra.define;
import com.orion.lang.utils.Strings;
import com.orion.ops.module.infra.enums.MessageClassifyEnum;
import java.util.Map;
/**
* 系统消息定义
*
* @author Jiahang Li
* @version 1.0.0
* @since 2024/5/14 17:06
*/
public interface SystemMessageDefine {
/**
* @return 消息分类
*/
MessageClassifyEnum getClassify();
/**
* @return 消息类型
*/
String getType();
/**
* @return 标题
*/
String getTitle();
/**
* @return 内容
*/
String getContent();
/**
* 格式化内容
*
* @param params params
* @return content
*/
default String formatContent(Map<String, Object> params) {
return Strings.format(this.getContent(), params);
}
}

View File

@@ -27,6 +27,11 @@ public class SystemMessageCreateDTO implements Serializable {
private static final long serialVersionUID = 1L;
@NotBlank
@Size(max = 10)
@Schema(description = "消息分类")
private String classify;
@NotBlank
@Size(max = 32)
@Schema(description = "消息类型")

View File

@@ -0,0 +1,46 @@
package com.orion.ops.module.infra.entity.dto.message;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;
import java.io.Serializable;
import java.util.Map;
/**
* 系统消息 请求业务对象
*
* @author Jiahang Li
* @version 1.0.8
* @since 2024-5-11 16:29
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Schema(name = "SystemMessageDTO", description = "系统消息 请求业务对象")
public class SystemMessageDTO implements Serializable {
private static final long serialVersionUID = 1L;
@NotBlank
@Size(max = 64)
@Schema(description = "消息关联")
private String relKey;
@NotNull
@Schema(description = "接收人id")
private Long receiverId;
@Schema(description = "接收人用户名")
private String receiverUsername;
@Schema(description = "参数")
private Map<String, Object> params;
}

View File

@@ -1,10 +1,11 @@
package com.orion.ops.module.infra.api.impl;
import com.alibaba.fastjson.JSON;
import com.orion.ops.framework.common.utils.Valid;
import com.orion.ops.module.infra.api.SystemMessageApi;
import com.orion.ops.module.infra.convert.SystemMessageProviderConvert;
import com.orion.ops.module.infra.define.SystemMessageDefine;
import com.orion.ops.module.infra.entity.dto.message.SystemMessageCreateDTO;
import com.orion.ops.module.infra.entity.dto.message.SystemMessageDTO;
import com.orion.ops.module.infra.entity.request.message.SystemMessageCreateRequest;
import com.orion.ops.module.infra.enums.MessageClassifyEnum;
import com.orion.ops.module.infra.service.SystemMessageService;
@@ -28,12 +29,28 @@ public class SystemMessageApiImpl implements SystemMessageApi {
private SystemMessageService systemMessageService;
@Override
public Long createSystemMessage(MessageClassifyEnum classify, SystemMessageCreateDTO dto) {
log.info("SystemMessageApi.createSystemMessage dto: {}", JSON.toJSONString(dto));
public Long create(SystemMessageDefine define, SystemMessageDTO dto) {
Valid.valid(dto);
// 转换
SystemMessageCreateRequest request = SystemMessageCreateRequest.builder()
.classify(define.getClassify().name())
.type(define.getType())
.title(define.getTitle())
.content(define.formatContent(dto.getParams()))
.relKey(dto.getRelKey())
.receiverId(dto.getReceiverId())
.receiverUsername(dto.getReceiverUsername())
.build();
// 创建
return systemMessageService.createSystemMessage(request);
}
@Override
public Long create(MessageClassifyEnum classify, SystemMessageCreateDTO dto) {
dto.setClassify(classify.name());
Valid.valid(dto);
// 转换
SystemMessageCreateRequest request = SystemMessageProviderConvert.MAPPER.toRequest(dto);
request.setClassify(classify.name());
// 创建
return systemMessageService.createSystemMessage(request);
}

View File

@@ -1,5 +1,6 @@
package com.orion.ops.module.infra.service.impl;
import com.alibaba.fastjson.JSON;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.orion.lang.function.Functions;
import com.orion.lang.utils.Booleans;
@@ -44,6 +45,7 @@ public class SystemMessageServiceImpl implements SystemMessageService {
@Override
public Long createSystemMessage(SystemMessageCreateRequest request) {
log.info("SystemMessageService.createSystemMessage request: {}", JSON.toJSONString(request));
// 设置接收人用户名
if (request.getReceiverUsername() == null) {
Optional.ofNullable(request.getReceiverId())

View File

@@ -99,7 +99,8 @@
position="br"
:show-arrow="false"
:popup-style="{ marginLeft: '198px' }"
:content-style="{ padding: 0, width: '498px' }">
:content-style="{ padding: 0, width: '428px' }"
@hide="pullHasUnreadMessage">
<div ref="messageRef" class="ref-btn" />
<template #content>
<message-box />
@@ -310,10 +311,6 @@
// 获取是否有未读的消息
const pullHasUnreadMessage = () => {
// 有未读的消息直接返回
if (messageCount.value) {
return;
}
// 查询
checkHasUnreadMessage().then(({ data }) => {
messageCount.value = data ? 1 : 0;

View File

@@ -26,13 +26,6 @@
checked-text="未读"
unchecked-text="全部"
@change="reloadAllMessage" />
<!-- 全部已读 -->
<a-button class="header-button"
type="text"
size="small"
@click="setAllRead">
全部已读
</a-button>
<!-- 清空 -->
<a-button class="header-button"
type="text"
@@ -40,6 +33,13 @@
@click="clearAllMessage">
清空
</a-button>
<!-- 全部已读 -->
<a-button class="header-button"
type="text"
size="small"
@click="setAllRead">
全部已读
</a-button>
</a-space>
</template>
</a-tabs>

View File

@@ -6,7 +6,7 @@
<!-- 加载中 -->
<a-skeleton class="skeleton-wrapper" :animation="true">
<a-skeleton-line :rows="3"
:line-height="86"
:line-height="96"
:line-spacing="8" />
</a-skeleton>
</div>
@@ -56,17 +56,21 @@
</a-button>
</div>
</div>
<!-- 文本 -->
<!-- 内容 -->
<div v-html="message.contentHtml"
class="message-item-content text-ellipsis"
class="message-item-content"
:title="message.content" />
<!-- 时间 -->
<div class="message-item-time">
{{ dateFormat(new Date(message.createTime))}}
</div>
</div>
<!-- 加载中 -->
<a-skeleton v-if="fetchLoading"
class="skeleton-wrapper"
:animation="true">
<a-skeleton-line :rows="3"
:line-height="86"
:line-height="96"
:line-spacing="8" />
</a-skeleton>
<!-- 加载更多 -->
@@ -92,6 +96,7 @@
import type { MessageRecordResponse } from '@/api/system/message';
import { MessageStatus, messageTypeKey } from './const';
import { useDictStore } from '@/store';
import { dateFormat } from '@/utils';
const emits = defineEmits(['load', 'click', 'view', 'delete']);
const props = defineProps<{
@@ -107,7 +112,6 @@
<style lang="less" scoped>
@gap: 8px;
@message-height: 86px;
@actions-width: 82px;
.skeleton-wrapper {
@@ -116,7 +120,7 @@
.message-list-container {
width: 100%;
height: 344px;
height: 338px;
display: block;
.message-list-wrapper {
@@ -133,17 +137,17 @@
}
.message-item {
height: @message-height;
padding: 16px 20px;
padding: 12px 20px;
border-bottom: 1px solid var(--color-neutral-3);
display: flex;
flex-direction: column;
justify-content: space-between;
font-size: 14px;
color: var(--color-text-1);
cursor: pointer;
transition: all .2s;
&-title {
height: 22px;
display: flex;
justify-content: space-between;
align-items: flex-start;
@@ -151,9 +155,9 @@
&-text {
width: calc(100% - @actions-width - @gap);
display: block;
font-size: 15px;
font-weight: 600;
font-size: 14px;
text-overflow: clip;
color: var(--color-text-1);
}
&-status {
@@ -181,9 +185,17 @@
&-content {
display: block;
padding-bottom: 2px;
color: var(--color-text-1);
text-overflow: clip;
margin-top: 4px;
font-size: 12px;
color: var(--color-text-2);
}
&-time {
height: 18px;
margin-top: 4px;
display: block;
font-size: 12px;
color: var(--color-text-2);
}
}
@@ -201,7 +213,7 @@
}
.message-item-read {
.message-item-title-text, .message-item-title-status, .message-item-content {
.message-item-title-text, .message-item-title-status, .message-item-content, .message-item-time {
opacity: .65;
}
}
@@ -210,10 +222,6 @@
position: absolute;
height: 100%;
width: 100%;
.arco-scrollbar-track-direction-horizontal {
display: none;
}
}
</style>

View File

@@ -3,12 +3,11 @@
title-align="start"
:title="record.title"
:top="80"
:width="720"
:align-center="false"
:unmount-on-close="true"
ok-text="删除"
:hide-cancel="true"
:ok-button-props="{ status: 'danger' }"
:ok-button-props="{ status: 'danger', size: 'small' }"
:body-style="{ padding: '20px' }"
@ok="emits('delete', record)">
<div class="content" v-html="record.contentHtml" />
@@ -45,5 +44,6 @@
<style lang="less" scoped>
.content {
font-size: 16px;
color: var(--color-text-2);
}
</style>