增加系统版本通知功能

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

View File

@ -9,7 +9,7 @@ spring:
max-request-size: 1GB # 整个请求最大大小
file-size-threshold: 0 # 文件写入磁盘的阈值
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
password: Qichen5210523
driver-class-name: com.mysql.cj.jdbc.Driver
@ -40,6 +40,8 @@ spring:
pool-name: HikariCP-Pool
# 是否允许JMX管理连接池
register-mbeans: true
# 连接泄漏检测阈值(毫秒),超过此时间未归还的连接将被记录
leak-detection-threshold: 60000
jpa:
hibernate:
ddl-auto: update
@ -66,7 +68,7 @@ spring:
cache-duration: 3600
liquibase:
enabled: true
change-log: classpath:db/changelog/db.changelog-master.yaml
change-log: classpath:db/changelog/db.changelog-master.xml
drop-first: false
default-schema: deploy-ease-platform
contexts: default
@ -103,6 +105,19 @@ logging:
org.hibernate.orm.jdbc.bind: INFO
com.qqchen.deploy.backend.framework.utils.EntityPathResolver: 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:
secret: 'thisIsAVeryVerySecretKeyForJwtTokenGenerationAndValidation123456789'
expiration: 86400
@ -120,3 +135,7 @@ deploy:
# 加密盐值(生产环境建议使用环境变量 ENCRYPTION_SALT
# 盐值必须是16位十六进制字符串只能包含0-9和a-f
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: ";"
rollback:
- 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),
-- 在线用户管理
(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;
@ -154,7 +156,7 @@ VALUES
INSERT INTO sys_role_menu (role_id, menu_id)
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),
-- 开发角色(只有工作台)
@ -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),
(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)
(101, NOW(), 201, 'deploy:team:list', '团队查询', 'FUNCTION', 1),

View File

@ -1355,3 +1355,29 @@ CREATE TABLE deploy_ssh_audit_log
-- 2. 删除用户/服务器时,审计日志不应被物理删除
-- 3. user_id/server_id 仅作为历史记录字段,通过冗余字段可查询
) 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='系统版本发布记录表';