From 7f7969868161f4b4754fdd79e4e01a5f82e79688 Mon Sep 17 00:00:00 2001 From: shihongwei <178899525@qq.com> Date: Mon, 1 Jun 2026 10:09:01 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=88=A0=E9=99=A4=E6=97=A0=E7=94=A8?= =?UTF-8?q?=E5=86=85=E5=AE=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/resources/deployment.env | 35 - src/main/resources/deployment.frontend.env | 30 - src/main/resources/deployment.py | 734 ------------------ src/main/resources/deployment_frontend.py | 193 ----- .../resources/企业权限依赖配置.json | 13 - .../resources/动态生成清理脚本.sql | 54 -- .../resources/清理测试数据脚本.sql | 22 - src/main/resources/清理脚本.sql | 83 -- 8 files changed, 1164 deletions(-) delete mode 100644 src/main/resources/deployment.env delete mode 100644 src/main/resources/deployment.frontend.env delete mode 100644 src/main/resources/deployment.py delete mode 100644 src/main/resources/deployment_frontend.py delete mode 100644 src/main/resources/企业权限依赖配置.json delete mode 100644 src/main/resources/动态生成清理脚本.sql delete mode 100644 src/main/resources/清理测试数据脚本.sql delete mode 100644 src/main/resources/清理脚本.sql diff --git a/src/main/resources/deployment.env b/src/main/resources/deployment.env deleted file mode 100644 index 6efb8b9..0000000 --- a/src/main/resources/deployment.env +++ /dev/null @@ -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 diff --git a/src/main/resources/deployment.frontend.env b/src/main/resources/deployment.frontend.env deleted file mode 100644 index 3787493..0000000 --- a/src/main/resources/deployment.frontend.env +++ /dev/null @@ -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 diff --git a/src/main/resources/deployment.py b/src/main/resources/deployment.py deleted file mode 100644 index 7ec3b3f..0000000 --- a/src/main/resources/deployment.py +++ /dev/null @@ -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[\\d\\.]+)', out) or re.search(r'openjdk version \"(?P[\\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() diff --git a/src/main/resources/deployment_frontend.py b/src/main/resources/deployment_frontend.py deleted file mode 100644 index c8d985b..0000000 --- a/src/main/resources/deployment_frontend.py +++ /dev/null @@ -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() - diff --git a/src/main/resources/企业权限依赖配置.json b/src/main/resources/企业权限依赖配置.json deleted file mode 100644 index fe463ba..0000000 --- a/src/main/resources/企业权限依赖配置.json +++ /dev/null @@ -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"] - } -} - diff --git a/src/main/resources/动态生成清理脚本.sql b/src/main/resources/动态生成清理脚本.sql deleted file mode 100644 index 957816e..0000000 --- a/src/main/resources/动态生成清理脚本.sql +++ /dev/null @@ -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; diff --git a/src/main/resources/清理测试数据脚本.sql b/src/main/resources/清理测试数据脚本.sql deleted file mode 100644 index 404f5c7..0000000 --- a/src/main/resources/清理测试数据脚本.sql +++ /dev/null @@ -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; diff --git a/src/main/resources/清理脚本.sql b/src/main/resources/清理脚本.sql deleted file mode 100644 index b0b9aad..0000000 --- a/src/main/resources/清理脚本.sql +++ /dev/null @@ -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; \ No newline at end of file