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

docs(prd): 移除远程更新程序相关需求和问题处理文档,梳理输出远程更新程序需求文档,处理生成自动化测试用例的死循环问题,处理行数读取问题

- 移除主脚本执行报错参数转换问题处理文档
- 移除参数转换问题处理执行计划文档
- 移除流程调整需求变更先项目后服务器文档
- 移除流程调整需求变更执行计划文档
上级 955f1583
{
"servers": {
"1": {
"name": "中广核大亚湾-前端测试环境",
"ip": "10.126.4.79",
"port": 1122,
"ssh_username": "root",
"ssh_password": "Admin@123Admin@123"
},
"2": {
"name": "中广核大亚湾-后端测试环境",
"ip": "10.126.4.81",
"port": 1122,
"ssh_username": "appadmin",
"ssh_password": "CGNadm!@345CGNadm!@345",
"su_username": "root",
"su_password": "Admin@123Admin@123"
}
}
}
{
"前端": {
"AI前端包": "/data/services/web/pc/pc-vue2-ai",
"后台前端包": "/data/services/web/pc/pc-vue2-backstage",
"主应用前端包": "/data/services/web/pc/pc-vue2-main",
"会控前端包": "/data/services/web/pc/pc-vue2-meetingControl",
"2前端包": "/data/services/web/pc/pc-vue2-meetngV2",
"3前端包": "/data/services/web/pc/pc-vue2-meetngV3",
"运维集控前端包": "/data/services/web/pc/pc-vue2-moniter",
"门户前端包": "/data/services/web/pc/pc-vue2-platform",
"讯飞前端包": "/data/services/web/pc/pc-vue2-voice/pc-vue2-voice"
},
"后端": {
"auth门户包": "/data/services/api/auth/auth-sso-auth",
"gatway门户包": "/data/services/api/auth/auth-sso-gatway",
"system门户包": "/data/services/api/auth/auth-sso-system",
"2服务包": "/data/services/api/java-meeting/java-meeting2.0",
"3服务包": "/data/services/api/java-meeting/java-meeting3.0",
"对外服务包": "/data/services/api/java-meeting/java-meeting-extapi",
"信息调度服务包": "/data/services/api/java-meeting/java-message-scheduling",
"定时任务服务包": "/data/services/api/java-meeting/java-quartz"
}
}
\ No newline at end of file
{
"前端": "./front",
"后端": "./back"
}
\ No newline at end of file
{
"传统平台": {
"预定系统": {
"前端更新": {
"services_path": "/var/www/java/ubains-web-2.0/",
"services_file": [
"*.js",
"index.html",
"static"
]
},
"后端更新": {
"inner_services_path": "/var/www/java/api-java-meeting2.0/",
"inner_services_file": "ubains-meeting-inner-api-1.0-SNAPSHOT.jar",
"extapi_services_path": "/var/www/java/external-meeting-api/",
"extapi_services_file": "ubains-meeting-api-1.0-SNAPSHOT.jar"
}
},
"运维系统": {
"前端更新": {
"services_path": "/var/www/html/web-vue-rms",
"services_file": [
"module",
"index.html",
"static"
]
},
"后端更新": {
"services_path": "/var/www/html/",
"services_file": [
"UbainsDevOps",
"cmdb"
]
}
}
},
"新统一平台": {
"预定系统": {
"前端更新": {
"ai_services_path": "/data/services/web/pc/pc-vue2-ai",
"ai_services_file": ["index.html","static"],
"back_services_path": "/data/services/web/pc/pc-vue2-backstage",
"back_services_file": ["index.html","static"],
"main_services_path": "/data/services/web/pc/pc-vue2-main",
"main_services_file": ["index.html","static","*.worker.js"],
"meetingControl_services_path": "/data/services/web/pc/pc-vue2-meetingControl",
"meetingControl_services_file": ["index.html","static"],
"meetingV2_services_path": "/data/services/web/pc/pc-vue2-meetngV2",
"meetingV2_services_file": ["index.html","static","*.worker.js"],
"meetingV3_services_path": "/data/services/web/pc/pc-vue2-meetngV3",
"meetingV3_services_file": ["index.html","static","*.worker.js"],
"monitor_services_path": "/data/services/web/pc/pc-vue2-moniter",
"monitor_services_file": ["index.html","static","module"],
"platform_services_path": "/data/services/web/pc/pc-vue2-platform",
"platform_services_file": ["index.html","static","temp"],
"voice_services_path": "/data/services/web/pc/pc-vue2-voice/pc-vue2-voice",
"voice_services_file": ["index.html","static"]
},
"后端更新": {
"auth_services_path": "/data/services/api/auth/auth-sso-auth",
"auth_services_file": "ubains-auth.jar",
"gatway_serices_path": "/data/services/api/auth/auth-sso-gatway",
"gatway_serices_file": "ubains-gateway.jar",
"system_services_path": "/data/services/api/auth/auth-sso-system",
"system_services_file": "ubains-modules-system.jar",
"java_meetingV2_services_path": "/data/services/api/java-meeting/java-meeting2.0",
"java_meetingV2_services_file": "ubains-meeting-inner-api-1.0-SNAPSHOT.jar",
"java_meetingV3_services_path": "/data/services/api/java-meeting/java-meeting3.0",
"java_meetingV3_services_file": "ubains-meeting-inner-api-1.0-SNAPSHOT.jar",
"java_meetingExtapi_services_path": "/data/services/api/java-meeting/java-meeting-extapi",
"java_meetingExtapi_services_file": "ubains-meeting-api-1.0-SNAPSHOT.jar",
"java_messageScheduling_services_path": "/data/services/api/java-meeting/java-message-scheduling",
"java_messageScheduling_services_file": "ubains-meeting-message-scheduling-1.0-SNAPSHOT.jar",
"java_quartz_services_path": "/data/services/api/java-meeting/java-quartz",
"java_quartz_services_file": "ubains-meeting-quartz-1.0-SNAPSHOT.jar"
}
}
}
}
\ No newline at end of file
# ==============================================================================
# database.ps1
# ------------------------------------------------------------------------------
# 数据库模块(预留功能)
#
# .SYNOPSIS
# 提供数据库备份和恢复功能(预留)
#
# .DESCRIPTION
# 本模块提供数据库的备份和恢复功能
# 由于不同项目使用的数据库类型可能不同(MySQL/达梦等),
# 部署方式也存在差异(容器化/非容器化),此功能暂时不执行,
# 仅预留函数接口供后续扩展。
#
# .NOTES
# 版本:1.0.0
# 创建日期:2026-02-10
# 状态:预留功能,暂不执行
#
# 依赖:
# - ssh.ps1 (SSH 连接模块)
# - log.ps1 (日志模块)
# ==============================================================================
# 获取脚本目录
if ($PSScriptRoot) {
$LIB_SCRIPT_DIR = $PSScriptRoot
} else {
$LIB_SCRIPT_DIR = Split-Path -Parent $MyInvocation.MyCommand.Path
}
# 导入依赖模块
Import-Module (Join-Path $LIB_SCRIPT_DIR "ssh.ps1") -Force
Import-Module (Join-Path $LIB_SCRIPT_DIR "log.ps1") -Force
# ==============================================================================
# 辅助函数:安全调用 Invoke-SSHCommand
# ==============================================================================
function Invoke-SSHCommandSafe {
param(
[Parameter(Mandatory=$true)]
[hashtable]$ServerInfo,
[Parameter(Mandatory=$true)]
[string]$Command
)
$params = @{
HostName = $ServerInfo.IP
User = $ServerInfo.SshUsername
Pass = $ServerInfo.SshPassword
Port = $ServerInfo.Port
Command = $Command
}
if ($ServerInfo.SuUsername) {
$params.SuUser = $ServerInfo.SuUsername
}
if ($ServerInfo.SuPassword) {
$params.SuPass = $ServerInfo.SuPassword
}
return Invoke-SSHCommand @params
}
# ==============================================================================
# 数据库备份(预留功能)
# ==============================================================================
function Backup-Database {
<#
.SYNOPSIS
数据库备份函数(预留)
.DESCRIPTION
根据数据库类型执行不同的备份逻辑:
- MySQL: 容器化部署,通过 docker exec 执行 mysqldump
- 达梦: 非容器化部署,通过 SSH 执行 dexp 命令
.PARAMETER ServerInfo
服务器连接信息哈希表
.PARAMETER DatabaseConfig
数据库配置信息
.PARAMETER DatabaseType
数据库类型(MySQL/DM)
.EXAMPLE
$dbConfig = @{
Type = "MySQL"
IsContainerized = $true
ContainerPattern = "umysql"
Username = "root"
Password = "password"
Databases = @("ubains", "devops")
}
Backup-Database -ServerInfo $server -DatabaseConfig $dbConfig -DatabaseType "MySQL"
.OUTPUTS
Boolean,备份成功返回 $true,否则返回 $false
#>
param(
[Parameter(Mandatory=$true)]
[hashtable]$ServerInfo,
[Parameter(Mandatory=$true)]
[hashtable]$DatabaseConfig,
[Parameter(Mandatory=$true)]
[string]$DatabaseType
)
# 预留功能,暂不执行
Write-Log -Level "WARN" -Message "数据库备份功能为预留功能,暂不执行"
Write-Log -Level "INFO" -Message "数据库类型: $DatabaseType"
<#
TODO: 根据数据库类型执行不同的备份逻辑
# MySQL 容器化部署示例
if ($DatabaseType -eq "MySQL" -and $DatabaseConfig.IsContainerized) {
# 模糊匹配容器
$containerPattern = $DatabaseConfig.ContainerPattern
$findCmd = "docker ps --format '{{.Names}}' | grep -E '$containerPattern'"
$result = Invoke-SSHCommandSafe -ServerInfo $ServerInfo -Command $findCmd
$containerName = ($result.Output | Where-Object { $_ -ne "" } | Select-Object -First 1).Trim()
foreach ($dbName in $DatabaseConfig.Databases) {
$backupFile = "$BackendPath/backup/${dbName}_backup_$(Get-Date -Format 'yyyyMMddHHmmss').sql"
$backupCmd = "docker exec $containerName mysqldump -u$($DatabaseConfig.Username) -p$($DatabaseConfig.Password) --skip-triggers --compact --no-create-info $dbName > $backupFile"
Invoke-SSHCommandSafe -ServerInfo $ServerInfo -Command $backupCmd
}
}
# 达梦数据库示例
if ($DatabaseType -eq "DM") {
$dexpCmd = "dexp USERID=$($DatabaseConfig.Username)/$($DatabaseConfig.Password) FILE=$backupFile LOG=$logFile FULL=N OWNER=$($DatabaseConfig.Username)"
Invoke-SSHCommandSafe -ServerInfo $ServerInfo -Command $dexpCmd
}
#>
return $true
}
# ==============================================================================
# 数据库恢复(预留功能)
# ==============================================================================
function Restore-Database {
<#
.SYNOPSIS
数据库恢复函数(预留)
.DESCRIPTION
根据数据库类型执行不同的恢复逻辑
.PARAMETER ServerInfo
服务器连接信息哈希表
.PARAMETER DatabaseConfig
数据库配置信息
.PARAMETER BackupFile
备份文件路径
.EXAMPLE
Restore-Database -ServerInfo $server -DatabaseConfig $dbConfig -BackupFile "/data/backup/ubains_backup.sql"
.OUTPUTS
Boolean,恢复成功返回 $true,否则返回 $false
#>
param(
[Parameter(Mandatory=$true)]
[hashtable]$ServerInfo,
[Parameter(Mandatory=$true)]
[hashtable]$DatabaseConfig,
[Parameter(Mandatory=$true)]
[string]$BackupFile
)
# 预留功能,暂不执行
Write-Log -Level "WARN" -Message "数据库恢复功能为预留功能,暂不执行"
Write-Log -Level "INFO" -Message "备份文件: $BackupFile"
<#
TODO: 根据数据库类型执行不同的恢复逻辑
# MySQL 容器化部署示例
if ($DatabaseConfig.Type -eq "MySQL" -and $DatabaseConfig.IsContainerized) {
$containerName = $DatabaseConfig.ContainerName
$restoreCmd = "docker exec -i $containerName mysql -u$($DatabaseConfig.Username) -p$($DatabaseConfig.Password) $dbName < $BackupFile"
Invoke-SSHCommandSafe -ServerInfo $ServerInfo -Command $restoreCmd
}
# 达梦数据库示例
if ($DatabaseConfig.Type -eq "DM") {
$dimpCmd = "dimp USERID=$($DatabaseConfig.Username)/$($DatabaseConfig.Password) FILE=$BackupFile LOG=$logFile"
Invoke-SSHCommandSafe -ServerInfo $ServerInfo -Command $dimpCmd
}
#>
return $true
}
# ==============================================================================
# file.ps1
# ------------------------------------------------------------------------------
# 文件操作模块
#
# .SYNOPSIS
# 提供文件上传和下载功能
#
# .DESCRIPTION
# 本模块通过 pscp.exe 实现文件的上传和下载
#
# .NOTES
# 版本:1.0.0
# 创建日期:2026-02-10
#
# 依赖:
# - pscp.exe (PuTTY 套件)
# - ssh.ps1 (SSH 连接模块)
# ==============================================================================
# 获取脚本目录
if ($PSScriptRoot) {
$LIB_SCRIPT_DIR = $PSScriptRoot
} else {
$LIB_SCRIPT_DIR = Split-Path -Parent $MyInvocation.MyCommand.Path
}
# 导入依赖模块
Import-Module (Join-Path $LIB_SCRIPT_DIR "ssh.ps1") -Force
# ==============================================================================
# 文件上传
# ==============================================================================
function Upload-File {
<#
.SYNOPSIS
上传本地文件到远程服务器
.DESCRIPTION
使用 pscp.exe 将本地文件上传到远程服务器指定目录
.PARAMETER LocalPath
本地文件完整路径
.PARAMETER ServerInfo
服务器连接信息哈希表
.PARAMETER RemotePath
远程目标目录
.EXAMPLE
$server = @{
IP = "192.168.1.1"
Port = 22
SshUsername = "root"
SshPassword = "password"
}
Upload-File -LocalPath "C:\test.zip" -ServerInfo $server -RemotePath "/home/appadmin/"
.OUTPUTS
Boolean,上传成功返回 $true,否则返回 $false
#>
param(
[Parameter(Mandatory=$true)]
[string]$LocalPath,
[Parameter(Mandatory=$true)]
[hashtable]$ServerInfo,
[Parameter(Mandatory=$true)]
[string]$RemotePath
)
if (-not (Test-Path -LiteralPath $LocalPath)) {
Write-Log -Level "ERROR" -Message "本地文件不存在: $LocalPath"
return $false
}
# 确保 pscp 可用
if (-not $global:PSCP_PATH) {
Initialize-SSHTools | Out-Null
}
if (-not $global:PSCP_PATH) {
Write-Log -Level "ERROR" -Message "pscp.exe 未找到,无法上传文件"
return $false
}
Write-Log -Level "INFO" -Message "上传文件: $LocalPath -> $($ServerInfo.IP):$RemotePath"
# 构建 pscp 参数
$pscpArgs = @(
"-P", $ServerInfo.Port,
"-l", $ServerInfo.SshUsername,
"-pw", $ServerInfo.SshPassword,
"-batch",
"-no-antispoof",
$LocalPath,
"$($ServerInfo.SshUsername)@$($ServerInfo.IP):$RemotePath/"
)
try {
$result = & $global:PSCP_PATH @pscpArgs 2>&1
$exitCode = $LASTEXITCODE
# 处理首次连接主机密钥问题
if ($exitCode -ne 0 -and ($result -match "host key" -or $result -match "Store key in cache")) {
$cmdLine = "echo y | `"$($global:PSCP_PATH)`" -P $($ServerInfo.Port) -l $($ServerInfo.SshUsername) -pw `"$($ServerInfo.SshPassword)`" `"$LocalPath`" `"$($ServerInfo.SshUsername)@$($ServerInfo.IP):$RemotePath/`""
$result = cmd /c $cmdLine 2>&1
$exitCode = $LASTEXITCODE
}
if ($exitCode -eq 0) {
Write-Log -Level "SUCCESS" -Message "文件上传成功"
return $true
} else {
Write-Log -Level "ERROR" -Message "文件上传失败: $result"
return $false
}
}
catch {
Write-Log -Level "ERROR" -Message "文件上传异常: $($_.Exception.Message)"
return $false
}
}
# ==============================================================================
# 文件下载
# ==============================================================================
function Download-File {
<#
.SYNOPSIS
从远程服务器下载文件到本地
.DESCRIPTION
使用 pscp.exe 从远程服务器下载文件到本地指定目录
.PARAMETER ServerInfo
服务器连接信息哈希表
.PARAMETER RemotePath
远程文件完整路径
.PARAMETER LocalPath
本地目标文件路径
.EXAMPLE
$server = @{
IP = "192.168.1.1"
Port = 22
SshUsername = "root"
SshPassword = "password"
}
Download-File -ServerInfo $server -RemotePath "/home/appadmin/test.zip" -LocalPath "C:\test.zip"
.OUTPUTS
Boolean,下载成功返回 $true,否则返回 $false
#>
param(
[Parameter(Mandatory=$true)]
[hashtable]$ServerInfo,
[Parameter(Mandatory=$true)]
[string]$RemotePath,
[Parameter(Mandatory=$true)]
[string]$LocalPath
)
# 确保 pscp 可用
if (-not $global:PSCP_PATH) {
Initialize-SSHTools | Out-Null
}
if (-not $global:PSCP_PATH) {
Write-Log -Level "WARN" -Message "pscp.exe 未找到,无法下载文件"
return $false
}
Write-Log -Level "INFO" -Message "下载文件: $($ServerInfo.IP):$RemotePath -> $LocalPath"
# 确保本地目录存在
$localDir = Split-Path -Parent $LocalPath
if (-not (Test-Path $localDir)) {
New-Item -ItemType Directory -Path $localDir -Force | Out-Null
}
# 构建 pscp 参数
$pscpArgs = @(
"-P", $ServerInfo.Port,
"-l", $ServerInfo.SshUsername,
"-pw", $ServerInfo.SshPassword,
"-batch",
"-no-antispoof",
"$($ServerInfo.SshUsername)@$($ServerInfo.IP):$RemotePath",
$LocalPath
)
try {
$result = & $global:PSCP_PATH @pscpArgs 2>&1
$exitCode = $LASTEXITCODE
# 处理首次连接主机密钥问题
if ($exitCode -ne 0 -and ($result -match "host key" -or $result -match "Store key in cache")) {
$cmdLine = "echo y | `"$($global:PSCP_PATH)`" -P $($ServerInfo.Port) -l $($ServerInfo.SshUsername) -pw `"$($ServerInfo.SshPassword)`" `"$($ServerInfo.SshUsername)@$($ServerInfo.IP):$RemotePath`" `"$LocalPath`""
$result = cmd /c $cmdLine 2>&1
$exitCode = $LASTEXITCODE
}
if ($exitCode -eq 0 -and (Test-Path $LocalPath)) {
Write-Log -Level "SUCCESS" -Message "文件下载成功"
return $true
} else {
Write-Log -Level "WARN" -Message "文件下载失败: $result"
return $false
}
}
catch {
Write-Log -Level "WARN" -Message "文件下载异常: $($_.Exception.Message)"
return $false
}
}
# ==============================================================================
# log.ps1
# ------------------------------------------------------------------------------
# 日志模块
#
# .SYNOPSIS
# 提供日志输出功能
#
# .DESCRIPTION
# 本模块提供日志输出到控制台和日志文件的功能
# 支持多个日志级别:INFO、WARN、ERROR、SUCCESS
#
# .NOTES
# 版本:1.0.0
# 创建日期:2026-02-10
# ==============================================================================
# ==============================================================================
# 全局变量
# ==============================================================================
$global:LOG_FILE = $null
$global:LOG_DIR = Join-Path $PSScriptRoot "..\logs"
# 确保日志目录存在
if (-not (Test-Path $global:LOG_DIR)) {
New-Item -ItemType Directory -Path $global:LOG_DIR -Force | Out-Null
}
# 初始化日志文件
function Initialize-Log {
$LOG_TIMESTAMP = Get-Date -Format "yyyyMMdd_HHmmss"
$global:LOG_FILE = Join-Path $global:LOG_DIR "remote_update_$LOG_TIMESTAMP.log"
}
# ==============================================================================
# 日志函数
# ==============================================================================
function Write-Log {
<#
.SYNOPSIS
输出日志信息到控制台和日志文件
.DESCRIPTION
该函数将日志信息输出到控制台(带颜色)并写入到日志文件。
支持多个日志级别:INFO、WARN、ERROR、SUCCESS
.PARAMETER Level
日志级别:INFO、WARN、ERROR、SUCCESS
.PARAMETER Message
日志消息内容(可为空字符串,此时只输出空行)
.EXAMPLE
Write-Log -Level "INFO" -Message "开始检测服务"
Write-Log -Level "ERROR" -Message "检测失败"
#>
param(
[Parameter(Mandatory=$false)]
[ValidateSet("INFO", "WARN", "ERROR", "SUCCESS")]
[string]$Level = "INFO",
[Parameter(Mandatory=$false)]
[AllowEmptyString()]
[string]$Message = ""
)
# 如果日志文件未初始化,先初始化
if (-not $global:LOG_FILE) {
Initialize-Log
}
# 如果消息为空,直接输出空行到控制台,不记录到日志文件
if ([string]::IsNullOrEmpty($Message)) {
Write-Host ""
return
}
$timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
$logLine = "[$timestamp] [$Level] $Message"
$colorMap = @{
"INFO" = "Cyan"
"WARN" = "Yellow"
"ERROR" = "Red"
"SUCCESS" = "Green"
}
$color = $colorMap[$Level]
Write-Host $logLine -ForegroundColor $color
try {
# 使用全局日志文件路径
if ($global:LOG_FILE) {
$logLine | Out-File -FilePath $global:LOG_FILE -Append -Encoding utf8
}
}
catch {
# 日志文件写入失败不影响主流程
}
}
# ==============================================================================
# ssh.ps1
# ------------------------------------------------------------------------------
# SSH 连接模块
#
# .SYNOPSIS
# 提供远程 SSH 连接和命令执行功能
#
# .DESCRIPTION
# 本模块通过 plink.exe 实现 SSH 连接和远程命令执行
# 支持直接 root 连接和普通用户 + su 切换两种方式
#
# .NOTES
# 版本:1.0.0
# 创建日期:2026-02-10
#
# 依赖:
# - plink.exe (PuTTY 套件)
# - pscp.exe (PuTTY 套件)
# ==============================================================================
# ==============================================================================
# 全局变量
# ==============================================================================
if ($PSScriptRoot) {
$LIB_SCRIPT_DIR = $PSScriptRoot
} else {
$LIB_SCRIPT_DIR = Split-Path -Parent $MyInvocation.MyCommand.Path
}
$global:PLINK_PATH = $null
$global:PSCP_PATH = $null
# ==============================================================================
# 查找 plink 和 pscp 工具
# ==============================================================================
function Initialize-SSHTools {
# 1. 检查 lib 目录的父目录下的 plink.exe(RemoteUpdate 目录)
$parentDir = Split-Path -Parent $LIB_SCRIPT_DIR
$localPlinkPath = Join-Path $parentDir "plink.exe"
if (Test-Path $localPlinkPath) {
$global:PLINK_PATH = $localPlinkPath
$localPscpPath = Join-Path $parentDir "pscp.exe"
if (Test-Path $localPscpPath) {
$global:PSCP_PATH = $localPscpPath
}
return $true
}
# 2. 检查系统 PATH 中的 plink
try {
$systemPlink = Get-Command plink -ErrorAction Stop
$global:PLINK_PATH = $systemPlink.Source
try {
$systemPscp = Get-Command pscp -ErrorAction Stop
$global:PSCP_PATH = $systemPscp.Source
}
catch {
# pscp 未找到,但 plink 存在
}
return $true
}
catch {
return $false
}
}
# ==============================================================================
# SSH 命令执行
# ==============================================================================
function Invoke-SSHCommand {
<#
.SYNOPSIS
通过 SSH 执行远程命令
.DESCRIPTION
使用 plink.exe 通过 SSH 连接到远程服务器并执行命令。
支持直接 root 连接和普通用户 + su 切换两种方式。
.PARAMETER HostName
目标主机 IP 地址或主机名
.PARAMETER User
SSH 登录用户名
.PARAMETER Pass
SSH 登录密码
.PARAMETER Port
SSH 端口号,默认为 22
.PARAMETER Command
要执行的远程命令
.PARAMETER SuUser
切换用户名称(可选),如果提供则先 su 切换再执行命令
.PARAMETER SuPass
切换用户密码(可选)
.EXAMPLE
# 直接 root 连接执行命令
$result = Invoke-SSHCommand -HostName "192.168.1.1" -User "root" -Pass "password" -Command "ls -la"
.EXAMPLE
# 普通用户连接后切换 root 执行命令
$result = Invoke-SSHCommand -HostName "192.168.1.1" -User "appadmin" -Pass "pass" -SuUser "root" -SuPass "rootpass" -Command "docker ps"
.OUTPUTS
System.Collections.Hashtable
包含 Output(命令输出)和 ExitCode(退出码)的哈希表
#>
param(
[Parameter(Mandatory=$true)]
[string]$HostName,
[Parameter(Mandatory=$true)]
[string]$User,
[Parameter(Mandatory=$true)]
[string]$Pass,
[Parameter(Mandatory=$false)]
[int]$Port = 22,
[Parameter(Mandatory=$true)]
[string]$Command,
[Parameter(Mandatory=$false)]
[string]$SuUser = $null,
[Parameter(Mandatory=$false)]
[string]$SuPass = $null
)
# 确保 plink 可用
if (-not $global:PLINK_PATH) {
if (-not (Initialize-SSHTools)) {
throw "plink.exe 未找到,请将 plink.exe 放在脚本同目录下或安装 PuTTY 到系统 PATH"
}
}
# 构建完整的命令
if ($SuUser) {
# 需要先 su 切换用户再执行命令
$fullCommand = "echo '$SuPass' | su - $SuUser -c '$Command'"
} else {
# 直接执行命令
$fullCommand = $Command
}
# 构建 plink 参数
$plinkArgs = @(
"-ssh",
"-P", $Port,
"-l", $User,
"-pw", $Pass,
"-batch",
"-no-antispoof", # 禁用反欺骗检测
$HostName,
$fullCommand
)
# 执行命令
try {
$result = & $global:PLINK_PATH @plinkArgs 2>&1
$exitCode = $LASTEXITCODE
# 处理首次连接主机密钥问题
if ($exitCode -ne 0 -and ($result -match "host key" -or $result -match "Store key in cache")) {
# 尝试接受主机密钥并重试
$plinkArgs = @(
"-ssh",
"-P", $Port,
"-l", $User,
"-pw", $Pass,
"-batch",
"-no-antispoof",
$HostName,
$fullCommand
)
# 通过管道输入 y 来接受主机密钥
$cmdInput = "y`n"
$result = $cmdInput | & $global:PLINK_PATH @plinkArgs 2>&1
$exitCode = $LASTEXITCODE
}
}
catch {
$result = $_.Exception.Message
$exitCode = -1
}
return @{
Output = $result
ExitCode = $exitCode
}
}
# ==============================================================================
# 测试 SSH 连接
# ==============================================================================
function Test-SSHConnection {
<#
.SYNOPSIS
测试 SSH 连接是否可用
.PARAMETER ServerInfo
服务器连接信息哈希表
.EXAMPLE
$server = @{
IP = "192.168.1.1"
Port = 22
SshUsername = "root"
SshPassword = "password"
}
Test-SSHConnection -ServerInfo $server
.OUTPUTS
Boolean,连接成功返回 $true,否则返回 $false
#>
param(
[Parameter(Mandatory=$true)]
[hashtable]$ServerInfo
)
$ipPort = "$($ServerInfo.IP):$($ServerInfo.Port)"
Write-Log -Level "INFO" -Message "测试 SSH 连接: $ipPort"
$testCmd = "echo 'CONNECTION_OK'"
$result = Invoke-SSHCommand `
-HostName $ServerInfo.IP `
-User $ServerInfo.SshUsername `
-Pass $ServerInfo.SshPassword `
-Port $ServerInfo.Port `
-Command $testCmd `
-SuUser $ServerInfo.SuUsername `
-SuPass $ServerInfo.SuPassword
if ($result.ExitCode -eq 0 -and $result.Output -match "CONNECTION_OK") {
Write-Log -Level "SUCCESS" -Message "SSH 连接测试通过"
return $true
} else {
Write-Log -Level "ERROR" -Message "SSH 连接测试失败"
Write-Log -Level "ERROR" -Message "输出信息: $($result.Output -join ' ')"
return $false
}
}
# 测试配置加载
$SCRIPT_DIR = Split-Path -Parent $MyInvocation.MyCommand.Path
# 定义 ConvertTo-Hashtable 函数
function ConvertTo-Hashtable {
param([object]$InputObject)
Write-Host "=== ConvertTo-Hashtable 调试 ===" -ForegroundColor Magenta
Write-Host "InputObject 为 null: $($null -eq $InputObject)" -ForegroundColor Yellow
if ($null -ne $InputObject) {
Write-Host "InputObject 类型: $($InputObject.GetType().FullName)" -ForegroundColor Yellow
Write-Host "InputObject 是 PSCustomObject: $($InputObject -is [System.Management.Automation.PSCustomObject])" -ForegroundColor Yellow
Write-Host "InputObject 是 PSObject: $($InputObject -is [System.Management.Automation.PSObject])" -ForegroundColor Yellow
}
if ($null -eq $InputObject) {
Write-Host "返回 null (InputObject 为 null)" -ForegroundColor Red
return $null
}
if ($InputObject -is [System.Collections.Hashtable]) {
Write-Host "返回 Hashtable" -ForegroundColor Green
return $InputObject
}
if ($InputObject -is [System.Management.Automation.PSCustomObject]) {
Write-Host "输入是 PSCustomObject" -ForegroundColor Yellow
Write-Host "属性数量: $($InputObject.PSObject.Properties.Name.Count)" -ForegroundColor Yellow
if ($InputObject.PSObject.Properties.Name.Count -eq 0) {
return @{}
}
$hashtable = @{}
foreach ($prop in $InputObject.PSObject.Properties) {
$value = $prop.Value
if ($value -is [System.Management.Automation.PSCustomObject]) {
$hashtable[$prop.Name] = ConvertTo-Hashtable -InputObject $value
} elseif ($value -is [System.Collections.Hashtable]) {
$hashtable[$prop.Name] = ConvertTo-Hashtable -InputObject $value
} else {
$hashtable[$prop.Name] = $value
}
}
return $hashtable
}
Write-Host "返回原始 InputObject (非 PSCustomObject)" -ForegroundColor Red
return $InputObject
}
$configFile = Join-Path $SCRIPT_DIR "config\servers.json"
$config = Get-Content $configFile -Raw | ConvertFrom-Json
Write-Host "=== 原始配置 ===" -ForegroundColor Cyan
Write-Host "config 类型: $($config.GetType().FullName)"
Write-Host "config.servers 类型: $($config.servers.GetType().FullName)"
Write-Host "config.servers 是数组: $($config.servers -is [array])"
Write-Host ""
Write-Host "=== 转换后配置 ===" -ForegroundColor Cyan
$servers = ConvertTo-Hashtable -InputObject $config.servers
Write-Host "servers 类型: $($servers.GetType().FullName)" -ForegroundColor Yellow
Write-Host "servers 为空: $($servers -eq $null)" -ForegroundColor Yellow
if ($servers -ne $null) {
Write-Host "servers.Count: $($servers.Count)"
Write-Host "servers.Keys: $($servers.Keys -join ', ')"
}
# Test projects config loading
$SCRIPT_DIR = Split-Path -Parent $MyInvocation.MyCommand.Path
function ConvertTo-Hashtable {
param([object]$InputObject)
if ($null -eq $InputObject) { return $null }
if ($InputObject -is [System.Collections.Hashtable]) {
$result = @{}
foreach ($key in $InputObject.Keys) {
if ($InputObject[$key] -is [System.Management.Automation.PSCustomObject]) {
$result[$key] = ConvertTo-Hashtable -InputObject $InputObject[$key]
} elseif ($InputObject[$key] -is [System.Collections.Hashtable]) {
$result[$key] = ConvertTo-Hashtable -InputObject $InputObject[$key]
} else {
$result[$key] = $InputObject[$key]
}
}
return $result
}
if ($InputObject -is [System.Management.Automation.PSCustomObject]) {
if ($InputObject.PSObject.Properties.Name.Count -eq 0) {
return @{}
}
$hashtable = @{}
foreach ($prop in $InputObject.PSObject.Properties) {
if ($prop.Value -is [System.Management.Automation.PSCustomObject]) {
$hashtable[$prop.Name] = ConvertTo-Hashtable -InputObject $prop.Value
} elseif ($prop.Value -is [System.Collections.Hashtable]) {
$hashtable[$prop.Name] = ConvertTo-Hashtable -InputObject $prop.Value
} else {
$hashtable[$prop.Name] = $prop.Value
}
}
return $hashtable
}
return $InputObject
}
try {
$configFile = Join-Path $SCRIPT_DIR "config\projects.json"
$config = Get-Content $configFile -Raw | ConvertFrom-Json
Write-Host "Config type: " $config.GetType().FullName
Write-Host "Config.projects type: " $config.projects.GetType().FullName
$projects = ConvertTo-Hashtable -InputObject $config.projects
Write-Host "Conversion successful!" -ForegroundColor Green
Write-Host "Projects type: " $projects.GetType().FullName
Write-Host "Projects.Count: " $projects.Count
}
catch {
Write-Host "Conversion failed: " $_.Exception.Message -ForegroundColor Red
Write-Host $_.ScriptStackTrace -ForegroundColor Yellow
}
#!/bin/bash
#####################################
#用于数据库备份 23-01-13
#####################################
#预定数据库备份
function ubainsbak()
{
echo -e "\033[33m 检查mysql...... \033[0m"
sudo docker images |grep mysql
sudo docker ps |grep umysql
if [ $? -eq 0 ]; then
sudo docker exec -i umysql bash <<'EOF'
find /home/mysql/ubains/* -mtime +30 -exec rm -rf {} \;
mkdir -p /home/mysql/ubains/$(date +%Y%m%d)
# 备份指定数据库
mysqldump -uroot -p"dNrprU&2S" ubains > /home/mysql/ubains/$(date +%Y%m%d)/ubains_$(date +%Y%m%d_%H%M%S).sql
#删除30天之前的备份
log3=$(date -d "30 day ago" +%Y%m%d)
rm /home/mysql/ubains/$log3* -rf
exit
EOF
sudo mkdir -p /data/bakup/mysql/ubains/$(date +%Y%m%d)
sudo docker cp umysql:/home/mysql/ubains/$(date +%Y%m%d) /data/bakup/mysql/ubains
#删除30天之前的备份
log3=$(date -d "30 day ago" +%Y%m%d)
sudo rm /data/bakup/mysql/ubains/$log3* -rf
else
echo -e "\033[33m 请正确安装数据库...... \033[0m"
fi
}
#运维数据库备份
function devopsbak()
{
echo -e "\033[33m 检查mysql...... \033[0m"
sudo docker images |grep mysql
sudo docker ps |grep umysql
if [ $? -eq 0 ]; then
sudo docker exec -i umysql bash <<'EOF'
find /home/mysql/devops/* -mtime +30 -exec rm -rf {} \;
mkdir -p /home/mysql/devops/$(date +%Y%m%d)
# 备份指定数据库
mysqldump -uroot -p"dNrprU&2S" devops > /home/mysql/devops/$(date +%Y%m%d)/devops_$(date +%Y%m%d_%H%M%S).sql
#删除30天之前的备份
log3=$(date -d "30 day ago" +%Y%m%d)
rm /home/mysql/devops/$log3* -rf
exit
EOF
sudo mkdir -p /data/bakup/mysql/devops/$(date +%Y%m%d)
sudo docker cp umysql:/home/mysql/devops/$(date +%Y%m%d) /data/bakup/mysql/devops
#删除30天之前的备份
log3=$(date -d "30 day ago" +%Y%m%d)
sudo rm /data/bakup/mysql/devops/$log3* -rf
else
echo -e "\033[33m 请正确安装数据库...... \033[0m"
fi
}
#############################################################脚本配置项##################################################################################################
#################预定系统 数据库本地备份###############################
ubainsbak
#################运维系统 数据库本地备份###############################
devopsbak
[ [
{
"system_type": "后台系统",
"module_name": "区域管理",
"module_name_son": "增值服务",
"module_function": ["添加","编辑","删除"]
},
{ {
"system_type": "后台系统", "system_type": "后台系统",
"module_name": "授权管理", "module_name": "系统管理",
"module_name_son": "会议授权", "module_name_son": "权限管理",
"module_function": ["编辑","停用","启用","批量启用","批量停用"] "module_function": [
} "权限组列表",
"权限组添加"
]
}
] ]
\ No newline at end of file
[
{
"system_type": "后台系统",
"module_name": "区域管理",
"module_name_son": "增值服务",
"module_function": ["添加","编辑","删除"]
},
{
"system_type": "后台系统",
"module_name": "授权管理",
"module_name_son": "会议授权",
"module_function": ["编辑","停用","启用","批量启用","批量停用"]
}
]
\ No newline at end of file
[
{
"system_type": "new_platform",
"system_front_url": "https://192.168.5.44",
"system_back_url": "https://192.168.5.44/#/LoginAdmin",
"username": "admin@xty",
"password": "Ubains@4321",
"code": "csba"
},
{
"system_type": "new_platform",
"system_front_url": "https://192.168.5.44",
"system_back_url": "https://192.168.5.44/#/LoginAdmin",
"username": "admin@xty",
"password": "Ubains@4321",
"code": "csba"
}
]
\ No newline at end of file
...@@ -45,8 +45,8 @@ class TestCaseGenerator: ...@@ -45,8 +45,8 @@ class TestCaseGenerator:
with open(os.path.join(self.config_dir, 'module_config.json'), 'r', encoding='utf-8') as f: with open(os.path.join(self.config_dir, 'module_config.json'), 'r', encoding='utf-8') as f:
self.module_config = json.load(f) self.module_config = json.load(f)
print(f" 加载系统配置: {len(self.system_config)} 个") print(f"[OK] 加载系统配置: {len(self.system_config)} 个")
print(f" 加载模块配置: {len(self.module_config)} 个") print(f"[OK] 加载模块配置: {len(self.module_config)} 个")
def init_driver(self): def init_driver(self):
"""初始化浏览器驱动""" """初始化浏览器驱动"""
...@@ -55,9 +55,9 @@ class TestCaseGenerator: ...@@ -55,9 +55,9 @@ class TestCaseGenerator:
# 根据配置决定是否使用无头模式 # 根据配置决定是否使用无头模式
if self.headless: if self.headless:
options.add_argument('--headless') # 无头模式:不显示浏览器窗口 options.add_argument('--headless') # 无头模式:不显示浏览器窗口
print(" 浏览器驱动初始化完成(无头模式)") print("[OK] 浏览器驱动初始化完成(无头模式)")
else: else:
print(" 浏览器驱动初始化完成(有界面模式)") print("[OK] 浏览器驱动初始化完成(有界面模式)")
options.add_argument('--no-sandbox') options.add_argument('--no-sandbox')
options.add_argument('--disable-dev-shm-usage') options.add_argument('--disable-dev-shm-usage')
...@@ -110,7 +110,7 @@ class TestCaseGenerator: ...@@ -110,7 +110,7 @@ class TestCaseGenerator:
) )
proceed_btn.click() proceed_btn.click()
time.sleep(2) time.sleep(2)
print(" SSL警告已处理,继续访问") print(" [OK] SSL警告已处理,继续访问")
except: except:
pass pass
...@@ -119,7 +119,7 @@ class TestCaseGenerator: ...@@ -119,7 +119,7 @@ class TestCaseGenerator:
proceed_link = self.driver.find_element(By.XPATH, "//a[@id='proceed-link'] or //a[contains(@href, 'proceed')]") proceed_link = self.driver.find_element(By.XPATH, "//a[@id='proceed-link'] or //a[contains(@href, 'proceed')]")
proceed_link.click() proceed_link.click()
time.sleep(2) time.sleep(2)
print(" SSL警告已处理,继续访问") print(" [OK] SSL警告已处理,继续访问")
except: except:
pass pass
except Exception as e: except Exception as e:
...@@ -163,7 +163,7 @@ class TestCaseGenerator: ...@@ -163,7 +163,7 @@ class TestCaseGenerator:
code_input = self.driver.find_elements(By.CSS_SELECTOR, "input[type='text']")[-1] code_input = self.driver.find_elements(By.CSS_SELECTOR, "input[type='text']")[-1]
code_input.clear() code_input.clear()
code_input.send_keys(code) code_input.send_keys(code)
print(f" 输入验证码: {code}") print(f" [OK] 输入验证码: {code}")
except: except:
print(" ! 验证码输入失败,跳过") print(" ! 验证码输入失败,跳过")
...@@ -199,7 +199,7 @@ class TestCaseGenerator: ...@@ -199,7 +199,7 @@ class TestCaseGenerator:
login_btn = self.driver.find_element(locator_type, locator_value) login_btn = self.driver.find_element(locator_type, locator_value)
login_btn.click() login_btn.click()
login_success = True login_success = True
print(f" 点击登录按钮 (定位: {locator_type})") print(f" [OK] 点击登录按钮 (定位: {locator_type})")
break break
except: except:
continue continue
...@@ -210,13 +210,13 @@ class TestCaseGenerator: ...@@ -210,13 +210,13 @@ class TestCaseGenerator:
# 最后的备选方案:通过JavaScript点击第一个按钮 # 最后的备选方案:通过JavaScript点击第一个按钮
self.driver.execute_script("document.querySelector('button').click()") self.driver.execute_script("document.querySelector('button').click()")
login_success = True login_success = True
print(" 通过JavaScript点击登录按钮") print(" [OK] 通过JavaScript点击登录按钮")
except Exception as e: except Exception as e:
print(f" 登录失败: {e}") print(f" [FAIL] 登录失败: {e}")
raise Exception("无法定位登录按钮,请检查登录页面结构") raise Exception("无法定位登录按钮,请检查登录页面结构")
time.sleep(3) time.sleep(3)
print(" 登录成功") print(" [OK] 登录成功")
def wait_for_element(self, locator, timeout=10): def wait_for_element(self, locator, timeout=10):
"""等待元素出现""" """等待元素出现"""
...@@ -262,7 +262,7 @@ class TestCaseGenerator: ...@@ -262,7 +262,7 @@ class TestCaseGenerator:
menu_item = self.driver.find_element(locator_type, locator_value) menu_item = self.driver.find_element(locator_type, locator_value)
menu_item.click() menu_item.click()
menu_found = True menu_found = True
print(f" 点击【{module_name}】菜单 (定位: {locator_type})") print(f" [OK] 点击【{module_name}】菜单 (定位: {locator_type})")
time.sleep(1) time.sleep(1)
break break
except: except:
...@@ -295,7 +295,7 @@ class TestCaseGenerator: ...@@ -295,7 +295,7 @@ class TestCaseGenerator:
sub_menu = self.driver.find_element(locator_type, locator_value) sub_menu = self.driver.find_element(locator_type, locator_value)
sub_menu.click() sub_menu.click()
sub_menu_found = True sub_menu_found = True
print(f" 点击【{sub_module_name}】子菜单 (定位: {locator_type})") print(f" [OK] 点击【{sub_module_name}】子菜单 (定位: {locator_type})")
time.sleep(2) time.sleep(2)
break break
except: except:
...@@ -310,7 +310,7 @@ class TestCaseGenerator: ...@@ -310,7 +310,7 @@ class TestCaseGenerator:
self.debug_page_structure("submenu_not_found") self.debug_page_structure("submenu_not_found")
return False return False
print(f" 成功进入: {sub_module_name}") print(f" [OK] 成功进入: {sub_module_name}")
return True return True
def normalize_text(self, text): def normalize_text(self, text):
...@@ -325,7 +325,7 @@ class TestCaseGenerator: ...@@ -325,7 +325,7 @@ class TestCaseGenerator:
WebDriverWait(self.driver, timeout).until( WebDriverWait(self.driver, timeout).until(
EC.presence_of_element_located((By.XPATH, "//li | //a | //button")) EC.presence_of_element_located((By.XPATH, "//li | //a | //button"))
) )
print(" 菜单已加载完成") print(" [OK] 菜单已加载完成")
except: except:
print(" ! 菜单加载超时") print(" ! 菜单加载超时")
...@@ -337,7 +337,7 @@ class TestCaseGenerator: ...@@ -337,7 +337,7 @@ class TestCaseGenerator:
self.driver.switch_to.frame(iframe) self.driver.switch_to.frame(iframe)
# 检查是否包含菜单元素 # 检查是否包含菜单元素
if len(self.driver.find_elements(By.XPATH, "//li | //a | //button")) > 0: if len(self.driver.find_elements(By.XPATH, "//li | //a | //button")) > 0:
print(" 切换到菜单iframe") print(" [OK] 切换到菜单iframe")
return True return True
self.driver.switch_to.default_content() self.driver.switch_to.default_content()
except: except:
...@@ -694,7 +694,7 @@ class TestCaseGenerator: ...@@ -694,7 +694,7 @@ class TestCaseGenerator:
with open(filepath, 'w', encoding='utf-8') as f: with open(filepath, 'w', encoding='utf-8') as f:
json.dump(testcase, f, ensure_ascii=False, indent=2) json.dump(testcase, f, ensure_ascii=False, indent=2)
print(f" 生成: {filename}") print(f" [OK] 生成: {filename}")
def run(self): def run(self):
"""执行生成流程""" """执行生成流程"""
...@@ -728,7 +728,7 @@ class TestCaseGenerator: ...@@ -728,7 +728,7 @@ class TestCaseGenerator:
# 导航到模块 # 导航到模块
if not self.navigate_to_module(module_name, sub_module_name): if not self.navigate_to_module(module_name, sub_module_name):
print(f" 跳过该模块") print(f" [FAIL] 跳过该模块")
continue continue
# 获取页面URL # 获取页面URL
...@@ -744,8 +744,8 @@ class TestCaseGenerator: ...@@ -744,8 +744,8 @@ class TestCaseGenerator:
total_count += 1 total_count += 1
print("\n" + "=" * 60) print("\n" + "=" * 60)
print(f" 生成完成!共生成 {total_count} 个测试用例文件") print(f"[OK] 生成完成!共生成 {total_count} 个测试用例文件")
print(f" 文件保存在: {self.testcases_dir}") print(f"[OK] 文件保存在: {self.testcases_dir}")
print("=" * 60) print("=" * 60)
def close(self): def close(self):
...@@ -787,7 +787,7 @@ def main(): ...@@ -787,7 +787,7 @@ def main():
try: try:
generator.run() generator.run()
except Exception as e: except Exception as e:
print(f"\n 执行出错: {e}") print(f"\n[FAIL] 执行出错: {e}")
import traceback import traceback
traceback.print_exc() traceback.print_exc()
finally: finally:
......
# -*- coding: utf-8 -*-
"""
测试用例JSON生成器 - 入口脚本
本脚本是测试用例JSON生成器的主入口
"""
import sys
import os
# 添加src目录到Python路径
ROOT_DIR = os.path.dirname(os.path.abspath(__file__))
SRC_DIR = os.path.join(ROOT_DIR, "src")
sys.path.insert(0, SRC_DIR)
sys.path.insert(0, ROOT_DIR)
from src.test_case_generator import main
if __name__ == "__main__":
main()
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论