提交 87b429a0 authored 作者: 陈泽健's avatar 陈泽健

feat(deploy): 新增 PowerShell脚本实现全栈更新- 包含前端和后端服务的更新流程

 -增加错误处理和日志记录
 - 优化文件传输和解压逻辑
 - 改进命令执行和备份流程
上级 848437de
......@@ -11,6 +11,8 @@
- pip install pyyaml
- pip install colorama
- pip install python-docx
- pip install tqdm
- pip install paramiko tqdm
- 若切换电脑环境后需要输入指令: 将本地库导入到外部库中。
- # 找到 site-packages 路径
......
Welcome to UOS Server 20
Upgradable packages: 1
Upgrade command line: yum upgrade
Activate the web console with: systemctl enable --now cockpit.socket
Last login: Thu Jun 5 16:14:44 2025 from 192.168.9.51
tail -f /var/www/java/api-java-meeting2.0/logs/ubains-INFO-AND-ERROR.log
Welcome to 4.19.90-2403.3.0.0270.87.uel20.x86_64
System information as of time: 2025年 06月 05日 星期四 16:47:27 CST
System load: 0.75
Processes: 407
Memory used: 68.0%
Swap used: 23.2%
Usage On: 60%
IP address: 192.168.5.218
IP address: 172.17.0.1
Users online: 1
[?2004h[root@localhost ~]# tail -f /var/www/java/api-java-meeting2.0/logs/ubains-INFO-AND-ERROR.log
[?2004l 2025-06-05 16:47:02.432 INFO : [ThirdSyncMeetCallable-第三方同步会议线程类][第三方会议同步][IP:][n03q9qwtsddfiwqqgfx7cipzt0lo2mhp][SYNC][会议室编号对比-相同才进行后面的判断-原编号n03q9qwtsddfiwqqgfx7cipzt0lo2mhp-为true]:true
2025-06-05 16:47:02.432 INFO : [ThirdSyncMeetCallable-第三方同步会议线程类][第三方会议同步][IP:][Thu Jun 05 16:46:02 CST 2025][SYNC][修改触发-更新会议的更新时间不同]:"2025-06-05 16:46:02"
2025-06-05 16:47:02.432 INFO
: [ThirdSyncMeetCallable-第三方同步会议线程类][第三方会议同步][IP:][2025-1748915224214][SYNC][被修改的会议名称为]:"结束正在召开的周期会议test1"
2025-06-05 16:47:02.432 INFO : [ThirdSyncServiceImpl第三方页面同步服务实现类][第三方会议同步][IP:][][SYNC][数据库操作-开始]:""
2025-06-05 16:47:02.433 INFO : [ThirdSyncServiceImpl第三方页面同步服务实现类][第三方会议同步][IP:][][SYNC][新增会议-失败]:""
2025-06-05 16:47:03.222 INFO : [ThirdSyncServiceImpl第三方页面同步服务实现类][第三方会议同步][IP:][][SYNC][修改会议-成功]:"会议数量1002"
2025-06-05 16:47:03.222 INFO : 插入会议操作完毕-------------------------------->耗时:790
2025-06-05 16:47:03.286 INFO : [ThirdSyncServiceImpl第三方页面同步服务实现类][第三方会议同步][IP:][][SYNC][结束]:"原有第三方会议数量为+1002新增的会议数量为-0修改的会议数量为-1002"
2025-06-05 16:47:03.287 INFO : [ZsjServi
\ No newline at end of file
# 预定系统内部服务打包上传服务器(执行此方法会自动执行upload_saveto_pan.ps1)
# 内部程序,执行编译与更新,调用build_local.sh
$Port = 22
$RemoteUser = "root"
# 变量赋值,切换服务器时需要调整
# 5.186
# $RemoteIP = "139.159.163.86"
# $RemotePath_inner = "/var/www/java/api-java-meeting2.0"
# $RemotePath_external = "/var/www/java/external-meeting-api"
#
# $RunCommand_inner = "docker exec ujava2 /var/www/java/api-java-meeting2.0/run.sh;"
# $RunCommand_external = "docker exec ujava2 /var/www/java/external-meeting-api/run.sh;"
# 5.218
# $RemoteIP = "139.159.163.86"
# $RemotePath_inner = "/var/www/java/api-java-meeting2.0"
# $RemotePath_external = "/var/www/java/external-meeting-api"
#
# $RunCommand_inner = "docker exec ujava2 /var/www/java/api-java-meeting2.0/run.sh;"
# $RunCommand_external = "docker exec ujava2 /var/www/java/external-meeting-api/run.sh;"
# 兰州中石化项目
$RemoteIP = "139.159.163.86"
$RemotePath_inner = "/var/www/java/api-java-meeting2.0"
$RemotePath_external = "/var/www/java/external-meeting-api"
$RunCommand_inner = "docker exec ujava2 /var/www/java/api-java-meeting2.0/run.sh;"
$RunCommand_external = "docker exec ujava2 /var/www/java/external-meeting-api/run.sh;"
# scp 上传本地脚本到远程服务器执行的目录:上传至预定内部服务路径
scp -P $Port "\\192.168.9.9\deploy\01会议预定\标准版本-长期运维\01版本管理\01后端运行服务\内部预定\COM_虹软4.0_V2.1.2526.586_2025_06_25_psl自测\ubains-meeting-inner-api-1.0-SNAPSHOT.jar" "${RemoteUser}@${RemoteIP}:${RemotePath_inner}"
# scp 上传本地脚本到远程服务器执行的目录:上传至预定外部服务路径
scp -P $Port "\\192.168.9.9\deploy\01会议预定\标准版本-长期运维\01版本管理\01后端运行服务\对外预定\COMVhx2.0.2526.234_2025_06_25_dhh自测\ubains-meeting-api-1.0-SNAPSHOT.jar" "${RemoteUser}@${RemoteIP}:${RemotePath_external}"
# ssh 在远程服务器执行脚本
# 更新预定对内服务
ssh -p $Port "${RemoteUser}@${RemoteIP}" "${RunCommand_inner}"
# 更新预定对外服务
ssh -p $Port "${RemoteUser}@${RemoteIP}" "${RunCommand_external}"
# 调用 upload_saveto_pan.ps1 脚本
# Write-Host "调用 upload_saveto_pan.ps1 脚本"
# $uploadScriptPath = ".\upload_saveto_pan.ps1"
# & $uploadScriptPath
\ No newline at end of file
import cv2
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
预定系统全栈更新脚本 - 完整稳定版
功能亮点:
1. 稳定可靠的文件传输机制
2. 完善的错误处理和日志记录
3. 固定名称的压缩包管理
4. 严格的目录和文件验证
"""
import paramiko
import os
from datetime import datetime
from tqdm import tqdm
import sys
import re
import tarfile
import tempfile
import shutil
import stat
class Config:
"""系统配置类,包含所有更新配置参数"""
# ===== 连接配置 =====
SSH_PORT = 22 # SSH端口
SSH_USER = "root" # SSH用户名
SSH_PASSWORD = os.getenv('DEPLOY_SSH_PASSWORD') or "hzpassw0RD@KP" # 从环境变量获取密码
# ===== 服务器配置 =====
SERVER_HOST = "139.159.163.86" # 服务器IP
# ===== 后端服务配置 =====
BACKEND_PATHS = {
'inner': { # 内部服务
'remote': "/var/www/java/api-java-meeting2.0/",
'local': r"\\192.168.9.9\deploy\01会议预定\标准版本-长期运维\01版本管理\01后端运行服务\内部预定\COM_虹软4.0_V2.1.2526.586_2025_06_25_psl自测\ubains-meeting-inner-api-1.0-SNAPSHOT.jar",
'command': "docker exec ujava2 /var/www/java/api-java-meeting2.0/run.sh"
},
'external': { # 对外服务
'remote': "/var/www/java/external-meeting-api/",
'local': r"\\192.168.9.9\deploy\01会议预定\标准版本-长期运维\01版本管理\01后端运行服务\对外预定\COMVhx2.0.2526.234_2025_06_25_dhh自测\ubains-meeting-api-1.0-SNAPSHOT.jar",
'command': "docker exec ujava2 /var/www/java/external-meeting-api/run.sh"
}
}
# ===== 前端服务配置 =====
FRONTEND_CONFIG = {
'remote_dir': "/var/www/java/ubains-web-2.0/",
'local_dir': r"\\192.168.9.9\deploy\01会议预定\标准版本-长期运维\01版本管理\02前端PC网页\标准版本-长期运维\2025年度\2.0.2525.1143 2025-06-23-LPH-自测",
'backup_script': "/var/www/java/ubains-web-2.0/bakup.sh",
'files_to_update': [
'*.worker.js',
'index.html',
'static/'
]
}
# ===== 日志过滤规则 =====
IGNORABLE_LOG_PATTERNS = [
re.compile(r'unexpected operator'),
re.compile(r'Usage: kill'),
re.compile(r'kill -l'),
re.compile(r'Java HotSpot\(TM\)'),
re.compile(r'SLF4J:'),
re.compile(r'Listening for transport'),
re.compile(r'Starting Application'),
re.compile(r'tail:.*file truncated'),
re.compile(r'log4j:WARN')
]
class Deployer:
"""更新器主类,负责整个更新流程"""
def __init__(self):
"""初始化更新器"""
self.ssh = paramiko.SSHClient()
self.ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
self.logger = self._setup_logger()
self.sftp = None
self.remote_archive_name = "frontend_update.tar.gz"
def _setup_logger(self):
"""配置日志系统"""
def log(msg, level="INFO", important=False):
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
log_msg = f"[{timestamp}] [{level}] {msg}"
if level in ("WARNING", "ERROR") or important:
print(f"\033[1m{log_msg}\033[0m") # 加粗显示
if level == "ERROR":
sys.stderr.write(log_msg + "\n")
else:
print(log_msg)
return log
def connect(self):
"""建立SSH连接"""
max_retries = 3
for attempt in range(1, max_retries + 1):
try:
self.logger(f"尝试连接服务器 ({attempt}/{max_retries})...", important=True)
self.ssh.connect(
hostname=Config.SERVER_HOST,
port=Config.SSH_PORT,
username=Config.SSH_USER,
password=Config.SSH_PASSWORD,
timeout=30,
banner_timeout=200
)
self.sftp = self.ssh.open_sftp()
self._ensure_remote_dir_exists(Config.FRONTEND_CONFIG['remote_dir'])
self.logger("服务器连接成功", important=True)
return True
except Exception as e:
self.logger(f"连接尝试 {attempt} 失败: {str(e)}", "WARNING")
if attempt == max_retries:
self.logger("连接服务器最终失败", "ERROR")
return False
def _ensure_remote_dir_exists(self, remote_dir):
"""确保远程目录存在"""
try:
self.sftp.stat(remote_dir)
except FileNotFoundError:
self.logger(f"创建远程目录: {remote_dir}", "INFO")
self.sftp.mkdir(remote_dir)
except Exception as e:
raise Exception(f"无法访问远程目录: {str(e)}")
def upload_file(self, local_path, remote_path):
"""安全上传文件"""
try:
# 验证本地文件
if not os.path.exists(local_path):
raise FileNotFoundError(f"本地文件不存在: {local_path}")
file_size = os.path.getsize(local_path)
self.logger(f"准备上传: {os.path.basename(local_path)} ({file_size / 1024 / 1024:.2f}MB)", important=True)
# 带进度条上传
with tqdm(total=file_size, unit='B', unit_scale=True, desc="上传进度") as pbar:
def callback(transferred, total):
pbar.update(transferred - pbar.n)
self.sftp.put(local_path, remote_path, callback=callback)
# 验证上传结果
remote_size = self.sftp.stat(remote_path).st_size
if remote_size != file_size:
raise Exception(f"文件大小不匹配 (本地: {file_size}, 远程: {remote_size})")
self.logger("文件上传验证成功", important=True)
return True
except Exception as e:
self.logger(f"文件上传失败: {str(e)}", "ERROR")
# 清理可能上传失败的部分文件
try:
self.sftp.remove(remote_path)
except:
pass
return False
def run_backup_script(self):
"""执行备份脚本"""
backup_cmd = Config.FRONTEND_CONFIG['backup_script']
self.logger(f"执行备份脚本: {backup_cmd}", important=True)
try:
stdin, stdout, stderr = self.ssh.exec_command(backup_cmd)
exit_status = stdout.channel.recv_exit_status()
def image_matching(floder_name,picture_name):
# 使用绝对路径
path1 = os.path.abspath(r"C:\information_meeting_on.png")
path2 = os.path.abspath(r"C:\information_brithday_on.png")
if exit_status != 0:
error = stderr.read().decode().strip()
raise Exception(f"备份失败: {error}")
if not os.path.exists(path1):
print(f"文件不存在: {path1}")
if not os.path.exists(path2):
print(f"文件不存在: {path2}")
output = stdout.read().decode().strip()
if output:
self.logger(f"备份输出: {output}", "INFO")
# 加载图片
image1 = cv2.imread(path1, cv2.IMREAD_GRAYSCALE)
image2 = cv2.imread(path2, cv2.IMREAD_GRAYSCALE)
return True
except Exception as e:
self.logger(str(e), "ERROR")
return False
# 检查图片是否加载成功
if image1 is None:
print("图片1加载失败,请检查路径或文件是否存在")
if image2 is None:
print("图片2加载失败,请检查路径或文件是否存在")
def run_command(self, command):
"""执行远程命令"""
try:
stdin, stdout, stderr = self.ssh.exec_command(command)
exit_status = stdout.channel.recv_exit_status()
# 如果图片加载成功,继续执行特征匹配
if image1 is not None and image2 is not None:
# 初始化ORB检测器
orb = cv2.ORB_create()
error_output = stderr.read().decode().strip()
if error_output and exit_status != 0:
for line in error_output.split('\n'):
if line and not self._should_ignore_log(line):
self.logger(f"命令错误: {line}", "ERROR")
return False
# 检测关键点和描述符
keypoints1, descriptors1 = orb.detectAndCompute(image1, None)
keypoints2, descriptors2 = orb.detectAndCompute(image2, None)
output = stdout.read().decode().strip()
if output:
self.logger(f"命令输出: {output}", "INFO")
# 使用BFMatcher进行匹配
bf = cv2.BFMatcher(cv2.NORM_HAMMING, crossCheck=True)
matches = bf.match(descriptors1, descriptors2)
return True
except Exception as e:
self.logger(f"命令执行异常: {str(e)}", "ERROR")
return False
# 根据匹配点数量判断相似性
matches_count = len(matches)
print(f"匹配点数量: {matches_count}")
def _should_ignore_log(self, log_line):
"""日志过滤器"""
if not log_line.strip():
return True
line_lower = log_line.lower()
return any(pattern.search(line_lower) for pattern in Config.IGNORABLE_LOG_PATTERNS)
def prepare_frontend_archive(self, local_dir):
"""准备前端压缩包"""
tmp_path = os.path.join(tempfile.gettempdir(), self.remote_archive_name)
try:
# 清理旧压缩包
if os.path.exists(tmp_path):
os.unlink(tmp_path)
# 创建新压缩包
with tarfile.open(tmp_path, "w:gz") as tar:
for item in Config.FRONTEND_CONFIG['files_to_update']:
src_path = os.path.join(local_dir, item)
if item.endswith('/'):
dir_name = item[:-1]
if os.path.exists(src_path):
tar.add(src_path, arcname=dir_name)
else:
raise FileNotFoundError(f"目录不存在: {src_path}")
elif item == '*.worker.js':
worker_files = [f for f in os.listdir(local_dir) if f.endswith('.worker.js')]
if not worker_files:
self.logger("未找到任何.worker.js文件", "WARNING")
for f in worker_files:
tar.add(os.path.join(local_dir, f), arcname=f)
else:
if os.path.exists(src_path):
tar.add(src_path, arcname=item)
else:
raise FileNotFoundError(f"文件不存在: {src_path}")
self.logger(f"成功创建压缩包: {tmp_path}", "INFO")
return tmp_path, os.path.getsize(tmp_path)
except Exception as e:
self.logger(f"创建压缩包失败: {str(e)}", "ERROR")
if os.path.exists(tmp_path):
os.unlink(tmp_path)
raise
def deploy_frontend(self):
"""更新前端"""
self.logger("\n===== 开始前端更新 =====", important=True)
# 执行备份
if not self.run_backup_script():
self.logger("备份失败但仍继续更新", "WARNING")
tmp_path = None
try:
# 准备压缩包
tmp_path, file_size = self.prepare_frontend_archive(Config.FRONTEND_CONFIG['local_dir'])
remote_path = os.path.join(Config.FRONTEND_CONFIG['remote_dir'], self.remote_archive_name)
# 上传文件
if not self.upload_file(tmp_path, remote_path):
return False
# 解压文件
cmd = f"""
cd {Config.FRONTEND_CONFIG['remote_dir']} && \
if [ -f {self.remote_archive_name} ]; then \
tar -xzf {self.remote_archive_name} && \
rm {self.remote_archive_name} && \
echo "解压成功"; \
else \
echo "错误:找不到压缩包" >&2; \
exit 1; \
fi
"""
if not self.run_command(cmd):
return False
self.logger("前端更新完成", important=True)
return True
except Exception as e:
self.logger(f"前端更新失败: {str(e)}", "ERROR")
return False
finally:
if tmp_path and os.path.exists(tmp_path):
os.unlink(tmp_path)
self.logger(f"已清理临时文件: {tmp_path}", "INFO")
def deploy_backend(self, service_type):
"""更新后端服务"""
config = Config.BACKEND_PATHS[service_type] # 修复拼写错误
self.logger(f"\n===== 开始更新 {service_type} 后端服务 =====", important=True)
try:
# 上传JAR文件
remote_jar_path = os.path.join(config['remote'], os.path.basename(config['local']))
if not self.upload_file(config['local'], remote_jar_path):
return False
# 重启服务
if not self.run_command(config['command']):
return False
self.logger(f"{service_type}后端更新成功", important=True)
return True
except Exception as e:
self.logger(f"{service_type}后端更新失败: {str(e)}", "ERROR")
return False
def _cleanup(self):
"""清理资源"""
if hasattr(self, 'sftp') and self.sftp:
self.sftp.close()
if hasattr(self, 'ssh') and self.ssh:
self.ssh.close()
self.logger("连接资源已释放", "INFO")
def deploy(self):
"""主更新流程"""
try:
if not self.connect():
return False
# 更新前端
if not self.deploy_frontend():
return False
# 更新后端服务
if not all(self.deploy_backend(svc) for svc in ['inner', 'external']):
return False
self.logger("\n===== 所有服务更新成功 =====", important=True)
return True
except Exception as e:
self.logger(f"更新过程异常: {str(e)}", "ERROR")
return False
finally:
self._cleanup()
if matches_count > 400: # 阈值可以根据需求调整
print("图片相似")
else:
print("图片不相似")
if __name__ == "__main__":
image_matching()
\ No newline at end of file
print("\n=== 预定系统服务更新工具 ===")
if Config.SSH_PASSWORD == "hzpassw0RD@KP":
print("\033[1;31m! 安全警告: 您正在使用默认密码,建议通过环境变量配置!\033[0m")
deployer = Deployer()
if deployer.deploy():
print("\n\033[1;32m全栈更新成功!\033[0m")
sys.exit(0)
else:
print("\n\033[1;31m更新失败!\033[0m")
sys.exit(1)
\ No newline at end of file
# 预定系统内部服务将包上传到企业网盘
# 忽略 SSL 证书验证(如果仍然需要)
add-type @"
using System.Net;
using System.Security.Cryptography.X509Certificates;
public class TrustAllCertsPolicy : ICertificatePolicy {
public bool CheckValidationResult(
ServicePoint srvPoint, X509Certificate certificate,
WebRequest request, int certificateProblem) {
return true;
}
}
"@
[System.Net.ServicePointManager]::CertificatePolicy = New-Object TrustAllCertsPolicy
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
# 源文件路径
$SourceDirectory = ".\..\ubains-meeting-inner-api\target\ubains-meeting-inner-api-1.0-SNAPSHOT.jar"
$ReadmeSourcePath = ".\..\ubains-meeting-inner-api\README.md"
# 目标文件名
$PackageName = "ubains-meeting-inner-api-1.0-SNAPSHOT.jar"
$PackageNameReadme = "README.md"
# 读取 packageVersion.txt 文件的内容并设置自定义文件夹名称
$testFilePath = ".\..\ubains-meeting-common\src\main\resources\packageVersion.txt"
if (Test-Path -Path $testFilePath) {
$CustomFolder = Get-Content -Path $testFilePath -Raw -Encoding UTF8
Write-Host "自定义文件夹名称: $CustomFolder"
} else {
Write-Host "文件 $testFilePath 未找到"
exit 1
}
# 清理路径中的非法字符,保留字母、数字、中文字符和点号
$CustomFolder = [System.Text.RegularExpressions.Regex]::Replace($CustomFolder, '[^a-zA-Z0-9\u4e00-\u9fa5.]', '_')
# 公司网盘的本地映射路径
# $WebDavBasePath = "\\192.168.9.9\研发管理\01会议预定\标准版本-长期运维\01版本管理\01后端运行服务\内部预定"
# TODO 3.1.1.0版本使用 开始配置
# $WebDavBasePath = "\\192.168.9.9\deploy\01会议预定\标准版本-长期运维\01版本管理\01后端运行服务\内部预定\COM_虹软3.0"
# TODO 3.1.1.0版本使用 结束配置
# TODO 4.1.1.0版本使用 开始配置
$WebDavBasePath = "\\192.168.9.9\deploy\01会议预定\标准版本-长期运维\01版本管理\01后端运行服务\内部预定"
# TODO 4.1.1.0版本使用 结束配置
# 构建完整的上传路径
$WebDavUrl = Join-Path -Path $WebDavBasePath -ChildPath $CustomFolder
$WebDavFilePath = Join-Path -Path $WebDavUrl -ChildPath $PackageName
$WebDavReadmePath = Join-Path -Path $WebDavBasePath -ChildPath "README.md"
# 临时路径(这里不需要临时路径,直接使用源文件路径)
$TempPath = $SourceDirectory
# 确保自定义文件夹存在
if (-Not (Test-Path -Path $WebDavUrl)) {
New-Item -Path $WebDavUrl -ItemType Directory -Force | Out-Null
Write-Host "创建网盘文件夹: $WebDavUrl"
}
# 将文件复制到公司网盘的本地映射路径
Write-Host "开始上传jar包..."
Copy-Item -Path $TempPath -Destination $WebDavFilePath -Force
Write-Host "jar包上传完成"
# 上传README.md文件
Write-Host "开始上传README.md..."
Copy-Item -Path $ReadmeSourcePath -Destination $WebDavReadmePath -Force
Write-Host "README.md上传完成"
# 打印上传路径
Write-Host "jar文件已上传到: $WebDavFilePath"
Write-Host "README.md已上传到: $WebDavReadmePath"
\ No newline at end of file
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论