Merge remote-tracking branch 'origin/main'
This commit is contained in:
@@ -5,6 +5,7 @@ import com.hotwj.platform.config.vehicleThreeInspect.domain.bo.HotVehicleThreeIn
|
|||||||
import com.hotwj.platform.config.vehicleThreeInspect.domain.vo.HotVehicleThreeInspectProcessVo;
|
import com.hotwj.platform.config.vehicleThreeInspect.domain.vo.HotVehicleThreeInspectProcessVo;
|
||||||
import com.hotwj.platform.config.vehicleThreeInspect.domain.bo.HotVehicleThreeInspectVehicleQueryBo;
|
import com.hotwj.platform.config.vehicleThreeInspect.domain.bo.HotVehicleThreeInspectVehicleQueryBo;
|
||||||
import com.hotwj.platform.config.vehicleThreeInspect.domain.vo.HotVehicleThreeInspectReceivedVehicleVo;
|
import com.hotwj.platform.config.vehicleThreeInspect.domain.vo.HotVehicleThreeInspectReceivedVehicleVo;
|
||||||
|
import com.hotwj.platform.config.vehicleThreeInspect.domain.vo.HotVehicleThreeInspectSummaryVo;
|
||||||
import com.hotwj.platform.config.vehicleThreeInspect.domain.vo.HotVehicleThreeInspectUnreceivedVehicleVo;
|
import com.hotwj.platform.config.vehicleThreeInspect.domain.vo.HotVehicleThreeInspectUnreceivedVehicleVo;
|
||||||
import com.hotwj.platform.config.vehicleThreeInspect.domain.vo.HotVehicleThreeInspectVo;
|
import com.hotwj.platform.config.vehicleThreeInspect.domain.vo.HotVehicleThreeInspectVo;
|
||||||
import com.hotwj.platform.config.vehicleThreeInspect.service.IHotVehicleThreeInspectPrintService;
|
import com.hotwj.platform.config.vehicleThreeInspect.service.IHotVehicleThreeInspectPrintService;
|
||||||
@@ -213,4 +214,13 @@ public class HotVehicleThreeInspectController extends BaseController {
|
|||||||
@PathVariable Long[] ids) {
|
@PathVariable Long[] ids) {
|
||||||
return toAjax(hotVehicleThreeInspectService.deleteWithValidByIds(List.of(ids), true));
|
return toAjax(hotVehicleThreeInspectService.deleteWithValidByIds(List.of(ids), true));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询车辆三检聚合列表
|
||||||
|
*/
|
||||||
|
@GetMapping("/listSummary")
|
||||||
|
@Operation(summary = "分页查询车辆三检聚合列表")
|
||||||
|
public TableDataInfo<HotVehicleThreeInspectSummaryVo> listSummary(HotVehicleThreeInspectBo bo, PageQuery pageQuery) {
|
||||||
|
return hotVehicleThreeInspectService.querySummaryPageList(bo, pageQuery);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,100 @@
|
|||||||
|
package com.hotwj.platform.config.vehicleThreeInspect.domain.vo;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.io.Serial;
|
||||||
|
import java.io.Serializable;
|
||||||
|
import java.util.Date;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 车辆三检聚合列表视图对象
|
||||||
|
*
|
||||||
|
* @author shihongwei
|
||||||
|
* @date 2026-06-01
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class HotVehicleThreeInspectSummaryVo implements Serializable {
|
||||||
|
|
||||||
|
@Serial
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 三检流程ID
|
||||||
|
*/
|
||||||
|
private Long inspectId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 公司ID
|
||||||
|
*/
|
||||||
|
private Long companyId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 车辆ID
|
||||||
|
*/
|
||||||
|
private Long vehicleId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 车牌号
|
||||||
|
*/
|
||||||
|
private String plateNumber;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 驾驶员ID
|
||||||
|
*/
|
||||||
|
private String driverId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 驾驶员姓名
|
||||||
|
*/
|
||||||
|
private String driverName;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 开始时间
|
||||||
|
*/
|
||||||
|
private Date startTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 结束时间
|
||||||
|
*/
|
||||||
|
private Date endTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查时间展示
|
||||||
|
*/
|
||||||
|
private String inspectTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 完成状态
|
||||||
|
*/
|
||||||
|
private String completeStatus;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否已完成
|
||||||
|
*/
|
||||||
|
private Boolean completed;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 审核完成状态
|
||||||
|
*/
|
||||||
|
private String auditCompleteStatus;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否完成审核
|
||||||
|
*/
|
||||||
|
private Boolean auditCompleted;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 出车检查
|
||||||
|
*/
|
||||||
|
private HotVehicleThreeInspectVo outCheck;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 行车检查
|
||||||
|
*/
|
||||||
|
private HotVehicleThreeInspectVo drivingCheck;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 收车检查
|
||||||
|
*/
|
||||||
|
private HotVehicleThreeInspectVo backCheck;
|
||||||
|
}
|
||||||
@@ -6,6 +6,7 @@ import com.hotwj.platform.config.vehicleThreeInspect.domain.bo.HotVehicleThreeIn
|
|||||||
import com.hotwj.platform.config.vehicleThreeInspect.domain.vo.HotVehicleThreeInspectProcessVo;
|
import com.hotwj.platform.config.vehicleThreeInspect.domain.vo.HotVehicleThreeInspectProcessVo;
|
||||||
import com.hotwj.platform.config.vehicleThreeInspect.domain.vo.HotVehicleThreeInspectPrintVo;
|
import com.hotwj.platform.config.vehicleThreeInspect.domain.vo.HotVehicleThreeInspectPrintVo;
|
||||||
import com.hotwj.platform.config.vehicleThreeInspect.domain.vo.HotVehicleThreeInspectReceivedVehicleVo;
|
import com.hotwj.platform.config.vehicleThreeInspect.domain.vo.HotVehicleThreeInspectReceivedVehicleVo;
|
||||||
|
import com.hotwj.platform.config.vehicleThreeInspect.domain.vo.HotVehicleThreeInspectSummaryVo;
|
||||||
import com.hotwj.platform.config.vehicleThreeInspect.domain.vo.HotVehicleThreeInspectUnreceivedVehicleVo;
|
import com.hotwj.platform.config.vehicleThreeInspect.domain.vo.HotVehicleThreeInspectUnreceivedVehicleVo;
|
||||||
import com.hotwj.platform.config.vehicleThreeInspect.domain.vo.HotVehicleThreeInspectVo;
|
import com.hotwj.platform.config.vehicleThreeInspect.domain.vo.HotVehicleThreeInspectVo;
|
||||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||||
@@ -44,6 +45,26 @@ public interface HotVehicleThreeInspectMapper extends BaseMapperPlus<HotVehicleT
|
|||||||
@Param("bo") HotVehicleThreeInspectBo bo
|
@Param("bo") HotVehicleThreeInspectBo bo
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分页查询车辆三检聚合列表
|
||||||
|
*
|
||||||
|
* @param page 分页参数
|
||||||
|
* @param bo 查询条件
|
||||||
|
* @return 聚合分页列表
|
||||||
|
*/
|
||||||
|
Page<HotVehicleThreeInspectSummaryVo> querySummaryPageList(
|
||||||
|
Page<HotVehicleThreeInspectSummaryVo> page,
|
||||||
|
@Param("bo") HotVehicleThreeInspectBo bo
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 按流程ID集合查询三检明细
|
||||||
|
*
|
||||||
|
* @param inspectIds 流程ID集合
|
||||||
|
* @return 三检明细列表
|
||||||
|
*/
|
||||||
|
List<HotVehicleThreeInspectVo> querySummaryDetailList(@Param("inspectIds") List<Long> inspectIds);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 查询所有三检未收车的车辆列表
|
* 查询所有三检未收车的车辆列表
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import com.hotwj.platform.config.vehicleThreeInspect.domain.bo.HotVehicleThreeIn
|
|||||||
import com.hotwj.platform.config.vehicleThreeInspect.domain.bo.HotVehicleThreeInspectVehicleQueryBo;
|
import com.hotwj.platform.config.vehicleThreeInspect.domain.bo.HotVehicleThreeInspectVehicleQueryBo;
|
||||||
import com.hotwj.platform.config.vehicleThreeInspect.domain.vo.HotVehicleThreeInspectProcessVo;
|
import com.hotwj.platform.config.vehicleThreeInspect.domain.vo.HotVehicleThreeInspectProcessVo;
|
||||||
import com.hotwj.platform.config.vehicleThreeInspect.domain.vo.HotVehicleThreeInspectReceivedVehicleVo;
|
import com.hotwj.platform.config.vehicleThreeInspect.domain.vo.HotVehicleThreeInspectReceivedVehicleVo;
|
||||||
|
import com.hotwj.platform.config.vehicleThreeInspect.domain.vo.HotVehicleThreeInspectSummaryVo;
|
||||||
import com.hotwj.platform.config.vehicleThreeInspect.domain.vo.HotVehicleThreeInspectUnreceivedVehicleVo;
|
import com.hotwj.platform.config.vehicleThreeInspect.domain.vo.HotVehicleThreeInspectUnreceivedVehicleVo;
|
||||||
import com.hotwj.platform.config.vehicleThreeInspect.domain.vo.HotVehicleThreeInspectVo;
|
import com.hotwj.platform.config.vehicleThreeInspect.domain.vo.HotVehicleThreeInspectVo;
|
||||||
import com.hotwj.platform.resourceManagement.vehicleManagement.domain.vo.HotVehicleVo;
|
import com.hotwj.platform.resourceManagement.vehicleManagement.domain.vo.HotVehicleVo;
|
||||||
@@ -72,6 +73,15 @@ public interface IHotVehicleThreeInspectService {
|
|||||||
*/
|
*/
|
||||||
TableDataInfo<HotVehicleThreeInspectProcessVo> queryProcessPageList(HotVehicleThreeInspectBo bo, PageQuery pageQuery);
|
TableDataInfo<HotVehicleThreeInspectProcessVo> queryProcessPageList(HotVehicleThreeInspectBo bo, PageQuery pageQuery);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分页查询车辆三检聚合列表
|
||||||
|
*
|
||||||
|
* @param bo 查询条件
|
||||||
|
* @param pageQuery 分页参数
|
||||||
|
* @return 车辆三检聚合分页列表
|
||||||
|
*/
|
||||||
|
TableDataInfo<HotVehicleThreeInspectSummaryVo> querySummaryPageList(HotVehicleThreeInspectBo bo, PageQuery pageQuery);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 查询符合条件的车辆三检列表
|
* 查询符合条件的车辆三检列表
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import com.hotwj.platform.config.vehicleThreeInspect.domain.bo.HotVehicleThreeIn
|
|||||||
import com.hotwj.platform.config.vehicleThreeInspect.domain.bo.HotVehicleThreeInspectVehicleQueryBo;
|
import com.hotwj.platform.config.vehicleThreeInspect.domain.bo.HotVehicleThreeInspectVehicleQueryBo;
|
||||||
import com.hotwj.platform.config.vehicleThreeInspect.domain.vo.HotVehicleThreeInspectProcessVo;
|
import com.hotwj.platform.config.vehicleThreeInspect.domain.vo.HotVehicleThreeInspectProcessVo;
|
||||||
import com.hotwj.platform.config.vehicleThreeInspect.domain.vo.HotVehicleThreeInspectReceivedVehicleVo;
|
import com.hotwj.platform.config.vehicleThreeInspect.domain.vo.HotVehicleThreeInspectReceivedVehicleVo;
|
||||||
|
import com.hotwj.platform.config.vehicleThreeInspect.domain.vo.HotVehicleThreeInspectSummaryVo;
|
||||||
import com.hotwj.platform.config.vehicleThreeInspect.domain.vo.HotVehicleThreeInspectUnreceivedVehicleVo;
|
import com.hotwj.platform.config.vehicleThreeInspect.domain.vo.HotVehicleThreeInspectUnreceivedVehicleVo;
|
||||||
import com.hotwj.platform.config.vehicleThreeInspect.domain.vo.HotVehicleThreeInspectVo;
|
import com.hotwj.platform.config.vehicleThreeInspect.domain.vo.HotVehicleThreeInspectVo;
|
||||||
import com.hotwj.platform.config.vehicleThreeInspectConfig.domain.HotVehicleThreeInspectConfig;
|
import com.hotwj.platform.config.vehicleThreeInspectConfig.domain.HotVehicleThreeInspectConfig;
|
||||||
@@ -197,6 +198,47 @@ public class HotVehicleThreeInspectServiceImpl implements IHotVehicleThreeInspec
|
|||||||
return TableDataInfo.build(result);
|
return TableDataInfo.build(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分页查询车辆三检聚合列表
|
||||||
|
*
|
||||||
|
* @param bo 查询条件
|
||||||
|
* @param pageQuery 分页参数
|
||||||
|
* @return 车辆三检聚合分页列表
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public TableDataInfo<HotVehicleThreeInspectSummaryVo> querySummaryPageList(HotVehicleThreeInspectBo bo, PageQuery pageQuery) {
|
||||||
|
Page<HotVehicleThreeInspectSummaryVo> result = baseMapper.querySummaryPageList(pageQuery.build(), bo);
|
||||||
|
List<HotVehicleThreeInspectSummaryVo> summaryList = result.getRecords();
|
||||||
|
if (summaryList == null || summaryList.isEmpty()) {
|
||||||
|
return TableDataInfo.build(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
List<Long> inspectIds = summaryList.stream()
|
||||||
|
.map(HotVehicleThreeInspectSummaryVo::getInspectId)
|
||||||
|
.filter(Objects::nonNull)
|
||||||
|
.toList();
|
||||||
|
if (inspectIds.isEmpty()) {
|
||||||
|
return TableDataInfo.build(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
List<HotVehicleThreeInspectVo> detailList = baseMapper.querySummaryDetailList(inspectIds);
|
||||||
|
fillTaskId(detailList);
|
||||||
|
|
||||||
|
Map<Long, HotVehicleThreeInspectSummaryVo> summaryMap = new HashMap<>(summaryList.size());
|
||||||
|
for (HotVehicleThreeInspectSummaryVo summary : summaryList) {
|
||||||
|
summaryMap.put(summary.getInspectId(), summary);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (HotVehicleThreeInspectVo detail : detailList) {
|
||||||
|
HotVehicleThreeInspectSummaryVo summary = summaryMap.get(detail.getInspectId());
|
||||||
|
if (summary == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
attachPhaseDetail(summary, detail);
|
||||||
|
}
|
||||||
|
return TableDataInfo.build(result);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 查询符合条件的车辆三检列表
|
* 查询符合条件的车辆三检列表
|
||||||
*
|
*
|
||||||
@@ -209,6 +251,39 @@ public class HotVehicleThreeInspectServiceImpl implements IHotVehicleThreeInspec
|
|||||||
return baseMapper.selectVoList(lqw);
|
return baseMapper.selectVoList(lqw);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void attachPhaseDetail(HotVehicleThreeInspectSummaryVo summary, HotVehicleThreeInspectVo detail) {
|
||||||
|
if (summary == null || detail == null || detail.getInspectPhase() == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (detail.getInspectPhase() == 1L) {
|
||||||
|
if (summary.getOutCheck() == null) {
|
||||||
|
summary.setOutCheck(detail);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (detail.getInspectPhase() == 2L) {
|
||||||
|
if (summary.getDrivingCheck() == null) {
|
||||||
|
summary.setDrivingCheck(detail);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (detail.getInspectPhase() == 3L && summary.getBackCheck() == null) {
|
||||||
|
summary.setBackCheck(detail);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void fillTaskId(List<HotVehicleThreeInspectVo> list) {
|
||||||
|
if (list == null || list.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
String businessUserId = LoginHelper.getBusinessUserId();
|
||||||
|
for (HotVehicleThreeInspectVo vo : list) {
|
||||||
|
if (StringUtils.isNotBlank(vo.getInstanceId())) {
|
||||||
|
vo.setTaskId(flowService.getTaskId(vo.getInstanceId(), businessUserId));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private LambdaQueryWrapper<HotVehicleThreeInspect> buildQueryWrapper(HotVehicleThreeInspectBo bo) {
|
private LambdaQueryWrapper<HotVehicleThreeInspect> buildQueryWrapper(HotVehicleThreeInspectBo bo) {
|
||||||
Map<String, Object> params = bo.getParams();
|
Map<String, Object> params = bo.getParams();
|
||||||
LambdaQueryWrapper<HotVehicleThreeInspect> lqw = Wrappers.lambdaQuery();
|
LambdaQueryWrapper<HotVehicleThreeInspect> lqw = Wrappers.lambdaQuery();
|
||||||
|
|||||||
@@ -1,35 +0,0 @@
|
|||||||
# 部署服务器信息
|
|
||||||
SERVER=162.14.96.191
|
|
||||||
PORT=6000
|
|
||||||
USER=root
|
|
||||||
PASSWORD=a631223
|
|
||||||
# KEYFILE=/home/user/.ssh/id_rsa
|
|
||||||
#FINGERPRINT=SHA256:REPLACE_WITH_SERVER_FINGERPRINT
|
|
||||||
|
|
||||||
# 部署环境
|
|
||||||
ENV=prod
|
|
||||||
|
|
||||||
# 构建工具
|
|
||||||
BUILD_TOOL=maven
|
|
||||||
PROJECT_DIR=G:\code\shw\hot\hot-platform-backend
|
|
||||||
MAVEN_HOME=G:\soft\apache-maven-3.9.3-bin\apache-maven-3.9.3
|
|
||||||
# GRADLE_HOME=
|
|
||||||
# TARGET_JAR_NAME=
|
|
||||||
|
|
||||||
# 远端部署目录
|
|
||||||
REMOTE_DIR=/apps/hot/
|
|
||||||
|
|
||||||
# JVM 启动参数
|
|
||||||
JVM_OPTS=-Xms512m -Xmx1024m
|
|
||||||
|
|
||||||
# 部署验证接口地址
|
|
||||||
DEPLOY_INFO_URL=http://api.xiaoshi98.top/deploy/info
|
|
||||||
|
|
||||||
# 回滚告警通知地址(可选)
|
|
||||||
# ALERT_WEBHOOK=https://example.com/webhook
|
|
||||||
|
|
||||||
# 运行选项
|
|
||||||
# 演示模式
|
|
||||||
DRY_RUN=false
|
|
||||||
# 跳过打包
|
|
||||||
SKIP_BUILD=false
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
# 部署服务器信息
|
|
||||||
SERVER=162.14.96.191
|
|
||||||
PORT=6000
|
|
||||||
USER=root
|
|
||||||
PASSWORD=a631223
|
|
||||||
# KEYFILE=/home/user/.ssh/id_rsa
|
|
||||||
# FINGERPRINT=SHA256:REPLACE_WITH_SERVER_FINGERPRINT
|
|
||||||
|
|
||||||
# 部署环境
|
|
||||||
ENV=prod
|
|
||||||
|
|
||||||
# 构建设置(前端)
|
|
||||||
# 构建命令;未设置时,脚本默认使用:npm run build:prod
|
|
||||||
BUILD_CMD=npm run build:prod
|
|
||||||
# 本地前端项目目录(请按实际路径调整)
|
|
||||||
PROJECT_DIR=G:\code\shw\hot\hot-platform-frontend
|
|
||||||
# 构建产物目录(相对 PROJECT_DIR 或绝对路径),未设置时默认为 PROJECT_DIR\dist
|
|
||||||
DIST_DIR=dist
|
|
||||||
|
|
||||||
# 远端部署目录(服务器上前端静态资源目录)
|
|
||||||
REMOTE_DIR=/apps/hot/front-end/dist
|
|
||||||
|
|
||||||
# 传输与运行选项
|
|
||||||
TRANSFER_TIMEOUT=300
|
|
||||||
# 演示模式(不执行真实操作)
|
|
||||||
DRY_RUN=false
|
|
||||||
# 跳过构建
|
|
||||||
SKIP_BUILD=false
|
|
||||||
# 跳过清理远端目录
|
|
||||||
SKIP_CLEAN_REMOTE=false
|
|
||||||
@@ -1,734 +0,0 @@
|
|||||||
"""部署脚本(支持 .env/.json 配置)
|
|
||||||
实现以下规范:
|
|
||||||
- 构建:Maven/Gradle clean 并跳过测试,生成可执行 jar
|
|
||||||
- 严格校验:校验 BOOT-INF/classes 配置文件与 MANIFEST 元数据
|
|
||||||
- 安全传输:SCP 非 22 端口 + 指纹校验 + MD5 验证
|
|
||||||
- 预检查:JDK17+、磁盘空间 >= 2GB、ulimit 满足 SpringBoot3
|
|
||||||
- 进程管理:优雅停止(SIGTERM 30s)超时强杀(SIGKILL)
|
|
||||||
- 备份保留:/opt/app/backups 保留最近 5 个版本
|
|
||||||
- 启动与健康:nohup 启动、120s 日志无 ERROR、actuator/health=200
|
|
||||||
- 回滚与告警:自动回滚最近可用版本并发送告警
|
|
||||||
- 日志:JSON Lines 记录所有步骤与环境信息
|
|
||||||
|
|
||||||
配置优先级:
|
|
||||||
- 优先读取 DEPLOY_CONFIG 指定的路径(.env 或 .json)
|
|
||||||
- 未指定时优先加载同目录下 deployment.env,其次 deployment.config.json
|
|
||||||
"""
|
|
||||||
import base64
|
|
||||||
import datetime
|
|
||||||
import hashlib
|
|
||||||
import json
|
|
||||||
import os
|
|
||||||
import re
|
|
||||||
import shutil
|
|
||||||
import socket
|
|
||||||
import subprocess
|
|
||||||
import sys
|
|
||||||
import time
|
|
||||||
import urllib.request
|
|
||||||
import urllib.error
|
|
||||||
import zipfile
|
|
||||||
import paramiko
|
|
||||||
from types import SimpleNamespace
|
|
||||||
|
|
||||||
|
|
||||||
def _ts_ms():
|
|
||||||
"""返回当前时间戳(毫秒)"""
|
|
||||||
return int(time.time() * 1000)
|
|
||||||
|
|
||||||
|
|
||||||
def _log_path():
|
|
||||||
"""返回部署日志文件路径(JSONL 格式)"""
|
|
||||||
return os.path.join(os.path.dirname(__file__), "deployment.log.jsonl")
|
|
||||||
|
|
||||||
|
|
||||||
def log_step(step, status, details=None, extra=None, env=None):
|
|
||||||
"""记录部署步骤到 JSONL 日志
|
|
||||||
step:步骤名称;status:ok/fail/skip 等;
|
|
||||||
details/extra:附加信息;env:环境信息(服务器、目录等)
|
|
||||||
"""
|
|
||||||
rec = {
|
|
||||||
"timestamp_ms": _ts_ms(),
|
|
||||||
"step": step,
|
|
||||||
"status": status,
|
|
||||||
"details": details or "",
|
|
||||||
"extra": extra or {},
|
|
||||||
}
|
|
||||||
if env:
|
|
||||||
rec["env"] = env
|
|
||||||
with open(_log_path(), "a", encoding="utf-8") as f:
|
|
||||||
f.write(json.dumps(rec, ensure_ascii=False) + "\n")
|
|
||||||
|
|
||||||
|
|
||||||
def print_progress(percent, step, status="进行中", details=None):
|
|
||||||
try:
|
|
||||||
msg = f"[{str(percent).rjust(3)}%] {step} - {status}"
|
|
||||||
if details:
|
|
||||||
msg += f" | {details}"
|
|
||||||
print(msg, flush=True)
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
def run_cmd(cmd, cwd=None, env=None, capture_output=True, timeout=None):
|
|
||||||
"""运行本地命令并返回 (返回码, 标准输出, 标准错误)"""
|
|
||||||
try:
|
|
||||||
p = subprocess.run(
|
|
||||||
cmd,
|
|
||||||
shell=True,
|
|
||||||
cwd=cwd,
|
|
||||||
env=env,
|
|
||||||
stdout=subprocess.PIPE if capture_output else None,
|
|
||||||
stderr=subprocess.PIPE if capture_output else None,
|
|
||||||
timeout=timeout,
|
|
||||||
)
|
|
||||||
out = p.stdout.decode("utf-8", errors="replace") if capture_output and p.stdout else ""
|
|
||||||
err = p.stderr.decode("utf-8", errors="replace") if capture_output and p.stderr else ""
|
|
||||||
return p.returncode, out, err
|
|
||||||
except subprocess.TimeoutExpired as te:
|
|
||||||
out = te.stdout.decode("utf-8", errors="replace") if capture_output and te.stdout else ""
|
|
||||||
err = te.stderr.decode("utf-8", errors="replace") if capture_output and te.stderr else "command timeout"
|
|
||||||
return 124, out, err
|
|
||||||
|
|
||||||
|
|
||||||
def md5_file(path):
|
|
||||||
"""计算本地文件 MD5 摘要"""
|
|
||||||
h = hashlib.md5()
|
|
||||||
with open(path, "rb") as f:
|
|
||||||
for chunk in iter(lambda: f.read(8192), b""):
|
|
||||||
h.update(chunk)
|
|
||||||
return h.hexdigest()
|
|
||||||
|
|
||||||
|
|
||||||
def sha256_bytes(b):
|
|
||||||
"""计算字节的 SHA256 并返回 base64 字符串(用于指纹)"""
|
|
||||||
return base64.b64encode(hashlib.sha256(b).digest()).decode("ascii")
|
|
||||||
|
|
||||||
|
|
||||||
def find_jar(project_dir, build_tool, target_jar_name=None):
|
|
||||||
"""定位构建产物 jar 文件(支持指定文件名或自动选择最新)"""
|
|
||||||
if target_jar_name:
|
|
||||||
maven_path = os.path.join(project_dir, "target", target_jar_name)
|
|
||||||
gradle_path = os.path.join(project_dir, "build", "libs", target_jar_name)
|
|
||||||
if os.path.exists(maven_path):
|
|
||||||
return maven_path
|
|
||||||
if os.path.exists(gradle_path):
|
|
||||||
return gradle_path
|
|
||||||
return target_jar_name
|
|
||||||
if build_tool == "maven":
|
|
||||||
target = os.path.join(project_dir, "target")
|
|
||||||
else:
|
|
||||||
target = os.path.join(project_dir, "build", "libs")
|
|
||||||
if not os.path.isdir(target):
|
|
||||||
return None
|
|
||||||
jars = []
|
|
||||||
for name in os.listdir(target):
|
|
||||||
if name.endswith(".jar"):
|
|
||||||
jars.append(os.path.join(target, name))
|
|
||||||
if not jars:
|
|
||||||
return None
|
|
||||||
jars.sort(key=lambda p: os.path.getmtime(p), reverse=True)
|
|
||||||
return jars[0]
|
|
||||||
|
|
||||||
|
|
||||||
def verify_jar_integrity(jar_path):
|
|
||||||
"""校验 jar 完整性(支持 Boot Jar 与 Boot War):
|
|
||||||
- 必须存在 application.yml、application-prod.yml、logback-plus.xml(在 classes 下)
|
|
||||||
- MANIFEST 必须包含 Start-Class / Implementation-Version / Build-Time
|
|
||||||
"""
|
|
||||||
z = zipfile.ZipFile(jar_path)
|
|
||||||
names = set(z.namelist())
|
|
||||||
prefixes = ["BOOT-INF/classes/", "WEB-INF/classes/"]
|
|
||||||
files = ["application.yml", "application-prod.yml", "logback-plus.xml"]
|
|
||||||
missing = []
|
|
||||||
for f in files:
|
|
||||||
if not any((p + f) in names for p in prefixes):
|
|
||||||
missing.append(f)
|
|
||||||
ok = len(missing) == 0
|
|
||||||
manifest = None
|
|
||||||
if "META-INF/MANIFEST.MF" in names:
|
|
||||||
manifest = z.read("META-INF/MANIFEST.MF").decode("utf-8", errors="replace")
|
|
||||||
else:
|
|
||||||
ok = False
|
|
||||||
if manifest:
|
|
||||||
has_start_class = bool(re.search(r"^Start-Class:\s*.+", manifest, re.MULTILINE))
|
|
||||||
has_impl_ver = bool(re.search(r"^Implementation-Version:\s*.+", manifest, re.MULTILINE))
|
|
||||||
has_build_time = bool(re.search(r"^Build-Time:\s*.+", manifest, re.MULTILINE))
|
|
||||||
if not (has_start_class and has_impl_ver and has_build_time):
|
|
||||||
ok = False
|
|
||||||
if not has_start_class:
|
|
||||||
missing.append("Start-Class")
|
|
||||||
if not has_impl_ver:
|
|
||||||
missing.append("Implementation-Version")
|
|
||||||
if not has_build_time:
|
|
||||||
missing.append("Build-Time")
|
|
||||||
z.close()
|
|
||||||
return ok, missing
|
|
||||||
|
|
||||||
|
|
||||||
def parse_manifest_info(jar_path):
|
|
||||||
"""解析 MANIFEST.MF 中的关键字段信息"""
|
|
||||||
z = zipfile.ZipFile(jar_path)
|
|
||||||
info = {}
|
|
||||||
if "META-INF/MANIFEST.MF" in z.namelist():
|
|
||||||
m = z.read("META-INF/MANIFEST.MF").decode("utf-8", errors="replace")
|
|
||||||
for k in ["Start-Class", "Implementation-Version", "Build-Time"]:
|
|
||||||
mobj = re.search(rf"^{k}:\s*(.+)$", m, re.MULTILINE)
|
|
||||||
if mobj:
|
|
||||||
info[k] = mobj.group(1).strip()
|
|
||||||
z.close()
|
|
||||||
return info
|
|
||||||
|
|
||||||
|
|
||||||
def maven_build(project_dir, maven_home):
|
|
||||||
"""执行 Maven 构建(跳过测试)"""
|
|
||||||
mvn = os.path.join(maven_home, "bin", "mvn.cmd") if os.name == "nt" else os.path.join(maven_home, "bin", "mvn")
|
|
||||||
cmd = f'"{mvn}" clean package -DskipTests=true'
|
|
||||||
return run_cmd(cmd, cwd=project_dir, capture_output=False)
|
|
||||||
|
|
||||||
|
|
||||||
def gradle_build(project_dir, gradle_home=None):
|
|
||||||
"""执行 Gradle 构建(跳过测试,优先使用 gradlew)"""
|
|
||||||
gradlew = os.path.join(project_dir, "gradlew.cmd") if os.name == "nt" else os.path.join(project_dir, "gradlew")
|
|
||||||
if os.path.exists(gradlew):
|
|
||||||
cmd = f'"{gradlew}" clean build -x test'
|
|
||||||
return run_cmd(cmd, cwd=project_dir, capture_output=False)
|
|
||||||
if gradle_home:
|
|
||||||
gradle_bin = os.path.join(gradle_home, "bin", "gradle")
|
|
||||||
if os.name == "nt":
|
|
||||||
gradle_bin += ".bat"
|
|
||||||
cmd = f'"{gradle_bin}" clean build -x test'
|
|
||||||
return run_cmd(cmd, cwd=project_dir, capture_output=False)
|
|
||||||
return 1, "", "gradle not found"
|
|
||||||
|
|
||||||
|
|
||||||
def connect_ssh(host, port, user, password=None, keyfile=None):
|
|
||||||
"""建立 SSH 连接(密码或密钥两种方式)"""
|
|
||||||
ssh = paramiko.SSHClient()
|
|
||||||
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
|
||||||
if keyfile:
|
|
||||||
ssh.connect(host, port=port, username=user, key_filename=keyfile)
|
|
||||||
else:
|
|
||||||
ssh.connect(host, port=port, username=user, password=password)
|
|
||||||
try:
|
|
||||||
ssh.get_transport().set_keepalive(15)
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
return ssh
|
|
||||||
|
|
||||||
|
|
||||||
def ensure_fingerprint(host, port, user, password, keyfile, expect_fp):
|
|
||||||
"""校验服务器指纹(要求与配置的 SHA256 指纹一致)"""
|
|
||||||
ssh = connect_ssh(host, port, user, password, keyfile)
|
|
||||||
k = ssh.get_transport().get_remote_server_key()
|
|
||||||
fp = "SHA256:" + sha256_bytes(base64.b64decode(k.get_base64()))
|
|
||||||
ssh.close()
|
|
||||||
return fp == expect_fp, fp
|
|
||||||
|
|
||||||
|
|
||||||
def scp_transfer(local_path, host, port, user, remote_path, timeout=None):
|
|
||||||
"""使用 scp 非标准端口传输文件到远端目录"""
|
|
||||||
cmd = (
|
|
||||||
f'scp -o BatchMode=yes '
|
|
||||||
f'-o ConnectTimeout=30 -o ServerAliveInterval=15 -o ServerAliveCountMax=3 '
|
|
||||||
f'-o StrictHostKeyChecking=accept-new -P {port} "{local_path}" {user}@{host}:"{remote_path}"'
|
|
||||||
)
|
|
||||||
return run_cmd(cmd, timeout=timeout)
|
|
||||||
|
|
||||||
|
|
||||||
def sftp_transfer(ssh, local_path, remote_path):
|
|
||||||
sftp = ssh.open_sftp()
|
|
||||||
try:
|
|
||||||
sftp.put(local_path, remote_path)
|
|
||||||
return 0, "", ""
|
|
||||||
finally:
|
|
||||||
try:
|
|
||||||
sftp.close()
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
def ssh_exec(ssh, cmd, timeout=None, print_output=True):
|
|
||||||
"""执行远端命令并返回 (返回码, 标准输出, 标准错误)"""
|
|
||||||
if print_output:
|
|
||||||
print(f'SSH$ {cmd}', flush=True)
|
|
||||||
stdin, stdout, stderr = ssh.exec_command(cmd, timeout=timeout)
|
|
||||||
out = stdout.read().decode("utf-8", errors="replace")
|
|
||||||
err = stderr.read().decode("utf-8", errors="replace")
|
|
||||||
rc = stdout.channel.recv_exit_status()
|
|
||||||
if print_output:
|
|
||||||
if out:
|
|
||||||
try:
|
|
||||||
print(out if out.endswith("\n") else out + "\n", end="", flush=True)
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
if err:
|
|
||||||
try:
|
|
||||||
print(err if err.endswith("\n") else err + "\n", end="", flush=True)
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
return rc, out, err
|
|
||||||
|
|
||||||
|
|
||||||
def remote_md5(ssh, path):
|
|
||||||
"""获取远端文件摘要(优先 md5sum,失败则尝试 sha256sum)"""
|
|
||||||
rc, out, _ = ssh_exec(ssh, f'md5sum "{path}" || sha256sum "{path}"')
|
|
||||||
algo = "md5"
|
|
||||||
if " " in out.strip():
|
|
||||||
val = out.strip().split()[0]
|
|
||||||
if rc != 0:
|
|
||||||
return None, algo
|
|
||||||
return val, algo
|
|
||||||
return None, algo
|
|
||||||
|
|
||||||
|
|
||||||
def pre_checks(ssh, remote_dir):
|
|
||||||
ok = True
|
|
||||||
details = {}
|
|
||||||
rc, out, _ = ssh_exec(ssh, "java -version")
|
|
||||||
version_match = re.search(r'version \"(?P<v>[\\d\\.]+)', out) or re.search(r'openjdk version \"(?P<v>[\\d\\.]+)', out)
|
|
||||||
if version_match:
|
|
||||||
major = int(version_match.group("v").split(".")[0])
|
|
||||||
details["java_major"] = major
|
|
||||||
details["java_ok"] = major >= 17
|
|
||||||
ok = ok and details["java_ok"]
|
|
||||||
else:
|
|
||||||
details["java_ok"] = False
|
|
||||||
ok = False
|
|
||||||
rc, out, _ = ssh_exec(ssh, f'df -Pk "{remote_dir}" | tail -1')
|
|
||||||
parts = out.split()
|
|
||||||
if len(parts) >= 4:
|
|
||||||
try:
|
|
||||||
avail_k = int(parts[3])
|
|
||||||
except:
|
|
||||||
avail_k = 0
|
|
||||||
details["disk_avail_k"] = avail_k
|
|
||||||
details["disk_ok"] = avail_k >= 2 * 1024 * 1024
|
|
||||||
ok = ok and details["disk_ok"]
|
|
||||||
else:
|
|
||||||
details["disk_ok"] = False
|
|
||||||
ok = False
|
|
||||||
rc, out, _ = ssh_exec(ssh, "ulimit -n")
|
|
||||||
try:
|
|
||||||
nofile = int(out.strip())
|
|
||||||
details["ulimit_nofile"] = nofile
|
|
||||||
details["ulimit_nofile_ok"] = nofile >= 65535
|
|
||||||
ok = ok and details["ulimit_nofile_ok"]
|
|
||||||
except:
|
|
||||||
details["ulimit_nofile_ok"] = False
|
|
||||||
ok = False
|
|
||||||
rc, out, _ = ssh_exec(ssh, "ulimit -u")
|
|
||||||
try:
|
|
||||||
nproc = int(out.strip())
|
|
||||||
details["ulimit_nproc"] = nproc
|
|
||||||
details["ulimit_nproc_ok"] = nproc >= 4096
|
|
||||||
ok = ok and details["ulimit_nproc_ok"]
|
|
||||||
except:
|
|
||||||
details["ulimit_nproc_ok"] = False
|
|
||||||
ok = False
|
|
||||||
return ok, details
|
|
||||||
|
|
||||||
|
|
||||||
def stop_process(ssh, jar_name):
|
|
||||||
"""优雅停止现有服务:SIGTERM 等待 30s,超时则 SIGKILL"""
|
|
||||||
rc, out, _ = ssh_exec(ssh, f"ps -ef | grep 'java.*{re.escape(jar_name)}' | grep -v grep | awk '{{print $2}}'")
|
|
||||||
pids = [p.strip() for p in out.splitlines() if p.strip()]
|
|
||||||
for pid in pids:
|
|
||||||
ssh_exec(ssh, f"kill -15 {pid}")
|
|
||||||
deadline = time.time() + 30
|
|
||||||
remaining = pids[:]
|
|
||||||
while time.time() < deadline and remaining:
|
|
||||||
rc, out, _ = ssh_exec(ssh, f"ps -o pid= -p {' '.join(remaining)}")
|
|
||||||
alive = [p.strip() for p in out.splitlines() if p.strip()]
|
|
||||||
remaining = alive
|
|
||||||
time.sleep(1)
|
|
||||||
for pid in remaining:
|
|
||||||
ssh_exec(ssh, f"kill -9 {pid}")
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
def backup_remote(ssh, remote_dir, jar_name, impl_ver):
|
|
||||||
"""备份当前版本至 /opt/app/backups,并保留最近 5 个版本"""
|
|
||||||
ssh_exec(ssh, f'mkdir -p "{remote_dir}/backups"')
|
|
||||||
ts = datetime.datetime.utcnow().strftime("%Y%m%d%H%M%S")
|
|
||||||
bn = os.path.splitext(jar_name)[0]
|
|
||||||
ver = impl_ver or "unknown"
|
|
||||||
backup_name = f"{bn}-{ver}-{ts}.jar"
|
|
||||||
ssh_exec(ssh, f'if [ -f "{remote_dir}/{jar_name}" ]; then cp -f "{remote_dir}/{jar_name}" "{remote_dir}/backups/{backup_name}"; fi')
|
|
||||||
rc, out, _ = ssh_exec(ssh, f'ls -1t "{remote_dir}/backups" | grep -E "^{re.escape(bn)}-.*\\.jar$"')
|
|
||||||
lines = [l.strip() for l in out.splitlines() if l.strip()]
|
|
||||||
if len(lines) > 5:
|
|
||||||
to_del = lines[5:]
|
|
||||||
names = " ".join([f'"{remote_dir}/backups/{x}"' for x in to_del])
|
|
||||||
ssh_exec(ssh, f"rm -f {names}")
|
|
||||||
return backup_name
|
|
||||||
|
|
||||||
|
|
||||||
def start_service(ssh, remote_dir, jar_name, jvm_opts):
|
|
||||||
"""使用 nohup 启动服务并返回 PID 与日志路径"""
|
|
||||||
log_path = f"{remote_dir}/log.out"
|
|
||||||
cmd = f'nohup java {jvm_opts} -jar "{remote_dir}/{jar_name}" > "{log_path}" 2>&1 &'
|
|
||||||
ssh_exec(ssh, cmd)
|
|
||||||
time.sleep(2)
|
|
||||||
rc, out, _ = ssh_exec(ssh, f"ps -ef | grep 'java.*{re.escape(jar_name)}' | grep -v grep | awk '{{print $2}}' | head -1")
|
|
||||||
pid = out.strip() if out.strip() else ""
|
|
||||||
return pid, log_path
|
|
||||||
|
|
||||||
|
|
||||||
def monitor_logs_no_error(ssh, log_path, seconds):
|
|
||||||
"""在指定时长内监控启动日志,若出现 ERROR 则失败"""
|
|
||||||
end = time.time() + seconds
|
|
||||||
while time.time() < end:
|
|
||||||
rc, out, _ = ssh_exec(ssh, f'tail -n 200 "{log_path}" || true', print_output=False)
|
|
||||||
if "ERROR" in out:
|
|
||||||
return False
|
|
||||||
time.sleep(2)
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
def detect_crash_within(ssh, jar_name, seconds):
|
|
||||||
"""监控指定时长,若进程曾启动后又消失,判定为崩溃"""
|
|
||||||
end = time.time() + seconds
|
|
||||||
alive_once = False
|
|
||||||
while time.time() < end:
|
|
||||||
rc, out, _ = ssh_exec(ssh, f"ps -ef | grep 'java.*{re.escape(jar_name)}' | grep -v grep | awk '{{print $2}}'")
|
|
||||||
pids = [p.strip() for p in out.splitlines() if p.strip()]
|
|
||||||
if pids:
|
|
||||||
alive_once = True
|
|
||||||
else:
|
|
||||||
if alive_once:
|
|
||||||
return True
|
|
||||||
time.sleep(5)
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
def verify_deploy_info(ssh, url, expected_version=None, expected_build_time=None):
|
|
||||||
"""调用部署验证接口并校验版本与构建时间"""
|
|
||||||
rc, code_str, _ = ssh_exec(ssh, f'curl -s -o /dev/null -w "%{{http_code}}" "{url}"')
|
|
||||||
rc2, body, _ = ssh_exec(ssh, f'curl -s "{url}"')
|
|
||||||
http_code = code_str.strip()
|
|
||||||
ok_http = http_code == "200"
|
|
||||||
version = None
|
|
||||||
build_time = None
|
|
||||||
try:
|
|
||||||
data = json.loads(body)
|
|
||||||
payload = data.get("data") if isinstance(data, dict) and "data" in data else data
|
|
||||||
if isinstance(payload, dict):
|
|
||||||
version = payload.get("version")
|
|
||||||
build_time = payload.get("buildTime")
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
match_ver = (expected_version is None) or (version == expected_version)
|
|
||||||
match_bt = (not expected_build_time) or (build_time == expected_build_time)
|
|
||||||
ok = ok_http and match_ver and match_bt
|
|
||||||
return ok, {"http_code": http_code, "version": version, "buildTime": build_time}
|
|
||||||
|
|
||||||
|
|
||||||
def rollback(ssh, remote_dir, jar_name, jvm_opts):
|
|
||||||
"""自动回滚到最近可用备份并重启服务"""
|
|
||||||
rc, out, _ = ssh_exec(ssh, f'ls -1t "{remote_dir}/backups" | grep -E "^{re.escape(os.path.splitext(jar_name)[0])}-.*\\.jar$" | head -1')
|
|
||||||
last = out.strip()
|
|
||||||
if not last:
|
|
||||||
return False
|
|
||||||
ssh_exec(ssh, f'cp -f "{remote_dir}/backups/{last}" "{remote_dir}/{jar_name}"')
|
|
||||||
pid, log_path = start_service(ssh, remote_dir, jar_name, jvm_opts)
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
def send_alert(webhook, payload):
|
|
||||||
"""向运维告警地址发送 JSON 通知(可选)"""
|
|
||||||
if not webhook:
|
|
||||||
return False
|
|
||||||
data = json.dumps(payload).encode("utf-8")
|
|
||||||
req = urllib.request.Request(webhook, data=data, headers={"Content-Type": "application/json"})
|
|
||||||
try:
|
|
||||||
urllib.request.urlopen(req, timeout=10)
|
|
||||||
return True
|
|
||||||
except urllib.error.URLError:
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
def parse_bool(s):
|
|
||||||
"""解析布尔字符串(支持 true/false/1/0/yes/no/on/off)"""
|
|
||||||
if s is None:
|
|
||||||
return False
|
|
||||||
v = str(s).strip().lower()
|
|
||||||
return v in ("1", "true", "yes", "y", "on")
|
|
||||||
|
|
||||||
|
|
||||||
def load_env_file(path):
|
|
||||||
"""加载 .env 配置文件:支持注释与引号包裹的值"""
|
|
||||||
data = {}
|
|
||||||
with open(path, "r", encoding="utf-8") as f:
|
|
||||||
for raw in f:
|
|
||||||
line = raw.strip()
|
|
||||||
if not line:
|
|
||||||
continue
|
|
||||||
if line.startswith("#") or line.startswith(";"):
|
|
||||||
continue
|
|
||||||
if "=" not in line:
|
|
||||||
continue
|
|
||||||
k, v = line.split("=", 1)
|
|
||||||
k = k.strip()
|
|
||||||
v = v.strip()
|
|
||||||
if (v.startswith('"') and v.endswith('"')) or (v.startswith("'") and v.endswith("'")):
|
|
||||||
v = v[1:-1]
|
|
||||||
data[k] = v
|
|
||||||
return {
|
|
||||||
"server": data.get("SERVER"),
|
|
||||||
"port": int(data.get("PORT")) if data.get("PORT") else None,
|
|
||||||
"user": data.get("USER"),
|
|
||||||
"password": data.get("PASSWORD"),
|
|
||||||
"keyfile": data.get("KEYFILE"),
|
|
||||||
"fingerprint": data.get("FINGERPRINT"),
|
|
||||||
"env": data.get("ENV"),
|
|
||||||
"build_tool": data.get("BUILD_TOOL"),
|
|
||||||
"project_dir": data.get("PROJECT_DIR"),
|
|
||||||
"maven_home": data.get("MAVEN_HOME"),
|
|
||||||
"gradle_home": data.get("GRADLE_HOME"),
|
|
||||||
"target_jar_name": data.get("TARGET_JAR_NAME"),
|
|
||||||
"remote_dir": data.get("REMOTE_DIR") or "/opt/app",
|
|
||||||
"jvm_opts": data.get("JVM_OPTS") or "-Xms512m -Xmx1024m",
|
|
||||||
"transfer_method": (data.get("TRANSFER_METHOD") or "sftp").lower(),
|
|
||||||
"transfer_timeout": int(data.get("TRANSFER_TIMEOUT")) if data.get("TRANSFER_TIMEOUT") else 300,
|
|
||||||
"deploy_info_url": data.get("DEPLOY_INFO_URL") or "http://localhost:8080/deploy/info",
|
|
||||||
"alert_webhook": data.get("ALERT_WEBHOOK"),
|
|
||||||
"dry_run": parse_bool(data.get("DRY_RUN")),
|
|
||||||
"skip_build": parse_bool(data.get("SKIP_BUILD")),
|
|
||||||
"skip_prechecks": parse_bool(data.get("SKIP_PRECHECKS")),
|
|
||||||
"precheck_strict": parse_bool(data.get("PRECHECK_STRICT")),
|
|
||||||
"crash_monitor_seconds": int(data.get("CRASH_MONITOR_SECONDS")) if data.get("CRASH_MONITOR_SECONDS") else 60,
|
|
||||||
"skip_crash_monitor": parse_bool(data.get("SKIP_CRASH_MONITOR")),
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
"""主流程:加载配置 -> 构建 -> 校验 -> 传输 -> 预检 -> 停止 -> 备份 -> 启动 -> 健康 -> 回滚/完成"""
|
|
||||||
default_env = os.path.join(os.path.dirname(__file__), "deployment.env")
|
|
||||||
cfg_path = os.getenv("DEPLOY_CONFIG") or default_env
|
|
||||||
# 仅支持 .env 配置文件,并校验必填字段
|
|
||||||
if not os.path.isfile(cfg_path):
|
|
||||||
log_step("config_load", "fail", {"path": cfg_path})
|
|
||||||
print(f"配置文件未找到:{cfg_path}", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
cfg = load_env_file(cfg_path)
|
|
||||||
required = ["server", "port", "user", "env", "build_tool", "project_dir"]
|
|
||||||
missing = []
|
|
||||||
for k in required:
|
|
||||||
v = cfg.get(k)
|
|
||||||
if v is None or (isinstance(v, str) and v.strip() == ""):
|
|
||||||
missing.append(k)
|
|
||||||
if missing:
|
|
||||||
log_step("config_validate", "fail", {"missing": missing, "path": cfg_path})
|
|
||||||
print(f"配置错误:缺少必填项 {', '.join(missing)};配置文件:{cfg_path}", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
print_progress(0, "启动部署", "进行中", f"配置文件:{cfg_path}")
|
|
||||||
|
|
||||||
args = SimpleNamespace(
|
|
||||||
server=cfg["server"],
|
|
||||||
port=int(cfg["port"]),
|
|
||||||
user=cfg["user"],
|
|
||||||
password=cfg.get("password"),
|
|
||||||
keyfile=cfg.get("keyfile"),
|
|
||||||
fingerprint=cfg["fingerprint"],
|
|
||||||
env=cfg["env"],
|
|
||||||
build_tool=cfg["build_tool"],
|
|
||||||
project_dir=cfg["project_dir"],
|
|
||||||
maven_home=cfg.get("maven_home"),
|
|
||||||
gradle_home=cfg.get("gradle_home"),
|
|
||||||
target_jar_name=cfg.get("target_jar_name"),
|
|
||||||
remote_dir=cfg.get("remote_dir", "/opt/app"),
|
|
||||||
jvm_opts=cfg.get("jvm_opts", "-Xms512m -Xmx1024m"),
|
|
||||||
deploy_info_url=cfg.get("deploy_info_url", "http://localhost:8080/deploy/info"),
|
|
||||||
alert_webhook=cfg.get("alert_webhook"),
|
|
||||||
dry_run=bool(cfg.get("dry_run", False)),
|
|
||||||
skip_build=bool(cfg.get("skip_build", False)),
|
|
||||||
transfer_method=cfg.get("transfer_method", "sftp"),
|
|
||||||
transfer_timeout=int(cfg.get("transfer_timeout", 300)),
|
|
||||||
skip_prechecks=bool(cfg.get("skip_prechecks", False)),
|
|
||||||
precheck_strict=bool(cfg.get("precheck_strict", False)),
|
|
||||||
crash_monitor_seconds=int(cfg.get("crash_monitor_seconds", 60)),
|
|
||||||
skip_crash_monitor=bool(cfg.get("skip_crash_monitor", False)),
|
|
||||||
)
|
|
||||||
|
|
||||||
env_info = {
|
|
||||||
"server": args.server,
|
|
||||||
"port": args.port,
|
|
||||||
"env": args.env,
|
|
||||||
"build_tool": args.build_tool,
|
|
||||||
"project_dir": args.project_dir,
|
|
||||||
"remote_dir": args.remote_dir,
|
|
||||||
}
|
|
||||||
|
|
||||||
try:
|
|
||||||
if args.dry_run:
|
|
||||||
log_step("start", "dry_run", {"config_path": cfg_path}, env=env_info)
|
|
||||||
print_progress(5, "加载配置", "完成")
|
|
||||||
else:
|
|
||||||
log_step("start", "running", {"config_path": cfg_path}, env=env_info)
|
|
||||||
print_progress(5, "加载配置", "完成")
|
|
||||||
|
|
||||||
if not args.skip_build:
|
|
||||||
print_progress(10, "开始构建", "进行中", args.build_tool)
|
|
||||||
if args.build_tool == "maven":
|
|
||||||
rc, out, err = maven_build(args.project_dir, args.maven_home or "")
|
|
||||||
else:
|
|
||||||
rc, out, err = gradle_build(args.project_dir, args.gradle_home)
|
|
||||||
log_step("build", "ok" if rc == 0 else "fail", {"stdout": out[-1000:], "stderr": err[-1000:]}, env=env_info)
|
|
||||||
print_progress(20, "构建完成", "成功" if rc == 0 else "失败")
|
|
||||||
if rc != 0 and not args.dry_run:
|
|
||||||
sys.exit(1)
|
|
||||||
else:
|
|
||||||
print_progress(10, "跳过构建", "已跳过")
|
|
||||||
|
|
||||||
jar_path = find_jar(args.project_dir, args.build_tool, args.target_jar_name)
|
|
||||||
if not jar_path or not os.path.isfile(jar_path):
|
|
||||||
log_step("find_jar", "fail", {"path": jar_path}, env=env_info)
|
|
||||||
print_progress(25, "查找构建产物", "失败")
|
|
||||||
if not args.dry_run:
|
|
||||||
sys.exit(1)
|
|
||||||
else:
|
|
||||||
log_step("find_jar", "ok", {"path": jar_path}, env=env_info)
|
|
||||||
print_progress(25, "查找构建产物", "成功", os.path.basename(jar_path))
|
|
||||||
|
|
||||||
if jar_path and os.path.isfile(jar_path):
|
|
||||||
ok, missing = verify_jar_integrity(jar_path)
|
|
||||||
manifest_info = parse_manifest_info(jar_path)
|
|
||||||
log_step("jar_integrity", "ok" if ok else "fail", {"manifest": manifest_info, "missing": missing}, env=env_info)
|
|
||||||
print_progress(35, "校验产物完整性", "成功" if ok else f"失败(缺失: {', '.join(missing)})")
|
|
||||||
if not ok and not args.dry_run:
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
if args.dry_run:
|
|
||||||
log_step("fingerprint_check", "skip", env=env_info)
|
|
||||||
log_step("transfer", "skip", env=env_info)
|
|
||||||
log_step("pre_checks", "skip", env=env_info)
|
|
||||||
log_step("stop_process", "skip", env=env_info)
|
|
||||||
log_step("backup", "skip", env=env_info)
|
|
||||||
log_step("start_service", "skip", env=env_info)
|
|
||||||
log_step("monitor_crash", "skip", env=env_info)
|
|
||||||
print_progress(100, "完成", "DRY-RUN")
|
|
||||||
log_step("done", "ok", env=env_info)
|
|
||||||
return
|
|
||||||
|
|
||||||
if args.fingerprint:
|
|
||||||
print_progress(40, "校验服务器指纹", "进行中")
|
|
||||||
ok_fp, actual_fp = ensure_fingerprint(args.server, args.port, args.user, args.password, args.keyfile, args.fingerprint)
|
|
||||||
log_step("fingerprint_check", "ok" if ok_fp else "fail", {"actual": actual_fp}, env=env_info)
|
|
||||||
print_progress(45, "校验服务器指纹", "成功" if ok_fp else "失败")
|
|
||||||
if not ok_fp:
|
|
||||||
print(f"服务器指纹不匹配:期望 {args.fingerprint},实际 {actual_fp}", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
else:
|
|
||||||
log_step("fingerprint_check", "skip", {"reason": "no_fingerprint_provided"}, env=env_info)
|
|
||||||
print_progress(40, "校验服务器指纹", "已跳过")
|
|
||||||
|
|
||||||
print_progress(50, "连接服务器", "进行中")
|
|
||||||
ssh = connect_ssh(args.server, args.port, args.user, args.password, args.keyfile)
|
|
||||||
ssh.exec_command(f'mkdir -p "{args.remote_dir}"')
|
|
||||||
print_progress(55, "连接服务器", "成功")
|
|
||||||
|
|
||||||
local_md5 = md5_file(jar_path)
|
|
||||||
print_progress(60, "传输文件", "进行中", os.path.basename(jar_path))
|
|
||||||
if args.transfer_method == "scp":
|
|
||||||
rc, out, err = scp_transfer(
|
|
||||||
jar_path,
|
|
||||||
args.server,
|
|
||||||
args.port,
|
|
||||||
args.user,
|
|
||||||
f'{args.remote_dir}/{os.path.basename(jar_path)}',
|
|
||||||
timeout=args.transfer_timeout,
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
rc, out, err = sftp_transfer(
|
|
||||||
ssh,
|
|
||||||
jar_path,
|
|
||||||
f'{args.remote_dir}/{os.path.basename(jar_path)}',
|
|
||||||
)
|
|
||||||
log_step("transfer", "ok" if rc == 0 else "fail", {"stdout": out[-1000:], "stderr": err[-1000:]}, env=env_info)
|
|
||||||
print_progress(65, "传输文件", "成功" if rc == 0 else "失败")
|
|
||||||
if rc != 0:
|
|
||||||
ssh.close()
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
remote_hash, algo = remote_md5(ssh, f'{args.remote_dir}/{os.path.basename(jar_path)}')
|
|
||||||
match = remote_hash == local_md5 if algo == "md5" and remote_hash else True
|
|
||||||
log_step("md5_verify", "ok" if match else "fail", {"local_md5": local_md5, "remote_hash": remote_hash, "algo": algo}, env=env_info)
|
|
||||||
print_progress(70, "校验文件摘要", "成功" if match else "失败")
|
|
||||||
if not match:
|
|
||||||
ssh.close()
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
if args.skip_prechecks:
|
|
||||||
log_step("pre_checks", "skip", env=env_info)
|
|
||||||
print_progress(75, "预部署检查", "已跳过")
|
|
||||||
else:
|
|
||||||
checks_ok, check_details = pre_checks(ssh, args.remote_dir)
|
|
||||||
status = "ok" if checks_ok else ("fail" if args.precheck_strict else "warn")
|
|
||||||
log_step("pre_checks", status, check_details, env=env_info)
|
|
||||||
print_progress(75, "预部署检查", "成功" if checks_ok else ("警告" if not args.precheck_strict else "失败"))
|
|
||||||
if not checks_ok and args.precheck_strict:
|
|
||||||
ssh.close()
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
jar_name = os.path.basename(jar_path)
|
|
||||||
print_progress(78, "停止现有服务", "进行中")
|
|
||||||
stop_process(ssh, jar_name)
|
|
||||||
log_step("stop_process", "ok", env=env_info)
|
|
||||||
print_progress(80, "停止现有服务", "完成")
|
|
||||||
|
|
||||||
backup_name = backup_remote(ssh, args.remote_dir, jar_name, parse_manifest_info(jar_path).get("Implementation-Version"))
|
|
||||||
log_step("backup", "ok", {"backup": backup_name}, env=env_info)
|
|
||||||
print_progress(82, "备份旧版本", "完成", backup_name)
|
|
||||||
|
|
||||||
pid, log_path = start_service(ssh, args.remote_dir, jar_name, args.jvm_opts)
|
|
||||||
log_step("start_service", "ok" if pid else "fail", {"pid": pid, "log": log_path}, env=env_info)
|
|
||||||
print_progress(85, "启动服务", "成功" if pid else "失败", f"pid={pid}")
|
|
||||||
if not pid:
|
|
||||||
ssh.close()
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
log_step("startup_logs", "skip", env=env_info)
|
|
||||||
print_progress(90, "监控启动日志", "已跳过")
|
|
||||||
|
|
||||||
last_metrics = {}
|
|
||||||
|
|
||||||
if args.skip_crash_monitor:
|
|
||||||
log_step("monitor_crash", "skip", env=env_info)
|
|
||||||
print_progress(97, f"崩溃监控({args.crash_monitor_seconds}秒)", "已跳过")
|
|
||||||
else:
|
|
||||||
crashed = detect_crash_within(ssh, jar_name, args.crash_monitor_seconds)
|
|
||||||
log_step("monitor_crash", "fail" if crashed else "ok", env=env_info)
|
|
||||||
print_progress(97, f"崩溃监控({args.crash_monitor_seconds}秒)", "失败" if crashed else "通过")
|
|
||||||
|
|
||||||
ver_expected = parse_manifest_info(jar_path).get("Implementation-Version")
|
|
||||||
bt_expected = parse_manifest_info(jar_path).get("Build-Time")
|
|
||||||
print_progress(98, "验证部署接口", "进行中")
|
|
||||||
ok_deploy, deploy_info = verify_deploy_info(ssh, args.deploy_info_url, ver_expected, bt_expected)
|
|
||||||
log_step("deploy_info_check", "ok" if ok_deploy else "fail", {"expect_version": ver_expected, "expect_build_time": bt_expected, "response": deploy_info}, env=env_info)
|
|
||||||
print_progress(98, "验证部署接口", "成功" if ok_deploy else "失败")
|
|
||||||
|
|
||||||
if crashed or not ok_deploy:
|
|
||||||
print_progress(98, "触发回滚", "进行中")
|
|
||||||
rb_ok = rollback(ssh, args.remote_dir, jar_name, args.jvm_opts)
|
|
||||||
log_step("rollback", "ok" if rb_ok else "fail", env=env_info)
|
|
||||||
print_progress(99, "回滚完成", "成功" if rb_ok else "失败")
|
|
||||||
send_alert(args.alert_webhook, {"event": "rollback", "server": args.server, "env": args.env, "reason": "deploy_info_fail_or_crash"})
|
|
||||||
ssh.close()
|
|
||||||
if not rb_ok:
|
|
||||||
sys.exit(1)
|
|
||||||
sys.exit(0)
|
|
||||||
|
|
||||||
log_step("done", "ok", {"metrics": last_metrics}, env=env_info)
|
|
||||||
print_progress(100, "部署完成", "成功")
|
|
||||||
ssh.close()
|
|
||||||
except Exception as e:
|
|
||||||
import traceback
|
|
||||||
log_step("exception", "fail", {"error": str(e), "trace": traceback.format_exc()}, env=env_info)
|
|
||||||
print_progress(100, "部署失败", "异常", str(e))
|
|
||||||
print(traceback.format_exc(), file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
@@ -1,193 +0,0 @@
|
|||||||
import json
|
|
||||||
import os
|
|
||||||
import sys
|
|
||||||
import time
|
|
||||||
from types import SimpleNamespace
|
|
||||||
|
|
||||||
from deployment import (
|
|
||||||
log_step,
|
|
||||||
print_progress,
|
|
||||||
run_cmd,
|
|
||||||
connect_ssh,
|
|
||||||
ssh_exec,
|
|
||||||
parse_bool,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def load_frontend_env(path):
|
|
||||||
data = {}
|
|
||||||
with open(path, "r", encoding="utf-8") as f:
|
|
||||||
for raw in f:
|
|
||||||
line = raw.strip()
|
|
||||||
if not line:
|
|
||||||
continue
|
|
||||||
if line.startswith("#") or line.startswith(";"):
|
|
||||||
continue
|
|
||||||
if "=" not in line:
|
|
||||||
continue
|
|
||||||
k, v = line.split("=", 1)
|
|
||||||
k = k.strip()
|
|
||||||
v = v.strip()
|
|
||||||
if (v.startswith('"') and v.endswith('"')) or (v.startswith("'") and v.endswith("'")):
|
|
||||||
v = v[1:-1]
|
|
||||||
data[k] = v
|
|
||||||
return {
|
|
||||||
"server": data.get("SERVER"),
|
|
||||||
"port": int(data.get("PORT")) if data.get("PORT") else None,
|
|
||||||
"user": data.get("USER"),
|
|
||||||
"password": data.get("PASSWORD"),
|
|
||||||
"keyfile": data.get("KEYFILE"),
|
|
||||||
"fingerprint": data.get("FINGERPRINT"),
|
|
||||||
"env": data.get("ENV"),
|
|
||||||
"project_dir": data.get("PROJECT_DIR"),
|
|
||||||
"build_cmd": data.get("BUILD_CMD") or "npm run build:prod",
|
|
||||||
"dist_dir": data.get("DIST_DIR"),
|
|
||||||
"remote_dir": data.get("REMOTE_DIR") or "/opt/app/frontend",
|
|
||||||
"transfer_timeout": int(data.get("TRANSFER_TIMEOUT")) if data.get("TRANSFER_TIMEOUT") else 300,
|
|
||||||
"skip_build": parse_bool(data.get("SKIP_BUILD")),
|
|
||||||
"dry_run": parse_bool(data.get("DRY_RUN")),
|
|
||||||
"skip_clean_remote": parse_bool(data.get("SKIP_CLEAN_REMOTE")),
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def sftp_upload_dir(ssh, local_dir, remote_dir):
|
|
||||||
sftp = ssh.open_sftp()
|
|
||||||
try:
|
|
||||||
try:
|
|
||||||
sftp.stat(remote_dir)
|
|
||||||
except IOError:
|
|
||||||
sftp.mkdir(remote_dir)
|
|
||||||
for root, dirs, files in os.walk(local_dir):
|
|
||||||
rel = os.path.relpath(root, local_dir)
|
|
||||||
if rel == ".":
|
|
||||||
remote_root = remote_dir
|
|
||||||
else:
|
|
||||||
remote_root = remote_dir.rstrip("/") + "/" + rel.replace(os.sep, "/")
|
|
||||||
try:
|
|
||||||
sftp.stat(remote_root)
|
|
||||||
except IOError:
|
|
||||||
sftp.mkdir(remote_root)
|
|
||||||
for name in files:
|
|
||||||
lp = os.path.join(root, name)
|
|
||||||
rp = remote_root.rstrip("/") + "/" + name
|
|
||||||
sftp.put(lp, rp)
|
|
||||||
finally:
|
|
||||||
try:
|
|
||||||
sftp.close()
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
default_env = os.path.join(os.path.dirname(__file__), "deployment.frontend.env")
|
|
||||||
cfg_path = os.getenv("DEPLOY_FRONTEND_CONFIG") or default_env
|
|
||||||
if not os.path.isfile(cfg_path):
|
|
||||||
log_step("fe_config_load", "fail", {"path": cfg_path})
|
|
||||||
print(f"前端配置文件未找到:{cfg_path}", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
cfg = load_frontend_env(cfg_path)
|
|
||||||
required = ["server", "port", "user", "project_dir", "remote_dir"]
|
|
||||||
missing = []
|
|
||||||
for k in required:
|
|
||||||
v = cfg.get(k)
|
|
||||||
if v is None or (isinstance(v, str) and v.strip() == ""):
|
|
||||||
missing.append(k)
|
|
||||||
if missing:
|
|
||||||
log_step("fe_config_validate", "fail", {"missing": missing, "path": cfg_path})
|
|
||||||
print(f"前端配置错误:缺少必填项 {', '.join(missing)};配置文件:{cfg_path}", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
args = SimpleNamespace(
|
|
||||||
server=cfg["server"],
|
|
||||||
port=int(cfg["port"]),
|
|
||||||
user=cfg["user"],
|
|
||||||
password=cfg.get("password"),
|
|
||||||
keyfile=cfg.get("keyfile"),
|
|
||||||
fingerprint=cfg.get("fingerprint"),
|
|
||||||
env=cfg.get("env") or "prod",
|
|
||||||
project_dir=cfg["project_dir"],
|
|
||||||
build_cmd=cfg["build_cmd"],
|
|
||||||
dist_dir=cfg.get("dist_dir"),
|
|
||||||
remote_dir=cfg["remote_dir"],
|
|
||||||
transfer_timeout=int(cfg.get("transfer_timeout", 300)),
|
|
||||||
skip_build=bool(cfg.get("skip_build", False)),
|
|
||||||
dry_run=bool(cfg.get("dry_run", False)),
|
|
||||||
skip_clean_remote=bool(cfg.get("skip_clean_remote", False)),
|
|
||||||
)
|
|
||||||
|
|
||||||
if not args.dist_dir:
|
|
||||||
args.dist_dir = os.path.join(args.project_dir, "dist")
|
|
||||||
elif not os.path.isabs(args.dist_dir):
|
|
||||||
args.dist_dir = os.path.join(args.project_dir, args.dist_dir)
|
|
||||||
|
|
||||||
env_info = {
|
|
||||||
"server": args.server,
|
|
||||||
"port": args.port,
|
|
||||||
"env": args.env,
|
|
||||||
"project_dir": args.project_dir,
|
|
||||||
"dist_dir": args.dist_dir,
|
|
||||||
"remote_dir": args.remote_dir,
|
|
||||||
}
|
|
||||||
|
|
||||||
log_step("fe_start", "running", {"config_path": cfg_path}, env=env_info)
|
|
||||||
print_progress(0, "前端部署启动", "进行中", f"配置文件:{cfg_path}")
|
|
||||||
|
|
||||||
try:
|
|
||||||
if not args.skip_build:
|
|
||||||
print_progress(10, "前端构建", "进行中", args.build_cmd)
|
|
||||||
rc, out, err = run_cmd(args.build_cmd, cwd=args.project_dir, capture_output=False)
|
|
||||||
log_step("fe_build", "ok" if rc == 0 else "fail", {}, env=env_info)
|
|
||||||
print_progress(20, "前端构建完成", "成功" if rc == 0 else "失败")
|
|
||||||
if rc != 0 and not args.dry_run:
|
|
||||||
sys.exit(1)
|
|
||||||
else:
|
|
||||||
print_progress(10, "跳过前端构建", "已跳过")
|
|
||||||
|
|
||||||
if not os.path.isdir(args.dist_dir):
|
|
||||||
log_step("fe_dist_check", "fail", {"path": args.dist_dir}, env=env_info)
|
|
||||||
print_progress(25, "检查前端构建目录", "失败")
|
|
||||||
if not args.dry_run:
|
|
||||||
sys.exit(1)
|
|
||||||
else:
|
|
||||||
log_step("fe_dist_check", "ok", {"path": args.dist_dir}, env=env_info)
|
|
||||||
print_progress(25, "检查前端构建目录", "成功")
|
|
||||||
|
|
||||||
if args.dry_run:
|
|
||||||
log_step("fe_connect", "skip", env=env_info)
|
|
||||||
log_step("fe_upload", "skip", env=env_info)
|
|
||||||
print_progress(100, "前端部署完成", "DRY-RUN")
|
|
||||||
log_step("fe_done", "ok", env=env_info)
|
|
||||||
return
|
|
||||||
|
|
||||||
print_progress(40, "连接前端服务器", "进行中")
|
|
||||||
ssh = connect_ssh(args.server, args.port, args.user, args.password, args.keyfile)
|
|
||||||
print_progress(50, "连接前端服务器", "成功")
|
|
||||||
|
|
||||||
ssh_exec(ssh, f'mkdir -p "{args.remote_dir}"')
|
|
||||||
|
|
||||||
if not args.skip_clean_remote:
|
|
||||||
print_progress(60, "清理远端目录", "进行中")
|
|
||||||
ssh_exec(ssh, f'find "{args.remote_dir}" -mindepth 1 -maxdepth 1 -exec rm -rf {{}} + || true')
|
|
||||||
print_progress(65, "清理远端目录", "完成")
|
|
||||||
|
|
||||||
print_progress(70, "上传前端静态资源", "进行中")
|
|
||||||
sftp_upload_dir(ssh, args.dist_dir, args.remote_dir)
|
|
||||||
log_step("fe_upload", "ok", {"dist": args.dist_dir}, env=env_info)
|
|
||||||
print_progress(90, "上传前端静态资源", "完成")
|
|
||||||
|
|
||||||
log_step("fe_done", "ok", env=env_info)
|
|
||||||
print_progress(100, "前端部署完成", "成功")
|
|
||||||
ssh.close()
|
|
||||||
except Exception as e:
|
|
||||||
import traceback
|
|
||||||
|
|
||||||
log_step("fe_exception", "fail", {"error": str(e), "trace": traceback.format_exc()}, env=env_info)
|
|
||||||
print_progress(100, "前端部署失败", "异常", str(e))
|
|
||||||
print(traceback.format_exc(), file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
|
|
||||||
@@ -106,6 +106,96 @@
|
|||||||
ORDER BY IFNULL(MAX(t.inspect_time), MAX(t.create_time)) DESC, MAX(t.create_time) DESC
|
ORDER BY IFNULL(MAX(t.inspect_time), MAX(t.create_time)) DESC, MAX(t.create_time) DESC
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
|
<select id="querySummaryPageList"
|
||||||
|
resultType="com.hotwj.platform.config.vehicleThreeInspect.domain.vo.HotVehicleThreeInspectSummaryVo">
|
||||||
|
SELECT
|
||||||
|
t.inspect_id AS inspectId,
|
||||||
|
MAX(t.company_id) AS companyId,
|
||||||
|
MAX(t.vehicle_id) AS vehicleId,
|
||||||
|
MAX(v.plate_number) AS plateNumber,
|
||||||
|
MAX(t.driver_id) AS driverId,
|
||||||
|
MAX(t.driver_name) AS driverName,
|
||||||
|
MIN(IFNULL(t.inspect_time, t.create_time)) AS startTime,
|
||||||
|
MAX(IFNULL(t.inspect_time, t.create_time)) AS endTime,
|
||||||
|
CASE
|
||||||
|
WHEN MIN(IFNULL(t.inspect_time, t.create_time)) = MAX(IFNULL(t.inspect_time, t.create_time))
|
||||||
|
THEN DATE_FORMAT(MIN(IFNULL(t.inspect_time, t.create_time)), '%Y-%m-%d %H:%i:%s')
|
||||||
|
ELSE CONCAT(
|
||||||
|
DATE_FORMAT(MIN(IFNULL(t.inspect_time, t.create_time)), '%Y-%m-%d %H:%i:%s'),
|
||||||
|
' ~ ',
|
||||||
|
DATE_FORMAT(MAX(IFNULL(t.inspect_time, t.create_time)), '%Y-%m-%d %H:%i:%s')
|
||||||
|
)
|
||||||
|
END AS inspectTime,
|
||||||
|
CASE
|
||||||
|
WHEN SUM(CASE WHEN t.inspect_phase = 3 THEN 1 ELSE 0 END) > 0 THEN '已完成'
|
||||||
|
ELSE '未完成'
|
||||||
|
END AS completeStatus,
|
||||||
|
CASE
|
||||||
|
WHEN SUM(CASE WHEN t.inspect_phase = 3 THEN 1 ELSE 0 END) > 0 THEN 1
|
||||||
|
ELSE 0
|
||||||
|
END AS completed,
|
||||||
|
CASE
|
||||||
|
WHEN COUNT(DISTINCT CASE WHEN t.flow_status = 'APPROVED' THEN t.inspect_phase END) = 3
|
||||||
|
THEN '已完成审核'
|
||||||
|
ELSE '未审核'
|
||||||
|
END AS auditCompleteStatus,
|
||||||
|
CASE
|
||||||
|
WHEN COUNT(DISTINCT CASE WHEN t.flow_status = 'APPROVED' THEN t.inspect_phase END) = 3
|
||||||
|
THEN 1
|
||||||
|
ELSE 0
|
||||||
|
END AS auditCompleted
|
||||||
|
FROM hot_vehicle_three_inspect t
|
||||||
|
LEFT JOIN hot_vehicle v ON t.vehicle_id = v.id
|
||||||
|
<where>
|
||||||
|
t.is_deleted = 0
|
||||||
|
AND t.inspect_id IS NOT NULL
|
||||||
|
<if test="bo.companyId != null">
|
||||||
|
AND t.company_id = #{bo.companyId}
|
||||||
|
</if>
|
||||||
|
<if test="bo.vehicleId != null">
|
||||||
|
AND t.vehicle_id = #{bo.vehicleId}
|
||||||
|
</if>
|
||||||
|
<if test="bo.driverId != null and bo.driverId != ''">
|
||||||
|
AND t.driver_id = #{bo.driverId}
|
||||||
|
</if>
|
||||||
|
<if test="bo.driverName != null and bo.driverName != ''">
|
||||||
|
AND t.driver_name LIKE CONCAT('%', #{bo.driverName}, '%')
|
||||||
|
</if>
|
||||||
|
<if test="bo.inspectId != null">
|
||||||
|
AND t.inspect_id = #{bo.inspectId}
|
||||||
|
</if>
|
||||||
|
<if test="bo.params != null and bo.params.beginTime != null and bo.params.beginTime != '' and bo.params.endTime != null and bo.params.endTime != ''">
|
||||||
|
AND EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM hot_vehicle_three_inspect t2
|
||||||
|
WHERE t2.inspect_id = t.inspect_id
|
||||||
|
AND t2.is_deleted = 0
|
||||||
|
AND t2.inspect_time BETWEEN #{bo.params.beginTime} AND #{bo.params.endTime}
|
||||||
|
)
|
||||||
|
</if>
|
||||||
|
<if test="bo.params != null and bo.params.beginTime != null and bo.params.beginTime != '' and (bo.params.endTime == null or bo.params.endTime == '')">
|
||||||
|
AND EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM hot_vehicle_three_inspect t2
|
||||||
|
WHERE t2.inspect_id = t.inspect_id
|
||||||
|
AND t2.is_deleted = 0
|
||||||
|
AND t2.inspect_time <![CDATA[>=]]> #{bo.params.beginTime}
|
||||||
|
)
|
||||||
|
</if>
|
||||||
|
<if test="bo.params != null and (bo.params.beginTime == null or bo.params.beginTime == '') and bo.params.endTime != null and bo.params.endTime != ''">
|
||||||
|
AND EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM hot_vehicle_three_inspect t2
|
||||||
|
WHERE t2.inspect_id = t.inspect_id
|
||||||
|
AND t2.is_deleted = 0
|
||||||
|
AND t2.inspect_time <![CDATA[<=]]> #{bo.params.endTime}
|
||||||
|
)
|
||||||
|
</if>
|
||||||
|
</where>
|
||||||
|
GROUP BY t.inspect_id
|
||||||
|
ORDER BY IFNULL(MAX(t.inspect_time), MAX(t.create_time)) DESC, MAX(t.create_time) DESC
|
||||||
|
</select>
|
||||||
|
|
||||||
<select id="selectPrintList"
|
<select id="selectPrintList"
|
||||||
resultType="com.hotwj.platform.config.vehicleThreeInspect.domain.vo.HotVehicleThreeInspectPrintVo">
|
resultType="com.hotwj.platform.config.vehicleThreeInspect.domain.vo.HotVehicleThreeInspectPrintVo">
|
||||||
WITH base AS (
|
WITH base AS (
|
||||||
@@ -175,6 +265,46 @@
|
|||||||
ORDER BY MAX(create_time) DESC
|
ORDER BY MAX(create_time) DESC
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
|
<select id="querySummaryDetailList"
|
||||||
|
resultType="com.hotwj.platform.config.vehicleThreeInspect.domain.vo.HotVehicleThreeInspectVo">
|
||||||
|
SELECT
|
||||||
|
t.id,
|
||||||
|
t.company_id,
|
||||||
|
t.vehicle_id,
|
||||||
|
t.driver_id,
|
||||||
|
t.driver_name,
|
||||||
|
t.inspect_phase,
|
||||||
|
t.inspect_id,
|
||||||
|
t.inspect_time,
|
||||||
|
t.auditor_id,
|
||||||
|
t.auditor_name,
|
||||||
|
t.inspect_result_json,
|
||||||
|
t.attach_img_url,
|
||||||
|
t.inspector_sign_img_url,
|
||||||
|
t.is_deleted,
|
||||||
|
t.is_false_report,
|
||||||
|
t.has_hidden_danger,
|
||||||
|
t.audit_time,
|
||||||
|
t.audit_result,
|
||||||
|
t.reject_reason,
|
||||||
|
t.auditor_sign_img_url,
|
||||||
|
t.instance_id,
|
||||||
|
t.flow_status,
|
||||||
|
t.evaluator_id
|
||||||
|
FROM hot_vehicle_three_inspect t
|
||||||
|
WHERE t.is_deleted = 0
|
||||||
|
AND t.inspect_id IS NOT NULL
|
||||||
|
AND t.inspect_id IN
|
||||||
|
<foreach collection="inspectIds" item="inspectId" open="(" separator="," close=")">
|
||||||
|
#{inspectId}
|
||||||
|
</foreach>
|
||||||
|
ORDER BY t.inspect_id DESC,
|
||||||
|
t.inspect_phase ASC,
|
||||||
|
IFNULL(t.inspect_time, t.create_time) DESC,
|
||||||
|
t.create_time DESC,
|
||||||
|
t.id DESC
|
||||||
|
</select>
|
||||||
|
|
||||||
<select id="queryUnreceivedVehicles"
|
<select id="queryUnreceivedVehicles"
|
||||||
resultType="com.hotwj.platform.config.vehicleThreeInspect.domain.vo.HotVehicleThreeInspectUnreceivedVehicleVo">
|
resultType="com.hotwj.platform.config.vehicleThreeInspect.domain.vo.HotVehicleThreeInspectUnreceivedVehicleVo">
|
||||||
SELECT
|
SELECT
|
||||||
|
|||||||
@@ -1,30 +0,0 @@
|
|||||||
# 要清除的表
|
|
||||||
|
|
||||||
# 更改oss地址
|
|
||||||
|
|
||||||
# 更改nginx配置,以适配大文件上传,在http模块添加:client_max_body_size 100M;
|
|
||||||
|
|
||||||
# nginx配置 解决h5 预览pdf问题
|
|
||||||
http {
|
|
||||||
include mime.types;
|
|
||||||
types {
|
|
||||||
application/javascript mjs;
|
|
||||||
application/javascript mjs.map;
|
|
||||||
}
|
|
||||||
...
|
|
||||||
}
|
|
||||||
|
|
||||||
# 密码:123.\HOT.WJ
|
|
||||||
|
|
||||||
账号:
|
|
||||||
lksy_lz
|
|
||||||
密码:
|
|
||||||
htwjj369
|
|
||||||
短信宝: https://console.smsbao.com/#/index
|
|
||||||
|
|
||||||
# 企业二维码注册链接
|
|
||||||
register-url: http://hott.xiaoshi98.top
|
|
||||||
|
|
||||||
# pdf预览连接 gotenberg
|
|
||||||
|
|
||||||
# app 验真与外网地址
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
{
|
|
||||||
"basics": [
|
|
||||||
"system:oss:upload",
|
|
||||||
"system:oss:query"
|
|
||||||
],
|
|
||||||
"dependencies": {
|
|
||||||
"securityManagement:vehicleThreeInspect:list": ["driverManagement:driver:list", "resourceManagement:vehicle:list", "config:vehicleThreeInspectConfig:list", "config:signAuditPermissionConfig:list", "resourceManagement:companySafetyManager:list"],
|
|
||||||
"securityManagement:hiddenDangerPlan:list": ["config:hiddenDangerCheckConfig:list"],
|
|
||||||
"securityManagement:accidentArchive:list": ["config:accidentLevelConfig:list", "config:accidentSeverityLevelConfig:list", "config:accidentLikelihoodLevelConfig:list"],
|
|
||||||
"noticeManagerment:noticeRecord:list": ["system:user:list"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,54 +0,0 @@
|
|||||||
-- 动态生成清理脚本的SQL
|
|
||||||
-- 运行此查询,它会返回一系列 TRUNCATE 语句
|
|
||||||
-- 逻辑:
|
|
||||||
-- 1. 包含所有 hot_ 开头的表
|
|
||||||
-- 2. 包含 sys_flow_ 开头的表
|
|
||||||
-- 3. 排除 _config 结尾的配置表
|
|
||||||
-- 4. 排除 _template 结尾的模板表
|
|
||||||
-- 5. 排除特定的系统/资源保留表
|
|
||||||
|
|
||||||
SELECT CONCAT('TRUNCATE TABLE ', TABLE_NAME, ';') AS cleanup_script
|
|
||||||
FROM information_schema.TABLES
|
|
||||||
WHERE TABLE_SCHEMA = (SELECT DATABASE())
|
|
||||||
AND (
|
|
||||||
-- 规则1:所有 hot_ 开头的表
|
|
||||||
(TABLE_NAME LIKE 'hot_%'
|
|
||||||
-- 排除配置表
|
|
||||||
AND TABLE_NAME NOT LIKE '%_config'
|
|
||||||
-- 排除模板表
|
|
||||||
AND TABLE_NAME NOT LIKE '%_template'
|
|
||||||
-- 排除特定保留表
|
|
||||||
AND TABLE_NAME NOT IN (
|
|
||||||
'hot_course_resource',
|
|
||||||
'hot_media_resource',
|
|
||||||
'hot_question_bank',
|
|
||||||
'hot_exam_paper',
|
|
||||||
'hot_system_agreement',
|
|
||||||
'hot_law_regulation',
|
|
||||||
'hot_vehicle_type',
|
|
||||||
'hot_vehicle_brand_model',
|
|
||||||
'hot_company_basic_config',
|
|
||||||
'hot_company_dept_config',
|
|
||||||
'hot_driver_annual_assessment_config',
|
|
||||||
'hot_sign_audit_permission_config',
|
|
||||||
'hot_vehicle_three_inspect_config',
|
|
||||||
'hot_hidden_danger_inspect_store',
|
|
||||||
'hot_hidden_danger_check_config',
|
|
||||||
'hot_company_basic_config',
|
|
||||||
'hot_vehicle_inspection_config',
|
|
||||||
'hot_vehicle_secondary_maintenance_config',
|
|
||||||
'hot_accident_likelihood_level_config',
|
|
||||||
'hot_accident_severity_level_config',
|
|
||||||
'hot_accident_level_config',
|
|
||||||
'hot_risk_level_config',
|
|
||||||
'hot_risk_score_level',
|
|
||||||
'hot_vehicle_brand_model',
|
|
||||||
'hot_system_template',
|
|
||||||
'hot_file_type_config'
|
|
||||||
)
|
|
||||||
)
|
|
||||||
OR
|
|
||||||
-- 规则2:流程相关表
|
|
||||||
TABLE_NAME IN ('sys_flow_instance', 'sys_flow_task', 'sys_flow_task_his')
|
|
||||||
)
|
|
||||||
ORDER BY TABLE_NAME;
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
-- 使用动态生成清理脚本的sql
|
|
||||||
|
|
||||||
|
|
||||||
delete from hot_system_template where company_id != 1;
|
|
||||||
delete from hot_vehicle_three_inspect_config where company_id != 1;
|
|
||||||
delete from hot_company_dept_config where company_id != 1;
|
|
||||||
delete from hot_driver_annual_assessment_config where company_id != 1;
|
|
||||||
delete from hot_hidden_danger_check_config where company_id != 1;
|
|
||||||
|
|
||||||
|
|
||||||
delete from sys_user where phonenumber not in ('15888888888', '15588888888');
|
|
||||||
delete from sys_user_role where user_id not in(2011415737717006338, 1);
|
|
||||||
delete from sys_user_login_port where user_id not in(2011415737717006338, 1);
|
|
||||||
|
|
||||||
delete
|
|
||||||
from sys_company_role;
|
|
||||||
delete
|
|
||||||
from sys_company;
|
|
||||||
delete
|
|
||||||
from hot_gov_enterprise_unit;
|
|
||||||
delete
|
|
||||||
from hot_personnel_config;
|
|
||||||
@@ -1,83 +0,0 @@
|
|||||||
DELIMITER
|
|
||||||
$$
|
|
||||||
|
|
||||||
DROP PROCEDURE IF EXISTS truncate_hot_tables $$
|
|
||||||
CREATE PROCEDURE truncate_hot_tables()
|
|
||||||
BEGIN
|
|
||||||
DECLARE
|
|
||||||
done INT DEFAULT 0;
|
|
||||||
DECLARE
|
|
||||||
v_table_name VARCHAR(128);
|
|
||||||
|
|
||||||
DECLARE
|
|
||||||
cur CURSOR FOR
|
|
||||||
SELECT TABLE_NAME
|
|
||||||
FROM information_schema.TABLES
|
|
||||||
WHERE TABLE_SCHEMA = DATABASE()
|
|
||||||
AND (
|
|
||||||
(
|
|
||||||
LEFT(TABLE_NAME, 4) = 'hot_'
|
|
||||||
AND RIGHT (TABLE_NAME, 7) <> '_config'
|
|
||||||
AND RIGHT (TABLE_NAME, 9) <> '_template'
|
|
||||||
AND TABLE_NAME NOT IN (
|
|
||||||
'hot_course_resource',
|
|
||||||
'hot_media_resource',
|
|
||||||
'hot_question_bank',
|
|
||||||
'hot_exam_paper',
|
|
||||||
'hot_system_agreement',
|
|
||||||
'hot_law_regulation',
|
|
||||||
'hot_vehicle_type',
|
|
||||||
'hot_vehicle_brand_model',
|
|
||||||
'hot_company_basic_config',
|
|
||||||
'hot_company_dept_config',
|
|
||||||
'hot_driver_annual_assessment_config',
|
|
||||||
'hot_sign_audit_permission_config',
|
|
||||||
'hot_vehicle_three_inspect_config',
|
|
||||||
'hot_hidden_danger_inspect_store',
|
|
||||||
'hot_hidden_danger_check_config',
|
|
||||||
'hot_vehicle_inspection_config',
|
|
||||||
'hot_vehicle_secondary_maintenance_config',
|
|
||||||
'hot_accident_likelihood_level_config',
|
|
||||||
'hot_accident_severity_level_config',
|
|
||||||
'hot_accident_level_config',
|
|
||||||
'hot_risk_level_config',
|
|
||||||
'hot_risk_score_level',
|
|
||||||
'hot_system_template',
|
|
||||||
'hot_file_type_config'
|
|
||||||
)
|
|
||||||
)
|
|
||||||
OR TABLE_NAME IN ('sys_flow_instance', 'sys_flow_task', 'sys_flow_task_his')
|
|
||||||
)
|
|
||||||
ORDER BY TABLE_NAME;
|
|
||||||
|
|
||||||
DECLARE
|
|
||||||
CONTINUE HANDLER FOR NOT FOUND SET done = 1;
|
|
||||||
|
|
||||||
SET
|
|
||||||
FOREIGN_KEY_CHECKS = 0;
|
|
||||||
|
|
||||||
OPEN cur;
|
|
||||||
read_loop
|
|
||||||
: LOOP
|
|
||||||
FETCH cur INTO v_table_name;
|
|
||||||
IF
|
|
||||||
done = 1 THEN
|
|
||||||
LEAVE read_loop;
|
|
||||||
END IF;
|
|
||||||
|
|
||||||
SET
|
|
||||||
@sql = CONCAT('TRUNCATE TABLE `', v_table_name, '`');
|
|
||||||
PREPARE stmt FROM @sql;
|
|
||||||
EXECUTE stmt;
|
|
||||||
DEALLOCATE PREPARE stmt;
|
|
||||||
END LOOP;
|
|
||||||
CLOSE cur;
|
|
||||||
|
|
||||||
SET
|
|
||||||
FOREIGN_KEY_CHECKS = 1;
|
|
||||||
END $$
|
|
||||||
|
|
||||||
DELIMITER ;
|
|
||||||
|
|
||||||
CALL truncate_hot_tables();
|
|
||||||
DROP PROCEDURE truncate_hot_tables;
|
|
||||||
Reference in New Issue
Block a user