Merge remote-tracking branch 'origin/main'

This commit is contained in:
2026-06-01 14:38:37 +08:00
15 changed files with 346 additions and 1194 deletions

View File

@@ -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.bo.HotVehicleThreeInspectVehicleQueryBo;
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.HotVehicleThreeInspectVo;
import com.hotwj.platform.config.vehicleThreeInspect.service.IHotVehicleThreeInspectPrintService;
@@ -213,4 +214,13 @@ public class HotVehicleThreeInspectController extends BaseController {
@PathVariable Long[] ids) {
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);
}
}

View File

@@ -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;
}

View File

@@ -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.HotVehicleThreeInspectPrintVo;
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.HotVehicleThreeInspectVo;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
@@ -44,6 +45,26 @@ public interface HotVehicleThreeInspectMapper extends BaseMapperPlus<HotVehicleT
@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);
/**
* 查询所有三检未收车的车辆列表
*

View File

@@ -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.vo.HotVehicleThreeInspectProcessVo;
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.HotVehicleThreeInspectVo;
import com.hotwj.platform.resourceManagement.vehicleManagement.domain.vo.HotVehicleVo;
@@ -72,6 +73,15 @@ public interface IHotVehicleThreeInspectService {
*/
TableDataInfo<HotVehicleThreeInspectProcessVo> queryProcessPageList(HotVehicleThreeInspectBo bo, PageQuery pageQuery);
/**
* 分页查询车辆三检聚合列表
*
* @param bo 查询条件
* @param pageQuery 分页参数
* @return 车辆三检聚合分页列表
*/
TableDataInfo<HotVehicleThreeInspectSummaryVo> querySummaryPageList(HotVehicleThreeInspectBo bo, PageQuery pageQuery);
/**
* 查询符合条件的车辆三检列表
*

View File

@@ -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.vo.HotVehicleThreeInspectProcessVo;
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.HotVehicleThreeInspectVo;
import com.hotwj.platform.config.vehicleThreeInspectConfig.domain.HotVehicleThreeInspectConfig;
@@ -197,6 +198,47 @@ public class HotVehicleThreeInspectServiceImpl implements IHotVehicleThreeInspec
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);
}
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) {
Map<String, Object> params = bo.getParams();
LambdaQueryWrapper<HotVehicleThreeInspect> lqw = Wrappers.lambdaQuery();

View File

@@ -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

View File

@@ -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

View File

@@ -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步骤名称statusok/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()

View File

@@ -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()

View File

@@ -106,6 +106,96 @@
ORDER BY IFNULL(MAX(t.inspect_time), MAX(t.create_time)) DESC, MAX(t.create_time) DESC
</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"
resultType="com.hotwj.platform.config.vehicleThreeInspect.domain.vo.HotVehicleThreeInspectPrintVo">
WITH base AS (
@@ -175,6 +265,46 @@
ORDER BY MAX(create_time) DESC
</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"
resultType="com.hotwj.platform.config.vehicleThreeInspect.domain.vo.HotVehicleThreeInspectUnreceivedVehicleVo">
SELECT

View File

@@ -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 验真与外网地址

View File

@@ -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"]
}
}

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;