增加系统版本通知功能

This commit is contained in:
dengqichen 2025-12-09 14:04:59 +08:00
parent 78c2ef0dd9
commit 94ef1ef9eb
17 changed files with 977 additions and 6 deletions

View File

@ -0,0 +1,124 @@
package com.qqchen.deploy.backend.system.api;
import com.qqchen.deploy.backend.framework.api.Response;
import com.qqchen.deploy.backend.framework.controller.BaseController;
import com.qqchen.deploy.backend.system.dto.SystemReleaseDTO;
import com.qqchen.deploy.backend.system.entity.SystemRelease;
import com.qqchen.deploy.backend.system.query.SystemReleaseQuery;
import com.qqchen.deploy.backend.system.service.ISystemReleaseService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.domain.Page;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.concurrent.CompletableFuture;
/**
* 系统版本发布记录 Controller
*/
@Slf4j
@RestController
@RequestMapping("/api/v1/system-release")
@Tag(name = "系统版本发布管理", description = "系统版本发布记录管理相关接口")
public class SystemReleaseApiController
extends BaseController<SystemRelease, SystemReleaseDTO, Long, SystemReleaseQuery> {
@Resource
private ISystemReleaseService systemReleaseService;
@Override
@PostMapping
@Operation(summary = "创建发布记录", description = "创建新的版本发布记录")
public Response<SystemReleaseDTO> create(@Validated @RequestBody SystemReleaseDTO dto) {
return super.create(dto);
}
@Override
@PutMapping("/{id}")
@Operation(summary = "更新发布记录", description = "更新指定ID的版本发布记录")
public Response<SystemReleaseDTO> update(@PathVariable Long id, @Validated @RequestBody SystemReleaseDTO dto) {
return super.update(id, dto);
}
@Override
@DeleteMapping("/{id}")
@Operation(summary = "删除发布记录", description = "删除指定ID的版本发布记录逻辑删除")
public Response<Void> delete(@PathVariable Long id) {
return super.delete(id);
}
@Override
@GetMapping("/{id}")
@Operation(summary = "查询发布记录详情", description = "根据ID查询版本发布记录详情")
public Response<SystemReleaseDTO> findById(@PathVariable Long id) {
return super.findById(id);
}
@Override
@GetMapping("/list")
@Operation(summary = "查询所有发布记录", description = "查询所有版本发布记录列表")
public Response<List<SystemReleaseDTO>> findAll() {
return super.findAll();
}
@Override
@GetMapping("/page")
@Operation(summary = "分页查询发布记录", description = "分页查询版本发布记录")
public Response<Page<SystemReleaseDTO>> page(SystemReleaseQuery query) {
return super.page(query);
}
@Override
@GetMapping("/query")
@Operation(summary = "条件查询发布记录", description = "根据条件查询版本发布记录列表")
public Response<List<SystemReleaseDTO>> findAll(SystemReleaseQuery query) {
return super.findAll(query);
}
@Override
@PostMapping("/batch")
@Operation(summary = "批量处理发布记录", description = "批量创建/更新版本发布记录")
public CompletableFuture<Response<Void>> batchProcess(@RequestBody List<SystemReleaseDTO> dtos) {
return super.batchProcess(dtos);
}
/**
* 获取未通知的发布记录列表
*/
@GetMapping("/unnotified")
@Operation(summary = "获取未通知的发布记录", description = "查询所有未发送通知的版本发布记录")
public Response<List<SystemReleaseDTO>> getUnnotifiedReleases() {
List<SystemReleaseDTO> releases = systemReleaseService.getUnnotifiedReleases();
return Response.success(releases);
}
/**
* 标记为已通知
*/
@PutMapping("/{id}/notify")
@Operation(summary = "标记为已通知", description = "将指定发布记录标记为已发送通知")
public Response<Void> markAsNotified(@PathVariable Long id) {
systemReleaseService.markAsNotified(id);
return Response.success();
}
/**
* 获取指定模块的最新发布记录
*/
@GetMapping("/latest/{module}")
@Operation(summary = "获取最新发布记录", description = "获取指定模块的最新版本发布记录")
public Response<SystemReleaseDTO> getLatestReleaseByModule(@PathVariable String module) {
SystemReleaseDTO release = systemReleaseService.getLatestReleaseByModule(module);
return Response.success(release);
}
@Override
protected void exportData(HttpServletResponse response, List<SystemReleaseDTO> data) {
log.info("导出系统版本发布记录数据,数据量:{}", data.size());
}
}

View File

@ -0,0 +1,28 @@
package com.qqchen.deploy.backend.system.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.TaskScheduler;
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
/**
* 系统版本发布配置
*/
@Configuration
public class SystemReleaseConfig {
/**
* 配置任务调度器
* 用于延迟执行系统维护任务
*/
@Bean
public TaskScheduler systemMaintenanceTaskScheduler() {
ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();
scheduler.setPoolSize(5);
scheduler.setThreadNamePrefix("system-maintenance-");
scheduler.setWaitForTasksToCompleteOnShutdown(false);
scheduler.setAwaitTerminationSeconds(0);
scheduler.initialize();
return scheduler;
}
}

View File

@ -0,0 +1,13 @@
package com.qqchen.deploy.backend.system.converter;
import com.qqchen.deploy.backend.framework.converter.BaseConverter;
import com.qqchen.deploy.backend.system.dto.SystemReleaseDTO;
import com.qqchen.deploy.backend.system.entity.SystemRelease;
import org.mapstruct.Mapper;
/**
* 系统版本发布记录转换器
*/
@Mapper(config = BaseConverter.class)
public interface SystemReleaseConverter extends BaseConverter<SystemRelease, SystemReleaseDTO> {
}

View File

@ -0,0 +1,63 @@
package com.qqchen.deploy.backend.system.dto;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.qqchen.deploy.backend.framework.dto.BaseDTO;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.math.BigDecimal;
import java.time.LocalDateTime;
/**
* 系统版本发布记录 DTO
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class SystemReleaseDTO extends BaseDTO {
/**
* 发布版本号 1.51
*/
@NotNull(message = "版本号不能为空")
private BigDecimal releaseVersion;
/**
* 模块类型BACKEND/FRONTEND/ALL
*/
@NotBlank(message = "模块类型不能为空")
private String module;
/**
* 发布时间
*/
@NotNull(message = "发布时间不能为空")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime releaseDate;
/**
* 变更内容换行分隔
*/
private String changes;
/**
* 是否已发送版本发布通知
*/
private Boolean notified;
/**
* 延迟执行分钟数创建后多久开始维护为空则不触发维护
*/
private Integer delayMinutes;
/**
* 预计维护时长分钟
*/
private Integer estimatedDuration;
/**
* 是否自动停止服务
*/
private Boolean enableAutoShutdown;
}

View File

@ -0,0 +1,67 @@
package com.qqchen.deploy.backend.system.entity;
import com.qqchen.deploy.backend.framework.domain.Entity;
import jakarta.persistence.*;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.math.BigDecimal;
import java.time.LocalDateTime;
/**
* 系统版本发布记录实体
*/
@Data
@EqualsAndHashCode(callSuper = true)
@jakarta.persistence.Entity
@Table(name = "system_release")
public class SystemRelease extends Entity<Long> {
/**
* 发布版本号 1.51
*/
@Column(name = "release_version", nullable = false, precision = 10, scale = 2)
private BigDecimal releaseVersion;
/**
* 模块类型BACKEND/FRONTEND/ALL
*/
@Column(name = "module", nullable = false, length = 20)
private String module;
/**
* 发布时间
*/
@Column(name = "release_date", nullable = false)
private LocalDateTime releaseDate;
/**
* 变更内容换行分隔
*/
@Column(name = "changes", columnDefinition = "TEXT")
private String changes;
/**
* 是否已发送版本发布通知
*/
@Column(name = "notified", nullable = false)
private Boolean notified = false;
/**
* 延迟执行分钟数创建后多久开始维护为空则不触发维护
*/
@Column(name = "delay_minutes")
private Integer delayMinutes;
/**
* 预计维护时长分钟
*/
@Column(name = "estimated_duration")
private Integer estimatedDuration;
/**
* 是否自动停止服务
*/
@Column(name = "enable_auto_shutdown", nullable = false)
private Boolean enableAutoShutdown = false;
}

View File

@ -0,0 +1,110 @@
package com.qqchen.deploy.backend.system.listener;
import com.qqchen.deploy.backend.framework.enums.ResponseCode;
import com.qqchen.deploy.backend.framework.exception.BusinessException;
import com.qqchen.deploy.backend.notification.dto.WeworkSendNotificationRequest;
import com.qqchen.deploy.backend.notification.entity.NotificationChannel;
import com.qqchen.deploy.backend.notification.enums.WeworkMessageTypeEnum;
import com.qqchen.deploy.backend.notification.repository.INotificationChannelRepository;
import com.qqchen.deploy.backend.notification.service.INotificationSendService;
import com.qqchen.deploy.backend.system.entity.SystemRelease;
import com.qqchen.deploy.backend.system.repository.ISystemReleaseRepository;
import com.qqchen.deploy.backend.system.service.ISystemReleaseService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;
/**
* 系统版本发布启动监听器
* 应用启动完成后自动发送最新版本通知
*/
@Slf4j
@Component
public class SystemReleaseStartupListener {
@Autowired
private ISystemReleaseRepository releaseRepository;
@Autowired
private ISystemReleaseService releaseService;
@Autowired
private INotificationChannelRepository notificationChannelRepository;
@Autowired
private INotificationSendService notificationSendService;
@Value("${deploy.notification.release.channel-id}")
private Long notificationChannelId;
/**
* 应用启动完成事件监听
*/
@EventListener(ApplicationReadyEvent.class)
public void onApplicationReady() {
try {
log.info("========================================");
log.info("检查是否有未通知的版本发布记录...");
// 查询最新的未通知版本按版本号降序
SystemRelease latestRelease = releaseRepository
.findFirstByNotifiedFalseAndDeletedFalseOrderByReleaseVersionDesc();
if (latestRelease != null) {
log.info("发现未通知的版本发布记录");
log.info("版本号: {}", latestRelease.getReleaseVersion());
log.info("模块: {}", latestRelease.getModule());
// 发送版本发布通知
sendReleaseNotification(latestRelease);
// 标记为已通知
releaseService.markAsNotified(latestRelease.getId());
log.info("版本发布通知已发送并标记");
} else {
log.info("没有未通知的版本发布记录");
}
log.info("========================================");
} catch (Exception e) {
log.error("版本发布通知处理失败", e);
}
}
/**
* 发送版本发布通知
*/
private void sendReleaseNotification(SystemRelease release) {
try {
// 查询通知渠道
NotificationChannel channel = notificationChannelRepository.findById(notificationChannelId)
.orElseThrow(() -> new BusinessException(ResponseCode.DATA_NOT_FOUND,
new Object[]{"通知渠道ID=" + notificationChannelId + "不存在"}));
// 准备通知内容加上版本号
String changes = release.getChanges() != null ? release.getChanges() : "暂无变更内容";
String message = String.format("版本号:%s\n\n%s", release.getReleaseVersion(), changes);
log.info("发送版本发布通知v{}\n{}", release.getReleaseVersion(), message);
// 创建企微通知请求
WeworkSendNotificationRequest request = new WeworkSendNotificationRequest();
request.setContent(message);
request.setTitle("系统版本上线通知");
request.setMessageType(WeworkMessageTypeEnum.TEXT);
// 发送通知
notificationSendService.send(channel, request);
log.info("版本发布通知发送成功");
} catch (Exception e) {
log.error("发送版本发布通知失败", e);
}
}
}

View File

@ -0,0 +1,60 @@
package com.qqchen.deploy.backend.system.query;
import com.qqchen.deploy.backend.framework.annotation.QueryField;
import com.qqchen.deploy.backend.framework.enums.QueryType;
import com.qqchen.deploy.backend.framework.query.BaseQuery;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.math.BigDecimal;
import java.time.LocalDateTime;
/**
* 系统版本发布记录查询对象
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class SystemReleaseQuery extends BaseQuery {
/**
* 版本号精确查询
*/
@QueryField(field = "releaseVersion")
private BigDecimal releaseVersion;
/**
* 版本号范围查询 - 最小版本
*/
@QueryField(field = "releaseVersion", type = QueryType.GREATER_EQUAL)
private BigDecimal releaseVersionMin;
/**
* 版本号范围查询 - 最大版本
*/
@QueryField(field = "releaseVersion", type = QueryType.LESS_EQUAL)
private BigDecimal releaseVersionMax;
/**
* 模块类型精确查询
*/
@QueryField(field = "module")
private String module;
/**
* 是否已通知
*/
@QueryField(field = "notified")
private Boolean notified;
/**
* 发布时间开始
*/
@QueryField(field = "releaseDate", type = QueryType.GREATER_EQUAL)
private LocalDateTime releaseDateStart;
/**
* 发布时间结束
*/
@QueryField(field = "releaseDate", type = QueryType.LESS_EQUAL)
private LocalDateTime releaseDateEnd;
}

View File

@ -0,0 +1,55 @@
package com.qqchen.deploy.backend.system.repository;
import com.qqchen.deploy.backend.framework.repository.IBaseRepository;
import com.qqchen.deploy.backend.system.entity.SystemRelease;
import org.springframework.stereotype.Repository;
import java.math.BigDecimal;
import java.util.List;
/**
* 系统版本发布记录仓储接口
*/
@Repository
public interface ISystemReleaseRepository extends IBaseRepository<SystemRelease, Long> {
/**
* 根据版本号和模块查询排除已删除
*
* @param releaseVersion 版本号
* @param module 模块类型
* @return 发布记录
*/
SystemRelease findByReleaseVersionAndModuleAndDeletedFalse(BigDecimal releaseVersion, String module);
/**
* 检查版本号和模块是否存在排除已删除
*
* @param releaseVersion 版本号
* @param module 模块类型
* @return 是否存在
*/
boolean existsByReleaseVersionAndModuleAndDeletedFalse(BigDecimal releaseVersion, String module);
/**
* 查询未通知的发布记录排除已删除
*
* @return 未通知的发布记录列表
*/
List<SystemRelease> findByNotifiedFalseAndDeletedFalseOrderByReleaseDateDesc();
/**
* 根据模块查询最新的发布记录排除已删除
*
* @param module 模块类型
* @return 最新的发布记录
*/
SystemRelease findTopByModuleAndDeletedFalseOrderByReleaseDateDesc(String module);
/**
* 查询最新的未通知版本按版本号降序
*
* @return 最新的未通知版本
*/
SystemRelease findFirstByNotifiedFalseAndDeletedFalseOrderByReleaseVersionDesc();
}

View File

@ -0,0 +1,36 @@
package com.qqchen.deploy.backend.system.service;
import com.qqchen.deploy.backend.framework.service.IBaseService;
import com.qqchen.deploy.backend.system.dto.SystemReleaseDTO;
import com.qqchen.deploy.backend.system.entity.SystemRelease;
import com.qqchen.deploy.backend.system.query.SystemReleaseQuery;
import java.util.List;
/**
* 系统版本发布记录服务接口
*/
public interface ISystemReleaseService extends IBaseService<SystemRelease, SystemReleaseDTO, SystemReleaseQuery, Long> {
/**
* 获取未通知的发布记录列表
*
* @return 未通知的发布记录列表
*/
List<SystemReleaseDTO> getUnnotifiedReleases();
/**
* 标记为已通知
*
* @param id 发布记录ID
*/
void markAsNotified(Long id);
/**
* 获取指定模块的最新发布记录
*
* @param module 模块类型
* @return 最新发布记录
*/
SystemReleaseDTO getLatestReleaseByModule(String module);
}

View File

@ -0,0 +1,261 @@
package com.qqchen.deploy.backend.system.service.impl;
import com.qqchen.deploy.backend.framework.annotation.ServiceType;
import com.qqchen.deploy.backend.framework.enums.ResponseCode;
import com.qqchen.deploy.backend.framework.exception.BusinessException;
import com.qqchen.deploy.backend.framework.service.impl.BaseServiceImpl;
import com.qqchen.deploy.backend.notification.dto.WeworkSendNotificationRequest;
import com.qqchen.deploy.backend.notification.entity.NotificationChannel;
import com.qqchen.deploy.backend.notification.enums.WeworkMessageTypeEnum;
import com.qqchen.deploy.backend.notification.repository.INotificationChannelRepository;
import com.qqchen.deploy.backend.notification.service.INotificationSendService;
import com.qqchen.deploy.backend.system.dto.SystemReleaseDTO;
import com.qqchen.deploy.backend.system.entity.SystemRelease;
import com.qqchen.deploy.backend.system.query.SystemReleaseQuery;
import com.qqchen.deploy.backend.system.repository.ISystemReleaseRepository;
import com.qqchen.deploy.backend.system.service.ISystemReleaseService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.scheduling.TaskScheduler;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ScheduledFuture;
import java.util.stream.Collectors;
/**
* 系统版本发布记录服务实现
*/
@Slf4j
@Service
@ServiceType(ServiceType.Type.DATABASE)
public class SystemReleaseServiceImpl
extends BaseServiceImpl<SystemRelease, SystemReleaseDTO, SystemReleaseQuery, Long>
implements ISystemReleaseService {
private final ISystemReleaseRepository systemReleaseRepository;
@Autowired
private TaskScheduler taskScheduler;
@Autowired
private ApplicationContext applicationContext;
@Autowired
private INotificationChannelRepository notificationChannelRepository;
@Autowired
private INotificationSendService notificationSendService;
@Value("${deploy.notification.release.channel-id}")
private Long notificationChannelId;
/**
* 维护任务映射releaseId -> ScheduledFuture
* 用于跟踪和取消已调度的维护任务
*/
private final Map<Long, ScheduledFuture<?>> scheduledTasks = new ConcurrentHashMap<>();
public SystemReleaseServiceImpl(ISystemReleaseRepository systemReleaseRepository) {
this.systemReleaseRepository = systemReleaseRepository;
}
@Override
@Transactional(rollbackFor = Exception.class)
public SystemReleaseDTO create(SystemReleaseDTO dto) {
// 调用父类方法保存
SystemReleaseDTO savedDto = super.create(dto);
// 如果设置了延迟时间则调度维护任务无论是否自动停止服务
if (dto.getDelayMinutes() != null && dto.getDelayMinutes() > 0) {
SystemRelease release = converter.toEntity(savedDto);
scheduleMaintenanceTask(release);
}
return savedDto;
}
@Override
@Transactional(rollbackFor = Exception.class)
public SystemReleaseDTO update(Long id, SystemReleaseDTO dto) {
// 先取消旧的维护任务
cancelMaintenanceTask(id);
// 调用父类方法更新
SystemReleaseDTO updatedDto = super.update(id, dto);
// 如果设置了延迟时间重新调度维护任务
if (dto.getDelayMinutes() != null && dto.getDelayMinutes() > 0) {
SystemRelease release = converter.toEntity(updatedDto);
scheduleMaintenanceTask(release);
}
return updatedDto;
}
@Override
@Transactional(rollbackFor = Exception.class)
public void delete(Long id) {
// 取消已调度的维护任务
cancelMaintenanceTask(id);
// 调用父类方法删除
super.delete(id);
}
/**
* 取消维护任务
*/
private void cancelMaintenanceTask(Long releaseId) {
ScheduledFuture<?> future = scheduledTasks.remove(releaseId);
if (future != null && !future.isDone()) {
boolean cancelled = future.cancel(false);
log.warn("取消维护任务Release ID: {}, 取消结果: {}", releaseId, cancelled);
} else {
log.debug("没有找到待取消的维护任务Release ID: {}", releaseId);
}
}
/**
* 调度维护任务延迟执行
*/
private void scheduleMaintenanceTask(SystemRelease release) {
// 计算执行时间
Date executeTime = Date.from(
LocalDateTime.now()
.plusMinutes(release.getDelayMinutes())
.atZone(ZoneId.systemDefault())
.toInstant()
);
log.warn("已调度系统维护任务,版本: {}, 执行时间: {}, 预计耗时: {}分钟",
release.getReleaseVersion(), executeTime, release.getEstimatedDuration());
// 延迟执行维护任务并保存任务引用
ScheduledFuture<?> future = taskScheduler.schedule(() -> {
executeMaintenance(release);
// 执行完成后从Map中移除
scheduledTasks.remove(release.getId());
}, executeTime);
// 保存任务引用用于后续取消
scheduledTasks.put(release.getId(), future);
log.info("维护任务已保存到调度器Release ID: {}", release.getId());
}
/**
* 执行维护任务
*/
private void executeMaintenance(SystemRelease release) {
try {
log.warn("========================================");
log.warn("系统维护任务开始执行");
log.warn("版本: {}", release.getReleaseVersion());
log.warn("预计耗时: {} 分钟", release.getEstimatedDuration());
log.warn("自动停止服务: {}", release.getEnableAutoShutdown());
log.warn("========================================");
// 1. 发送维护通知
sendMaintenanceNotification(release);
// 2. 检查是否需要自动停止服务
if (!Boolean.TRUE.equals(release.getEnableAutoShutdown())) {
log.warn("未启用自动停止服务,维护通知已发送,请手动停止服务进行维护");
return;
}
// 3. 等待3秒让通知发出去
log.warn("等待3秒确保通知发送完成...");
Thread.sleep(3000);
// 4. 优雅关闭应用
log.warn("系统开始优雅关闭...");
((ConfigurableApplicationContext) applicationContext).close();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
log.error("系统维护任务被中断", e);
} catch (Exception e) {
log.error("系统维护任务执行失败", e);
}
}
/**
* 发送维护通知
*/
private void sendMaintenanceNotification(SystemRelease release) {
try {
// 查询通知渠道
NotificationChannel channel = notificationChannelRepository.findById(notificationChannelId)
.orElseThrow(() -> new BusinessException(ResponseCode.DATA_NOT_FOUND,
new Object[]{"通知渠道ID=" + notificationChannelId + "不存在"}));
// 准备维护通知内容简化版只显示关键信息
StringBuilder messageBuilder = new StringBuilder();
messageBuilder.append(String.format("版本号:%s\n", release.getReleaseVersion()));
messageBuilder.append("系统即将开始维护升级\n");
messageBuilder.append(String.format("预计维护时长:%d 分钟\n",
release.getEstimatedDuration() != null ? release.getEstimatedDuration() : 10));
// 如果启用自动停止增加提示
if (Boolean.TRUE.equals(release.getEnableAutoShutdown())) {
messageBuilder.append("本次升级程序将自动停止\n");
}
messageBuilder.append("\n望周知。");
String message = messageBuilder.toString();
log.warn("发送维护通知v{}\n{}", release.getReleaseVersion(), message);
// 创建企微通知请求
WeworkSendNotificationRequest request = new WeworkSendNotificationRequest();
request.setContent(message);
request.setTitle("系统维护通知");
request.setMessageType(WeworkMessageTypeEnum.TEXT);
// 发送通知
notificationSendService.send(channel, request);
log.info("维护通知发送成功");
} catch (Exception e) {
log.error("发送维护通知失败", e);
}
}
@Override
public List<SystemReleaseDTO> getUnnotifiedReleases() {
log.info("获取未通知的发布记录列表");
List<SystemRelease> releases = systemReleaseRepository.findByNotifiedFalseAndDeletedFalseOrderByReleaseDateDesc();
return releases.stream()
.map(converter::toDto)
.collect(Collectors.toList());
}
@Override
@Transactional(rollbackFor = Exception.class)
public void markAsNotified(Long id) {
log.info("标记发布记录为已通知ID: {}", id);
SystemRelease release = systemReleaseRepository.findById(id)
.orElseThrow(() -> new RuntimeException("发布记录不存在"));
release.setNotified(true);
systemReleaseRepository.save(release);
}
@Override
public SystemReleaseDTO getLatestReleaseByModule(String module) {
log.info("获取模块最新发布记录,模块: {}", module);
SystemRelease release = systemReleaseRepository.findTopByModuleAndDeletedFalseOrderByReleaseDateDesc(module);
return release != null ? converter.toDto(release) : null;
}
}

View File

@ -9,7 +9,7 @@ spring:
max-request-size: 1GB # 整个请求最大大小 max-request-size: 1GB # 整个请求最大大小
file-size-threshold: 0 # 文件写入磁盘的阈值 file-size-threshold: 0 # 文件写入磁盘的阈值
datasource: datasource:
url: jdbc:mysql://172.16.0.116:3306/deploy-ease-platform?characterEncoding=utf8&useSSL=false&serverTimezone=UTC&allowPublicKeyRetrieval=true&createDatabaseIfNotExist=true url: jdbc:mysql://172.16.0.116:3306/deploy-ease-platform?characterEncoding=utf8&useSSL=false&serverTimezone=UTC&allowPublicKeyRetrieval=true&createDatabaseIfNotExist=true&allowMultiQueries=true
username: root username: root
password: lianyu_123 password: lianyu_123
driver-class-name: com.mysql.cj.jdbc.Driver driver-class-name: com.mysql.cj.jdbc.Driver
@ -40,6 +40,8 @@ spring:
pool-name: HikariCP-Pool pool-name: HikariCP-Pool
# 是否允许JMX管理连接池 # 是否允许JMX管理连接池
register-mbeans: true register-mbeans: true
# 连接泄漏检测阈值(毫秒),超过此时间未归还的连接将被记录
leak-detection-threshold: 60000
jpa: jpa:
hibernate: hibernate:
ddl-auto: update ddl-auto: update
@ -66,7 +68,7 @@ spring:
cache-duration: 3600 cache-duration: 3600
liquibase: liquibase:
enabled: true enabled: true
change-log: classpath:db/changelog/db.changelog-master.yaml change-log: classpath:db/changelog/db.changelog-master.xml
drop-first: false drop-first: false
default-schema: deploy-ease-platform default-schema: deploy-ease-platform
contexts: default contexts: default
@ -103,6 +105,19 @@ logging:
org.hibernate.orm.jdbc.bind: INFO org.hibernate.orm.jdbc.bind: INFO
com.qqchen.deploy.backend.framework.utils.EntityPathResolver: DEBUG com.qqchen.deploy.backend.framework.utils.EntityPathResolver: DEBUG
com.qqchen.deploy.backend: DEBUG com.qqchen.deploy.backend: DEBUG
# 监控配置
management:
endpoints:
web:
exposure:
include: health,metrics,info
metrics:
enable:
hikari: true
tomcat: true
jvm: true
system: true
jwt: jwt:
secret: 'thisIsAVeryVerySecretKeyForJwtTokenGenerationAndValidation123456789' secret: 'thisIsAVeryVerySecretKeyForJwtTokenGenerationAndValidation123456789'
expiration: 86400 expiration: 86400
@ -120,3 +135,7 @@ deploy:
# 加密盐值(强烈建议使用环境变量 ENCRYPTION_SALT # 加密盐值(强烈建议使用环境变量 ENCRYPTION_SALT
# 盐值必须是16位十六进制字符串只能包含0-9和a-f # 盐值必须是16位十六进制字符串只能包含0-9和a-f
salt: ${ENCRYPTION_SALT:a1b2c3d4e5f6a7b8} salt: ${ENCRYPTION_SALT:a1b2c3d4e5f6a7b8}
# 系统版本发布通知配置
notification:
release:
channel-id: 5 # 版本通知渠道ID生产环境

View File

@ -9,7 +9,7 @@ spring:
max-request-size: 1GB # 整个请求最大大小 max-request-size: 1GB # 整个请求最大大小
file-size-threshold: 0 # 文件写入磁盘的阈值 file-size-threshold: 0 # 文件写入磁盘的阈值
datasource: datasource:
url: jdbc:mysql://172.22.222.111:3306/deploy-ease-platform?characterEncoding=utf8&useSSL=false&serverTimezone=UTC&allowPublicKeyRetrieval=true&createDatabaseIfNotExist=true url: jdbc:mysql://172.22.222.111:3306/deploy-ease-platform?characterEncoding=utf8&useSSL=false&serverTimezone=UTC&allowPublicKeyRetrieval=true&createDatabaseIfNotExist=true&allowMultiQueries=true
username: deploy-ease-platform username: deploy-ease-platform
password: Qichen5210523 password: Qichen5210523
driver-class-name: com.mysql.cj.jdbc.Driver driver-class-name: com.mysql.cj.jdbc.Driver
@ -40,6 +40,8 @@ spring:
pool-name: HikariCP-Pool pool-name: HikariCP-Pool
# 是否允许JMX管理连接池 # 是否允许JMX管理连接池
register-mbeans: true register-mbeans: true
# 连接泄漏检测阈值(毫秒),超过此时间未归还的连接将被记录
leak-detection-threshold: 60000
jpa: jpa:
hibernate: hibernate:
ddl-auto: update ddl-auto: update
@ -66,7 +68,7 @@ spring:
cache-duration: 3600 cache-duration: 3600
liquibase: liquibase:
enabled: true enabled: true
change-log: classpath:db/changelog/db.changelog-master.yaml change-log: classpath:db/changelog/db.changelog-master.xml
drop-first: false drop-first: false
default-schema: deploy-ease-platform default-schema: deploy-ease-platform
contexts: default contexts: default
@ -103,6 +105,19 @@ logging:
org.hibernate.orm.jdbc.bind: INFO org.hibernate.orm.jdbc.bind: INFO
com.qqchen.deploy.backend.framework.utils.EntityPathResolver: DEBUG com.qqchen.deploy.backend.framework.utils.EntityPathResolver: DEBUG
com.qqchen.deploy.backend: DEBUG com.qqchen.deploy.backend: DEBUG
# 监控配置
management:
endpoints:
web:
exposure:
include: health,metrics,info
metrics:
enable:
hikari: true
tomcat: true
jvm: true
system: true
jwt: jwt:
secret: 'thisIsAVeryVerySecretKeyForJwtTokenGenerationAndValidation123456789' secret: 'thisIsAVeryVerySecretKeyForJwtTokenGenerationAndValidation123456789'
expiration: 86400 expiration: 86400
@ -120,3 +135,7 @@ deploy:
# 加密盐值(生产环境建议使用环境变量 ENCRYPTION_SALT # 加密盐值(生产环境建议使用环境变量 ENCRYPTION_SALT
# 盐值必须是16位十六进制字符串只能包含0-9和a-f # 盐值必须是16位十六进制字符串只能包含0-9和a-f
salt: ${ENCRYPTION_SALT:a1b2c3d4e5f6a7b8} salt: ${ENCRYPTION_SALT:a1b2c3d4e5f6a7b8}
# 系统版本发布通知配置
notification:
release:
channel-id: 2 # 版本通知渠道ID

View File

@ -0,0 +1,44 @@
-- --------------------------------------------------------------------------------------
-- 系统版本发布记录 - 初始数据
-- 功能:插入当前版本的发布记录
-- 作者qqchen
-- 日期2025-12-09
-- --------------------------------------------------------------------------------------
-- 插入 1.50 前后端统一发布记录
INSERT INTO system_release (
create_by, create_time, update_by, update_time, version, deleted,
release_version, module, release_date, changes, notified, delay_minutes, estimated_duration, enable_auto_shutdown
)
VALUES (
'system', NOW(), 'system', NOW(), 1, 0,
1.0, 'ALL', NOW(),
'【后端】
ConcurrentHashMap管理
+
/
MySQL连接池配置不足问题200/80
enableAutoShutdown
DTO字段设计
Git分支同步日志
ID配置外部化application.yml/application-prod.yml
"v1.50""版本号1.50"
CRUD
//
',
0, NULL, NULL, 0
);

View File

@ -0,0 +1,20 @@
<?xml version="1.0" encoding="UTF-8"?>
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog
http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.3.xsd">
<!-- 清理旧数据(如果表结构已变更) -->
<changeSet id="20251209112700-cleanup" author="qqchen">
<preConditions onFail="CONTINUE">
<tableExists tableName="system_release"/>
</preConditions>
<sql>TRUNCATE TABLE system_release;</sql>
<sql>DELETE FROM DATABASECHANGELOG WHERE ID='20251209112700';</sql>
</changeSet>
<!-- 插入新数据 -->
<changeSet id="20251209112700-v2" author="qqchen">
<sqlFile path="20251209112700-01.sql" relativeToChangelogFile="true"/>
</changeSet>
</databaseChangeLog>

View File

@ -27,3 +27,17 @@ databaseChangeLog:
endDelimiter: ";" endDelimiter: ";"
rollback: rollback:
- empty - empty
- changeSet:
id: 20251209112700
author: qqchen
runOnChange: false
failOnError: true
comment: "系统版本发布记录 - v1.5.0初始数据"
sqlFile:
path: db/changelog/changes/20251209112700-01.sql
stripComments: false
splitStatements: true
endDelimiter: ";"
rollback:
- empty

View File

@ -108,7 +108,9 @@ VALUES
-- 权限管理(隐藏菜单) -- 权限管理(隐藏菜单)
(6, '权限管理', '/system/permissions', 'System/Permission/List', 'SafetyOutlined', 'system:permission', 2, 1, 50, TRUE, TRUE, 'system', NOW(), 0, FALSE), (6, '权限管理', '/system/permissions', 'System/Permission/List', 'SafetyOutlined', 'system:permission', 2, 1, 50, TRUE, TRUE, 'system', NOW(), 0, FALSE),
-- 在线用户管理 -- 在线用户管理
(7, '在线用户', '/system/online', 'System/Online/List', 'UserSwitchOutlined', 'system:online:view', 2, 1, 60, FALSE, TRUE, 'system', NOW(), 0, FALSE); (7, '在线用户', '/system/online', 'System/Online/List', 'UserSwitchOutlined', 'system:online:view', 2, 1, 60, FALSE, TRUE, 'system', NOW(), 0, FALSE),
-- 系统版本管理
(8, '系统版本', '/system/releases', 'System/Release/List', 'RocketOutlined', 'system:release', 2, 1, 70, FALSE, TRUE, 'system', NOW(), 0, FALSE);
-- ==================== 初始化角色数据 ==================== -- ==================== 初始化角色数据 ====================
DELETE FROM sys_role WHERE id < 100; DELETE FROM sys_role WHERE id < 100;
@ -154,7 +156,7 @@ VALUES
INSERT INTO sys_role_menu (role_id, menu_id) INSERT INTO sys_role_menu (role_id, menu_id)
VALUES VALUES
-- 管理员角色(拥有所有菜单) -- 管理员角色(拥有所有菜单)
(1, 1), (1, 2), (1, 3), (1, 4), (1, 5), (1, 6), (1, 7), (1, 99), (1, 100), (1, 101), (1, 102), (1, 104), (1, 200), (1, 201), (1, 202), (1, 203), (1, 204), (1, 205), (1, 206), (1, 300), (1, 301), (1, 302), (1, 303), (1, 304), (1, 1011), (1, 1041), (1, 1042), (1, 1), (1, 2), (1, 3), (1, 4), (1, 5), (1, 6), (1, 7), (1, 8), (1, 99), (1, 100), (1, 101), (1, 102), (1, 104), (1, 200), (1, 201), (1, 202), (1, 203), (1, 204), (1, 205), (1, 206), (1, 300), (1, 301), (1, 302), (1, 303), (1, 304), (1, 1011), (1, 1041), (1, 1042),
-- 运维角色 -- 运维角色
(2, 99), (2, 200), (2, 201), (2, 202), (2, 203), (2, 204), (2, 205), (2, 300), (2, 301), (2, 302), (2, 303), (2, 304), (2, 99), (2, 200), (2, 201), (2, 202), (2, 203), (2, 204), (2, 205), (2, 300), (2, 301), (2, 302), (2, 303), (2, 304),
-- 开发角色(只有工作台) -- 开发角色(只有工作台)
@ -221,6 +223,16 @@ INSERT INTO sys_permission (id, create_time, menu_id, code, name, type, sort) VA
(51, NOW(), 7, 'system:online:view', '查看在线用户', 'FUNCTION', 1), (51, NOW(), 7, 'system:online:view', '查看在线用户', 'FUNCTION', 1),
(52, NOW(), 7, 'system:online:kick', '强制下线', 'FUNCTION', 2), (52, NOW(), 7, 'system:online:kick', '强制下线', 'FUNCTION', 2),
-- 系统版本管理 (menu_id=8)
(61, NOW(), 8, 'system:release:list', '版本查询', 'FUNCTION', 1),
(62, NOW(), 8, 'system:release:view', '版本详情', 'FUNCTION', 2),
(63, NOW(), 8, 'system:release:create', '版本创建', 'FUNCTION', 3),
(64, NOW(), 8, 'system:release:update', '版本修改', 'FUNCTION', 4),
(65, NOW(), 8, 'system:release:delete', '版本删除', 'FUNCTION', 5),
(66, NOW(), 8, 'system:release:notify', '标记为已通知', 'FUNCTION', 6),
(67, NOW(), 8, 'system:release:latest', '获取最新版本', 'FUNCTION', 7),
(68, NOW(), 8, 'system:release:unnotified', '获取未通知版本', 'FUNCTION', 8),
-- 运维管理权限 -- 运维管理权限
-- 团队管理 (menu_id=201) -- 团队管理 (menu_id=201)
(101, NOW(), 201, 'deploy:team:list', '团队查询', 'FUNCTION', 1), (101, NOW(), 201, 'deploy:team:list', '团队查询', 'FUNCTION', 1),

View File

@ -1355,3 +1355,29 @@ CREATE TABLE deploy_ssh_audit_log
-- 2. 删除用户/服务器时,审计日志不应被物理删除 -- 2. 删除用户/服务器时,审计日志不应被物理删除
-- 3. user_id/server_id 仅作为历史记录字段,通过冗余字段可查询 -- 3. user_id/server_id 仅作为历史记录字段,通过冗余字段可查询
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='SSH终端审计日志表'; ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='SSH终端审计日志表';
-- 系统版本发布记录表
CREATE TABLE system_release
(
id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '主键ID',
create_by VARCHAR(100) NULL COMMENT '创建人',
create_time DATETIME(6) NULL COMMENT '创建时间',
update_by VARCHAR(100) NULL COMMENT '更新人',
update_time DATETIME(6) NULL COMMENT '更新时间',
version INT NOT NULL DEFAULT 1 COMMENT '乐观锁版本号',
deleted BIT NOT NULL DEFAULT 0 COMMENT '是否删除',
release_version DECIMAL(10,2) NOT NULL COMMENT '发布版本号(如 1.50',
module VARCHAR(20) NOT NULL COMMENT '模块类型BACKEND/FRONTEND/ALL',
release_date DATETIME(6) NOT NULL COMMENT '发布时间',
changes TEXT NULL COMMENT '变更内容(换行分隔)',
notified BIT NOT NULL DEFAULT 0 COMMENT '是否已发送版本发布通知',
delay_minutes INT NULL COMMENT '延迟执行分钟数(创建后多久开始维护,为空则不触发维护)',
estimated_duration INT NULL COMMENT '预计维护时长(分钟)',
enable_auto_shutdown BIT NOT NULL DEFAULT 0 COMMENT '是否自动停止服务',
UNIQUE KEY uk_version_module (release_version, module),
KEY idx_release_date (release_date),
KEY idx_notified (notified),
KEY idx_deleted (deleted)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='系统版本发布记录表';