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

feat(skills): 新增UploadSelfCheck技能、Troubleshoot排查知识库页面及网盘上传模块

UploadSelfCheck 技能 (SKILL.md):
- 服务自检工具网盘上传技能,上传前自动备份原有工具包

Troubleshoot 排查知识库页面 (Troubleshoot助手.html):
- 独立 HTML 页面,18 个问题分类覆盖 346 条历史记录
- 支持选分类→填现象→直接输出排查步骤、典型根因、处置方案
- 拆分设备终端为门口屏/无纸化/桌牌,各子设备独立排查链路
- 新增安卓信息界面、人脸签到、数据同步、安全漏洞、版本兼容分类
- 离线可用,无需服务器

DailyReport 技能 (SKILL.md):
- 工作日报生成技能(上次提交 5e69d228 遗漏补充)

网盘上传模块 (upload_to_nas.py):
- 服务自检工具网盘上传模块,支持打包并上传至网盘指定目录
Co-Authored-By: 's avatarClaude <noreply@anthropic.com>
上级 5e69d228
---
name: UploadSelfCheck
description: 服务自检工具网盘上传 - 将验证好的ServiceSelfInspection工具打包并上传至网盘指定目录,上传前自动备份原有工具包
---
将验证完成的服务自检工具(ServiceSelfInspection)打包为 zip 并上传至网盘指定目录。上传前自动检测网盘已有工具包,先备份到 `backup/` 子目录再上传。
## Usage
/UploadSelfCheck
无额外参数,执行完整流程:验证源目录 → 打包 → 备份旧包 → 上传 → 生成MD5。
## Description
`AuxiliaryTool/ScriptTool/ServiceSelfInspection/` 目录整体打包为 zip,上传至网盘 `\\192.168.9.9\发布版本\10辅助工具箱\现场排查\Window\` 目录。上传前检查网盘是否已有旧工具包,如有则移动到 `backup/` 子目录备份。
- **上传脚本**: `AuxiliaryTool/ScriptTool/ServiceSelfInspection/upload_to_nas.py`
**功能:**
- 验证 ServiceSelfInspection 目录完整性(核心脚本必须存在)
- 将整个目录打包为 `ServiceSelfInspection.zip`
- 检测网盘已有工具包,自动备份到 `backup/` 子目录
- 上传 zip 包和 MD5 校验文件到网盘
- 输出上传结果摘要(文件大小、MD5、网盘路径)
- 清理本地临时 zip 文件
## 关键配置信息
### 源目录
- **路径**: `AuxiliaryTool/ScriptTool/ServiceSelfInspection/`
- **核心文件(必须存在)**:
- `check_server_health.sh` — Linux 服务健康检查主脚本
- `check_server_health.ps1` — Windows 服务健康检查主脚本
- `check_server_health_auto.sh` — 自动模式检查脚本
- `lib/` — 共享库目录
- `modules/` — 检查模块目录
### 网盘路径
- **目标目录**: `\\192.168.9.9\发布版本\10辅助工具箱\现场排查\Window\`
- **备份目录**: `\\192.168.9.9\发布版本\10辅助工具箱\现场排查\Window\backup\`
- **访问方式**: Windows UNC 路径(无需网络驱动器映射)
### 网盘账号
- **IP**: 192.168.9.9
- **共享名**: 发布版本
## 执行步骤
### 阶段1: 源目录验证
1. 检查 `AuxiliaryTool/ScriptTool/ServiceSelfInspection/` 目录是否存在
2. 验证核心脚本文件存在:
- `check_server_health.sh`
- `check_server_health.ps1`
- `check_server_health_auto.sh`
- `lib/` 目录
- `modules/` 目录
3. 如有缺失文件,列出缺失项并终止操作
### 阶段2: 网盘连接验证
1. 检查网盘目标路径 `\\192.168.9.9\发布版本\10辅助工具箱\现场排查\Window\` 是否可访问
2. 如不可访问,尝试创建目标目录
3. 确保 `backup/` 子目录存在
4. 如路径不可达,输出明确提示:"网盘路径不可访问,请检查:① 是否连接公司网络 ② 是否具有 \\192.168.9.9\发布版本 的访问权限"并终止
### 阶段3: 打包
1. 使用 Python `zipfile``shutil.make_archive` 将整个 ServiceSelfInspection 目录打包为 `ServiceSelfInspection.zip`
2. 打包路径:项目根目录下的临时文件
3. 计算打包后的文件大小和 MD5
4. **禁止中断打包操作**
### 阶段4: 备份旧工具包
1. 检查网盘目标目录是否存在 `ServiceSelfInspection.zip`
2. 如存在,移动到 `backup/` 子目录,重命名为 `ServiceSelfInspection_备份_YYYYMMDD_HHMMSS.zip`
3. 同步备份对应的 `.md5` 文件(如有)
4. 打印备份信息
### 阶段5: 上传
1.`ServiceSelfInspection.zip` 复制到网盘目标目录
2.`ServiceSelfInspection.zip.md5` 复制到网盘目标目录
3. 验证上传完整性(网盘文件大小与本地一致)
### 阶段6: 清理与报告
1. 删除本地临时 zip 和 md5 文件
2. 输出上传结果摘要:
```
=== 服务自检工具上传完成 ===
上传路径: \\192.168.9.9\发布版本\10辅助工具箱\现场排查\Window\ServiceSelfInspection.zip
文件大小: XX.XX MB
MD5: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
备份文件: ServiceSelfInspection_备份_YYYYMMDD_HHMMSS.zip
状态: 成功
```
## 注意事项
1. **上传前必须先备份**:网盘已有工具包必须先移到 backup/ 再上传新包,不可直接覆盖
2. **打包过程不可中断**:zip 打包过程中禁止中断操作
3. **网盘不可达时终止**:路径不可访问时明确告知用户原因并终止,不得跳过
4. **核心文件校验**:源目录缺少核心文件时必须终止,不得上传不完整的工具包
5. **本地临时文件清理**:上传成功或失败后均需清理本地临时 zip/md5 文件
6. **MD5 必须同步上传**:zip 包和 .md5 校验文件必须一起上传
## 排除项(打包时不包含)
打包时自动排除以下文件和目录:
- `__pycache__/` 目录
- `*.pyc` 文件
- `.git/` 目录
- 临时文件(`*.tmp`
- `upload_to_nas.py`(上传脚本自身不打包进工具包)
# -*- coding: utf-8 -*-
"""
服务自检工具 - 网盘上传模块
将验证好的 ServiceSelfInspection 目录打包为 zip,上传至网盘指定目录。
上传前自动备份网盘已有工具包到 backup/ 子目录。
网盘路径: \\\\192.168.9.9\\发布版本\\10辅助工具箱\\现场排查\\Window\\
备份路径: \\\\192.168.9.9\\发布版本\\10辅助工具箱\\现场排查\\Window\\backup\\
使用方式:
python upload_to_nas.py
或从项目根目录:
python AuxiliaryTool/ScriptTool/ServiceSelfInspection/upload_to_nas.py
"""
import os
import sys
import shutil
import hashlib
import zipfile
from datetime import datetime
from pathlib import Path
# ============================================================
# 配置区
# ============================================================
# 网盘目标路径 (UNC路径)
NAS_TARGET_DIR = r"\\192.168.9.9\发布版本\10辅助工具箱\现场排查\Window"
NAS_BACKUP_DIR = os.path.join(NAS_TARGET_DIR, "backup")
# 工具包名称
PACKAGE_NAME = "ServiceSelfInspection"
ZIP_FILENAME = f"{PACKAGE_NAME}.zip"
MD5_FILENAME = f"{ZIP_FILENAME}.md5"
# 备份时间戳格式
BACKUP_TIMESTAMP_FORMAT = "%Y%m%d_%H%M%S"
# 核心文件列表(必须存在,否则终止上传)
CORE_FILES = [
"check_server_health.sh",
"check_server_health.ps1",
"check_server_health_auto.sh",
"lib", # 共享库目录
"modules", # 检查模块目录
]
# 打包时排除的文件和目录
EXCLUDE_PATTERNS = [
"__pycache__",
"*.pyc",
".git",
"*.tmp",
"upload_to_nas.py", # 上传脚本自身不打包
]
def validate_source_dir(source_dir: str) -> list:
"""
验证源目录是否存在且包含核心文件
检查 ServiceSelfInspection 目录是否存在,以及所有核心脚本和子目录是否齐全。
Args:
source_dir: ServiceSelfInspection 目录的绝对路径
Returns:
缺失的核心文件/目录列表,空列表表示验证通过
"""
source_path = Path(source_dir)
if not source_path.exists():
raise FileNotFoundError(f"源目录不存在: {source_dir}")
if not source_path.is_dir():
raise NotADirectoryError(f"路径不是目录: {source_dir}")
missing = []
for core_file in CORE_FILES:
core_path = source_path / core_file
if not core_path.exists():
missing.append(core_file)
if missing:
print(f"\n 缺失核心文件/目录 ({len(missing)}项):")
for f in missing:
print(f" - {f}")
return missing
def validate_nas_path(path: str) -> bool:
"""
验证网盘路径是否可访问
先检查路径是否存在,如不存在则尝试创建(验证父目录权限)。
Args:
path: 网盘路径(UNC路径)
Returns:
True 表示路径可访问,False 表示不可访问
"""
target = Path(path)
try:
if target.exists():
if os.access(str(target), os.W_OK):
print(f" 网盘路径可访问: {path}")
return True
else:
print(f" 网盘路径无写入权限: {path}")
return False
else:
# 路径不存在,尝试创建
target.mkdir(parents=True, exist_ok=True)
print(f" 网盘目录已创建: {path}")
return True
except (OSError, PermissionError) as e:
print(f" 网盘路径不可访问: {path}")
print(f" 错误: {e}")
return False
def backup_existing(target_dir: str, filename: str) -> bool:
"""
备份网盘已有工具包到 backup/ 子目录
将目标目录中已存在的工具包移动到 backup/ 子目录,
重命名为 原文件名_备份_时间戳.扩展名。
Args:
target_dir: 网盘目标目录路径
filename: 需要备份的文件名(如 ServiceSelfInspection.zip)
Returns:
True 表示备份成功或无需备份,False 表示备份失败
"""
target_file = Path(target_dir) / filename
if not target_file.exists():
print(f" 网盘无旧工具包,跳过备份")
return True
try:
# 确保备份目录存在
backup_dir = Path(NAS_BACKUP_DIR)
backup_dir.mkdir(parents=True, exist_ok=True)
# 生成备份文件名
timestamp = datetime.now().strftime(BACKUP_TIMESTAMP_FORMAT)
stem = Path(filename).stem
suffix = Path(filename).suffix
backup_name = f"{stem}_备份_{timestamp}{suffix}"
backup_path = backup_dir / backup_name
# 移动文件到备份目录
shutil.move(str(target_file), str(backup_path))
print(f" 已备份: {filename} -> backup/{backup_name}")
# 同步备份对应的 .md5 文件(如有)
md5_file = Path(target_dir) / f"{filename}.md5"
if md5_file.exists():
md5_backup_name = f"{backup_name}.md5"
md5_backup_path = backup_dir / md5_backup_name
shutil.move(str(md5_file), str(md5_backup_path))
print(f" 已备份: {filename}.md5 -> backup/{md5_backup_name}")
return True
except (OSError, PermissionError) as e:
print(f" 备份失败: {filename}, 错误: {e}")
return False
def generate_md5(file_path: str) -> str:
"""
生成文件的 MD5 校验值
分块读取大文件,避免内存溢出。
Args:
file_path: 文件路径
Returns:
文件的 MD5 十六进制字符串
"""
md5_hash = hashlib.md5()
with open(file_path, "rb") as f:
for chunk in iter(lambda: f.read(8192), b""):
md5_hash.update(chunk)
return md5_hash.hexdigest()
def write_md5_file(file_path: str, md5_value: str) -> str:
"""
将 MD5 值写入 .md5 校验文件
格式: <md5_value> <filename>
Args:
file_path: 被校验的文件路径(用于提取文件名)
md5_value: MD5 十六进制字符串
Returns:
.md5 文件的路径
"""
md5_file_path = file_path + ".md5"
filename = os.path.basename(file_path)
with open(md5_file_path, "w", encoding="utf-8") as f:
f.write(f"{md5_value} {filename}\n")
return md5_file_path
def upload_file(source_path: str, target_dir: str) -> str:
"""
上传单个文件到网盘目标目录
通过 shutil.copy2 复制文件(保留元数据),
上传后验证目标文件大小与源文件一致。
Args:
source_path: 本地文件路径
target_dir: 网盘目标目录路径
Returns:
上传后的完整文件路径
Raises:
FileNotFoundError: 源文件不存在
PermissionError: 目标目录无写入权限
OSError: 文件复制失败或大小不一致
"""
source = Path(source_path)
if not source.exists():
raise FileNotFoundError(f"源文件不存在: {source_path}")
target = Path(target_dir) / source.name
try:
shutil.copy2(str(source), str(target))
# 验证上传完整性
source_size = source.stat().st_size
target_size = target.stat().st_size
if source_size != target_size:
raise OSError(
f"上传后文件大小不一致: 本地 {source_size} bytes, "
f"网盘 {target_size} bytes"
)
return str(target)
except (OSError, PermissionError) as e:
raise OSError(f"文件上传失败: {source_path} -> {target_dir}, 错误: {e}")
def format_size(size_bytes: int) -> str:
"""
将字节数格式化为可读的文件大小
Args:
size_bytes: 文件大小(字节)
Returns:
格式化的文件大小字符串(如 "12.35 MB")
"""
for unit in ["B", "KB", "MB", "GB"]:
if size_bytes < 1024.0:
return f"{size_bytes:.2f} {unit}"
size_bytes /= 1024.0
return f"{size_bytes:.2f} TB"
def _should_exclude(file_path: str, source_dir: str) -> bool:
"""
判断文件/目录是否应排除在打包之外
Args:
file_path: 文件或目录的绝对路径
source_dir: 源目录的绝对路径
Returns:
True 表示应排除
"""
import fnmatch
# 获取相对于源目录的路径
rel_path = os.path.relpath(file_path, source_dir)
name = os.path.basename(file_path)
for pattern in EXCLUDE_PATTERNS:
# 精确匹配文件名
if name == pattern:
return True
# 通配符匹配
if "*" in pattern and fnmatch.fnmatch(name, pattern):
return True
# 匹配路径中的任意部分
if pattern in rel_path.split(os.sep):
return True
return False
def package_directory(source_dir: str, output_zip: str) -> tuple:
"""
将目录打包为 zip 文件
递归打包整个目录,自动排除 EXCLUDE_PATTERNS 中指定的文件。
Args:
source_dir: 要打包的源目录路径
output_zip: 输出的 zip 文件路径
Returns:
(zip文件路径, 文件大小bytes, MD5值)
"""
source_path = Path(source_dir)
print(f"\n 正在打包: {source_dir}")
print(f" 输出: {output_zip}")
file_count = 0
excluded_count = 0
with zipfile.ZipFile(output_zip, "w", zipfile.ZIP_DEFLATED) as zf:
for root, dirs, files in os.walk(source_dir):
# 过滤要排除的目录(原地修改 dirs 列表)
dirs[:] = [
d for d in dirs
if not _should_exclude(os.path.join(root, d), source_dir)
]
for file in files:
file_abs = os.path.join(root, file)
if _should_exclude(file_abs, source_dir):
excluded_count += 1
continue
# zip 内的相对路径
arcname = os.path.relpath(file_abs, os.path.dirname(source_dir))
zf.write(file_abs, arcname)
file_count += 1
# 计算文件大小和 MD5
zip_size = os.path.getsize(output_zip)
zip_md5 = generate_md5(output_zip)
print(f" 打包完成: {file_count} 个文件 (排除 {excluded_count} 个)")
print(f" 文件大小: {format_size(zip_size)}")
print(f" MD5: {zip_md5}")
return output_zip, zip_size, zip_md5
def package_and_upload(source_dir: str, target_dir: str) -> dict:
"""
完整上传流程:验证 → 打包 → 备份 → 上传 → MD5 → 清理
这是主入口函数,执行从源目录验证到上传完成的全部步骤。
Args:
source_dir: ServiceSelfInspection 目录的绝对路径
target_dir: 网盘目标目录路径
Returns:
结果字典: {
"success": bool,
"zip_path": str, # 网盘上的 zip 完整路径
"md5_path": str, # 网盘上的 md5 完整路径
"file_size": str, # 可读的文件大小
"md5_value": str, # MD5 值
"backup_name": str|None, # 备份文件名(如无备份则为 None)
"error": str|None, # 错误信息(成功时为 None)
}
"""
result = {
"success": False,
"zip_path": "",
"md5_path": "",
"file_size": "",
"md5_value": "",
"backup_name": None,
"error": None,
}
# 临时文件路径(放在项目根目录)
script_dir = os.path.dirname(os.path.abspath(__file__))
project_root = os.path.dirname(os.path.dirname(os.path.dirname(script_dir)))
local_zip = os.path.join(project_root, ZIP_FILENAME)
try:
# ---- 阶段1: 验证源目录 ----
print("=" * 60)
print("阶段1: 验证源目录")
print("=" * 60)
missing = validate_source_dir(source_dir)
if missing:
result["error"] = f"源目录缺少核心文件: {', '.join(missing)}"
print(f"\n ✗ {result['error']}")
return result
print(" ✓ 源目录验证通过")
# ---- 阶段2: 网盘连接验证 ----
print(f"\n{'=' * 60}")
print("阶段2: 网盘连接验证")
print("=" * 60)
if not validate_nas_path(target_dir):
result["error"] = (
"网盘路径不可访问,请检查:\n"
" ① 是否连接公司网络\n"
" ② 是否具有 \\\\192.168.9.9\\发布版本 的访问权限\n"
" ③ 当前 Windows 用户是否已授权该共享目录"
)
print(f"\n ✗ {result['error']}")
return result
print(" ✓ 网盘连接验证通过")
# ---- 阶段3: 打包 ----
print(f"\n{'=' * 60}")
print("阶段3: 打包工具包")
print("=" * 60)
zip_path, zip_size, zip_md5 = package_directory(source_dir, local_zip)
result["file_size"] = format_size(zip_size)
result["md5_value"] = zip_md5
# ---- 阶段4: 备份旧工具包 ----
print(f"\n{'=' * 60}")
print("阶段4: 备份网盘旧工具包")
print("=" * 60)
if not backup_existing(target_dir, ZIP_FILENAME):
result["error"] = "备份网盘旧工具包失败,终止上传"
print(f"\n ✗ {result['error']}")
return result
print(" ✓ 备份阶段完成")
# ---- 阶段5: 上传 ----
print(f"\n{'=' * 60}")
print("阶段5: 上传到网盘")
print("=" * 60)
# 5a. 生成 MD5 校验文件
local_md5 = write_md5_file(local_zip, zip_md5)
print(f" 生成MD5校验文件: {os.path.basename(local_md5)}")
# 5b. 上传 zip 包
print(f" 正在上传: {ZIP_FILENAME} ...")
uploaded_zip = upload_file(local_zip, target_dir)
result["zip_path"] = uploaded_zip
print(f" ✓ {ZIP_FILENAME} 上传成功")
# 5c. 上传 md5 文件
md5_basename = os.path.basename(local_md5)
print(f" 正在上传: {md5_basename} ...")
uploaded_md5 = upload_file(local_md5, target_dir)
result["md5_path"] = uploaded_md5
print(f" ✓ {md5_basename} 上传成功")
result["success"] = True
# ---- 阶段6: 清理 ----
print(f"\n{'=' * 60}")
print("阶段6: 清理本地临时文件")
print("=" * 60)
for tmp_file in [local_zip, local_md5]:
try:
if os.path.exists(tmp_file):
os.remove(tmp_file)
print(f" 已清理: {os.path.basename(tmp_file)}")
except OSError as e:
print(f" 清理失败: {os.path.basename(tmp_file)}, 错误: {e}")
print(" ✓ 清理完成")
return result
except Exception as e:
result["error"] = str(e)
print(f"\n ✗ 上传流程异常: {e}")
# 异常时也尝试清理临时文件
for tmp_file in [local_zip, local_zip + ".md5"]:
try:
if os.path.exists(tmp_file):
os.remove(tmp_file)
except OSError:
pass
return result
def print_summary(result: dict):
"""
打印上传结果摘要
Args:
result: package_and_upload() 返回的结果字典
"""
print(f"\n{'=' * 60}")
print("上传结果摘要")
print("=" * 60)
if result["success"]:
print(f" 状态: ✓ 成功")
print(f" 上传路径: {result['zip_path']}")
print(f" MD5路径: {result['md5_path']}")
print(f" 文件大小: {result['file_size']}")
print(f" MD5: {result['md5_value']}")
if result["backup_name"]:
print(f" 备份文件: {result['backup_name']}")
else:
print(f" 备份文件: 无(首次上传)")
else:
print(f" 状态: ✗ 失败")
print(f" 错误信息: {result['error']}")
print("=" * 60)
def main():
"""
命令行入口函数
从脚本所在位置推导项目根目录和 ServiceSelfInspection 源目录,
执行完整的上传流程。
"""
# 推导路径:脚本在 AuxiliaryTool/ScriptTool/ServiceSelfInspection/ 下
script_dir = os.path.dirname(os.path.abspath(__file__))
source_dir = script_dir # 脚本所在目录即为源目录
print("=" * 60)
print(" 服务自检工具 - 网盘上传")
print("=" * 60)
print(f" 源目录: {source_dir}")
print(f" 目标路径: {NAS_TARGET_DIR}")
print(f" 备份路径: {NAS_BACKUP_DIR}")
result = package_and_upload(source_dir, NAS_TARGET_DIR)
print_summary(result)
# 返回退出码(0=成功, 1=失败)
sys.exit(0 if result["success"] else 1)
if __name__ == "__main__":
main()
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>问题排查助手 - Troubleshoot</title>
<style>
:root {
--bg: #0f172a; --card: #1e293b; --border: #334155; --text: #e2e8f0;
--muted: #94a3b8; --accent: #38bdf8; --accent-hover: #0ea5e9;
--danger: #f87171; --success: #34d399; --warn: #fbbf24; --tag-bg: #1e3a5f;
--step-bg: #0c1929;
}
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: -apple-system, "Microsoft YaHei", "PingFang SC", sans-serif;
background: var(--bg); color: var(--text); min-height: 100vh; line-height: 1.6;
}
.container { max-width: 960px; margin: 0 auto; padding: 24px 16px; }
header {
text-align: center; padding: 32px 0 24px;
border-bottom: 1px solid var(--border); margin-bottom: 28px;
}
header h1 { font-size: 28px; font-weight: 700; margin-bottom: 6px; }
header h1 span { color: var(--accent); }
header p { color: var(--muted); font-size: 13px; }
.section {
background: var(--card); border: 1px solid var(--border);
border-radius: 12px; padding: 24px; margin-bottom: 20px;
}
.section-title {
font-size: 15px; font-weight: 600; margin-bottom: 16px;
display: flex; align-items: center; gap: 8px;
}
.section-title .icon { font-size: 18px; }
/* Category tags */
.tag-group { display: flex; flex-wrap: wrap; gap: 8px; }
.tag {
padding: 7px 14px; border-radius: 18px; border: 1px solid var(--border);
background: transparent; color: var(--muted); cursor: pointer;
font-size: 13px; transition: all 0.2s; user-select: none;
}
.tag:hover { border-color: var(--accent); color: var(--accent); }
.tag.active {
background: var(--tag-bg); border-color: var(--accent);
color: var(--accent); font-weight: 600;
}
/* Inputs */
.field { margin-bottom: 14px; }
.field:last-child { margin-bottom: 0; }
.field label {
display: block; font-size: 13px; font-weight: 500;
color: var(--muted); margin-bottom: 5px;
}
.field label .required { color: var(--danger); }
input, textarea {
width: 100%; padding: 10px 14px; background: #0f172a;
border: 1px solid var(--border); border-radius: 8px;
color: var(--text); font-size: 14px; font-family: inherit; transition: border-color 0.2s;
}
input:focus, textarea:focus {
outline: none; border-color: var(--accent);
box-shadow: 0 0 0 3px rgba(56,189,248,0.1);
}
textarea { resize: vertical; min-height: 64px; }
.quick-fill { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
@media (max-width: 600px) { .quick-fill { grid-template-columns: 1fr; } }
/* Buttons */
.btn-group { display: flex; gap: 12px; margin-top: 20px; }
.btn {
padding: 12px 28px; border-radius: 8px; font-size: 14px;
font-weight: 600; cursor: pointer; border: none; transition: all 0.2s; font-family: inherit;
}
.btn-primary { background: var(--accent); color: #0f172a; }
.btn-primary:hover { background: var(--accent-hover); }
.btn-outline { background: transparent; border: 1px solid var(--border); color: var(--text); }
.btn-outline:hover { border-color: var(--muted); }
/* === RESULT: Rich troubleshooting guide === */
.result { margin-top: 28px; display: none; }
.result.show { display: block; }
.result-summary {
background: linear-gradient(135deg, #1e3a5f 0%, #1e293b 100%);
border: 1px solid var(--accent); border-radius: 12px; padding: 20px 24px; margin-bottom: 20px;
}
.result-summary .rs-title { font-size: 16px; font-weight: 700; color: var(--accent); margin-bottom: 8px; }
.result-summary .rs-row { font-size: 13px; color: var(--muted); margin-bottom: 4px; }
.result-summary .rs-row b { color: var(--text); }
/* Step card */
.step-card {
background: var(--card); border: 1px solid var(--border);
border-radius: 10px; margin-bottom: 14px; overflow: hidden;
}
.step-card-header {
padding: 14px 18px; display: flex; align-items: center; gap: 10px;
cursor: pointer; user-select: none; transition: background 0.2s;
}
.step-card-header:hover { background: rgba(56,189,248,0.04); }
.step-num {
width: 28px; height: 28px; border-radius: 50%;
background: var(--tag-bg); color: var(--accent);
display: flex; align-items: center; justify-content: center;
font-size: 13px; font-weight: 700; flex-shrink: 0;
}
.step-title { font-size: 14px; font-weight: 600; flex: 1; }
.step-arrow { color: var(--muted); font-size: 12px; transition: transform 0.2s; }
.step-card.open .step-arrow { transform: rotate(180deg); }
.step-body { display: none; padding: 0 18px 18px 56px; font-size: 13px; }
.step-card.open .step-body { display: block; }
.step-body ul { list-style: none; padding: 0; }
.step-body ul li {
padding: 7px 0; border-bottom: 1px solid rgba(51,65,85,0.4);
display: flex; gap: 8px;
}
.step-body ul li:last-child { border-bottom: none; }
.step-body .li-icon { flex-shrink: 0; width: 18px; text-align: center; }
.step-body code {
background: #0f172a; padding: 2px 7px; border-radius: 4px;
font-size: 12px; color: var(--accent); font-family: "Cascadia Code", "Fira Code", monospace;
}
/* Root cause table */
.root-cause-table {
width: 100%; border-collapse: collapse; font-size: 13px; margin-top: 8px;
}
.root-cause-table th {
text-align: left; padding: 8px 12px; background: var(--step-bg);
color: var(--muted); font-weight: 500; border-bottom: 1px solid var(--border);
}
.root-cause-table td {
padding: 10px 12px; border-bottom: 1px solid rgba(51,65,85,0.4);
vertical-align: top;
}
.root-cause-table tr:last-child td { border-bottom: none; }
.root-cause-table .rc-phenomenon { color: var(--warn); font-weight: 500; }
.root-cause-table .rc-fix { color: var(--success); }
/* Command block */
.cmd-block {
background: #0c1929; border: 1px solid var(--border);
border-radius: 8px; padding: 14px 18px; margin: 10px 0;
font-family: "Cascadia Code", "Fira Code", monospace; font-size: 12px;
line-height: 1.8; overflow-x: auto; white-space: pre;
}
/* Known trap alert */
.known-trap {
background: rgba(251,191,36,0.08); border-left: 3px solid var(--warn);
padding: 12px 16px; border-radius: 0 8px 8px 0; font-size: 13px; margin: 12px 0;
}
.known-trap .trap-label { color: var(--warn); font-weight: 600; }
/* Empty state */
.empty-state { text-align: center; padding: 40px 20px; color: var(--muted); }
.empty-state .empty-icon { font-size: 48px; margin-bottom: 12px; }
</style>
</head>
<body>
<div class="container">
<header>
<h1>🔍 <span>Troubleshoot</span> 问题排查知识库</h1>
<p>选分类、填现象 → 直接输出排查思路、典型根因、处置方案</p>
</header>
<!-- Category -->
<div class="section">
<div class="section-title"><span class="icon">📂</span> 问题分类(必选,可多选)</div>
<div class="tag-group" id="categoryTags">
<span class="tag" data-cat="服务/容器异常">服务/容器异常</span>
<span class="tag" data-cat="门口屏">门口屏</span>
<span class="tag" data-cat="无纸化">无纸化</span>
<span class="tag" data-cat="桌牌">桌牌</span>
<span class="tag" data-cat="配置文件">配置/参数不生效</span>
<span class="tag" data-cat="前端UI">前端/页面显示</span>
<span class="tag" data-cat="MQTT">MQTT/EMQX</span>
<span class="tag" data-cat="数据库">数据库/mysql/redis</span>
<span class="tag" data-cat="部署升级">部署/升级</span>
<span class="tag" data-cat="网络连通">网络/SSH/端口</span>
<span class="tag" data-cat="时区时间">时区/时间</span>
<span class="tag" data-cat="第三方对接">SMC/华为/腾讯/钉钉</span>
<span class="tag" data-cat="授权激活">授权/激活/cert</span>
<span class="tag" data-cat="磁盘资源">磁盘/CPU/内存</span>
<span class="tag" data-cat="邮件短信">邮件/短信通知</span>
<span class="tag" data-cat="安卓信息界面">安卓信息界面</span>
<span class="tag" data-cat="人脸签到">人脸/签到/白名单</span>
<span class="tag" data-cat="数据同步">数据同步(OA/第三方)</span>
<span class="tag" data-cat="安全漏洞">安全漏洞/等保</span>
<span class="tag" data-cat="版本兼容">版本兼容/升级降级</span>
</div>
</div>
<!-- Core Info -->
<div class="section">
<div class="section-title"><span class="icon">📝</span> 问题详情</div>
<div class="field">
<label><span class="required">*</span> 现象描述</label>
<textarea id="phenomenon" rows="2" placeholder="尽量原样描述:报错原文?哪个页面/接口?什么操作后出现的?"></textarea>
</div>
<div class="quick-fill">
<div class="field">
<label>环境/服务器</label>
<input type="text" id="env" placeholder="例:9.76 / 5.46 / 展厅 …">
</div>
<div class="field">
<label>最近变更</label>
<input type="text" id="recentChange" placeholder="例:刚升级了后端包 / 改了配置 …">
</div>
</div>
</div>
<!-- Buttons -->
<div class="btn-group">
<button class="btn btn-primary" onclick="diagnose()">🔬 分析排查思路</button>
<button class="btn btn-outline" onclick="clearAll()">🔄 清空重来</button>
</div>
<!-- Result -->
<div class="result" id="resultBox">
<!-- Populated by JS -->
</div>
<footer style="text-align:center;color:var(--muted);font-size:12px;padding:32px 0;border-top:1px solid var(--border);margin-top:40px;">
基于 346 条历史问题记录提炼 · 覆盖 13 类高频故障场景 · 与 Troubleshoot Skill 联动
</footer>
</div>
<script>
// ============================================================
// Knowledge Base — extracted from 346 historical problem records
// ============================================================
const KB = {
'服务/容器异常': {
summary: '服务无法访问、页面 502/500、接口报错、容器 Exited → 自底向上排:容器存活 → 退出原因 → 数据库连通 → Redis 连通 → Nacos → 磁盘空间',
steps: [
{
title: '容器存活检查',
body: `<ul>
<li><span class="li-icon">▶</span> <code>docker ps -a | grep &lt;服务名&gt;</code> — 区分 Running / Exited / 不存在</li>
<li><span class="li-icon">▶</span> 如果容器 Exited:看退出码。退出码 0 + 日志有 terminate_signal → 外部 stop;非 0 / OOMKilled → 服务异常或内存溢出</li>
<li><span class="li-icon">▶</span> 如果容器不存在:查是否被误删或部署未完成</li>
</ul>`
},
{
title: '查日志找退出原因',
body: `<ul>
<li><span class="li-icon">▶</span> <code>docker logs --tail 100 &lt;容器&gt;</code>,找 ERROR / FATAL / Connection refused</li>
<li><span class="li-icon">▶</span> 最常见关键词:<code>mysql connection refused</code> → 数据库问题;<code>redis connection refused</code> → Redis 问题</li>
</ul>`
},
{
title: '数据库连通性(最高频根因)',
body: `<ul>
<li><span class="li-icon">▶</span> 数据库容器是否正常:<code>docker ps | grep umysql</code></li>
<li><span class="li-icon">▶</span> 数据库是否导入完成:磁盘 IO 慢时(5-6秒/表),服务可能先于数据库就绪 → 等数据库完全就绪再重启服务</li>
<li><span class="li-icon">▶</span> 凭据是否正确:<code>grep</code> 检查服务配置文件中的数据库密码是否和历史一致</li>
<li><span class="li-icon">▶</span> 等保测评后密码修改可能漏掉某些用户(如 mysql@172)</li>
</ul>`
},
{
title: 'Redis 连通性',
body: `<ul>
<li><span class="li-icon">▶</span> <code>docker exec &lt;ujava容器&gt; curl -s uredis:6379</code> — 容器内需能通</li>
<li><span class="li-icon">▶</span> ⚠️ 常见坑:服务器网口变更后 docker 网络隔离,容器间不通 → <code>systemctl restart docker</code></li>
<li><span class="li-icon">▶</span> Redis 文件损坏(断电导致)→ 删除数据文件 + 重启容器</li>
</ul>`
},
{
title: 'Nacos 状态',
body: `<ul>
<li><span class="li-icon">▶</span> mysql 未就绪时 nacos 先启动,连不上库且不自动重连 → 配置监测脚本 + 定时自愈</li>
<li><span class="li-icon">▶</span> Nacos 登录"账号密码错误" → 数据库被替换后凭据不一致 → 替换整个 nacos + 数据库</li>
</ul>`
},
{
title: '磁盘空间',
body: `<ul>
<li><span class="li-icon">▶</span> <code>df -h</code> 确认磁盘使用率</li>
<li><span class="li-icon">▶</span> df 显示正常但实际满了?→ <code>lsof | grep deleted</code> 查"删除但进程持有未释放"的文件 → kill 进程 + 重启</li>
</ul>`
},
],
rootCauses: [
{ phenomenon: '服务启动后 500,日志 mysql connection refused', cause: '数据库未就绪 / 连不上', fix: '等数据库完全导入后再重启服务;磁盘 IO 慢时设 5 分钟等待' },
{ phenomenon: '后端 URL 指向错误 IP(如 193)', cause: '配置文件缺少 ubains.inner.server.url', fix: '补充配置项 + 重启' },
{ phenomenon: '容器内 curl 不通 uredis', cause: '服务器网口变更后 docker 网络隔离', fix: '重启 docker 服务' },
{ phenomenon: '服务器重启后服务没自动启动', cause: 'rc.local 不生效', fix: '增加定时检测脚本(每 N 分钟检测 + 自启动)' },
{ phenomenon: 'Nacos 所有服务正常但登录失败', cause: 'Nacos 数据库被替换凭据不一致', fix: '替换 nacos 和对应数据库' },
],
traps: [
'ARM Java 启动约 12 分钟,勿因慢判挂',
'Nacos 控制台登录坏 ≠ nacos 挂了,业务面可能正常',
'容器内勿用 netstat(EMQX 5.x 无此命令),用 ss -tln',
],
},
'门口屏': {
summary: '门口屏不显示会议/绑定失败/签到失败/闪退/时间不同步 → 三板斧:版本 → 日志 → MQTT 连通',
steps: [
{
title: '先看版本(最多坑)',
body: `<ul>
<li><span class="li-icon">▶</span> 门口屏 apk/pak 版本是否最新?旧版本有已知 bug</li>
<li><span class="li-icon">▶</span> 旧版没有默认 MQTT 账号密码 → 升级 pak(新版已内置默认凭据)</li>
<li><span class="li-icon">▶</span> 门口屏 4.0 vs 5.0:人脸同步策略不同;刷新方式不同(4.0 web 端并发刷新 → 5.0 设备主动上报)</li>
</ul>`
},
{
title: '看设备日志(通常直接指向问题)',
body: `<ul>
<li><span class="li-icon">▶</span> "授权码不存在" → 数据库被重建,授权码变化</li>
<li><span class="li-icon">▶</span> "无效 token" / "JWT 校验失败" → 授权码绑定的是旧服务器</li>
<li><span class="li-icon">▶</span> "ip 无权限" → 出口 IP 不在企业微信可信域名</li>
<li><span class="li-icon">▶</span> "占位错误 404" → 版本不匹配</li>
<li><span class="li-icon">▶</span> "接口请求超时" → 对外服务挂了 / 内存不够(run.sh 内存从 1G 改到 2G)</li>
<li><span class="li-icon">▶</span> MQTT 连接错误 / 网络异常 → MQTT 账号密码缺失或错误</li>
</ul>`
},
{
title: 'MQTT 连通性',
body: `<ul>
<li><span class="li-icon">▶</span> EMQX 容器是否正常:<code>docker ps | grep uemqx</code></li>
<li><span class="li-icon">▶</span> 门口屏程序是否有默认 MQTT 账号密码?(新版本有)</li>
<li><span class="li-icon">▶</span> 后端配置文件(config.json/prod)是否配置了 MQTT 信息</li>
</ul>`
},
{
title: 'NTP 时间同步(门口屏绑定/显示异常常见根因)',
body: `<ul>
<li><span class="li-icon">▶</span> 服务器 chronyd 服务是否启动:<code>systemctl status chronyd</code></li>
<li><span class="li-icon">▶</span> chronyd.conf 是否有 <code>allow all</code> 配置</li>
<li><span class="li-icon">▶</span> 123 端口是否监听:<code>ss -tln | grep 123</code></li>
<li><span class="li-icon">▶</span> 后端 prod 配置文件是否缺少 <code>ubains.ntp.url</code> 配置项</li>
</ul>`
},
{
title: '批量更新 / 设备管理',
body: `<ul>
<li><span class="li-icon">▶</span> 批量更新 apk 部分失败 → 旧版前端包逐个调用 → 升级新版支持批量更新</li>
<li><span class="li-icon">▶</span> 更新后没重启设备 → 仍显示旧数据 → 更新后强制重启</li>
<li><span class="li-icon">▶</span> 修改会议室名称后门口屏未同步 → 预定系统会发 MQTT 通知,统一平台需新增逻辑</li>
<li><span class="li-icon">▶</span> 会议数据量过大(14万条)→ 128 台设备并发请求超时 → 定时备份 + 清理历史数据</li>
</ul>`
},
],
rootCauses: [
{ phenomenon: '门口屏绑定 MQTT 失败 / 提示网络异常', cause: '旧版门口屏没有默认 MQTT 账号密码', fix: '升级门口屏 pak(新版已内置)' },
{ phenomenon: '更新包后仍显示旧数据', cause: '更新后没有重启设备应用', fix: '重启门口屏 APP' },
{ phenomenon: '升级后所有人脸都能签到', cause: '人脸数据未清空重新同步', fix: '清空人脸库 → 重新同步' },
{ phenomenon: '旧设备报无效 token', cause: '服务器重装后授权码变化', fix: '升级无纸化程序重新绑定新授权码' },
{ phenomenon: '门口屏获取信息失败提示 ip 无权限', cause: '出口 IP 变更不在可信域名', fix: '重新配置企业微信可信域名' },
{ phenomenon: '绑定会议室提示接口超时', cause: '对外服务挂了 / 内存不够', fix: '重启对外服务 + 调整 run.sh 内存配置' },
{ phenomenon: '绑定会议室提示错误的路径(含123端口)', cause: '后端缺少 ntp.server.url 配置项', fix: 'prod 文件增加 ubains.ntp.url 配置' },
{ phenomenon: '门口屏不显示,日志接口超时', cause: '历史会议数据量过大(14万条)', fix: '定时备份清理 + 限制返回量' },
],
traps: [
'旧版门口屏没有默认 MQTT 账号密码,必须升级 pak',
'更新包后一定要重启设备 APP,否则不生效',
'NTP 123 端口不通是绑定失败的常见隐蔽原因',
'对外服务 run.sh 内存默认 1G 不够,建议最小 2G 最大 8G',
],
},
'无纸化': {
summary: '无纸化屏报错/不刷新/同屏异常/白板上传失败 → 设备日志 → 座位编排 → 缓存 → 终端版本',
steps: [
{
title: '设备日志与报错定位',
body: `<ul>
<li><span class="li-icon">▶</span> 无纸化屏直接提示报错信息 → 先看屏幕上显示什么</li>
<li><span class="li-icon">▶</span> 报错通常指向:MQTT 连接失败、授权码问题、版本不匹配</li>
<li><span class="li-icon">▶</span> 无纸化系统有相关日志,后端没有相关日志时看设备端</li>
</ul>`
},
{
title: '座位编排后无纸化不刷新',
body: `<ul>
<li><span class="li-icon">▶</span> 座位号出现重复 ID → 前端增加去重加强限制</li>
<li><span class="li-icon">▶</span> 无纸化和桌牌同时不刷新 → 查座位编排接口提交日志</li>
</ul>`
},
{
title: '同屏/投屏问题',
body: `<ul>
<li><span class="li-icon">▶</span> 同屏时快时慢/自动断开 → 网络稳定性问题</li>
<li><span class="li-icon">▶</span> 大屏 ID 输入后保存重启清空 → 需要勾选"管理员"才能保存(界面右下角)</li>
</ul>`
},
{
title: '缓存与版本问题',
body: `<ul>
<li><span class="li-icon">▶</span> 同名文件打开的还是旧内容 → 平板缓存未清除 → 增加清除缓存机制</li>
<li><span class="li-icon">▶</span> 旧设备无法重新绑定 → 升级无纸化程序,绑定新授权码</li>
<li><span class="li-icon">▶</span> 全开 50 台 CPU 500% → 每 15 秒轮询接口 + 缺少索引 → 增加数据库索引</li>
</ul>`
},
],
rootCauses: [
{ phenomenon: '无纸化屏报错(断电后)', cause: 'EMQX 容器文件损坏', fix: '重建 EMQX 容器' },
{ phenomenon: '座位编排后无纸化不刷新', cause: '座位号 ID 重复', fix: '前端增加去重限制' },
{ phenomenon: '同名文件打开旧内容', cause: '平板缓存未清除', fix: '增加会议结束清缓存 + 应用启动清缓存' },
{ phenomenon: '大屏 ID 保存后重启清空', cause: '未勾选管理员选项', fix: '勾选右下角管理员后保存' },
{ phenomenon: '50台全开 CPU 500%', cause: '频繁轮询 + 无索引', fix: '增加数据库索引' },
{ phenomenon: '旧平板设备无法重新绑定', cause: '授权码变化', fix: '升级无纸化程序重新绑定' },
],
traps: [
'无纸化屏的报错信息通常直接显示在屏幕上,先看屏幕',
'大屏 ID 保存需要勾选管理员,容易被忽略',
'平板有本地缓存,同名文件不会自动刷新',
],
},
'桌牌': {
summary: '桌牌不显示/投图失败/同步异常/单点登录无设备 → config 配置(color/epd_type) → 版本兼容 → 底图格式 → 路径',
steps: [
{
title: 'config 配置排查(最高频根因——三色/四色桌牌配置不同)',
body: `<ul>
<li><span class="li-icon">▶</span> <b>核心配置项</b>:<code>color</code> 和 <code>epd_type</code> — 三色桌牌 vs 四色桌牌值不同</li>
<li><span class="li-icon">▶</span> 四色桌牌:<code>color=4, epd_type=9</code></li>
<li><span class="li-icon">▶</span> 更新服务后 config 配置可能被覆盖 → 需要复原</li>
<li><span class="li-icon">▶</span> 非标准版桌牌 → config 中 <code>epd_type</code> 参数不一致 → 查桌牌日志获取实际值</li>
</ul>`
},
{
title: '桌牌投图不显示',
body: `<ul>
<li><span class="li-icon">▶</span> 配置项正确但投图报错"缺少字段" → 对内包版本太低(需超过 734 版本)</li>
<li><span class="li-icon">▶</span> 定时任务包需要配置项支持 → 检查 prod 配置文件</li>
<li><span class="li-icon">▶</span> 底图格式问题:jpg 可能不生效 → 改用 png 格式</li>
<li><span class="li-icon">▶</span> 桌牌系统日志无异常但就是不刷新 → 查基站连接、端口、配置</li>
</ul>`
},
{
title: '单点登录后桌牌设备不显示',
body: `<ul>
<li><span class="li-icon">▶</span> 前端 config 中 color/epd_type 配置缺失或错误 → 修改配置后刷新</li>
<li><span class="li-icon">▶</span> 桌牌未部署 → 按桌牌部署文档部署</li>
<li><span class="li-icon">▶</span> 跳转类型错误 → 排查登录 vs 直接登录参数区别</li>
</ul>`
},
{
title: '桌牌同步/下载异常',
body: `<ul>
<li><span class="li-icon">▶</span> 电子桌牌同步异常 → 非标准版桌牌 + config 参数不对</li>
<li><span class="li-icon">▶</span> 名单模版下载报错 → 版本包路径变化 → 移动模板到 <code>/var/www/wifi-local/public/meeting/pc/smartTable</code></li>
<li><span class="li-icon">▶</span> 桌牌开关未开启 → 后台开启桌牌配置</li>
<li><span class="li-icon">▶</span> 会议室同步桌牌数据失败 → config 中 color/epd_type 需更新</li>
</ul>`
},
],
rootCauses: [
{ phenomenon: '桌牌投图不显示 / 报错缺少字段', cause: '对内包版本低于 734 + config 配置缺失', fix: '升级对内包 + 补全配置项' },
{ phenomenon: '单点登录后桌牌设备列表为空', cause: 'config 中 color/epd_type 配置错误', fix: '四色桌牌设置 color=4, epd_type=9' },
{ phenomenon: '桌牌不刷新', cause: '底图 jpg 格式不兼容', fix: '改用 png 格式底图' },
{ phenomenon: '电子桌牌同步异常', cause: '非标准版桌牌 + config 参数不匹配', fix: '修改 config 中的 epd_type 为桌牌实际值' },
{ phenomenon: '桌牌名单模版下载报错', cause: '版本包路径变更', fix: '移动模板到 smartTable 目录' },
{ phenomenon: '更新服务后桌牌设备消失', cause: 'config 配置被覆盖', fix: '恢复 color 和 epd_type 配置值' },
],
traps: [
'三色桌牌和四色桌牌的 color/epd_type 值不同,搞混就会不显示',
'更新服务后 config 配置可能被覆盖,务必检查复原',
'jpg 底图可能不兼容,优先用 png',
'桌牌未部署时提示不明确,先确认是否已部署',
],
},
'配置文件': {
summary: '改了配置不生效 / 开关关闭仍执行 / 参数值不对 → 确认生效文件 → 查重复 key → 查代码是否读了配置项',
steps: [
{
title: '确认改的是服务实际加载的文件',
body: `<ul>
<li><span class="li-icon">▶</span> 配置文件常有"挂载卷/模板/容器内"多份副本,改的可能不是服务读取的那份</li>
<li><span class="li-icon">▶</span> <code>docker inspect &lt;容器&gt;</code> 看挂载路径;查进程启动参数指向的路径</li>
</ul>`
},
{
title: '全文搜索同名配置项(核心坑)',
body: `<ul>
<li><span class="li-icon">▶</span> <code>grep -n "配置项名" &lt;文件&gt;</code> — 列出所有匹配行</li>
<li><span class="li-icon">▶</span> ⚠️ 同文件两个相同 key → <b>后一个覆盖前一个</b>。改了前面没改后面 = 改了等于没改</li>
<li><span class="li-icon">▶</span> 处理:删掉重复的 key 只保留一份(推荐),或全部改齐</li>
</ul>`
},
{
title: '检查代码是否读取了该配置项',
body: `<ul>
<li><span class="li-icon">▶</span> 界面有关闭开关但代码未判断该配置项 → 配置不生效</li>
<li><span class="li-icon">▶</span> 后端需增加判断逻辑</li>
</ul>`
},
{
title: '特殊场景',
body: `<ul>
<li><span class="li-icon">▶</span> 配置文件缺失:新版本代码需要新配置项,旧配置文件没有 → 补上</li>
<li><span class="li-icon">▶</span> config.js 中字段名拼错 / 与代码中不一致 → 修正</li>
<li><span class="li-icon">▶</span> 数据库同时存在加密/不加密两条配置 → 删掉错误的那条</li>
<li><span class="li-icon">▶</span> Nginx 中 /api 和 /backstage/api 的 proxy_set_header 不一致 → token 绑定 IP 识别错误 → 闪退登录页</li>
</ul>`
},
],
rootCauses: [
{ phenomenon: '改了配置项重启后不生效', cause: '同文件存在两个相同 key,后一个覆盖前一个', fix: 'grep -n 全文确认无重复,删重复项或全部改齐' },
{ phenomenon: '关闭某功能开关后仍执行', cause: '代码中未读取/判断该配置项', fix: '后端增加配置项判断逻辑' },
{ phenomenon: '法语开关不生效', cause: 'config.js 中字段名写错', fix: '修正字段名' },
{ phenomenon: '短信发送失败', cause: '数据库同时存在加密/不加密两条阿里云配置', fix: '删除不加密的那条' },
{ phenomenon: '后台跳转闪退到登录页', cause: 'nginx 中 /api 和 /backstage/api 配置不一致', fix: '统一两个 location 的 proxy_set_header' },
],
traps: [
'改配置前先 grep -n 全文扫一遍,确认该项只出现一次',
'H5/Angular 部署后需清浏览器缓存才能看到配置变更',
],
},
'MQTT': {
summary: 'MQTT 连不上/认证失败/消息收不到/权限异常 → EMQX 容器状态 → 端口 → 认证(bootstrap) → ACL → 超级用户',
steps: [
{
title: 'EMQX 容器状态',
body: `<ul>
<li><span class="li-icon">▶</span> <code>docker ps -a --filter name=uemqx</code></li>
<li><span class="li-icon">▶</span> Exited(0) + 日志 received_terminate_signal → 外部 docker stop,非崩溃</li>
<li><span class="li-icon">▶</span> 异常断电 → EMQX 容器文件可能损坏 → 重建容器</li>
</ul>`
},
{
title: '端口监听检查',
body: `<ul>
<li><span class="li-icon">▶</span> <code>ss -tln | grep 1883</code>(宿主机侧)</li>
<li><span class="li-icon">▶</span> ⚠️ 勿在 EMQX 5.x 容器内用 netstat——镜像无此命令,会误判</li>
<li><span class="li-icon">▶</span> <code>bash -c 'exec 3<>"/dev/tcp/127.0.0.1/1883"'</code> 测试 TCP 握手</li>
</ul>`
},
{
title: '认证失败排查(bad_username_or_password)',
body: `<ul>
<li><span class="li-icon">▶</span> <code>docker logs --tail 50 uemqx | grep authentication_failure</code></li>
<li><span class="li-icon">▶</span> 查 bootstrap 凭据:<code>docker exec uemqx cat /opt/emqx/etc/auth-built-in-db-bootstrap.csv</code></li>
<li><span class="li-icon">▶</span> 常见坑:密码字符混淆(O vs 0,l vs 1,I vs l)</li>
</ul>`
},
{
title: 'ACL 授权排查',
body: `<ul>
<li><span class="li-icon">▶</span> 查 ACL 文件:<code>docker exec uemqx cat /opt/emqx/etc/acl.conf</code></li>
<li><span class="li-icon">▶</span> 规则从上到下匹配,先匹配到的生效</li>
<li><span class="li-icon">▶</span> 查超级用户:<code>grep</code> bootstrap 中的 is_superuser=TRUE → 绕过所有 ACL</li>
<li><span class="li-icon">▶</span> 查 no_match 策略:allow = 默认放行,deny = 默认拒绝</li>
</ul>`
},
],
rootCauses: [
{ phenomenon: 'MQTT 认证失败 bad_username_or_password', cause: '密码字符混淆(O vs 0 等)', fix: '查 bootstrap.csv 对照正确密码' },
{ phenomenon: 'mqtt@cmdb 能订阅"未授权"主题', cause: 'is_superuser=TRUE 绕过 ACL', fix: '确认超级用户预期行为' },
{ phenomenon: 'MQTT 连接报 Client is not connected', cause: '门口屏并发刷新过大导致 Java 连接通道断开', fix: '升级门口屏 5.0(设备主动上报)' },
{ phenomenon: 'EMQX 容器 Exited(0)', cause: '外部 docker stop,非服务崩溃', fix: 'docker start uemqx + 排查 cron/手动操作来源' },
{ phenomenon: 'EMQX 断电后异常连不上', cause: '容器文件损坏', fix: '重建 EMQX 容器' },
],
traps: [
`mqtt@client 密码 client@passwORD — ORD 是字母 O 不是数字 0`,
`mqtt@cmdb 是超级用户,默认绕过 ACL 检查`,
'EMQX 5.x 镜像无 netstat,端口检测必须在宿主机侧',
],
},
'数据库': {
summary: 'mysql/redis 连接失败、导入慢、数据不同步 → 容器状态 → 连通性 → 磁盘 IO → 凭据',
steps: [
{
title: 'mysql 连接失败',
body: `<ul>
<li><span class="li-icon">▶</span> 容器是否运行:<code>docker ps | grep umysql</code></li>
<li><span class="li-icon">▶</span> 端口是否监听:<code>ss -tln | grep 3306</code>(9.76 实际是 8306)</li>
<li><span class="li-icon">▶</span> 密码是否正确:等保整改后可能被修改 → 确认所有用户密码一致</li>
</ul>`
},
{
title: '数据库导入慢',
body: `<ul>
<li><span class="li-icon">▶</span> 磁盘 IO 性能差 → 5-6 秒/张表 → 服务先于数据库就绪 → 启动失败</li>
<li><span class="li-icon">▶</span> 自动化脚本增加等待时间(5 分钟)+ 超时提示</li>
</ul>`
},
{
title: 'Redis 问题',
body: `<ul>
<li><span class="li-icon">▶</span> 容器间连通:<code>docker exec ujava2 curl -s uredis:6379</code></li>
<li><span class="li-icon">▶</span> 文件损坏(断电):删除 dump.rdb + 重启</li>
</ul>`
},
],
rootCauses: [
{ phenomenon: '服务日志 mysql connection refused', cause: '数据库未完全就绪 / 密码错误', fix: '等数据库就绪 → 查密码' },
{ phenomenon: '数据库导入慢导致服务启动失败', cause: '磁盘 IO 性能差', fix: '增加等待时间 + 超时提示;换 SSD' },
{ phenomenon: 'Redis 容器正常但服务连不上', cause: '网口变更后 docker 网络隔离', fix: '重启 docker' },
],
traps: [
'9.76 的 mysql 端口是 8306(非默认 3306)',
'等保测评改密码可能漏掉 mysql@172 等用户',
],
},
'部署升级': {
summary: '部署失败/升级后异常 → SSH 登录方式 → 解压完整性 → 容器重启 → 脚本兼容性',
steps: [
{
title: 'SSH 登录',
body: `<ul>
<li><span class="li-icon">▶</span> 9.76:root 被禁,用 <b>admin/Ubains@123</b> + sudo</li>
<li><span class="li-icon">▶</span> 其他服务器:确认登录方式和凭据</li>
</ul>`
},
{
title: '部署过程中的坑',
body: `<ul>
<li><span class="li-icon">▶</span> 解压/传输中断:严禁中途打断解压,断了重来</li>
<li><span class="li-icon">▶</span> 数据库导入慢 → 脚本增加等待时间</li>
<li><span class="li-icon">▶</span> 旧脚本可能含废弃配置(如旧版 ntp)→ 屏蔽</li>
<li><span class="li-icon">▶</span> chronyd.conf 缺 allow all → NTP 同步失败</li>
</ul>`
},
{
title: '升级后异常',
body: `<ul>
<li><span class="li-icon">▶</span> cmdb 的 upython 子串误匹配到 upython_voice → 手动补重启</li>
<li><span class="li-icon">▶</span> ARM Nacos 反复崩溃 → cron 监控竞态,查 cron 非 nacos</li>
<li><span class="li-icon">▶</span> 升级 openssh 后系统重启 → 新版与统信内核不兼容 → 回退</li>
</ul>`
},
],
rootCauses: [
{ phenomenon: 'SSH root Authentication failed', cause: 'root 登录被禁', fix: '改用 admin + sudo' },
{ phenomenon: '升级 cmdb 后 upython 没更新', cause: 'upython 子串误匹配到 upython_voice', fix: '手动补重启 cmdb 的 upython 容器' },
{ phenomenon: 'openssh 升级后连接就重启', cause: '新版 openssh 与统信 1070e 内核不兼容', fix: '回退到官方 openssh 版本' },
{ phenomenon: 'ARM Nacos 反复崩溃', cause: 'cron 监控脚本竞态', fix: '修复 cron 脚本,非 nacos 问题' },
],
traps: [
'严禁中断解压操作',
'ARM Java 启动需 12 分钟,不要反复重启',
],
},
'网络连通': {
summary: 'SSH 不通/端口不通/网页访问不了 → 先确认网络层 → 防火墙 → 容器网络 → 应用层',
steps: [
{
title: '基础网络检查',
body: `<ul>
<li><span class="li-icon">▶</span> <code>nc -z &lt;ip&gt; &lt;port&gt;</code> 端口连通性</li>
<li><span class="li-icon">▶</span> <code>ping &lt;ip&gt;</code> 基础连通</li>
<li><span class="li-icon">▶</span> <code>ss -tln</code> 本机端口监听</li>
<li><span class="li-icon">▶</span> 防火墙:<code>firewall-cmd --list-all</code> 或 <code>iptables -L</code></li>
</ul>`
},
{
title: '容器网络',
body: `<ul>
<li><span class="li-icon">▶</span> 容器间通信:<code>docker exec &lt;容器A&gt; curl -s &lt;容器B&gt;:&lt;端口&gt;</code></li>
<li><span class="li-icon">▶</span> 网口变更后 docker 网络隔离 → 重启 docker</li>
<li><span class="li-icon">▶</span> 企业微信可信域名:出口 IP 变更后需重新配置</li>
</ul>`
},
],
rootCauses: [
{ phenomenon: '网页无法访问', cause: '防火墙 / 端口未放行', fix: '检查端口开放和防火墙规则' },
{ phenomenon: '容器间互不通', cause: '网口变更后 docker 网络隔离', fix: '重启 docker 服务' },
{ phenomenon: '设备提示 IP 无权限访问', cause: '出口 IP 不在企业微信可信域名', fix: '重新配置可信域名' },
],
traps: [
'网口/IP 变更后必须重启 docker,否则容器网络隔离',
'容器的 host 网络模式(uredis/utracker)端口直接在宿主上',
],
},
'前端UI': {
summary: '页面报错/显示异常/响应不生效/浏览器差异 → F12 Console → 浏览器差异 → 接口数据量 → Vue 数据流',
steps: [
{
title: 'F12 Console 看 JS 报错',
body: `<ul>
<li><span class="li-icon">▶</span> 打开开发者工具 → Console 面板 → 红色报错直接指向问题</li>
<li><span class="li-icon">▶</span> Network 面板看接口返回:状态码 + 响应体</li>
</ul>`
},
{
title: '浏览器差异',
body: `<ul>
<li><span class="li-icon">▶</span> Edge/Chrome 正常、360 浏览器异常 → 浏览器安全策略差异(如只能关闭代码打开的窗口)</li>
<li><span class="li-icon">▶</span> macOS 钉钉内置浏览器 → 特定兼容问题</li>
</ul>`
},
{
title: '接口返回数据量',
body: `<ul>
<li><span class="li-icon">▶</span> 页面卡顿/崩溃但无报错 → 可能是接口返回数据太大(>4MB)</li>
<li><span class="li-icon">▶</span> 启用 gzip:<code>server.compression.enabled=true</code></li>
<li><span class="li-icon">▶</span> 减少返回字段 / 分页 / 引入 Redis 缓存</li>
</ul>`
},
{
title: 'Vue/React 数据流',
body: `<ul>
<li><span class="li-icon">▶</span> prop 直接修改报错 → 在 data 中新建变量接收</li>
<li><span class="li-icon">▶</span> 屏幕适配:小屏幕显示不全 → 增加响应式判断</li>
</ul>`
},
],
rootCauses: [
{ phenomenon: '页面崩溃/系统网络异常提示', cause: '接口返回数据太大(>4MB)', fix: '启用 gzip 压缩 + 减少返回字段' },
{ phenomenon: 'Vue eslint 报错修改 prop', cause: '代码中直接修改 prop 参数', fix: '在 data 中新建变量接收' },
{ phenomenon: '360 浏览器无法关闭窗口', cause: '浏览器安全策略限制', fix: '改为打开新窗口而非路由替换' },
],
traps: [
'接口返回 >4MB 时页面崩溃但无报错——容易误判为网络问题',
'H5 页面更新后需清缓存才能看到变更',
],
},
'第三方对接': {
summary: 'SMC/华为/腾讯/钉钉/Exchange 对接失败 → 隔离测试 → 配置对比 → 日志分析',
steps: [
{
title: '先隔离——第三方平台自身是否正常',
body: `<ul>
<li><span class="li-icon">▶</span> 直接在 SMC/腾讯/华为平台创建会议是否正常?→ 排除第三方自身问题</li>
<li><span class="li-icon">▶</span> Exchange:在 Outlook 中直接操作是否正常?</li>
</ul>`
},
{
title: 'SMC 专项',
body: `<ul>
<li><span class="li-icon">▶</span> 提示"资源不足" → 会议室未分配终端时 SMC 服务区默认指向 default→ 后台编辑添加服务区</li>
<li><span class="li-icon">▶</span> 腾讯会议呼叫失败 → SMC 上腾讯会议配置不对 → 查 SC 管理配置</li>
<li><span class="li-icon">▶</span> 签名校验报错 → 未关闭签名校验 → 重新打包</li>
</ul>`
},
{
title: 'Exchange 专项',
body: `<ul>
<li><span class="li-icon">▶</span> 会议不同步 → 查同步日志 → 常见:账号密码错误 / 邮箱绑定不对</li>
<li><span class="li-icon">▶</span> 查 EWS 配置和会议室邮箱绑定</li>
</ul>`
},
],
rootCauses: [
{ phenomenon: '创建 SMC 会议提示"资源不足"', cause: '会议室未分配终端,服务区默认 default', fix: '后台添加可用服务区(如"111")' },
{ phenomenon: '腾讯会议呼叫失败', cause: 'SMC 上腾讯会议配置不正确', fix: '修改 SC 管理配置中的区域管理和查找规则' },
{ phenomenon: 'Exchange 会议不同步', cause: '账号密码错误 / 邮箱绑定不对', fix: '核对 EWS 配置和邮箱绑定' },
],
traps: [
'SMC 服务区为 default 的无法创建会议',
'腾讯会议账号需绑定手机号才能发送短信通知',
],
},
'授权激活': {
summary: '激活码失效/token 无效/登录失败/证书过期 → 激活码变化 → 证书 → JWT → 手动分发',
steps: [
{
title: '激活码变化',
body: `<ul>
<li><span class="li-icon">▶</span> 服务器重装/磁盘更换 → 激活码变化 → 需重新激活</li>
<li><span class="li-icon">▶</span> 现场机器激活码出现变化(如钉钉 H5 提示请求失败)→ 重新激活</li>
</ul>`
},
{
title: '证书 / JWT',
body: `<ul>
<li><span class="li-icon">▶</span> 单点登录失败 → 查证书是否过期 → 更新证书</li>
<li><span class="li-icon">▶</span> 老设备报无效 token / JWT 校验失败 → 旧设备绑定旧授权码 → 重新绑定</li>
</ul>`
},
{
title: 'ARM 架构授权',
body: `<ul>
<li><span class="li-icon">▶</span> license.zip 浏览器上传后不会自动分发 → 需手动解压 .uas 到各服务 license.txt</li>
</ul>`
},
],
rootCauses: [
{ phenomenon: '钉钉 H5 提示请求失败', cause: '机器激活码变化需重新激活', fix: '重新激活预定激活码' },
{ phenomenon: '单点登录失败', cause: '证书过期', fix: '更新证书' },
{ phenomenon: '旧设备无效 token', cause: '服务器重装授权码变化', fix: '重新绑定新授权码' },
{ phenomenon: 'ARM 授权后预定 500', cause: 'license 未手动分发', fix: '手动解压 .uas 到各服务 license.txt' },
],
traps: [
'ARM 架构授权必须手动分发 .uas 文件',
'admin 用户被禁用 → 用 SQL 启用即可',
],
},
'磁盘资源': {
summary: '磁盘满/CPU 高/内存溢出/OOM → df -h → lsof 查 deleted → 定时清理日志',
steps: [
{
title: '磁盘检查',
body: `<ul>
<li><span class="li-icon">▶</span> <code>df -h</code> — 看各分区使用率</li>
<li><span class="li-icon">▶</span> <code>du -sh /* 2>/dev/null | sort -rh | head -10</code> — 找空间大户</li>
<li><span class="li-icon">▶</span> df 显示有余量但实际满了 → <code>lsof | grep deleted</code> — 删除的文件被进程持有未释放</li>
</ul>`
},
{
title: 'CPU/内存',
body: `<ul>
<li><span class="li-icon">▶</span> <code>free -h</code> / <code>top</code> — 内存和 CPU 使用情况</li>
<li><span class="li-icon">▶</span> 无纸化屏 50 台全开 CPU 500% → 15 秒轮询接口 + 缺少索引 → 增加索引</li>
</ul>`
},
],
rootCauses: [
{ phenomenon: '服务不可用,查磁盘满', cause: '日志未清理 / 删除文件未释放', fix: '清理日志 + kill 持有进程 + 增加定时清理脚本' },
{ phenomenon: '无纸化屏全开 CPU 高', cause: '频繁轮询 + 无索引', fix: '增加数据库索引' },
{ phenomenon: '音频数据占满磁盘', cause: '历史音频数据太多', fix: '清理旧数据' },
],
traps: [
'删除文件后 df 不变化 → lsof | grep deleted → kill 持有进程',
'出货避免使用旧磁盘(IO 性能差导致数据库导入慢)',
],
},
'时区时间': {
summary: '会议时间偏差 8 小时 / 不能小于当前时间 → date → 容器时区 → Java 参数 → Python zoneinfo',
steps: [
{
title: '逐层检查时区',
body: `<ul>
<li><span class="li-icon">▶</span> 服务器:<code>date</code> + <code>timedatectl</code></li>
<li><span class="li-icon">▶</span> 容器:<code>docker exec &lt;容器&gt; date</code>(确认映射 /etc/localtime)</li>
<li><span class="li-icon">▶</span> Java:run.sh 中是否有 <code>-Duser.timezone=</code> 参数</li>
<li><span class="li-icon">▶</span> Python:精简容器缺 <code>/usr/share/zoneinfo</code> → 拷贝时区文件</li>
</ul>`
},
],
rootCauses: [
{ phenomenon: '会议时间晚 8 小时', cause: 'Python 精简容器缺少 zoneinfo', fix: '拷贝 /usr/share/zoneinfo 到容器 + 重新制作容器' },
{ phenomenon: '海外项目提示早于当前时间', cause: 'Java 未指定当地时区', fix: 'run.sh 增加 -Duser.timezone=Europe/Madrid' },
],
traps: [
'最简操作系统版容器可能没有 zoneinfo → python 时区不对',
'Java 启动参数 -Duser.timezone 要明确指定',
],
},
'邮件短信': {
summary: '收不到邮件/短信通知 → 查日志确认是否发送成功 → 第三方通道问题',
steps: [
{
title: '确认发送端是否正常',
body: `<ul>
<li><span class="li-icon">▶</span> 查后端日志是否打印"发送成功" → 打印了但收不到 = 第三方通道问题</li>
<li><span class="li-icon">▶</span> 邮件:查 Exchange / 邮件服务器配置、会议室邮箱绑定</li>
<li><span class="li-icon">▶</span> 短信:阿里云确认加密/不加密配置(不能同时有两条)</li>
<li><span class="li-icon">▶</span> 华为云短信:确认是否已对接</li>
<li><span class="li-icon">▶</span> 腾讯会议短信:需绑定手机号</li>
</ul>`
},
],
rootCauses: [
{ phenomenon: '日志显示发送成功但收不到', cause: '第三方通道配置问题', fix: '检查阿里云加密配置 / 邮箱服务器' },
{ phenomenon: '短信发送失败', cause: '数据库同时存在加密+不加密两条配置', fix: '删除不加密的那条' },
{ phenomenon: '腾讯会议添加参会人无短信', cause: '腾讯会议账号没绑定手机号', fix: '在用户客户端绑定手机号' },
],
traps: [
'阿里云短信加密/不加密配置不能共存',
'腾讯会议发短信需账号绑定手机号',
],
},
'安卓信息界面': {
summary: '安卓信息界面操作不生效 / 点击无响应 / WebSocket 连接不上 / 刷新设备失败 / 设备不显示 → 三板斧:wss连接 → config配置 → 版本兼容',
steps: [
{
title: 'WebSocket / wss 连接排查(最常见根因)',
body: `<ul>
<li><span class="li-icon">▶</span> 打开浏览器 F12 → <b>Console</b> 面板 → 看 wss 连接报错信息</li>
<li><span class="li-icon">▶</span> wss 连接失败最常见的两个原因:</li>
<li><span class="li-icon">▸</span> <b>config.js 中 mqttSetting 配置项的 IP 地址错误</b>(修改 IP 后最容易遗漏)</li>
<li><span class="li-icon">▸</span> <b>后端 config.json 中 MQTT 相关 IP 地址未更新</b>(修改 IP 脚本可能只改了部分配置)</li>
<li><span class="li-icon">▶</span> 验证:直接看浏览器 Network 面板中 WebSocket 请求的 URL 是否正确</li>
</ul>`
},
{
title: '配置文件逐项排查(改 IP 后必查)',
body: `<ul>
<li><span class="li-icon">▶</span> 前端:<code>config.js</code> 中的 <code>mqttSetting</code> 配置项 — IP/端口/路径是否指向正确服务器</li>
<li><span class="li-icon">▶</span> 后端:<code>config.json</code> 中的 MQTT 相关配置 — 修改 IP 脚本可能漏了这里</li>
<li><span class="li-icon">▶</span> ⚠️ 常见坑:用脚本修改 IP 后,后端 config.json 的 MQTT IP 仍是旧地址 → 安卓信息界面操作请求发到旧服务器</li>
</ul>`
},
{
title: '刷新设备 / 操作不生效',
body: `<ul>
<li><span class="li-icon">▶</span> 门口屏 4.0 版本:web 端向设备主动发起刷新请求,N 个终端 = N 次并发 → Java 连接通道可能断开 → 报 <code>Client is not connected</code></li>
<li><span class="li-icon">▶</span> 门口屏 5.0 版本:改为设备主动上报,不再需要 web 端刷新 → 升级后可解决</li>
<li><span class="li-icon">▶</span> 临时处理:判断版本号 ≥5 时,刷新接口直接返回"无需刷新,设备主动上报"</li>
</ul>`
},
{
title: '设备不显示 / 筛选不到',
body: `<ul>
<li><span class="li-icon">▶</span> 旧版本前端包可能未限制设备类型筛选 → 更新前端包</li>
<li><span class="li-icon">▶</span> 门口屏设备需要"门口屏"标识才能被筛选 → 后端兼容空字段</li>
<li><span class="li-icon">▶</span> 更新前端包后模块消失 → 版本不兼容需要恢复到兼容版本</li>
</ul>`
},
{
title: '操作结果不生效 / 显示不正确',
body: `<ul>
<li><span class="li-icon">▶</span> 签名校验未关闭 → 旧版安卓信息页上报日志报错 → 重新打包关闭签名</li>
<li><span class="li-icon">▶</span> 门口屏时间显示不对 → 后端未转换时区 → 后端代码兼容上报时区处理</li>
<li><span class="li-icon">▶</span> 点击发送消息/更新后设备没反应 → 确认 MQTT 主题订阅是否正常(查 EMQX)</li>
</ul>`
},
],
rootCauses: [
{ phenomenon: '安卓信息界面点击无响应 / wss 连接报错', cause: 'config.js 中 mqttSetting 的 IP 地址错误', fix: 'F12 Console 确认 wss 报错 → 修改 config.js 中 MQTT IP' },
{ phenomenon: '修改服务器 IP 后安卓信息操作仍失败', cause: '后端 config.json 的 MQTT IP 未更新', fix: '修改 config.json 的 MQTT 配置项 IP 地址' },
{ phenomenon: '刷新设备接口报 Client is not connected', cause: '门口屏 4.0 web 端并发刷新 → Java 连接通道断开', fix: '升级门口屏 5.0(设备主动上报)' },
{ phenomenon: '安卓信息中看不到门口屏设备类型', cause: '旧前端包未限制设备类型 / 后端不兼容空字段', fix: '更新前端包 + 后端兼容空字段标识' },
{ phenomenon: '安卓信息页上报日志报错', cause: '签名校验未关闭', fix: '重新打包关闭签名' },
{ phenomenon: '安卓信息界面门口屏时间不正确', cause: '后端未转换上报时区', fix: '后端代码兼容上报时区处理' },
],
traps: [
'修改 IP 脚本可能遗漏 config.json 中的 MQTT IP → 操作发到旧服务器',
'门口屏 4.0 并发刷新是已知设计缺陷 → 5.0 已改为设备主动上报',
'wss 连接问题看 Console 比看日志更快',
'更新前端包可能导致模块消失 → 确认版本兼容性',
],
},
'人脸签到': {
summary: '人脸同步失败/签到无反应/所有人可签到/人脸特征生成异常 → 人脸库版本 → 同步策略 → 白名单 → 区域配置',
steps: [
{
title: '确认门口屏版本和人脸同步策略',
body: `<ul>
<li><span class="li-icon">▶</span> 旧版门口屏:同步所有人脸数据 → 升级后需<b>清空人脸库重新同步</b></li>
<li><span class="li-icon">▶</span> 门口屏 5.0:只同步会议的参会人人脸数据 → 没有会议时无法人脸开门(需开启白名单)</li>
<li><span class="li-icon">▶</span> 白名单功能需授权并计入标准版模块</li>
</ul>`
},
{
title: '人脸同步失败排查',
body: `<ul>
<li><span class="li-icon">▶</span> 用户表特征和 URL 地址是否为空 → 查数据库</li>
<li><span class="li-icon">▶</span> 用户角色是否满足同步条件:<code>r.role_type in ('admin','attendant','attendantMonitor')</code></li>
<li><span class="li-icon">▶</span> 区域 ID 是否匹配:<code>ubains.auth.area</code> 和 <code>ubains.meeting.areaId</code></li>
<li><span class="li-icon">▶</span> 手动调同步接口:<code>/station/app/getBookFacePage</code></li>
<li><span class="li-icon">▶</span> 门口屏日志是否获取到了人脸数据</li>
</ul>`
},
{
title: '人脸签到/开门不生效',
body: `<ul>
<li><span class="li-icon">▶</span> 人脸特征是否已生成(上传人脸后状态未改变 → 前端缺少字段判断)</li>
<li><span class="li-icon">▶</span> 门口屏时间不对 → 会议和人脸未及时同步 → 配置 NTP 时间同步</li>
<li><span class="li-icon">▶</span> ARM 架构人脸服务激活失败 → 不影响主流程 → 关闭人脸激活配置</li>
<li><span class="li-icon">▶</span> OA 用户无人脸数据 → 让海康/人脸平台协助确认是否上传</li>
</ul>`
},
],
rootCauses: [
{ phenomenon: '升级后所有人脸都能签到', cause: '旧人脸数据未清空重新同步', fix: '清空人脸库 → 重新同步' },
{ phenomenon: '没有会议时无法人脸开门', cause: '门口屏 5.0 只同步参会人人脸', fix: '开启白名单功能并授权' },
{ phenomenon: '部分用户人脸签到无弹窗', cause: '用户无人脸特征数据', fix: '联系海康/人脸平台上传人脸' },
{ phenomenon: '人脸同步到错误区域的门ロ屏', cause: '区域 ID 变更与配置不对应', fix: '核对 ubains.auth.area 配置' },
{ phenomenon: '上传人脸后状态未改变', cause: '前端接口缺少字段判断', fix: '前端代码兼容处理' },
],
traps: [
'旧版→新版升级必须清空人脸库重新同步',
'门口屏 5.0 不自动同步所有人脸,无会议=无法人脸开门',
'ARM 架构人脸激活失败不影响主业务,勿因此判服务异常',
],
},
'数据同步': {
summary: 'OA/飞书/钉钉/企业微信/Exchange/Welink 等第三方数据同步失败或数据不一致 → 查同步日志 → 对比接口返回 → 确认字段映射',
steps: [
{
title: '查同步日志定位问题环节',
body: `<ul>
<li><span class="li-icon">▶</span> <code>tail -f ubains-INFO-AND-ERROR.log | grep "同步"</code> 或按关键字过滤</li>
<li><span class="li-icon">▶</span> 常用日志过滤命令:<code>grep "总共需要封装"</code> / <code>grep "插入用户操作"</code> / <code>grep "会议详情"</code></li>
<li><span class="li-icon">▶</span> 如果日志不够详细 → 先补充日志再排查</li>
</ul>`
},
{
title: '对比接口返回数据',
body: `<ul>
<li><span class="li-icon">▶</span> 将第三方接口调用参数转成 curl 直接在服务器上调用 → 确认第三方是否正常返回</li>
<li><span class="li-icon">▶</span> 会议列表 vs 会议详情接口返回是否一致(如列表时间对但详情时间错 → 以列表为准)</li>
<li><span class="li-icon">▶</span> 接口返回解密后数据是否完整(如缺少 messagetheme 字段 → 解密方法不一致)</li>
</ul>`
},
{
title: '常见同步失败根因',
body: `<ul>
<li><span class="li-icon">▶</span> OA 接口权限不足 → 部分部门/用户的会议查不到 → 联系对方扩大 key 权限</li>
<li><span class="li-icon">▶</span> 手机号重复 → 同步失败 → 确认唯一标识字段</li>
<li><span class="li-icon">▶</span> 飞书审批会议过期后才通过 → 不再同步(逻辑设计如此)</li>
<li><span class="li-icon">▶</span> 飞书拒绝日程不发通知 → 预定系统无法同步删除 → 需增加轮询逻辑</li>
<li><span class="li-icon">▶</span> 第三方系统自身故障 → 先用 curl 直接调用对方接口确认</li>
<li><span class="li-icon">▶</span> 组织架构同步失败 → 北斗用户被移除但排班未同步删除 → 同步时增加删除逻辑</li>
</ul>`
},
],
rootCauses: [
{ phenomenon: 'OA 会议不同步/数量不对', cause: '接口权限不足/返回数据不完整', fix: '扩大 key 权限 + curl 直调验证' },
{ phenomenon: 'OA 会议时间与预定不一致', cause: '会议详情接口返回时间错误', fix: '改用会议列表接口返回值' },
{ phenomenon: '用户同步数量对不上', cause: '手机号重复/接口返回缺字段', fix: '确认唯一标识 + 检查解密一致性' },
{ phenomenon: '飞书取消会议后预定仍显示', cause: '飞书拒绝日程不发回调通知', fix: '增加定时轮询对比' },
{ phenomenon: '北斗同步后用户排班异常', cause: '移除用户时未同步删除排班', fix: '同步时增加排班同步删除' },
],
traps: [
'第三方接口返回数据可能不一致(列表 vs 详情),用 curl 直调最可靠',
'飞书审批过期会议不再同步是设计行为,非 bug',
'同步日志不够时先补日志,不要硬猜',
],
},
'安全漏洞': {
summary: '安全漏洞报告/等保整改/弱密码/未授权访问 → 按漏洞报告逐项处理 → 升级版本 → 关闭不必要的端口/服务',
steps: [
{
title: '常见安全漏洞及处理',
body: `<ul>
<li><span class="li-icon">▶</span> <b>Nacos 未授权访问</b> → 开启安全校验:<code>nacos.core.auth.enabled=true</code>,配置密钥 ≥32 位</li>
<li><span class="li-icon">▶</span> <b>Nacos 开启安全校验后其他服务连不上</b> → auth 等服务需配置 nacos 账号密码,或暂时关闭:<code>nacos.core.auth.enabled=false</code></li>
<li><span class="li-icon">▶</span> <b>EMQX 弱密码/RCE 漏洞</b> → 升级 EMQX 版本(注意国产系统兼容性)</li>
<li><span class="li-icon">▶</span> <b>MQTT 无密码访问</b> → 开启认证配置项 + 重新生成 EMQX 容器</li>
<li><span class="li-icon">▶</span> <b>Nginx 漏洞</b> → 升级 nginx 版本</li>
<li><span class="li-icon">▶</span> <b>短信轰炸</b> → 增加验证码/频率限制</li>
<li><span class="li-icon">▶</span> <b>openssh 漏洞</b> → 升级但注意兼容性(新版可能与统信内核不兼容→回退)</li>
</ul>`
},
{
title: '等保测评改数据库密码',
body: `<ul>
<li><span class="li-icon">▶</span> 改密码后需覆盖所有系统:预定、运维、新统一平台、统一平台</li>
<li><span class="li-icon">▶</span> ⚠️ 常见坑:脚本修改可能漏掉某些用户(如 mysql@172)→ 输出一键修改脚本</li>
<li><span class="li-icon">▶</span> 改密后验证所有服务能否正常连接数据库</li>
</ul>`
},
{
title: '端口/服务加固',
body: `<ul>
<li><span class="li-icon">▶</span> 关闭不必要的端口(如 11211)</li>
<li><span class="li-icon">▶</span> Nacos 不要私自开放到公网</li>
<li><span class="li-icon">▶</span> 使用统信官网漏洞工具扫描 + 生成主机漏洞检测修复脚本</li>
</ul>`
},
],
rootCauses: [
{ phenomenon: '安全扫描报告 Nacos 未授权访问', cause: 'Nacos 未开启安全校验', fix: '配置 nacos.core.auth.enabled=true + 密钥 ≥32位' },
{ phenomenon: 'EMQX 弱密码漏洞', cause: '版本过低 / 未配置密码', fix: '升级 EMQX + 开启认证' },
{ phenomenon: '等保改密后服务连不上数据库', cause: '脚本漏改部分用户密码', fix: '输出全系统一键改密脚本' },
{ phenomenon: 'openssh 漏洞修复后系统异常', cause: '新版 openssh 与统信内核不兼容', fix: '回退官方版本' },
],
traps: [
'Nacos 开启安全校验后其他服务也要配账号密码,否则全挂',
'openssh 升级前确认内核兼容性',
'安全加固后验证所有服务连通性',
],
},
'版本兼容': {
summary: '升级后功能异常/回退版本/新旧版本混用导致的问题 → 确认版本号 → 对比变更 → 判断兼容性 → 回退或适配',
steps: [
{
title: '先确认所有相关组件版本',
body: `<ul>
<li><span class="li-icon">▶</span> 前端包版本 / 后端包版本 / 门口屏 apk 版本 / pak 版本 / 容器镜像版本</li>
<li><span class="li-icon">▶</span> 多个组件版本是否匹配?(前后端分离后版本一致性尤其重要)</li>
<li><span class="li-icon">▶</span> 现场只部署了基础版本没有更新已测试版本 → 版本功能不完整</li>
</ul>`
},
{
title: '常见版本不兼容场景',
body: `<ul>
<li><span class="li-icon">▶</span> <b>后端包新旧不兼容</b> → 最新后端包 + 旧门口屏 2.0 时间格式不匹配 → 后端兼容处理</li>
<li><span class="li-icon">▶</span> <b>前端包更新后模块消失</b> → 版本不兼容 → 恢复兼容版本</li>
<li><span class="li-icon">▶</span> <b>旧版容器镜像 vs 新版代码</b> → MySQL 5.x vs 8.x 不兼容 / 缺少 uswgi 服务</li>
<li><span class="li-icon">▶</span> <b>旧版数据库缺少表/字段</b> → 手动新增</li>
<li><span class="li-icon">▶</span> <b>加密文件版本不一致</b> → 超管登录失败 → 更新加密文件</li>
</ul>`
},
{
title: '回退操作要点',
body: `<ul>
<li><span class="li-icon">▶</span> 记录当前版本号(git commit hash)→ 回退到已知正常版本</li>
<li><span class="li-icon">▶</span> 回退后验证核心功能(登录→预定→门口屏显示→取消会议)</li>
<li><span class="li-icon">▶</span> 如果回退版本后期有代码调整 → 将调整的代码复制过来</li>
</ul>`
},
],
rootCauses: [
{ phenomenon: '更新后端包后门口屏不显示会议', cause: '后端时间格式与门口屏 2.0 不兼容', fix: '后端代码向下兼容时间戳格式' },
{ phenomenon: '更新前端包后某模块消失', cause: '版本分支不兼容', fix: '回退到兼容版本' },
{ phenomenon: '运行服务提示 MySQL 连接失败', cause: '旧后端包用 MySQL 5.x,现场装 8.x', fix: '使用匹配的容器镜像版本' },
{ phenomenon: '超管登录报错', cause: '加密文件与当前版本不一致', fix: '更新加密文件' },
],
traps: [
'现场可能只部署基础版本 → 功能不全 → 需要更新到已测试版本',
'新旧版本混用是最常见的人为故障',
'回退前记录版本号,回退后验证核心功能',
],
},
};
// ============================================================
// Rendering
// ============================================================
const state = { cats: new Set() };
document.getElementById('categoryTags').addEventListener('click', e => {
const tag = e.target.closest('.tag');
if (!tag) return;
const cat = tag.dataset.cat;
if (state.cats.has(cat)) { state.cats.delete(cat); tag.classList.remove('active'); }
else { state.cats.add(cat); tag.classList.add('active'); }
});
function diagnose() {
const phenomenon = document.getElementById('phenomenon').value.trim();
const env = document.getElementById('env').value.trim();
const recentChange = document.getElementById('recentChange').value.trim();
if (state.cats.size === 0) {
alert('请至少选择一个问题分类');
return;
}
const box = document.getElementById('resultBox');
let html = '';
// Summary header
const catNames = [...state.cats].join('、');
html += `<div class="result-summary">`;
html += `<div class="rs-title">🔬 排查诊断 — ${catNames}</div>`;
if (phenomenon) html += `<div class="rs-row"><b>现象:</b>${phenomenon}</div>`;
if (env) html += `<div class="rs-row"><b>环境:</b>${env}</div>`;
if (recentChange) html += `<div class="rs-row"><b>最近变更:</b>${recentChange} ⚠️ 优先排查变更引入的问题</div>`;
html += `</div>`;
// Per category
for (const cat of state.cats) {
const kb = KB[cat];
if (!kb) continue;
html += `<div style="margin-bottom:24px;">`;
html += `<h3 style="font-size:15px;color:var(--accent);margin-bottom:10px;padding-bottom:6px;border-bottom:1px solid var(--border);">📌 ${cat}</h3>`;
// Strategy summary
html += `<div style="background:rgba(56,189,248,0.06);border-left:3px solid var(--accent);padding:10px 14px;border-radius:0 8px 8px 0;font-size:13px;margin-bottom:14px;">${kb.summary}</div>`;
// Steps
kb.steps.forEach((step, i) => {
html += `<div class="step-card open">`;
html += `<div class="step-card-header" onclick="this.parentElement.classList.toggle('open')">`;
html += `<span class="step-num">${i+1}</span>`;
html += `<span class="step-title">${step.title}</span>`;
html += `<span class="step-arrow">▼</span>`;
html += `</div>`;
html += `<div class="step-body">${step.body}</div>`;
html += `</div>`;
});
// Root causes table
if (kb.rootCauses && kb.rootCauses.length > 0) {
html += `<div style="margin-top:14px; background:var(--card); border:1px solid var(--border); border-radius:10px; overflow:hidden;">`;
html += `<div style="padding:12px 16px;font-size:14px;font-weight:600;border-bottom:1px solid var(--border);">🎯 典型根因速查</div>`;
html += `<table class="root-cause-table"><tr><th>现象特征</th><th>根因</th><th>处置</th></tr>`;
kb.rootCauses.forEach(rc => {
html += `<tr>`;
html += `<td class="rc-phenomenon">${rc.phenomenon}</td>`;
html += `<td>${rc.cause}</td>`;
html += `<td class="rc-fix">${rc.fix}</td>`;
html += `</tr>`;
});
html += `</table></div>`;
}
// Known traps
if (kb.traps && kb.traps.length > 0) {
html += `<div class="known-trap" style="margin-top:12px;">`;
html += `<div class="trap-label">⚠️ 已知坑(先对照,避免重复排查)</div>`;
kb.traps.forEach(t => { html += `<div style="margin-top:4px;">• ${t}</div>`; });
html += `</div>`;
}
html += `</div>`; // end category block
}
// If multiple categories, show dedup hint
if (state.cats.size > 1) {
html += `<div class="known-trap" style="border-color:var(--accent);background:rgba(56,189,248,0.06);">
<div class="trap-label" style="color:var(--accent);">💡 多分类交叉排查提示</div>
<div style="margin-top:4px;">多个分类可能有关联——优先排查<b>最近变更</b>对应的分类,再自底向上逐层排除(网络 → 容器 → 数据库 → 服务 → 应用)。</div>
</div>`;
}
box.innerHTML = html;
box.classList.add('show');
box.scrollIntoView({ behavior: 'smooth', block: 'start' });
}
function clearAll() {
document.querySelectorAll('input[type="text"], textarea').forEach(el => el.value = '');
document.querySelectorAll('.tag.active').forEach(t => t.classList.remove('active'));
state.cats.clear();
document.getElementById('resultBox').classList.remove('show');
document.getElementById('phenomenon').focus();
window.scrollTo({ top: 0, behavior: 'smooth' });
}
// Open first step card by default (handled by .open class above in rendering)
</script>
</body>
</html>
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论