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

fix(scripts): 修复脚本工具中的安全漏洞和编码问题

- 修复 MySQL 深度检查脚本密码传递方式,支持环境变量、文件和命令行参数多种方式
- 优化 Redis 检查脚本指标提取逻辑,使用 tr 命令去除换行符避免整数比较错误
- 添加 UTF-8 编码设置防止服务器健康检查脚本中文输出乱码
- 修复多个脚本中项目路径名称错误,将 meetngV2/V3 统一修正为 meetingV2/V3
- 解决 Python GUI 应用中异常变量作用域问题,避免 NameError 阻止真实错误显示
- 添加详细的错误处理文档说明 GUI 异常处理的根因和解决方案
上级 ab3ddd19
...@@ -482,10 +482,13 @@ class ReportGeneratorGUI: ...@@ -482,10 +482,13 @@ class ReportGeneratorGUI:
# ===== ERP上传功能结束 ===== # ===== ERP上传功能结束 =====
except Exception as e: except Exception as e:
self._log(f"\n✗ 生成报告失败: {str(e)}", "error") # 提前将异常信息捕获到普通局部变量(避免 except 变量 e 被自动删除后,
# 延迟执行的 lambda 引用到已删除变量而触发 NameError)
err_msg = str(e)
self._log(f"\n✗ 生成报告失败: {err_msg}", "error")
# 在主线程中显示错误消息框(使用自定义对话框) # 在主线程中显示错误消息框(使用自定义对话框)
self.root.after(0, lambda: self._show_error_dialog("错误", f"生成报告失败:\n{str(e)}")) self.root.after(0, lambda msg=err_msg: self._show_error_dialog("错误", f"生成报告失败:\n{msg}"))
finally: finally:
# 恢复生成按钮 # 恢复生成按钮
...@@ -896,8 +899,14 @@ class ReportGeneratorGUI: ...@@ -896,8 +899,14 @@ class ReportGeneratorGUI:
self.root.after(0, lambda: self._show_error_dialog("失败", "报告上传失败,请查看日志了解详情")) self.root.after(0, lambda: self._show_error_dialog("失败", "报告上传失败,请查看日志了解详情"))
except Exception as e: except Exception as e:
self._log(f"[X] 上传异常: {str(e)}", "error") # 提前将异常信息捕获到普通局部变量
self.root.after(0, lambda: self._show_error_dialog("错误", f"上传异常:\n{str(e)}")) # 原因:Python 3 在 except 块结束时会自动删除异常变量 e,
# 而 root.after 延迟执行的 lambda 以闭包形式按名引用 e,
# 等 lambda 真正执行时 e 已被删除,导致 NameError。
# 因此先转为字符串并通过默认参数按值绑定到 lambda。
err_msg = str(e)
self._log(f"[X] 上传异常: {err_msg}", "error")
self.root.after(0, lambda msg=err_msg: self._show_error_dialog("错误", f"上传异常:\n{msg}"))
def _on_exit(self): def _on_exit(self):
"""退出按钮点击事件""" """退出按钮点击事件"""
......
...@@ -23,8 +23,8 @@ ...@@ -23,8 +23,8 @@
{"name": "ai包", "path": "web/pc/pc-vue2-ai"}, {"name": "ai包", "path": "web/pc/pc-vue2-ai"},
{"name": "后台包", "path": "web/pc/pc-vue2-backstage"}, {"name": "后台包", "path": "web/pc/pc-vue2-backstage"},
{"name": "main包", "path": "web/pc/pc-vue2-main"}, {"name": "main包", "path": "web/pc/pc-vue2-main"},
{"name": "meetngV2包", "path": "web/pc/pc-vue2-meetngV2"}, {"name": "meetingV2包", "path": "web/pc/pc-vue2-meetingV2"},
{"name": "meetngV3包", "path": "web/pc/pc-vue2-meetngV3"}, {"name": "meetingV3包", "path": "web/pc/pc-vue2-meetingV3"},
{"name": "meetingControl包", "path": "web/pc/pc-vue2-meetingControl"}, {"name": "meetingControl包", "path": "web/pc/pc-vue2-meetingControl"},
{"name": "monitor包", "path": "web/pc/pc-vue2-moniter"}, {"name": "monitor包", "path": "web/pc/pc-vue2-moniter"},
{"name": "platform包", "path": "web/pc/pc-vue2-platform"}, {"name": "platform包", "path": "web/pc/pc-vue2-platform"},
...@@ -50,8 +50,6 @@ ...@@ -50,8 +50,6 @@
] ]
}, },
"path_mapping": { "path_mapping": {
"web/pc/pc-vue2-meetngV2": "web/pc/pc-vue2-meetingV2",
"web/pc/pc-vue2-meetngV3": "web/pc/pc-vue2-meetingV3"
}, },
"deploy_servers": [ "deploy_servers": [
{ {
......
...@@ -4,10 +4,21 @@ ...@@ -4,10 +4,21 @@
# 输出格式: KEY=VALUE 便于 PowerShell 解析 # 输出格式: KEY=VALUE 便于 PowerShell 解析
# 参数 # 参数
# 容器名通过命令行参数传递
CONTAINER="${1:-umysql}" CONTAINER="${1:-umysql}"
PASSWORD="$2" # 密码读取优先级:
# 1. 环境变量 MYSQL_DEEP_PASSWORD(最安全,避免命令行暴露)
if [[ -z "$PASSWORD" ]]; then # 2. 密码文件路径(第2个参数指向文件,base64编码传递后解码写入)
# 3. 命令行参数直接传递(兼容旧方式,但不推荐特殊字符密码)
if [[ -n "$MYSQL_DEEP_PASSWORD" ]]; then
PASSWORD="$MYSQL_DEEP_PASSWORD"
elif [[ -n "$2" ]] && [[ -f "$2" ]]; then
# 第二个参数是密码文件路径
PASSWORD=$(cat "$2" 2>/dev/null | tr -d '\n\r')
elif [[ -n "$2" ]]; then
# 第二个参数直接是密码(兼容旧方式)
PASSWORD="$2"
else
echo "ERROR: MySQL password not provided" echo "ERROR: MySQL password not provided"
exit 1 exit 1
fi fi
......
...@@ -323,18 +323,18 @@ get_redis_metrics() { ...@@ -323,18 +323,18 @@ get_redis_metrics() {
return 1 return 1
fi fi
# 提取关键指标 # 提取关键指标(使用 tr 去除换行符和空格,避免整数比较错误)
REDIS_VERSION=$(echo "$redis_info" | grep "^redis_version:" | cut -d: -f2) REDIS_VERSION=$(echo "$redis_info" | grep "^redis_version:" | cut -d: -f2 | tr -d '\n\r ')
REDIS_UPTIME=$(echo "$redis_info" | grep "^uptime_in_days:" | cut -d: -f2) REDIS_UPTIME=$(echo "$redis_info" | grep "^uptime_in_days:" | cut -d: -f2 | tr -d '\n\r ')
REDIS_CONNECTIONS=$(echo "$redis_info" | grep "^connected_clients:" | cut -d: -f2) REDIS_CONNECTIONS=$(echo "$redis_info" | grep "^connected_clients:" | cut -d: -f2 | tr -d '\n\r ')
REDIS_MEMORY=$(echo "$redis_info" | grep "^used_memory_human:" | cut -d: -f2) REDIS_MEMORY=$(echo "$redis_info" | grep "^used_memory_human:" | cut -d: -f2 | tr -d '\n\r ')
REDIS_OPS=$(echo "$redis_info" | grep "^instantaneous_ops_per_sec:" | cut -d: -f2) REDIS_OPS=$(echo "$redis_info" | grep "^instantaneous_ops_per_sec:" | cut -d: -f2 | tr -d '\n\r ')
REDIS_KEYS=$(echo "$redis_info" | grep "^db0:keys=" | sed 's/.*keys=\([0-9]*\).*/\1/') REDIS_KEYS=$(echo "$redis_info" | grep "^db0:keys=" | sed 's/.*keys=\([0-9]*\).*/\1/' | tr -d '\n\r ')
REDIS_HITS=$(echo "$redis_info" | grep "^keyspace_hits:" | cut -d: -f2) REDIS_HITS=$(echo "$redis_info" | grep "^keyspace_hits:" | cut -d: -f2 | tr -d '\n\r ')
REDIS_MISSES=$(echo "$redis_info" | grep "^keyspace_misses:" | cut -d: -f2) REDIS_MISSES=$(echo "$redis_info" | grep "^keyspace_misses:" | cut -d: -f2 | tr -d '\n\r ')
# 计算命中率 # 计算命中率(确保变量为纯数字,避免整数表达式错误)
if [ -n "$REDIS_HITS" ] && [ -n "$REDIS_MISSES" ] && [ "$REDIS_HITS" -gt 0 ]; then if [ -n "$REDIS_HITS" ] && [ -n "$REDIS_MISSES" ] && echo "$REDIS_HITS" | grep -qE '^[0-9]+$' && [ "$REDIS_HITS" -gt 0 ] 2>/dev/null; then
local total=$((REDIS_HITS + REDIS_MISSES)) local total=$((REDIS_HITS + REDIS_MISSES))
REDIS_HIT_RATE=$(echo "scale=2; $REDIS_HITS * 100 / $total" | bc) REDIS_HIT_RATE=$(echo "scale=2; $REDIS_HITS * 100 / $total" | bc)
fi fi
......
...@@ -14,6 +14,9 @@ ...@@ -14,6 +14,9 @@
set -euo pipefail set -euo pipefail
# 设置终端编码为 UTF-8,防止中文输出乱码
export LC_ALL=C.UTF-8 2>/dev/null || export LC_ALL=en_US.UTF-8 2>/dev/null || true
# ------------------------------ # ------------------------------
# 基础配置 # 基础配置
# ------------------------------ # ------------------------------
...@@ -1338,8 +1341,8 @@ _cfgip_list_files_new() { ...@@ -1338,8 +1341,8 @@ _cfgip_list_files_new() {
_cfgip_add_find_files "/data/services/web/pc/pc-vue2-editor" 5000 js json conf _cfgip_add_find_files "/data/services/web/pc/pc-vue2-editor" 5000 js json conf
_cfgip_add_find_files "/data/services/web/pc/pc-vue2-main" 5000 js json conf _cfgip_add_find_files "/data/services/web/pc/pc-vue2-main" 5000 js json conf
_cfgip_add_find_files "/data/services/web/pc/pc-vue2-meetingControl" 5000 js json conf _cfgip_add_find_files "/data/services/web/pc/pc-vue2-meetingControl" 5000 js json conf
_cfgip_add_find_files "/data/services/web/pc/pc-vue2-meetngV2" 5000 js json conf _cfgip_add_find_files "/data/services/web/pc/pc-vue2-meetingV2" 5000 js json conf
_cfgip_add_find_files "/data/services/web/pc/pc-vue2-meetngV3" 5000 js json conf _cfgip_add_find_files "/data/services/web/pc/pc-vue2-meetingV3" 5000 js json conf
_cfgip_add_find_files "/data/services/web/pc/pc-vue2-moniter" 5000 js json conf _cfgip_add_find_files "/data/services/web/pc/pc-vue2-moniter" 5000 js json conf
_cfgip_add_find_files "/data/services/web/pc/pc-vue2-platform" 5000 js json conf _cfgip_add_find_files "/data/services/web/pc/pc-vue2-platform" 5000 js json conf
_cfgip_add_find_files "/data/services/web/pc/pc-vue2-voice" 5000 js json conf _cfgip_add_find_files "/data/services/web/pc/pc-vue2-voice" 5000 js json conf
...@@ -3685,8 +3688,8 @@ export_config_and_logs() { ...@@ -3685,8 +3688,8 @@ export_config_and_logs() {
"pc-vue2-ai|/data/services/web/pc/pc-vue2-ai|static/config.json" "pc-vue2-ai|/data/services/web/pc/pc-vue2-ai|static/config.json"
"pc-vue2-backstage|/data/services/web/pc/pc-vue2-backstage|static/config.json" "pc-vue2-backstage|/data/services/web/pc/pc-vue2-backstage|static/config.json"
"pc-vue2-main|/data/services/web/pc/pc-vue2-main|static/config.json" "pc-vue2-main|/data/services/web/pc/pc-vue2-main|static/config.json"
"pc-vue2-meetngV2|/data/services/web/pc/pc-vue2-meetngV2|static/config.json" "pc-vue2-meetingV2|/data/services/web/pc/pc-vue2-meetingV2|static/config.json"
"pc-vue2-meetngV3|/data/services/web/pc/pc-vue2-meetngV3|static/config.json" "pc-vue2-meetingV3|/data/services/web/pc/pc-vue2-meetingV3|static/config.json"
"pc-vue2-meetingControl|/data/services/web/pc/pc-vue2-meetingControl|static/config.json" "pc-vue2-meetingControl|/data/services/web/pc/pc-vue2-meetingControl|static/config.json"
"pc-vue2-moniter|/data/services/web/pc/pc-vue2-moniter|static/config.json" "pc-vue2-moniter|/data/services/web/pc/pc-vue2-moniter|static/config.json"
"pc-vue2-platform|/data/services/web/pc/pc-vue2-platform|static/config.json" "pc-vue2-platform|/data/services/web/pc/pc-vue2-platform|static/config.json"
......
...@@ -14,6 +14,9 @@ ...@@ -14,6 +14,9 @@
set -euo pipefail set -euo pipefail
# 设置终端编码为 UTF-8,防止中文输出乱码
export LC_ALL=C.UTF-8 2>/dev/null || export LC_ALL=en_US.UTF-8 2>/dev/null || true
# ------------------------------ # ------------------------------
# 基础配置 # 基础配置
# ------------------------------ # ------------------------------
...@@ -1321,8 +1324,8 @@ _cfgip_list_files_new() { ...@@ -1321,8 +1324,8 @@ _cfgip_list_files_new() {
_cfgip_add_find_files "/data/services/web/pc/pc-vue2-editor" 5000 js json conf _cfgip_add_find_files "/data/services/web/pc/pc-vue2-editor" 5000 js json conf
_cfgip_add_find_files "/data/services/web/pc/pc-vue2-main" 5000 js json conf _cfgip_add_find_files "/data/services/web/pc/pc-vue2-main" 5000 js json conf
_cfgip_add_find_files "/data/services/web/pc/pc-vue2-meetingControl" 5000 js json conf _cfgip_add_find_files "/data/services/web/pc/pc-vue2-meetingControl" 5000 js json conf
_cfgip_add_find_files "/data/services/web/pc/pc-vue2-meetngV2" 5000 js json conf _cfgip_add_find_files "/data/services/web/pc/pc-vue2-meetingV2" 5000 js json conf
_cfgip_add_find_files "/data/services/web/pc/pc-vue2-meetngV3" 5000 js json conf _cfgip_add_find_files "/data/services/web/pc/pc-vue2-meetingV3" 5000 js json conf
_cfgip_add_find_files "/data/services/web/pc/pc-vue2-moniter" 5000 js json conf _cfgip_add_find_files "/data/services/web/pc/pc-vue2-moniter" 5000 js json conf
_cfgip_add_find_files "/data/services/web/pc/pc-vue2-platform" 5000 js json conf _cfgip_add_find_files "/data/services/web/pc/pc-vue2-platform" 5000 js json conf
_cfgip_add_find_files "/data/services/web/pc/pc-vue2-voice" 5000 js json conf _cfgip_add_find_files "/data/services/web/pc/pc-vue2-voice" 5000 js json conf
......
...@@ -362,8 +362,8 @@ update_frontend() { ...@@ -362,8 +362,8 @@ update_frontend() {
log_info "更新预定系统前台包" log_info "更新预定系统前台包"
if [ "$platform" = "new" ]; then if [ "$platform" = "new" ]; then
frontend_path="/data/services/web/pc/pc-vue2-meetngV2" frontend_path="/data/services/web/pc/pc-vue2-meetingV2"
package_name="pc-vue2-meetngV2" package_name="pc-vue2-meetingV2"
package_dir="" package_dir=""
else else
frontend_path="/var/www/java/ubains-web-2.0" frontend_path="/var/www/java/ubains-web-2.0"
......
...@@ -116,8 +116,8 @@ function Test-NewPlatformIPs { ...@@ -116,8 +116,8 @@ function Test-NewPlatformIPs {
"/data/services/web/pc/pc-vue2-editor", "/data/services/web/pc/pc-vue2-editor",
"/data/services/web/pc/pc-vue2-main", "/data/services/web/pc/pc-vue2-main",
"/data/services/web/pc/pc-vue2-meetingControl", "/data/services/web/pc/pc-vue2-meetingControl",
"/data/services/web/pc/pc-vue2-meetngV2", "/data/services/web/pc/pc-vue2-meetingV2",
"/data/services/web/pc/pc-vue2-meetngV3", "/data/services/web/pc/pc-vue2-meetingV3",
"/data/services/web/pc/pc-vue2-moniter", "/data/services/web/pc/pc-vue2-moniter",
"/data/services/web/pc/pc-vue2-platform", "/data/services/web/pc/pc-vue2-platform",
"/data/services/web/pc/pc-vue2-voice", "/data/services/web/pc/pc-vue2-voice",
...@@ -335,8 +335,8 @@ function Test-NewPlatformConsole { ...@@ -335,8 +335,8 @@ function Test-NewPlatformConsole {
"/data/services/web/pc/pc-vue2-editor", "/data/services/web/pc/pc-vue2-editor",
"/data/services/web/pc/pc-vue2-main", "/data/services/web/pc/pc-vue2-main",
"/data/services/web/pc/pc-vue2-meetingControl", "/data/services/web/pc/pc-vue2-meetingControl",
"/data/services/web/pc/pc-vue2-meetngV2", "/data/services/web/pc/pc-vue2-meetingV2",
"/data/services/web/pc/pc-vue2-meetngV3", "/data/services/web/pc/pc-vue2-meetingV3",
"/data/services/web/pc/pc-vue2-moniter", "/data/services/web/pc/pc-vue2-moniter",
"/data/services/web/pc/pc-vue2-platform", "/data/services/web/pc/pc-vue2-platform",
"/data/services/web/pc/pc-vue2-voice", "/data/services/web/pc/pc-vue2-voice",
......
...@@ -30,8 +30,8 @@ $script:NewPlatformFrontend = @( ...@@ -30,8 +30,8 @@ $script:NewPlatformFrontend = @(
@{ Name = "pc-vue2-ai"; BaseDir = "/data/services/web/pc/pc-vue2-ai"; Files = @("static/config.json") } @{ Name = "pc-vue2-ai"; BaseDir = "/data/services/web/pc/pc-vue2-ai"; Files = @("static/config.json") }
@{ Name = "pc-vue2-backstage"; BaseDir = "/data/services/web/pc/pc-vue2-backstage"; Files = @("static/config.json") } @{ Name = "pc-vue2-backstage"; BaseDir = "/data/services/web/pc/pc-vue2-backstage"; Files = @("static/config.json") }
@{ Name = "pc-vue2-main"; BaseDir = "/data/services/web/pc/pc-vue2-main"; Files = @("static/config.json") } @{ Name = "pc-vue2-main"; BaseDir = "/data/services/web/pc/pc-vue2-main"; Files = @("static/config.json") }
@{ Name = "pc-vue2-meetngV2"; BaseDir = "/data/services/web/pc/pc-vue2-meetngV2"; Files = @("static/config.json") } @{ Name = "pc-vue2-meetingV2"; BaseDir = "/data/services/web/pc/pc-vue2-meetingV2"; Files = @("static/config.json") }
@{ Name = "pc-vue2-meetngV3"; BaseDir = "/data/services/web/pc/pc-vue2-meetngV3"; Files = @("static/config.json") } @{ Name = "pc-vue2-meetingV3"; BaseDir = "/data/services/web/pc/pc-vue2-meetingV3"; Files = @("static/config.json") }
@{ Name = "pc-vue2-meetingControl"; BaseDir = "/data/services/web/pc/pc-vue2-meetingControl"; Files = @("static/config.json") } @{ Name = "pc-vue2-meetingControl"; BaseDir = "/data/services/web/pc/pc-vue2-meetingControl"; Files = @("static/config.json") }
@{ Name = "pc-vue2-moniter"; BaseDir = "/data/services/web/pc/pc-vue2-moniter"; Files = @("static/config.json") } @{ Name = "pc-vue2-moniter"; BaseDir = "/data/services/web/pc/pc-vue2-moniter"; Files = @("static/config.json") }
@{ Name = "pc-vue2-platform"; BaseDir = "/data/services/web/pc/pc-vue2-platform"; Files = @("static/config.json") } @{ Name = "pc-vue2-platform"; BaseDir = "/data/services/web/pc/pc-vue2-platform"; Files = @("static/config.json") }
......
...@@ -1284,13 +1284,24 @@ function Test-MySQLDeepCheck { ...@@ -1284,13 +1284,24 @@ function Test-MySQLDeepCheck {
return $results return $results
} }
# 执行远程脚本(参数:容器名 密码) # 执行远程脚本
# 密码中的特殊字符(如 &)需要用反引号转义,避免 PowerShell 将其解释为后台运算符 # 密码中可能包含 shell 特殊字符(反引号、&、$ 等),直接通过命令行传递会被多层解释。
$escapedPassword = $mysqlPassword -replace '&', '`&' # 解决方案:先通过 SSH 将密码写入远程临时文件,脚本从文件读取密码,执行后立即删除临时文件。
$execCmd = "/root/$deepScriptName $actualContainer $escapedPassword" $tmpPwdFile = "/tmp/.mysql_deep_pwd_$$"
Write-Log -Level "INFO" -Message "[MySQL深度] 执行: $execCmd" # 使用 base64 编码传递密码,避免任何 shell 特殊字符被解释
$pwdBytes = [System.Text.Encoding]::UTF8.GetBytes($mysqlPassword)
$pwdB64 = [Convert]::ToBase64String($pwdBytes)
$writePwdCmd = "echo '$pwdB64' | base64 -d > $tmpPwdFile && chmod 600 $tmpPwdFile"
Invoke-SSHCommand -HostName $Server.IP -User $Server.User -Pass $Server.Pass -Port $Server.Port -Command $writePwdCmd | Out-Null
# 执行脚本(从文件读取密码,不通过命令行传递)
$execCmd = "/root/$deepScriptName $actualContainer $tmpPwdFile"
Write-Log -Level "INFO" -Message "[MySQL深度] 执行: /root/$deepScriptName $actualContainer [密码通过文件传递]"
$deepResult = Invoke-SSHCommand -HostName $Server.IP -User $Server.User -Pass $Server.Pass -Port $Server.Port -Command $execCmd $deepResult = Invoke-SSHCommand -HostName $Server.IP -User $Server.User -Pass $Server.Pass -Port $Server.Port -Command $execCmd
# 清理临时密码文件
Invoke-SSHCommand -HostName $Server.IP -User $Server.User -Pass $Server.Pass -Port $Server.Port -Command "rm -f $tmpPwdFile" | Out-Null
if ($deepResult.ExitCode -ne 0 -or -not $deepResult.Output) { if ($deepResult.ExitCode -ne 0 -or -not $deepResult.Output) {
Write-Log -Level "WARN" -Message "[MySQL深度] 脚本执行失败 (ExitCode=$($deepResult.ExitCode))" Write-Log -Level "WARN" -Message "[MySQL深度] 脚本执行失败 (ExitCode=$($deepResult.ExitCode))"
Write-Log -Level "INFO" -Message "========== MySQL 深度检测完成(共 0 项) ==========" Write-Log -Level "INFO" -Message "========== MySQL 深度检测完成(共 0 项) =========="
......
...@@ -455,11 +455,11 @@ package_meeting_system_services() { ...@@ -455,11 +455,11 @@ package_meeting_system_services() {
((fail_count++)) ((fail_count++))
fi fi
# 14. pc-vue2-meetngV2 (static + index.html + js) # 14. pc-vue2-meetingV2 (static + index.html + js)
if copy_frontend_files \ if copy_frontend_files \
"/data/services/web/pc/pc-vue2-meetngV2" \ "/data/services/web/pc/pc-vue2-meetingV2" \
"${OFFLINE_PACKAGE_PATH}/data/services/web/pc/pc-vue2-meetngV2" \ "${OFFLINE_PACKAGE_PATH}/data/services/web/pc/pc-vue2-meetingV2" \
"pc-vue2-meetngV2" \ "pc-vue2-meetingV2" \
"true" \ "true" \
"false"; then "false"; then
((success_count++)) ((success_count++))
...@@ -467,11 +467,11 @@ package_meeting_system_services() { ...@@ -467,11 +467,11 @@ package_meeting_system_services() {
((fail_count++)) ((fail_count++))
fi fi
# 15. pc-vue2-meetngV3 (static + index.html + js) # 15. pc-vue2-meetingV3 (static + index.html + js)
if copy_frontend_files \ if copy_frontend_files \
"/data/services/web/pc/pc-vue2-meetngV3" \ "/data/services/web/pc/pc-vue2-meetingV3" \
"${OFFLINE_PACKAGE_PATH}/data/services/web/pc/pc-vue2-meetngV3" \ "${OFFLINE_PACKAGE_PATH}/data/services/web/pc/pc-vue2-meetingV3" \
"pc-vue2-meetngV3" \ "pc-vue2-meetingV3" \
"true" \ "true" \
"false"; then "false"; then
((success_count++)) ((success_count++))
......
...@@ -362,8 +362,8 @@ update_frontend() { ...@@ -362,8 +362,8 @@ update_frontend() {
log_info "更新预定系统前台包" log_info "更新预定系统前台包"
if [ "$platform" = "new" ]; then if [ "$platform" = "new" ]; then
frontend_path="/data/services/web/pc/pc-vue2-meetngV2" frontend_path="/data/services/web/pc/pc-vue2-meetingV2"
package_name="pc-vue2-meetngV2" package_name="pc-vue2-meetingV2"
package_dir="" package_dir=""
else else
frontend_path="/var/www/java/ubains-web-2.0" frontend_path="/var/www/java/ubains-web-2.0"
......
# 问题描述
## 问题现象
- 在执行代码后GUI模式下,上传ERP失败
# 报错日志信息
```ignorelang
2026-06-16 14:18:37,965 - erp_uploader - INFO - ✓ 抄送人匹配: 陈泽键 → ID: 441
Exception in Tkinter callback
Traceback (most recent call last):
File "E:\Python\lib\tkinter\__init__.py", line 1921, in __call__
return self.func(*args)
File "E:\Python\lib\tkinter\__init__.py", line 839, in callit
func(*args)
File "E:\GithubData\ubains-module-test\AuxiliaryTool\FunctionalTestReportGeneration\src\gui.py", line 900, in <lambda>
self.root.after(0, lambda: self._show_error_dialog("错误", f"上传异常:\n{str(e)}"))
NameError: free variable 'e' referenced before assignment in enclosing scope
```
\ No newline at end of file
# 计划执行文档 - GUI模式下提示上传异常问题处理
## 执行时间
- 创建时间:2026-06-16
- 更新时间:2026-06-16
- 执行状态:已完成
---
## 问题概述
在 GUI 模式下执行代码后,上传 ERP 失败时,弹窗未正确显示真正的上传异常信息,反而抛出 `NameError`,导致用户既看不到上传失败的真实原因,又产生一个新的 Tkinter 回调异常。
**报错日志信息:**
```ignorelang
2026-06-16 14:18:37,965 - erp_uploader - INFO - ✓ 抄送人匹配: 陈泽键 → ID: 441
Exception in Tkinter callback
Traceback (most recent call last):
File "E:\Python\lib\tkinter\__init__.py", line 1921, in __call__
return self.func(*args)
File "E:\Python\lib\tkinter\__init__.py", line 839, in callit
func(*args)
File "...\src\gui.py", line 900, in <lambda>
self.root.after(0, lambda: self._show_error_dialog("错误", f"上传异常:\n{str(e)}"))
NameError: free variable 'e' referenced before assignment in enclosing scope
```
**预期行为:** 上传失败时弹窗应正确显示真实的上传异常信息(即 `upload_report_to_erp` 抛出的异常内容),不应产生新的 `NameError`
---
## 执行步骤
### 步骤 1:问题定位
- [x] 读取问题文档
- [x] 分析报错堆栈:异常发生在 `self.root.after(0, lambda: ...)` 调度的 lambda 回调中
- [x] 定位问题代码:
- `src/gui.py` 第 900 行(`_upload_to_erp_thread` 方法内的 `except` 块)—— 报告的 Bug
- `src/gui.py` 第 488 行(`_generate_report_thread` 方法内的 `except` 块)—— 相同模式的潜在 Bug
### 步骤 2:根因分析
**问题代码位置:**
```python
# src/gui.py 第 898-900 行 - _upload_to_erp_thread 方法
except Exception as e:
self._log(f"[X] 上传异常: {str(e)}", "error")
self.root.after(0, lambda: self._show_error_dialog("错误", f"上传异常:\n{str(e)}"))
```
**根本原因:Python 3 异常变量作用域 + 闭包延迟执行的冲突**
1. **`except ... as e` 变量会被自动删除**:根据 PEP 3110,Python 3 在 `except` 块结束时(无论正常结束还是发生异常)会**自动删除**异常变量 `e`,以打破异常对象造成的引用循环。因此 `e` 仅在 `except` 块执行期间有效。
2. **lambda 以闭包引用(按名)而非按值捕获 `e`**:lambda 内部的 `{str(e)}` 引用了外层作用域的 `e`,Python 编译器据此将 `e` 标记为闭包单元格变量(closure cell)。
3. **`root.after(0, ...)` 延迟执行导致捕获到「空」变量**`after` 把 lambda 调度到 Tkinter 主事件循环稍后执行。等 lambda 真正被调用时,`except` 块早已结束、`e` 已被删除,闭包单元格为空 → 触发 `NameError: free variable 'e' referenced before assignment in enclosing scope`
4. **为何 `self._log(...)` 第 899 行不报错**`self._log(f"[X] 上传异常: {str(e)}")``except` 块内**立即执行**,此时 `e` 仍然有效,所以能正常打印真实异常。只有**延迟执行**的 lambda 才会失败。这也是该 Bug 具有迷惑性的原因——日志里能看到真实异常,但弹窗却报 `NameError`
5. **潜在 Bug**`_generate_report_thread` 方法的第 488 行存在完全相同的代码模式 `self.root.after(0, lambda: self._show_error_dialog("错误", f"生成报告失败:\n{str(e)}"))`,一旦生成报告抛出异常,将触发同样的 `NameError`
### 步骤 3:解决方案
**方案:在构造 lambda 之前将异常信息捕获到普通局部变量,并通过默认参数绑定到 lambda**
闭包问题的本质是「按名延迟引用了会被删除的变量」。解决方法是让 lambda 持有**值**而非**对 `e` 的引用**
1. 进入 `except` 块后,立即将 `str(e)` 保存到普通局部变量 `err_msg`(普通局部变量不会被自动删除)。
2. lambda 通过默认参数 `msg=err_msg` 绑定该值(默认参数在 lambda 创建时求值,瞬间完成快照)。
3. 这样无论 `e` 何时被删除,lambda 始终持有有效的错误信息字符串。
**修改位置:**
- `src/gui.py` 第 898-900 行(`_upload_to_erp_thread` 方法)—— 报告的 Bug
- `src/gui.py` 第 484-488 行(`_generate_report_thread` 方法)—— 相同模式的潜在 Bug
### 步骤 4:修复执行
- [x] 修复 `_upload_to_erp_thread` 中第 900 行的 lambda 闭包 Bug
- [x] 修复 `_generate_report_thread` 中第 488 行的同类潜在 Bug
- [x] 验证修复结果(语法、闭包捕获逻辑正确)
---
## 执行结果
### 已修改文件
| 文件 | 修改位置 | 修改内容 | 状态 |
|------|----------|----------|------|
| `src/gui.py` | 第 898-900 行(`_upload_to_erp_thread`) | 修复 lambda 闭包捕获已删除异常变量 `e` 的 Bug | ✅ 已修复 |
| `src/gui.py` | 第 484-488 行(`_generate_report_thread`) | 修复同类潜在 lambda 闭包 Bug | ✅ 已修复 |
### 修复详情
**修复 1:`src/gui.py` `_upload_to_erp_thread` 方法(第 898-900 行)**
**修复前代码:**
```python
except Exception as e:
self._log(f"[X] 上传异常: {str(e)}", "error")
self.root.after(0, lambda: self._show_error_dialog("错误", f"上传异常:\n{str(e)}"))
```
**修复后代码:**
```python
except Exception as e:
# 提前将异常信息捕获到普通局部变量
# 原因:Python 3 在 except 块结束时会自动删除异常变量 e,
# 而 root.after 延迟执行的 lambda 以闭包形式按名引用 e,
# 等 lambda 真正执行时 e 已被删除,导致 NameError。
# 因此先转为字符串并通过默认参数按值绑定到 lambda。
err_msg = str(e)
self._log(f"[X] 上传异常: {err_msg}", "error")
self.root.after(0, lambda msg=err_msg: self._show_error_dialog("错误", f"上传异常:\n{msg}"))
```
**修复 2:`src/gui.py` `_generate_report_thread` 方法(第 484-488 行)**
**修复前代码:**
```python
except Exception as e:
self._log(f"\n✗ 生成报告失败: {str(e)}", "error")
# 在主线程中显示错误消息框(使用自定义对话框)
self.root.after(0, lambda: self._show_error_dialog("错误", f"生成报告失败:\n{str(e)}"))
```
**修复后代码:**
```python
except Exception as e:
# 提前将异常信息捕获到普通局部变量(避免 except 变量 e 被自动删除后
# 延迟执行的 lambda 引用到已删除变量而触发 NameError)
err_msg = str(e)
self._log(f"\n✗ 生成报告失败: {err_msg}", "error")
# 在主线程中显示错误消息框(使用自定义对话框)
self.root.after(0, lambda msg=err_msg: self._show_error_dialog("错误", f"生成报告失败:\n{msg}"))
```
---
## 问题状态
| 状态 | 说明 |
|------|------|
| 问题类型 | GUI 异常处理逻辑缺陷(闭包变量作用域) |
| 根本原因 | `except ... as e` 的变量 `e` 在块结束被自动删除,而 `root.after` 延迟执行的 lambda 按闭包引用了 `e` |
| 影响范围 | GUI 模式下报告生成失败 / ERP 上传失败的错误弹窗提示(次要影响:掩盖了真实异常的弹窗展示,日志中仍可见真实异常) |
| 当前状态 | ✅ 已修复 |
| 修复日期 | 2026-06-16 |
| 验证状态 | ✅ 已验证(代码逻辑与闭包捕获正确) |
---
## 规范文档
- 代码规范: `Docs/PRD/01规范文档/_PRD_规范文档_代码规范.md`
- 问题总结: `Docs/PRD/01规范文档/_PRD_问题总结_记录文档.md`
- 方法总结: `Docs/PRD/01规范文档/_PRD_方法总结_记录文档.md`
- 文档规范: `Docs/PRD/01规范文档/_PRD_规范文档_文档规范.md`
---
*文档结束*
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论