diff --git a/backend/src/main/java/com/qqchen/deploy/backend/system/api/SystemReleaseApiController.java b/backend/src/main/java/com/qqchen/deploy/backend/system/api/SystemReleaseApiController.java new file mode 100644 index 00000000..c0a0e60e --- /dev/null +++ b/backend/src/main/java/com/qqchen/deploy/backend/system/api/SystemReleaseApiController.java @@ -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 { + + @Resource + private ISystemReleaseService systemReleaseService; + + @Override + @PostMapping + @Operation(summary = "创建发布记录", description = "创建新的版本发布记录") + public Response create(@Validated @RequestBody SystemReleaseDTO dto) { + return super.create(dto); + } + + @Override + @PutMapping("/{id}") + @Operation(summary = "更新发布记录", description = "更新指定ID的版本发布记录") + public Response update(@PathVariable Long id, @Validated @RequestBody SystemReleaseDTO dto) { + return super.update(id, dto); + } + + @Override + @DeleteMapping("/{id}") + @Operation(summary = "删除发布记录", description = "删除指定ID的版本发布记录(逻辑删除)") + public Response delete(@PathVariable Long id) { + return super.delete(id); + } + + @Override + @GetMapping("/{id}") + @Operation(summary = "查询发布记录详情", description = "根据ID查询版本发布记录详情") + public Response findById(@PathVariable Long id) { + return super.findById(id); + } + + @Override + @GetMapping("/list") + @Operation(summary = "查询所有发布记录", description = "查询所有版本发布记录列表") + public Response> findAll() { + return super.findAll(); + } + + @Override + @GetMapping("/page") + @Operation(summary = "分页查询发布记录", description = "分页查询版本发布记录") + public Response> page(SystemReleaseQuery query) { + return super.page(query); + } + + @Override + @GetMapping("/query") + @Operation(summary = "条件查询发布记录", description = "根据条件查询版本发布记录列表") + public Response> findAll(SystemReleaseQuery query) { + return super.findAll(query); + } + + @Override + @PostMapping("/batch") + @Operation(summary = "批量处理发布记录", description = "批量创建/更新版本发布记录") + public CompletableFuture> batchProcess(@RequestBody List dtos) { + return super.batchProcess(dtos); + } + + /** + * 获取未通知的发布记录列表 + */ + @GetMapping("/unnotified") + @Operation(summary = "获取未通知的发布记录", description = "查询所有未发送通知的版本发布记录") + public Response> getUnnotifiedReleases() { + List releases = systemReleaseService.getUnnotifiedReleases(); + return Response.success(releases); + } + + /** + * 标记为已通知 + */ + @PutMapping("/{id}/notify") + @Operation(summary = "标记为已通知", description = "将指定发布记录标记为已发送通知") + public Response markAsNotified(@PathVariable Long id) { + systemReleaseService.markAsNotified(id); + return Response.success(); + } + + /** + * 获取指定模块的最新发布记录 + */ + @GetMapping("/latest/{module}") + @Operation(summary = "获取最新发布记录", description = "获取指定模块的最新版本发布记录") + public Response getLatestReleaseByModule(@PathVariable String module) { + SystemReleaseDTO release = systemReleaseService.getLatestReleaseByModule(module); + return Response.success(release); + } + + @Override + protected void exportData(HttpServletResponse response, List data) { + log.info("导出系统版本发布记录数据,数据量:{}", data.size()); + } +} diff --git a/backend/src/main/java/com/qqchen/deploy/backend/system/config/SystemReleaseConfig.java b/backend/src/main/java/com/qqchen/deploy/backend/system/config/SystemReleaseConfig.java new file mode 100644 index 00000000..601a9b67 --- /dev/null +++ b/backend/src/main/java/com/qqchen/deploy/backend/system/config/SystemReleaseConfig.java @@ -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; + } +} diff --git a/backend/src/main/java/com/qqchen/deploy/backend/system/converter/SystemReleaseConverter.java b/backend/src/main/java/com/qqchen/deploy/backend/system/converter/SystemReleaseConverter.java new file mode 100644 index 00000000..3aa37f84 --- /dev/null +++ b/backend/src/main/java/com/qqchen/deploy/backend/system/converter/SystemReleaseConverter.java @@ -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 { +} diff --git a/backend/src/main/java/com/qqchen/deploy/backend/system/dto/SystemReleaseDTO.java b/backend/src/main/java/com/qqchen/deploy/backend/system/dto/SystemReleaseDTO.java new file mode 100644 index 00000000..6000c0b7 --- /dev/null +++ b/backend/src/main/java/com/qqchen/deploy/backend/system/dto/SystemReleaseDTO.java @@ -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; +} diff --git a/backend/src/main/java/com/qqchen/deploy/backend/system/entity/SystemRelease.java b/backend/src/main/java/com/qqchen/deploy/backend/system/entity/SystemRelease.java new file mode 100644 index 00000000..2b30f74e --- /dev/null +++ b/backend/src/main/java/com/qqchen/deploy/backend/system/entity/SystemRelease.java @@ -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 { + + /** + * 发布版本号(如 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; +} diff --git a/backend/src/main/java/com/qqchen/deploy/backend/system/listener/SystemReleaseStartupListener.java b/backend/src/main/java/com/qqchen/deploy/backend/system/listener/SystemReleaseStartupListener.java new file mode 100644 index 00000000..e060b670 --- /dev/null +++ b/backend/src/main/java/com/qqchen/deploy/backend/system/listener/SystemReleaseStartupListener.java @@ -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); + } + } +} diff --git a/backend/src/main/java/com/qqchen/deploy/backend/system/query/SystemReleaseQuery.java b/backend/src/main/java/com/qqchen/deploy/backend/system/query/SystemReleaseQuery.java new file mode 100644 index 00000000..5a90c5f8 --- /dev/null +++ b/backend/src/main/java/com/qqchen/deploy/backend/system/query/SystemReleaseQuery.java @@ -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; +} diff --git a/backend/src/main/java/com/qqchen/deploy/backend/system/repository/ISystemReleaseRepository.java b/backend/src/main/java/com/qqchen/deploy/backend/system/repository/ISystemReleaseRepository.java new file mode 100644 index 00000000..f43cf582 --- /dev/null +++ b/backend/src/main/java/com/qqchen/deploy/backend/system/repository/ISystemReleaseRepository.java @@ -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 { + + /** + * 根据版本号和模块查询(排除已删除) + * + * @param releaseVersion 版本号 + * @param module 模块类型 + * @return 发布记录 + */ + SystemRelease findByReleaseVersionAndModuleAndDeletedFalse(BigDecimal releaseVersion, String module); + + /** + * 检查版本号和模块是否存在(排除已删除) + * + * @param releaseVersion 版本号 + * @param module 模块类型 + * @return 是否存在 + */ + boolean existsByReleaseVersionAndModuleAndDeletedFalse(BigDecimal releaseVersion, String module); + + /** + * 查询未通知的发布记录(排除已删除) + * + * @return 未通知的发布记录列表 + */ + List findByNotifiedFalseAndDeletedFalseOrderByReleaseDateDesc(); + + /** + * 根据模块查询最新的发布记录(排除已删除) + * + * @param module 模块类型 + * @return 最新的发布记录 + */ + SystemRelease findTopByModuleAndDeletedFalseOrderByReleaseDateDesc(String module); + + /** + * 查询最新的未通知版本(按版本号降序) + * + * @return 最新的未通知版本 + */ + SystemRelease findFirstByNotifiedFalseAndDeletedFalseOrderByReleaseVersionDesc(); +} diff --git a/backend/src/main/java/com/qqchen/deploy/backend/system/service/ISystemReleaseService.java b/backend/src/main/java/com/qqchen/deploy/backend/system/service/ISystemReleaseService.java new file mode 100644 index 00000000..241da6e7 --- /dev/null +++ b/backend/src/main/java/com/qqchen/deploy/backend/system/service/ISystemReleaseService.java @@ -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 { + + /** + * 获取未通知的发布记录列表 + * + * @return 未通知的发布记录列表 + */ + List getUnnotifiedReleases(); + + /** + * 标记为已通知 + * + * @param id 发布记录ID + */ + void markAsNotified(Long id); + + /** + * 获取指定模块的最新发布记录 + * + * @param module 模块类型 + * @return 最新发布记录 + */ + SystemReleaseDTO getLatestReleaseByModule(String module); +} diff --git a/backend/src/main/java/com/qqchen/deploy/backend/system/service/impl/SystemReleaseServiceImpl.java b/backend/src/main/java/com/qqchen/deploy/backend/system/service/impl/SystemReleaseServiceImpl.java new file mode 100644 index 00000000..9159724f --- /dev/null +++ b/backend/src/main/java/com/qqchen/deploy/backend/system/service/impl/SystemReleaseServiceImpl.java @@ -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 + 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> 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 getUnnotifiedReleases() { + log.info("获取未通知的发布记录列表"); + List 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; + } +} diff --git a/backend/src/main/resources/application-prod.yml b/backend/src/main/resources/application-prod.yml index 3cfcdb61..35eaa93a 100644 --- a/backend/src/main/resources/application-prod.yml +++ b/backend/src/main/resources/application-prod.yml @@ -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(生产环境) diff --git a/backend/src/main/resources/application.yml b/backend/src/main/resources/application.yml index 03a8ccf9..7a6cba78 100644 --- a/backend/src/main/resources/application.yml +++ b/backend/src/main/resources/application.yml @@ -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 diff --git a/backend/src/main/resources/db/changelog/changes/20251209112700-01.sql b/backend/src/main/resources/db/changelog/changes/20251209112700-01.sql new file mode 100644 index 00000000..0e37373c --- /dev/null +++ b/backend/src/main/resources/db/changelog/changes/20251209112700-01.sql @@ -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 +); diff --git a/backend/src/main/resources/db/changelog/changes/20251209112700-changelog.xml b/backend/src/main/resources/db/changelog/changes/20251209112700-changelog.xml new file mode 100644 index 00000000..319ebaa9 --- /dev/null +++ b/backend/src/main/resources/db/changelog/changes/20251209112700-changelog.xml @@ -0,0 +1,20 @@ + + + + + + + + + TRUNCATE TABLE system_release; + DELETE FROM DATABASECHANGELOG WHERE ID='20251209112700'; + + + + + + + diff --git a/backend/src/main/resources/db/changelog/db.changelog-master.yaml b/backend/src/main/resources/db/changelog/db.changelog-master.yaml111 similarity index 64% rename from backend/src/main/resources/db/changelog/db.changelog-master.yaml rename to backend/src/main/resources/db/changelog/db.changelog-master.yaml111 index 3d3a6566..b502e919 100644 --- a/backend/src/main/resources/db/changelog/db.changelog-master.yaml +++ b/backend/src/main/resources/db/changelog/db.changelog-master.yaml111 @@ -25,5 +25,19 @@ databaseChangeLog: stripComments: false splitStatements: true 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 \ No newline at end of file diff --git a/backend/src/main/resources/db/changelog/changes/v1.0.0-data.sql b/backend/src/main/resources/db/changelog/init/v1.0.0-data.sql similarity index 99% rename from backend/src/main/resources/db/changelog/changes/v1.0.0-data.sql rename to backend/src/main/resources/db/changelog/init/v1.0.0-data.sql index 45010aab..00b30759 100644 --- a/backend/src/main/resources/db/changelog/changes/v1.0.0-data.sql +++ b/backend/src/main/resources/db/changelog/init/v1.0.0-data.sql @@ -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), diff --git a/backend/src/main/resources/db/changelog/changes/v1.0.0-schema.sql b/backend/src/main/resources/db/changelog/init/v1.0.0-schema.sql similarity index 97% rename from backend/src/main/resources/db/changelog/changes/v1.0.0-schema.sql rename to backend/src/main/resources/db/changelog/init/v1.0.0-schema.sql index 23d048d3..25d77f62 100644 --- a/backend/src/main/resources/db/changelog/changes/v1.0.0-schema.sql +++ b/backend/src/main/resources/db/changelog/init/v1.0.0-schema.sql @@ -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='系统版本发布记录表';