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

feat(test): 实现根据PRD文档自动生成测试用例功能

- 新增PRD解析功能,支持从Markdown文档中提取需求条目
- 实现交互式PRD选择,支持多文档合并生成用例到同一sheet
- 添加需求分类算法,自动识别异常场景、安全鉴权等功能类别
- 实现测试步骤和预期结果的智能生成,基于PRD内容提取
- 支持用例编号前缀和模块名称的灵活配置
- 添加时间戳命名的输出文件生成功能
- 重构服务自检文档,新增Linux shell脚本支持
- 移除手动JSON配置文件依赖,直接从PRD文档生成用例数据
上级 f9cccbd9
#!/usr/bin/env bash
# ==============================================================================
# 服务自检脚本 (Linux 版本,单机运行)
# 目标:在服务器本机运行,无需 SSH 连接逻辑;实现与 check_server_health.ps1 主要功能一致。
#
# 输出:
# - logs/health_check_YYYYmmdd_HHMMSS.log
# - Reports/health_report_<ip>_YYYYmmdd_HHMMSS.md
#
# 说明:
# - 默认以 root 执行(涉及 docker/iptables/firewalld/读日志等)。
# - 修复动作依赖同目录 issue_handler.sh(如 fix_ntp_config / fix_dns_config / fix_port_access 等)。
# ==============================================================================
set -euo pipefail
# ------------------------------
# 基础配置
# ------------------------------
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
SCRIPT_VERSION="1.0.0"
SSH_TIMEOUT=30 # 占位:与 ps1 一致,这里不使用
LOG_DIR="$SCRIPT_DIR/logs"
REPORT_DIR="$SCRIPT_DIR/Reports"
TS="$(date '+%Y%m%d_%H%M%S')"
LOG_FILE="$LOG_DIR/health_check_${TS}.log"
mkdir -p "$LOG_DIR" "$REPORT_DIR"
# ------------------------------
# 日志函数
# ------------------------------
log() {
local level="$1"; shift
local msg="${*:-}"
local now
now="$(date '+%Y-%m-%d %H:%M:%S')"
if [[ -z "$msg" ]]; then
echo ""
return 0
fi
local line="[$now] [$level] $msg"
echo "$line" | tee -a "$LOG_FILE" >/dev/null
}
# ✅ 新增:阶段标题(让执行过程更“显眼”地打印)
section() {
local title="$*"
log INFO "=================================================================="
log INFO "$title"
log INFO "=================================================================="
}
# ---- helper: 将多行压缩成一行,避免 Markdown 反引号被换行破坏
_squash_ws_one_line() {
# stdin -> stdout
tr '\r\n\t' ' ' | sed 's/ */ /g; s/^ *//; s/ *$//'
}
# ---- helper: 安全取值,空则返回 N/A
_kv_or_na() {
local v="$1"
if [[ -n "$v" ]]; then echo "$v"; else echo "N/A"; fi
}
get_resolv_conf_raw() {
if [[ -r /etc/resolv.conf ]]; then
sed 's/\r$//' /etc/resolv.conf
else
echo "N/A (cannot read /etc/resolv.conf)"
fi
}
# ------------------------------
# 报告汇总(增强:同时支持关键值/状态)
# ------------------------------
REPORT_LINES=()
report_add() { REPORT_LINES+=("$*"); }
report_add_block() {
while [[ $# -gt 0 ]]; do
REPORT_LINES+=("$1"); shift
done
}
# 可选:埋点到报告里(key/value 级别的数据)
declare -A REPORT_KV=()
report_kv_set() {
local k="$1"; shift
local v="${*:-}"
REPORT_KV["$k"]="$v"
}
report_kv_get() {
local k="$1"
echo "${REPORT_KV[$k]:-}"
}
# ------------------------------
# 工具函数
# ------------------------------
command_exists() { command -v "$1" >/dev/null 2>&1; }
get_primary_ip() {
local ip=""
if command_exists ip; then
ip="$(ip route get 1 2>/dev/null | awk '{for(i=1;i<=NF;i++){if($i=="src"){print $(i+1); exit}}}')"
fi
if [[ -z "$ip" ]] && command_exists hostname; then
ip="$(hostname -I 2>/dev/null | awk '{print $1}')"
fi
echo "${ip:-unknown}"
}
# 执行修复脚本:同目录 issue_handler.sh
run_issue_handler() {
local action="$1"
local platform="${2:-auto}"
local extra="${3:-}"
local issue="$SCRIPT_DIR/issue_handler.sh"
if [[ ! -f "$issue" ]]; then
log ERROR "[修复] 未找到 issue_handler.sh:$issue"
return 1
fi
chmod +x "$issue" 2>/dev/null || true
# dos2unix 可选
if command_exists dos2unix; then
dos2unix "$issue" >/dev/null 2>&1 || true
fi
local cmd="$issue --action $action --platform $platform $extra"
log WARN "[修复] 执行:$cmd"
if bash -c "$cmd" >>"$LOG_FILE" 2>&1; then
log SUCCESS "[修复] 执行成功:$action"
return 0
else
log ERROR "[修复] 执行失败:$action(详见日志:$LOG_FILE)"
return 1
fi
}
# ------------------------------
# 1) 平台识别
# ------------------------------
detect_platform() {
if [[ -d /data/services ]]; then
echo "new"
else
echo "old"
fi
}
# ------------------------------
# 2) 系统识别(容器)
# ------------------------------
detect_systems() {
local out=""
if command_exists docker; then
out="$(docker ps --format '{{.Names}}' 2>/dev/null || true)"
fi
local has_ujava=0 has_upython=0 has_upython_voice=0
local ujava_name="" upython_name="" upython_voice_name=""
while IFS= read -r c; do
[[ -z "$c" ]] && continue
if [[ "$c" =~ ^ujava[0-9]*$ ]]; then
has_ujava=1; ujava_name="$c"
elif [[ "$c" =~ ^upython[0-9]*$ ]] && [[ ! "$c" =~ voice ]]; then
has_upython=1; upython_name="$c"
elif [[ "$c" =~ ^upython_voice[0-9]*$ ]]; then
has_upython_voice=1; upython_voice_name="$c"
fi
done <<< "$out"
# 输出为 KEY=VALUE,供调用方 eval
cat <<EOF
HAS_UJAVA=$has_ujava
HAS_UPYTHON=$has_upython
HAS_UPYTHON_VOICE=$has_upython_voice
UJAVA_CONTAINER=${ujava_name:-}
UPYTHON_CONTAINER=${upython_name:-}
UPYTHON_VOICE_CONTAINER=${upython_voice_name:-}
EOF
}
# ------------------------------
# 3) 服务检测配置(对齐 ps1)
# ------------------------------
declare -A UJAVA_SERVICES=(
["auth"]="ubains-auth.jar"
["gateway"]="ubains-gateway.jar"
["system"]="ubains-modules-system.jar"
["meeting2.0"]="ubains-meeting-inner-api-1.0-SNAPSHOT.jar"
["meeting3.0"]="ubains-meeting-inner-api-1.0-SNAPSHOT.jar"
["mqtt"]="ubains-meeting-mqtt-1.0-SNAPSHOT.jar"
["quartz"]="ubains-meeting-quartz-1.0-SNAPSHOT.jar"
["message"]="ubains-meeting-message-scheduling-1.0-SNAPSHOT.jar"
)
declare -A UJAVA_HOST_SERVICES=(
["extapi"]="ubains-meeting-api-1.0-SNAPSHOT.jar"
)
# 传统平台 ujava 容器内(nginx + meeting)
declare -A UJAVA_OLD_CONTAINER_SERVICES=(
["nginx"]="nginx: master process"
["meeting"]="ubains-meeting-inner-api-1.0-SNAPSHOT.jar"
)
# ------------------------------
# 4) 进程检测:ujava(容器/宿主机)
# ------------------------------
count_proc_in_container() {
local container="$1"
local pattern="$2"
local extra_grep="${3:-}"
local cmd="docker exec $container sh -c \"ps aux 2>/dev/null | grep -v grep | grep '$pattern' ${extra_grep} | wc -l\""
# shellcheck disable=SC2086
local n
n="$(eval "$cmd" 2>/dev/null | tail -n 1 | tr -d '\r' || true)"
[[ "$n" =~ ^[0-9]+$ ]] || n=0
echo "$n"
}
count_proc_host() {
local pattern="$1"
local extra_grep="${2:-}"
local n
# shellcheck disable=SC2009
n="$(ps aux 2>/dev/null | grep -v grep | grep "$pattern" ${extra_grep} | wc -l | tail -n 1 | tr -d '\r' || true)"
[[ "$n" =~ ^[0-9]+$ ]] || n=0
echo "$n"
}
test_ujava_services() {
local container="${1:-}" # 可空:表示宿主机检测
local platform="${2:-new}"
local results=()
log INFO "========== 检测 ujava 服务 ($( [[ -n "$container" ]] && echo "容器:$container" || echo "宿主机" )) =========="
for svc in "${!UJAVA_SERVICES[@]}"; do
local jar="${UJAVA_SERVICES[$svc]}"
local n=0
if [[ -n "$container" ]]; then
if [[ "$svc" == "meeting2.0" ]]; then
n="$(count_proc_in_container "$container" "$jar" "| grep 'java-meeting2.0'")"
elif [[ "$svc" == "meeting3.0" ]]; then
n="$(count_proc_in_container "$container" "$jar" "| grep 'java-meeting3.0'")"
else
n="$(count_proc_in_container "$container" "$jar")"
fi
# 容器内未命中则 fallback 宿主机(与 ps1 一致的兜底)
if [[ "$n" -eq 0 ]]; then
if [[ "$svc" == "meeting2.0" ]]; then
n="$(count_proc_host "$jar" "| grep 'java-meeting2.0'")"
elif [[ "$svc" == "meeting3.0" ]]; then
n="$(count_proc_host "$jar" "| grep 'java-meeting3.0'")"
else
n="$(count_proc_host "$jar")"
fi
fi
else
if [[ "$svc" == "meeting2.0" ]]; then
n="$(count_proc_host "$jar" "| grep 'java-meeting2.0'")"
elif [[ "$svc" == "meeting3.0" ]]; then
n="$(count_proc_host "$jar" "| grep 'java-meeting3.0'")"
else
n="$(count_proc_host "$jar")"
fi
fi
if [[ "$n" -gt 0 ]]; then
log SUCCESS " [OK] $svc ($jar): 运行中"
results+=("$svc|$jar|running")
else
log ERROR " [FAIL] $svc ($jar): 未运行"
results+=("$svc|$jar|stopped")
fi
done
printf "%s\n" "${results[@]}"
}
test_ujava_host_services() {
log INFO "========== 检测 ujava 宿主机服务 (extapi) =========="
local results=()
for svc in "${!UJAVA_HOST_SERVICES[@]}"; do
local jar="${UJAVA_HOST_SERVICES[$svc]}"
local n
n="$(count_proc_host "$jar")"
if [[ "$n" -gt 0 ]]; then
log SUCCESS " [OK] $svc ($jar): 运行中"
results+=("$svc|$jar|running")
else
log ERROR " [FAIL] $svc ($jar): 未运行"
results+=("$svc|$jar|stopped")
fi
done
printf "%s\n" "${results[@]}"
}
test_ujava_old_container_services() {
local container="$1"
log INFO "========== 检测传统平台 ujava 容器内服务 ($container) =========="
local results=()
for svc in "${!UJAVA_OLD_CONTAINER_SERVICES[@]}"; do
local pattern="${UJAVA_OLD_CONTAINER_SERVICES[$svc]}"
local n
n="$(count_proc_in_container "$container" "$pattern")"
if [[ "$n" -gt 0 ]]; then
log SUCCESS " [OK] $svc ($pattern): 运行中"
results+=("$svc|$pattern|running")
else
log ERROR " [FAIL] $svc ($pattern): 未运行"
results+=("$svc|$pattern|stopped")
fi
done
printf "%s\n" "${results[@]}"
}
# ------------------------------
# 5) 端口检测(容器内)
# ------------------------------
container_port_listening() {
local container="$1"
local port="$2"
# netstat/ss 任一可用
local cmd="docker exec $container sh -c \"(netstat -tlnp 2>/dev/null || ss -tlnp 2>/dev/null) | grep -E ':[[:space:]]*${port}[[:space:]]'\""
# shellcheck disable=SC2086
if eval "$cmd" >/dev/null 2>&1; then
return 0
fi
return 1
}
test_container_ports() {
local container="$1"
local service_type="$2"
shift 2
local ports=("$@") # 形如 "11211|memcached|xxx"
log INFO "========== 检测 ${service_type} 容器内端口 ($container) =========="
local results=()
for p in "${ports[@]}"; do
local port proc desc
IFS="|" read -r port proc desc <<<"$p"
if container_port_listening "$container" "$port"; then
log SUCCESS " [OK] 端口 $port ($desc): 监听中"
results+=("$port|$proc|$desc|listening")
else
log ERROR " [FAIL] 端口 $port ($desc): 未监听"
results+=("$port|$proc|$desc|closed")
fi
done
printf "%s\n" "${results[@]}"
}
# 新统一平台端口要求(对齐 ps1 的 UpythonPorts / UpythonVoicePorts)
UPYTHON_PORTS_NEW=(
"11211|memcached|Memcached 缓存服务"
"8000|uwsgi|uWSGI 应用服务"
)
UPYTHON_VOICE_PORTS_NEW=(
"1883|mosquitto|MQTT Broker 服务"
"8000|uwsgi|uWSGI 应用服务"
"9001|mosquitto|MQTT WebSocket 服务"
"11211|memcached|Memcached 缓冲服务"
"8080|nginx|Nginx 代理服务 (8080)"
"80|nginx|Nginx Web 服务 (80)"
)
# 传统平台 upython 端口要求
UPYTHON_PORTS_OLD=(
"8081|nginx|Nginx 代理服务 (8081)"
"8443|nginx|Nginx HTTPS 服务 (8443)"
"8000|uwsgi|uWSGI 应用服务"
"8002|httpd|Apache HTTPD 服务"
"11211|memcached|Memcached 缓冲服务"
)
# ------------------------------
# 6) DNS 检测 + 修复 + 复检(增强:记录 details 到报告)
# ------------------------------
DNS_TEST_DOMAINS=("www.baidu.com" "www.qq.com" "www.aliyun.com")
get_dns_nameservers() {
if [[ -r /etc/resolv.conf ]]; then
grep -E '^nameserver' /etc/resolv.conf 2>/dev/null | awk '{print $2}' | head -n 5 | paste -sd',' - || true
else
echo ""
fi
}
# ------------------------------
# 6) DNS 检测 + 修复 + 复检(增强:记录 details 到报告 + 记录 raw)
# ------------------------------
test_dns() {
log INFO "========== 检测 DNS 解析功能 =========="
local resolv_raw dns_servers
resolv_raw="$(get_resolv_conf_raw)"
dns_servers="$(get_dns_nameservers)"
report_kv_set "dns.resolv_conf" "$resolv_raw"
report_kv_set "dns.nameservers" "${dns_servers:-N/A}"
if [[ -n "$dns_servers" ]]; then
log SUCCESS " DNS配置: nameserver=${dns_servers}"
else
log WARN " DNS配置: 未检测到 nameserver 或无法读取 /etc/resolv.conf"
fi
local ok=0 total="${#DNS_TEST_DOMAINS[@]}"
local detail_lines=()
for d in "${DNS_TEST_DOMAINS[@]}"; do
if command_exists nslookup && nslookup "$d" >/dev/null 2>&1; then
log SUCCESS " [OK] $d : 解析成功(nslookup)"
ok=$((ok+1))
detail_lines+=("$d|OK|nslookup")
elif command_exists host && host "$d" >/dev/null 2>&1; then
log SUCCESS " [OK] $d : 解析成功(host)"
ok=$((ok+1))
detail_lines+=("$d|OK|host")
else
log ERROR " [FAIL] $d : 解析失败"
detail_lines+=("$d|FAIL|-")
fi
done
if [[ "${#detail_lines[@]}" -gt 0 ]]; then
report_kv_set "dns.details" "$(printf "%s\n" "${detail_lines[@]}")"
fi
# raw(像日志)
local raw=""
raw+="[resolv.conf]\n${resolv_raw}\n\n"
raw+="DNS配置: nameserver=${dns_servers:-N/A}\n"
for l in "${detail_lines[@]}"; do
local dd rr tt
IFS="|" read -r dd rr tt <<<"$l"
if [[ "$rr" == "OK" ]]; then
raw+="[OK] ${dd} : 解析成功(${tt})\n"
else
raw+="[FAIL] ${dd} : 解析失败\n"
fi
done
report_kv_set "dns.raw" "$(printf "%b" "$raw")"
if [[ "$ok" -eq "$total" ]]; then
echo "OK"
elif [[ "$ok" -gt 0 ]]; then
echo "PARTIAL"
else
echo "FAIL"
fi
}
repair_dns_if_needed() {
local status="$1"
if [[ "$status" == "OK" ]]; then
return 0
fi
log WARN "[DNS] 检测到 DNS 异常($status),触发修复:fix_dns_config"
run_issue_handler "fix_dns_config" "auto" "--non-interactive --yes" || true
log INFO "[DNS] 修复后复检..."
local post
post="$(test_dns)"
report_kv_set "dns.recheck" "$post"
if [[ "$post" == "OK" ]]; then
log SUCCESS "[DNS] 复检成功:DNS 已恢复"
else
log WARN "[DNS] 复检仍异常:$post(需人工排查)"
fi
}
# ------------------------------
# 7) 资源分析(CPU/内存/磁盘/防火墙)
# ------------------------------
get_cpu_usage() {
# top 或 mpstat 任取
local usage="0"
if command_exists top; then
usage="$(LC_ALL=C top -bn1 2>/dev/null | awk -F'[:, ]+' '/Cpu\(s\)/{print $3+$5; exit}' || true)"
fi
if [[ -z "$usage" || "$usage" == "0" ]] && command_exists mpstat; then
usage="$(mpstat 1 1 2>/dev/null | awk 'END{print 100-$NF}' || true)"
fi
echo "${usage:-0}"
}
get_mem_usage_gb_pct() {
# 输出:total_gb,used_gb,pct
local line=""
if command_exists free; then
line="$(LC_ALL=C free -m 2>/dev/null | awk '$1=="Mem:"{total=$2; avail=$7; if(avail==""||avail==0){used=$3}else{used=total-avail}; pct=(total>0)?used*100/total:0; printf "%.2f,%.2f,%.1f\n", total/1024, used/1024, pct }' || true)"
fi
if [[ -z "$line" ]] && [[ -r /proc/meminfo ]]; then
local total_kb=0 avail_kb=0
total_kb="$(awk '/MemTotal/{print $2; exit}' /proc/meminfo 2>/dev/null || echo 0)"
avail_kb="$(awk '/MemAvailable/{print $2; exit}' /proc/meminfo 2>/dev/null || echo 0)"
[[ "$avail_kb" -eq 0 ]] && avail_kb="$(awk '/MemFree/{print $2; exit}' /proc/meminfo 2>/dev/null || echo 0)"
local used_kb=$(( total_kb - avail_kb ))
local pct=0
[[ "$total_kb" -gt 0 ]] && pct=$(( used_kb * 100 / total_kb ))
local tot_gb=$(( total_kb / 1024 / 1024 ))
local use_gb=$(( used_kb / 1024 / 1024 ))
line="${tot_gb},${use_gb},${pct}"
fi
echo "${line:-0,0,0}"
}
# ------------------------------
# 7) 资源分析(CPU/内存/磁盘/防火墙)—增强:记录 raw
# ------------------------------
test_resources() {
log INFO "========== 服务器资源分析 =========="
local os=""
if [[ -f /etc/os-release ]]; then
os="$(grep -E '^(NAME|VERSION)=' /etc/os-release | head -n 2 | paste -sd' ' - | sed 's/NAME=//;s/VERSION=//;s/"//g' || true)"
fi
[[ -z "$os" ]] && os="$(uname -a 2>/dev/null || echo unknown)"
log INFO " 操作系统: $os"
report_kv_set "res.os" "$os"
local cpu cores
cpu="$(get_cpu_usage)"
cores="$(command_exists nproc && nproc || grep -c processor /proc/cpuinfo 2>/dev/null || echo 0)"
log INFO " CPU 使用率: ${cpu}% (核心数: $cores)"
report_kv_set "res.cpu_usage" "${cpu}%"
report_kv_set "res.cpu_cores" "$cores"
local mem_line total used pct
mem_line="$(get_mem_usage_gb_pct)"
IFS="," read -r total used pct <<<"$mem_line"
log INFO " 内存使用: ${used}GB / ${total}GB (${pct}%)"
report_kv_set "res.mem_total_gb" "${total}GB"
report_kv_set "res.mem_used_gb" "${used}GB"
report_kv_set "res.mem_pct" "${pct}%"
log INFO " 磁盘使用情况:"
local disk_lines=""
if command_exists df; then
disk_lines="$(df -h 2>/dev/null | awk 'NR==1{next} $1 ~ "^/dev/" {printf "%s|%s|%s|%s|%s\n",$6,$2,$3,$4,$5}' | head -n 20 || true)"
if [[ -n "$disk_lines" ]]; then
while IFS= read -r l; do [[ -n "$l" ]] && log INFO " $l"; done <<< "$disk_lines"
report_kv_set "res.disk" "$disk_lines"
else
report_kv_set "res.disk" "N/A"
fi
else
log WARN " df 不可用,跳过"
report_kv_set "res.disk" "N/A"
fi
local fw_active=0 fw_type="unknown" fw_open="" fw_raw=""
if command_exists systemctl && systemctl is-active firewalld >/dev/null 2>&1; then
fw_active=1; fw_type="firewalld"
local ports services
ports="$(firewall-cmd --list-ports 2>/dev/null || true)"
services="$(firewall-cmd --list-services 2>/dev/null || true)"
# ✅ fw.open 强制压成单行,避免 md 反引号被换行破坏
fw_open="$( { printf "%s\n" "$ports"; printf "%s\n" "$services"; } | _squash_ws_one_line )"
fw_raw="$( { echo "[firewall-cmd --list-ports]"; printf "%s\n" "$ports"; echo; echo "[firewall-cmd --list-services]"; printf "%s\n" "$services"; } | sed 's/\r$//' )"
elif command_exists iptables && iptables -L INPUT >/dev/null 2>&1; then
fw_type="iptables"
fw_raw="$(iptables -S 2>/dev/null || iptables -L -n -v 2>/dev/null || true)"
fw_open="$(printf "%s\n" "$fw_raw" | grep -oE 'dpt:[0-9]+' | cut -d: -f2 | sort -u | paste -sd',' - || true)"
[[ -n "$fw_open" ]] && fw_active=1 || fw_active=0
fi
report_kv_set "fw.type" "$fw_type"
report_kv_set "fw.active" "$fw_active"
report_kv_set "fw.open" "$(_kv_or_na "$fw_open")"
[[ -n "$fw_raw" ]] && report_kv_set "fw.raw" "$fw_raw"
if [[ "$fw_active" -eq 1 ]]; then
log INFO " 防火墙状态: 已启用 ($fw_type)"
[[ -n "$fw_open" ]] && log INFO " 开放端口/服务: $fw_open"
else
log WARN " 防火墙状态: 未启用或未知 ($fw_type)"
fi
local raw=""
raw+="操作系统: ${os}\n"
raw+="CPU 使用率: ${cpu}% (核心数: ${cores})\n"
raw+="内存使用: ${used}GB / ${total}GB (${pct}%)\n"
raw+="磁盘使用情况:\n"
if [[ -n "$disk_lines" && "$disk_lines" != "N/A" ]]; then
while IFS="|" read -r m t u a p; do
[[ -z "$m" ]] && continue
raw+=" ${m}|${t}|${u}|${a}|${p}\n"
done <<< "$disk_lines"
else
raw+=" N/A\n"
fi
raw+="防火墙状态: ${fw_active} (${fw_type})\n"
raw+="开放端口/服务: ${fw_open}\n"
report_kv_set "res.raw" "$(printf "%b" "$raw")"
}
# ------------------------------
# 8) NTP 检测(增强:写入 raw)
# ------------------------------
test_ntp() {
log INFO "========== NTP 服务检测 =========="
local impl="unknown"
local diff="N/A"
local raw=""
# 服务状态
local chrony_active=0 ntp_active=0
if command_exists systemctl; then
systemctl is-active chronyd >/dev/null 2>&1 && chrony_active=1 || true
systemctl is-active ntpd >/dev/null 2>&1 && ntp_active=1 || true
fi
if [[ "$chrony_active" -eq 1 ]]; then impl="chronyd"; fi
if [[ "$ntp_active" -eq 1 ]]; then impl="ntpd"; fi
# 尝试用 chrony 获取 offset(比你之前 date/local_ts 更靠谱)
if [[ "$impl" == "chronyd" ]] && command_exists chronyc; then
raw+="[chronyc tracking]\n"
raw+="$(chronyc tracking 2>/dev/null || true)\n\n"
# 从 tracking 中取 Last offset(可能是 +/-0.0000xxx seconds)
local off
off="$(chronyc tracking 2>/dev/null | awk -F: '/Last offset/ {gsub(/^[ \t]+/,"",$2); print $2}' | head -n1 || true)"
if [[ -n "$off" ]]; then
diff="$off"
fi
elif [[ "$impl" == "ntpd" ]] && command_exists ntpq; then
raw+="[ntpq -p]\n"
raw+="$(ntpq -p 2>/dev/null || true)\n\n"
fi
report_kv_set "ntp.impl" "$impl"
report_kv_set "ntp.diff_seconds" "$diff"
report_kv_set "ntp.raw" "${raw:-N/A}"
if [[ "$impl" == "unknown" ]]; then
log WARN "[NTP] 未检测到 chronyd/ntpd 正在运行"
echo "NOT_RUNNING"
return 0
fi
log INFO "[NTP] impl=$impl diff=$diff"
echo "OK"
}
repair_ntp_if_needed() {
local status="$1"
if [[ "$status" == "OK" ]]; then
return 0
fi
log WARN "[NTP] 状态=$status,触发修复:fix_ntp_config"
run_issue_handler "fix_ntp_config" "auto" "--ntp-auto" || true
log INFO "[NTP] 修复后复检..."
local post
post="$(test_ntp)"
report_kv_set "ntp.recheck" "$post"
if [[ "$post" == "OK" ]]; then
log SUCCESS "[NTP] 复检成功:已恢复正常"
else
log WARN "[NTP] 复检仍异常:$post"
fi
}
# ------------------------------
# 9) 文件权限检测(增强:统计缺失项 + 写入报告)
# ------------------------------
PERM_REPORT_INCLUDE_LISTING=1 # 1=在报告中列出存在项的ls -l;0=只统计
check_file_permissions() {
local platform="$1"
log INFO "========== 文件权限检测 (平台: $platform) =========="
local targets=()
if [[ "$platform" == "new" ]]; then
targets+=( "/data/services/api/auth/auth-sso-auth/run.sh" "/data/services/api/auth/auth-sso-gatway/run.sh" "/data/services/api/auth/auth-sso-system/run.sh"
"/data/services/api/java-meeting/java-meeting2.0/run.sh" "/data/services/api/java-meeting/java-meeting3.0/run.sh" "/data/services/api/java-meeting/java-meeting-extapi/run.sh"
"/data/services/api/java-meeting/java-message-scheduling/run.sh" "/data/services/api/java-meeting/java-mqtt/run.sh" "/data/services/api/java-meeting/java-quartz/run.sh"
"/data/services/api/start.sh" "/data/services/scripts/*.sh"
"/data/third_party/paperless/run.sh" "/data/third_party/paperless/start.sh"
"/data/third_party/wifi-local/config.ini" "/data/third_party/wifi-local/startDB.sh" "/data/third_party/wifi-local/wifi*"
"/etc/rc.d/rc.local"
"/data/middleware/nginx/config/*.conf" "/data/middleware/emqx/config/*.conf"
"/data/services/api/python-cmdb/*.sh" "/data/services/api/python-voice/*.sh" )
else
targets+=( "/var/www/java/api-java-meeting2.0/run.sh" "/var/www/java/external-meeting-api/run.sh" "/var/www/java/start.sh"
"/var/www/html/start.sh"
"/var/www/wifi-local/config.ini" "/var/www/wifi-local/startDB.sh" "/var/www/wifi-local/wifi*"
"/var/www/paperless/run.sh" "/var/www/paperless/start.sh"
"/var/www/redis/redis-*.conf"
"/var/www/emqx/config/*.conf"
"/etc/rc.d/rc.local" )
fi
local miss=0 exist=0
local report_list=()
local report_missing=()
for p in "${targets[@]}"; do
if ls -l $p >/dev/null 2>&1; then
exist=$((exist+1))
if [[ "$PERM_REPORT_INCLUDE_LISTING" -eq 1 ]]; then
# 只取前 50 行,防止 *.conf 太多
while IFS= read -r l; do
[[ -n "$l" ]] && report_list+=("$l")
log SUCCESS "[PERM] $l"
done < <(ls -l $p 2>/dev/null | awk '/^[-dl]/{print}' | head -n 50)
else
log SUCCESS "[PERM] EXISTS $p"
fi
else
log WARN "[PERM] MISS $p"
miss=$((miss+1))
report_missing+=("$p")
fi
done
log INFO "[PERM] 检测结束:存在项=${exist} 缺失项=${miss}"
report_kv_set "perm.exist_count" "$exist"
report_kv_set "perm.miss_count" "$miss"
if [[ "${#report_missing[@]}" -gt 0 ]]; then
report_kv_set "perm.missing" "$(printf "%s\n" "${report_missing[@]}")"
fi
if [[ "${#report_list[@]}" -gt 0 ]]; then
report_kv_set "perm.listing" "$(printf "%s\n" "${report_list[@]}")"
fi
}
# ------------------------------
# 10) 容器信息收集(增强:写入报告KV)
# ------------------------------
collect_container_info() {
log INFO "========== 容器信息收集 =========="
if ! command_exists docker; then
log WARN "Docker 不可用,跳过容器信息收集"
report_kv_set "docker.available" "false"
return 0
fi
report_kv_set "docker.available" "true"
local all
all="$(docker ps -a --format '{{.ID}} {{.Names}} {{.Status}}' 2>/dev/null || true)"
local running=() stopped=()
while IFS= read -r line; do
[[ -z "$line" ]] && continue
local id name status
id="$(awk '{print $1}' <<<"$line")"
name="$(awk '{print $2}' <<<"$line")"
status="$(cut -d' ' -f3- <<<"$line")"
if [[ "$status" =~ ^Up ]]; then
running+=("$name|$id|$status")
else
stopped+=("$name|$id|$status")
fi
done <<< "$all"
if [[ "${#running[@]}" -gt 0 ]]; then
report_kv_set "docker.running" "$(printf "%s\n" "${running[@]}")"
fi
if [[ "${#stopped[@]}" -gt 0 ]]; then
report_kv_set "docker.stopped" "$(printf "%s\n" "${stopped[@]}")"
fi
log INFO "----- 运行中的容器 -----"
for x in "${running[@]}"; do
IFS="|" read -r name id status <<<"$x"
log INFO " [RUNNING] $name ($id) - $status"
done
log INFO "----- 未运行的容器 -----"
for x in "${stopped[@]}"; do
IFS="|" read -r name id status <<<"$x"
log INFO " [STOPPED] $name ($id) - $status"
done
local out_base="$SCRIPT_DIR/output/$(get_primary_ip)"
mkdir -p "$out_base/logs/redis" "$out_base/logs/emqx"
report_kv_set "middleware.export_base" "$out_base"
local platform="$1"
local redis_log="/var/www/redis/data/redis.log"
local emqx_log="/var/www/emqx/log/emqx.log.1"
if [[ "$platform" == "new" ]]; then
redis_log="/data/middleware/redis/data/redis.log"
emqx_log="/data/middleware/emqx/log/emqx.log.1"
fi
local redis_export="" emqx_export=""
if [[ -f "$redis_log" ]]; then
redis_export="$out_base/logs/redis/redis.log"
cp -f "$redis_log" "$redis_export" 2>/dev/null || true
log SUCCESS "[Redis] redis.log 已导出:$redis_export"
else
log WARN "[Redis] redis.log 不存在:$redis_log"
fi
if [[ -f "$emqx_log" ]]; then
emqx_export="$out_base/logs/emqx/emqx.log.1"
cp -f "$emqx_log" "$emqx_export" 2>/dev/null || true
log SUCCESS "[Emqx] emqx.log.1 已导出:$emqx_export"
else
log WARN "[Emqx] emqx.log.1 不存在:$emqx_log"
fi
report_kv_set "redis.export" "${redis_export:-N/A}"
report_kv_set "emqx.export" "${emqx_export:-N/A}"
# Redis 异常判定
local redis_running=0 uredis_running=0 uredis_stopped=0
for x in "${running[@]}"; do
IFS="|" read -r name _ _ <<<"$x"
[[ "$name" =~ [Rr][Ee][Dd][Ii][Ss] ]] && redis_running=1
[[ "$name" == "uredis" ]] && uredis_running=1
done
for x in "${stopped[@]}"; do
IFS="|" read -r name _ _ <<<"$x"
[[ "$name" == "uredis" ]] && uredis_stopped=1
done
report_kv_set "redis.redis_running" "$redis_running"
report_kv_set "redis.uredis_running" "$uredis_running"
report_kv_set "redis.uredis_stopped" "$uredis_stopped"
if [[ "$uredis_running" -eq 0 && "$uredis_stopped" -eq 1 && "$redis_running" -eq 0 ]]; then
log ERROR "[Redis] 判定 Redis 容器异常:uredis 未运行且无其他 redis 容器运行,触发修复"
report_kv_set "redis.exception" "true"
run_issue_handler "redis_container_exception" "auto" "--non-interactive --yes" || true
if docker ps --format '{{.Names}}' | grep -w uredis >/dev/null 2>&1; then
log SUCCESS "[Redis] 复检成功:uredis 已运行"
report_kv_set "redis.recheck" "OK"
else
log WARN "[Redis] 复检失败:uredis 仍未运行(需人工排查)"
report_kv_set "redis.recheck" "FAIL"
fi
else
log INFO "[Redis] 未检测到需要自动修复的 Redis 容器异常"
report_kv_set "redis.exception" "false"
fi
}
# ------------------------------
# 11) 日志导出(增强:记录导出文件列表)
# ------------------------------
export_logs() {
local platform="$1"
local has_ujava="$2"
local has_upython="$3"
local ujava_container="$4"
local upython_container="$5"
log INFO "========== 服务日志导出 =========="
local export_dir="$SCRIPT_DIR/logs_$(get_primary_ip)_${TS}"
mkdir -p "$export_dir"
# 收集导出结果(写入报告KV)
local exported_files=()
copy_if_exists() {
local src="$1"
local dst="$2"
if [[ -f "$src" ]]; then
cp -f "$src" "$dst" 2>/dev/null || true
exported_files+=("$(basename "$dst")|$src")
return 0
fi
return 1
}
if [[ "$platform" == "new" && "$has_ujava" -eq 1 ]]; then
# ...existing code...(保持不变,但把 cp 的地方尽量改用 copy_if_exists;不改也能跑)
:
fi
if [[ "$platform" == "old" ]]; then
if [[ "$has_ujava" -eq 1 ]]; then
copy_if_exists "/var/www/java/api-java-meeting2.0/logs/ubains-INFO-AND-ERROR.log" "$export_dir/对内后端_ubains-INFO-AND-ERROR.log" || true
copy_if_exists "/var/www/java/external-meeting-api/logs/ubains-INFO-AND-ERROR.log" "$export_dir/对外后端_ubains-INFO-AND-ERROR.log" || true
fi
if [[ "$has_upython" -eq 1 ]]; then
copy_if_exists "/var/www/html/log/error.log" "$export_dir/运维集控_error.log" || true
copy_if_exists "/var/www/html/log/uinfo.log" "$export_dir/运维集控_uinfo.log" || true
copy_if_exists "/var/www/html/log/uwsgi.log" "$export_dir/运维集控_uwsgi.log" || true
fi
fi
if [[ "${#exported_files[@]}" -gt 0 ]]; then
report_kv_set "log_export.dir" "$export_dir"
report_kv_set "log_export.files" "$(printf "%s\n" "${exported_files[@]}")"
else
report_kv_set "log_export.dir" "$export_dir"
report_kv_set "log_export.files" ""
fi
log SUCCESS "日志导出目录: $export_dir"
echo "$export_dir"
}
# ------------------------------
# 12) Android 自检(增强:记录状态/导出目录)
# ------------------------------
android_self_check() {
log INFO "========== 安卓设备自检 (PRD 15) =========="
if ! command_exists adb; then
log WARN "[Android] 未检测到 adb,跳过"
report_kv_set "android.status" "SKIP(no_adb)"
return 0
fi
read -r -p "请输入安卓设备IP(留空跳过安卓自检): " dev_ip
dev_ip="${dev_ip// /}"
if [[ -z "$dev_ip" || "$dev_ip" =~ ^(n|no|skip)$ ]]; then
log INFO "[Android] 已跳过安卓自检"
report_kv_set "android.status" "SKIP"
return 0
fi
read -r -p "请输入安卓设备端口 [默认 5555]: " dev_port
dev_port="${dev_port// /}"
dev_port="${dev_port:-5555}"
if ! [[ "$dev_port" =~ ^[0-9]+$ ]]; then
log WARN "[Android] 端口输入无效:$dev_port,使用默认 5555"
dev_port="5555"
fi
local target="${dev_ip}:${dev_port}"
report_kv_set "android.target" "$target"
log INFO "[Android] 连接设备: adb connect $target"
if ! adb connect "$target" >/dev/null 2>&1; then
log ERROR "[Android] 连接失败: $target"
report_kv_set "android.status" "FAIL(connect)"
return 0
fi
log SUCCESS "[Android] 连接成功: $target"
local out_dir="$SCRIPT_DIR/logs/android/${target//[:\/]/_}_${TS}"
mkdir -p "$out_dir"
report_kv_set "android.export_dir" "$out_dir"
adb -s "$target" pull "/sdcard/Android/data/com.ubains.local.gviewer/files/" "$out_dir/files" >/dev/null 2>&1 || true
if adb -s "$target" shell "test -d /sdcard/Android/data/com.ubains.local.gviewer/cache" >/dev/null 2>&1; then
adb -s "$target" pull "/sdcard/Android/data/com.ubains.local.gviewer/cache/" "$out_dir/cache" >/dev/null 2>&1 || true
fi
adb disconnect "$target" >/dev/null 2>&1 || true
report_kv_set "android.status" "OK"
log SUCCESS "[Android] 日志导出目录: $out_dir"
}
# ------------------------------
# 14) 配置文件 IP 检测(补齐:按 PRD 8;只记录异常/需关注项;合法项不打印)
# 规则:
# - 允许:目标服务器IP / 127.0.0.1 / 172.17.0.1
# - 仅打印出:出现“疑似 IP”但不在允许列表内的行
# - 不修改任何文件
# 输出:
# - cfgip.status = OK/WARN/FAIL
# - cfgip.server_ip
# - cfgip.scan_root (平台根)
# - cfgip.finding.count
# - cfgip.finding.lines(代码块原样输出,格式:file:line:content)
# ------------------------------
CFGIP_ALLOWED_EXTRA=("127.0.0.1" "172.17.0.1")
_cfgip_allowed() {
local ip="$1"
local server_ip="$2"
[[ "$ip" == "$server_ip" ]] && return 0
local x
for x in "${CFGIP_ALLOWED_EXTRA[@]}"; do
[[ "$ip" == "$x" ]] && return 0
done
return 1
}
_cfgip_add_file_if_exists() {
local f="$1"
[[ -f "$f" ]] && printf "%s\n" "$f"
}
_cfgip_add_glob_files() {
local dir="$1"; shift
local ext f
[[ -d "$dir" ]] || return 0
shopt -s nullglob
for ext in "$@"; do
for f in "$dir"/*."$ext"; do
[[ -f "$f" ]] && printf "%s\n" "$f"
done
done
shopt -u nullglob
}
_cfgip_add_find_files() {
# 用法:_cfgip_add_find_files "/data/services/web/pc/pc-vue2-main" 5000 js json conf
local dir="$1"; shift
local max="${1:-5000}"; shift
local exts=("$@")
[[ -d "$dir" ]] || return 0
[[ "${#exts[@]}" -gt 0 ]] || exts=(js json conf)
local cond="" ext
for ext in "${exts[@]}"; do
cond="${cond} -name '*.${ext}' -o"
done
cond="${cond% -o}"
find "$dir" \
-type d \( -name node_modules -o -name dist -o -name build -o -name .git \) -prune -o \
-type f \( $cond \) -print 2>/dev/null | head -n "$max"
}
_cfgip_list_files_new() {
{
_cfgip_add_glob_files "/data/services/api/auth/auth-sso-auth/config" yml yaml properties js json conf
_cfgip_add_glob_files "/data/services/api/auth/auth-sso-gatway/config" yml yaml properties js json conf
_cfgip_add_glob_files "/data/services/api/auth/auth-sso-system/config" yml yaml properties js json conf
_cfgip_add_glob_files "/data/services/api/java-meeting/java-meeting2.0/config" yml yaml properties js json conf
_cfgip_add_glob_files "/data/services/api/java-meeting/java-meeting3.0/config" yml yaml properties js json conf
_cfgip_add_glob_files "/data/services/api/java-meeting/java-meeting-extapi/config" yml yaml properties js json conf
_cfgip_add_glob_files "/data/services/api/java-meeting/java-message-scheduling/config" yml yaml properties js json conf
_cfgip_add_glob_files "/data/services/api/java-meeting/java-mqtt/config" yml yaml properties js json conf
_cfgip_add_glob_files "/data/services/api/java-meeting/java-quartz/config" yml yaml properties js json conf
_cfgip_add_glob_files "/data/services/api/python-cmdb" yml yaml properties js json conf
_cfgip_add_find_files "/data/services/web/pc/pc-vue2-ai" 5000 js json conf
_cfgip_add_find_files "/data/services/web/pc/pc-vue2-backstage" 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-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-meetngV3" 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-voice" 5000 js json conf
_cfgip_add_glob_files "/data/middleware/nginx/config" conf
} | awk 'NF' | sort -u
}
_cfgip_list_files_old() {
{
_cfgip_add_glob_files "/var/www/java/api-java-meeting2.0/config" yml yaml properties js json conf
_cfgip_add_glob_files "/var/www/java/external-meeting-api/config" yml yaml properties js json conf
_cfgip_add_file_if_exists "/var/www/java/ubains-web-2.0/static/config.json"
_cfgip_add_file_if_exists "/var/www/java/ubains-web-admin/static/config.json"
_cfgip_add_file_if_exists "/var/www/java/ubains-web-h5/static/h5/config.js"
_cfgip_add_file_if_exists "/var/www/java/ubains-web-h5/static/h5/config.json"
_cfgip_add_glob_files "/var/www/java/nginx-conf.d" conf
_cfgip_add_glob_files "/var/www/html/conf" conf json js
_cfgip_add_glob_files "/var/www/html/nginx-conf" conf
_cfgip_add_file_if_exists "/var/www/html/web-vue-rms/static/config.json"
_cfgip_add_file_if_exists "/var/www/html/web-vue-h5/static/h5/config.json"
_cfgip_add_file_if_exists "/var/www/html/web-vue-h5/static/h5/config.js"
} | awk 'NF' | sort -u
}
# ---- helper: 永远用安全格式输出(避免 printf %sn 之类拼写问题)
p() {
local s
for s in "$@"; do
printf '%s\n' "$s"
done
}
test_config_ip() {
local platform="$1"
local server_ip="$2"
section "配置文件 IP 检测"
log INFO "[CFGIP] 开始扫描配置文件中的 IP(只输出异常/需关注项)"
report_kv_set "cfgip.server_ip" "$server_ip"
report_kv_set "cfgip.status" "OK"
local scan_root
if [[ "$platform" == "new" ]]; then
scan_root="/data"
else
scan_root="/var/www"
fi
report_kv_set "cfgip.scan_root" "$scan_root"
local ip_re='([0-9]{1,3}\.){3}[0-9]{1,3}'
local findings=()
local count=0
# 关键修复点:
# 1) 不把文件列表落到变量里(避免 CRLF/不可见字符污染后续处理)
# 2) 对读到的路径做强过滤:去 CR、跳过含冒号、跳过空白异常
local list_cmd=""
if [[ "$platform" == "new" ]]; then
list_cmd="_cfgip_list_files_new"
else
list_cmd="_cfgip_list_files_old"
fi
while IFS= read -r f; do
# 去掉潜在的 CR(Windows 换行)
f="${f%$'\r'}"
[[ -z "$f" ]] && continue
# 防御:过滤掉意外混入的 "file:line:content" 或其他带冒号的行
[[ "$f" == *":"* ]] && continue
# 防御:路径不应包含制表符等
[[ "$f" =~ [[:space:]] ]] && {
# 如果你确认你的路径不会有空格,直接跳过更安全
continue
}
[[ -f "$f" ]] || continue
# 文本过滤(可选但建议)
if command_exists file; then
if ! file "$f" 2>/dev/null | grep -qiE 'text|json|xml|yaml|ascii|utf-8'; then
continue
fi
fi
local hits
hits="$(grep -nE "$ip_re" "$f" 2>/dev/null || true)"
[[ -z "$hits" ]] && continue
while IFS= read -r line; do
line="${line%$'\r'}"
[[ -z "$line" ]] && continue
local ln content
ln="${line%%:*}"
content="${line#*:}"
# ✅ 关键:清理控制字符,避免写报告时 printf/解析错位
content="${content//$'\r'/}"
content="${content//$'\0'/}"
local ips
ips="$(grep -oE "$ip_re" <<<"$content" | sort -u || true)"
[[ -z "$ips" ]] && continue
local bad=0 ip
while IFS= read -r ip; do
ip="${ip%$'\r'}"
[[ -z "$ip" ]] && continue
if ! _cfgip_allowed "$ip" "$server_ip"; then
bad=1
break
fi
done <<<"$ips"
if [[ "$bad" -eq 1 ]]; then
count=$((count+1))
findings+=("${f}:${ln}:${content}")
log WARN "[CFGIP] 异常IP行: ${f}:${ln}:${content}"
fi
done <<<"$hits"
done < <($list_cmd)
report_kv_set "cfgip.finding.count" "$count"
if [[ "$count" -gt 0 ]]; then
report_kv_set "cfgip.status" "WARN"
# 改成 helper,避免引号问题
report_kv_set "cfgip.finding.lines" "$(p "${findings[@]}")"
else
report_kv_set "cfgip.finding.lines" ""
fi
report_add ""
report_add "## 配置文件 IP 检测"
report_add "- status: $(report_kv_get "cfgip.status")"
report_add "- finding_count: $count"
}
# ------------------------------
# 15) 定时任务查询(补齐:PRD 13)
# - 只检测、记录,不改动
# - 输出:
# sched.status = OK/WARN
# sched.crontab.lines = 原样输出
# sched.expected.miss = 缺失项(如果能识别平台)
# ------------------------------
test_scheduled_tasks() {
section "定时任务查询(crontab)"
local platform="$1"
local cron
cron="$(crontab -l 2>/dev/null || true)"
if [[ -z "$cron" ]]; then
log WARN "[CRON] 当前用户无 crontab 或无法读取"
report_kv_set "sched.status" "WARN"
report_kv_set "sched.crontab.lines" "N/A"
return 0
fi
log INFO "[CRON] crontab -l:"
while IFS= read -r l; do
[[ -n "$l" ]] && log INFO " $l"
done <<< "$cron"
report_kv_set "sched.crontab.lines" "$cron"
local expected=()
expected+=("0 13 * * * bash /usr/local/docker/UbainsmysqlBakUp.sh")
if [[ "$platform" == "new" ]]; then
expected+=("*/3 * * * * /data/services/scripts/ujava2-startup.sh")
fi
local miss=()
local e
for e in "${expected[@]}"; do
if grep -Fq "$e" <<<"$cron"; then
:
else
miss+=("$e")
fi
done
if [[ "${#miss[@]}" -gt 0 ]]; then
report_kv_set "sched.status" "WARN"
report_kv_set "sched.expected.miss" "$(printf "%s\n" "${miss[@]}")"
log WARN "[CRON] 缺失预期任务:"
for e in "${miss[@]}"; do log WARN " $e"; done
else
report_kv_set "sched.status" "OK"
report_kv_set "sched.expected.miss" ""
log SUCCESS "[CRON] 预期任务已存在"
fi
report_add ""
report_add "## 定时任务查询"
report_add "- status: $(report_kv_get "sched.status")"
}
# ------------------------------
# 16) 容器异常:Emqx(补齐:PRD 12)
# 规则:
# - 如果 uemqx 未运行 && 没有其它包含 emqx 的运行容器 => 异常
# - 执行 issue_handler.sh --action emqx_container_exception
# - 复检 uemqx 是否 Up
# - 输出:
# emqx.exception, emqx.recheck
# ------------------------------
check_emqx_container_exception() {
local running_txt stopped_txt
running_txt="$(report_kv_get "docker.running")"
stopped_txt="$(report_kv_get "docker.stopped")"
# docker 不可用直接跳过
if [[ "$(report_kv_get "docker.available")" != "true" ]]; then
report_kv_set "emqx.exception" "SKIP(no_docker)"
return 0
fi
local has_emqx_running=0 uemqx_running=0 uemqx_stopped=0
while IFS= read -r l; do
[[ -z "$l" ]] && continue
local name="${l%%|*}"
[[ "$name" == "uemqx" ]] && uemqx_running=1
[[ "$name" =~ [Ee][Mm][Qq][Xx] ]] && has_emqx_running=1
done <<<"$running_txt"
while IFS= read -r l; do
[[ -z "$l" ]] && continue
local name="${l%%|*}"
[[ "$name" == "uemqx" ]] && uemqx_stopped=1
done <<<"$stopped_txt"
if [[ "$uemqx_running" -eq 0 && "$uemqx_stopped" -eq 1 && "$has_emqx_running" -eq 0 ]]; then
log ERROR "[Emqx] 判定 Emqx 容器异常:uemqx 未运行且无其他 emqx 容器运行,触发修复"
report_kv_set "emqx.exception" "true"
run_issue_handler "emqx_container_exception" "auto" "--non-interactive --yes" || true
if docker ps --format '{{.Names}}' | grep -w uemqx >/dev/null 2>&1; then
log SUCCESS "[Emqx] 复检成功:uemqx 已运行"
report_kv_set "emqx.recheck" "OK"
else
log WARN "[Emqx] 复检失败:uemqx 仍未运行(需人工排查)"
report_kv_set "emqx.recheck" "FAIL"
fi
else
log INFO "[Emqx] 未检测到需要自动修复的 Emqx 容器异常"
report_kv_set "emqx.exception" "false"
fi
}
# ------------------------------
# 17) 报告生成(Markdown)—增强:把 KV 也写入
# ------------------------------
write_report() {
local server_ip="$1"
local platform="$2"
local systems_txt="$3"
local report_file="$4"
: > "$report_file" || return 1
w() {
local s
for s in "$@"; do
printf '%s\n' "$s" >>"$report_file"
done
}
w "<!-- REPORT_GENERATOR: sh-write_report-v4(unified) $(date '+%F %T') -->"
w ""
w "# 服务自检报告"
w ""
w "- 服务器地址: ${server_ip}"
w "- 平台类型: $( [[ "$platform" == "new" ]] && echo "新统一平台" || echo "传统平台" )"
w "- 检测时间: $(date '+%Y-%m-%d %H:%M:%S')"
w "- 脚本版本: $SCRIPT_VERSION"
w ""
w "## 系统识别"
w ""
w '```'
printf "%s\n" "$systems_txt" >> "$report_file"
w '```'
w ""
# ✅ 摘要:只放结论,不放长明细(避免重复、不易读)
w "## 检测结果摘要"
w ""
w "- DNS: \`$(report_kv_get "dns.status")\` (nameserver: $(report_kv_get "dns.nameservers"))"
w "- CPU: \`$(report_kv_get "res.cpu_usage")\` MEM: \`$(report_kv_get "res.mem_used_gb")/$(report_kv_get "res.mem_total_gb") ($(report_kv_get "res.mem_pct"))\`"
w "- DISK: 详见“检测详情 -> 资源分析/磁盘”(关注 /home 这类高占用挂载点)"
w "- Firewall: type=\`$(report_kv_get "fw.type")\` active=\`$(report_kv_get "fw.active")\` open=\`$(report_kv_get "fw.open")\`"
w "- NTP: \`$(report_kv_get "ntp.status")\` (impl: $(report_kv_get "ntp.impl"), diff: $(report_kv_get "ntp.diff_seconds"))"
w "- Perm: miss_count=\`$(report_kv_get "perm.miss_count")\`"
w "- Docker: \`$(report_kv_get "docker.available")\`"
w ""
w "## 检测详情"
w ""
# DNS 详情(raw)
w "### DNS 检测(详细)"
w "- 初次检测结果: \`$(report_kv_get "dns.status")\`"
w "- nameserver: \`$(report_kv_get "dns.nameservers")\`"
local dns_raw
dns_raw="$(report_kv_get "dns.raw")"
if [[ -n "$dns_raw" ]]; then
w ""
w '```'
printf "%s\n" "$dns_raw" >> "$report_file"
w '```'
fi
w ""
# 资源 raw + disk 表格
w "### 资源分析(详细)"
local res_raw
res_raw="$(report_kv_get "res.raw")"
if [[ -n "$res_raw" ]]; then
w '```'
printf "%s\n" "$res_raw" >> "$report_file"
w '```'
fi
local disk
disk="$(report_kv_get "res.disk")"
if [[ -n "$disk" && "$disk" != "N/A" ]]; then
w ""
w "| 挂载点 | 总量 | 已用 | 可用 | 使用率 |"
w "|---|---:|---:|---:|---:|"
while IFS="|" read -r m t u a p; do
[[ -z "$m" ]] && continue
w "| $m | $t | $u | $a | $p |"
done <<< "$disk"
fi
w ""
# 防火墙:open 单行 + raw 代码块
w "### 防火墙(详细)"
w "- type: \`$(report_kv_get "fw.type")\`"
w "- active: \`$(report_kv_get "fw.active")\`"
w "- open: \`$(report_kv_get "fw.open")\`"
local fw_raw
fw_raw="$(report_kv_get "fw.raw")"
if [[ -n "$fw_raw" ]]; then
w ""
w '```'
printf "%s\n" "$fw_raw" >> "$report_file"
w '```'
fi
w ""
# NTP:保证不空 + raw
w "### NTP 检测(详细)"
w "- 初次检测结果: \`$(report_kv_get "ntp.status")\`"
w "- impl: \`$(report_kv_get "ntp.impl")\`"
w "- diff: \`$(report_kv_get "ntp.diff_seconds")\`"
local ntp_raw
ntp_raw="$(report_kv_get "ntp.raw")"
if [[ -n "$ntp_raw" && "$ntp_raw" != "N/A" ]]; then
w ""
w '```'
printf "%s\n" "$ntp_raw" >> "$report_file"
w '```'
fi
w ""
# 权限:listing + missing
w "### 文件权限检测(详细)"
w "- exist_count: \`$(report_kv_get "perm.exist_count")\`"
w "- miss_count: \`$(report_kv_get "perm.miss_count")\`"
local perm_listing perm_missing
perm_listing="$(report_kv_get "perm.listing")"
perm_missing="$(report_kv_get "perm.missing")"
if [[ -n "$perm_listing" ]]; then
w ""
w "**存在项(ls -l):**"
w '```'
printf "%s\n" "$perm_listing" >> "$report_file"
w '```'
fi
if [[ -n "$perm_missing" ]]; then
w ""
w "**缺失项:**"
w '```'
printf "%s\n" "$perm_missing" >> "$report_file"
w '```'
fi
w ""
# 容器(原样输出)
w "### 容器与中间件(详细)"
w "- docker: \`$(report_kv_get "docker.available")\`"
local dr ds
dr="$(report_kv_get "docker.running")"
ds="$(report_kv_get "docker.stopped")"
if [[ -n "$dr" ]]; then
w ""
w "**运行容器(name|id|status):**"
w '```'
printf "%s\n" "$dr" >> "$report_file"
w '```'
fi
if [[ -n "$ds" ]]; then
w ""
w "**停止容器(name|id|status):**"
w '```'
printf "%s\n" "$ds" >> "$report_file"
w '```'
fi
w "- redis.log export: \`$(report_kv_get "redis.export")\`"
w "- emqx.log export: \`$(report_kv_get "emqx.export")\`"
w "- redis container exception: \`$(report_kv_get "redis.exception")\`"
local redis_recheck
redis_recheck="$(report_kv_get "redis.recheck")"
[[ -n "$redis_recheck" ]] && w "- redis recheck: \`$redis_recheck\`"
w ""
w "### 配置文件 IP 检测(详细)"
w "- status: \`$(report_kv_get "cfgip.status")\`"
w "- server_ip: \`$(report_kv_get "cfgip.server_ip")\`"
w "- scan_root: \`$(report_kv_get "cfgip.scan_root")\`"
w "- finding_count: \`$(report_kv_get "cfgip.finding.count")\`"
local cfg_lines
cfg_lines="$(report_kv_get "cfgip.finding.lines")"
if [[ -n "$cfg_lines" ]]; then
w ""
w '```'
printf "%s\n" "$cfg_lines" >>"$report_file"
w '```'
else
w "- findings: (无异常IP行)"
fi
w ""
w "### 定时任务查询(详细)"
w "- status: \`$(report_kv_get "sched.status")\`"
local cron_lines miss_lines
cron_lines="$(report_kv_get "sched.crontab.lines")"
miss_lines="$(report_kv_get "sched.expected.miss")"
if [[ -n "$cron_lines" && "$cron_lines" != "N/A" ]]; then
w ""
w "**crontab -l:**"
w '```'
printf "%s\n" "$cron_lines" >>"$report_file"
w '```'
fi
if [[ -n "$miss_lines" ]]; then
w ""
w "**缺失的预期任务:**"
w '```'
printf "%s\n" "$miss_lines" >>"$report_file"
w '```'
fi
w ""
w "### Emqx 容器异常(详细)"
w "- exception: \`$(report_kv_get "emqx.exception")\`"
local emqx_recheck
emqx_recheck="$(report_kv_get "emqx.recheck")"
[[ -n "$emqx_recheck" ]] && w "- recheck: \`$emqx_recheck\`"
w ""
# 说明
w "## 说明"
w ""
w "- 详细过程日志见:\`${LOG_FILE}\`"
}
# ------------------------------
# 主流程
# ------------------------------
main() {
section "服务自检开始 (Linux 版本,本机运行)"
log INFO "脚本版本: $SCRIPT_VERSION"
log INFO "脚本路径: $SCRIPT_DIR"
log INFO "日志文件: $LOG_FILE"
section "平台识别/系统识别"
local server_ip platform
server_ip="$(get_primary_ip)"
platform="$(detect_platform)"
log INFO "[平台识别] 平台类型=$( [[ "$platform" == "new" ]] && echo "新统一平台(/data/services)" || echo "传统平台(/var/www)" )"
report_add "## 平台识别"
report_add "- 平台类型: $( [[ "$platform" == "new" ]] && echo "新统一平台(/data/services)" || echo "传统平台(/var/www)" )"
local sys_kv
sys_kv="$(detect_systems)"
eval "$sys_kv"
log INFO "[系统识别] HAS_UJAVA=$HAS_UJAVA HAS_UPYTHON=$HAS_UPYTHON HAS_UPYTHON_VOICE=$HAS_UPYTHON_VOICE containers=(ujava=$UJAVA_CONTAINER upython=$UPYTHON_CONTAINER upython_voice=$UPYTHON_VOICE_CONTAINER)"
report_add ""
report_add "## 系统识别(摘要)"
report_add_block '```' "$sys_kv" '```'
# 1) 服务检测
section "服务检测"
local ujava_container_results=""
local ujava_host_results=""
local upython_results=""
local upython_voice_results=""
if [[ "$platform" == "new" ]]; then
if [[ "$HAS_UJAVA" -eq 1 && -n "${UJAVA_CONTAINER:-}" ]]; then
ujava_container_results="$(test_ujava_services "$UJAVA_CONTAINER" "$platform" || true)"
ujava_host_results="$(test_ujava_host_services || true)"
if echo "$ujava_host_results" | grep -q "extapi|ubains-meeting-api-1.0-SNAPSHOT.jar|stopped"; then
log WARN "[EXT] 检测到对外服务未运行,触发修复: fix_external_service_disconnect"
run_issue_handler "fix_external_service_disconnect" "$platform" "--non-interactive --yes" || true
log INFO "[EXT] 修复后复检 extapi..."
test_ujava_host_services >/dev/null || true
fi
else
log WARN "未检测到 ujava 容器,改为宿主机检测 ujava 进程"
ujava_container_results="$(test_ujava_services "" "$platform" || true)"
ujava_host_results="$(test_ujava_host_services || true)"
fi
if [[ "$HAS_UPYTHON" -eq 1 && -n "${UPYTHON_CONTAINER:-}" ]]; then
upython_results="$(test_container_ports "$UPYTHON_CONTAINER" "upython" "${UPYTHON_PORTS_NEW[@]}" || true)"
fi
if [[ "$HAS_UPYTHON_VOICE" -eq 1 && -n "${UPYTHON_VOICE_CONTAINER:-}" ]]; then
upython_voice_results="$(test_container_ports "$UPYTHON_VOICE_CONTAINER" "upython_voice" "${UPYTHON_VOICE_PORTS_NEW[@]}" || true)"
fi
else
log INFO "传统平台:使用传统平台检测逻辑"
if [[ "$HAS_UJAVA" -eq 1 && -n "${UJAVA_CONTAINER:-}" ]]; then
ujava_container_results="$(test_ujava_old_container_services "$UJAVA_CONTAINER" || true)"
else
log WARN "未检测到 ujava 容器,跳过容器内服务检测"
fi
ujava_host_results="$(test_ujava_host_services || true)"
if echo "$ujava_host_results" | grep -q "extapi|ubains-meeting-api-1.0-SNAPSHOT.jar|stopped"; then
log WARN "[EXT] 检测到对外服务未运行,触发修复: fix_external_service_disconnect"
run_issue_handler "fix_external_service_disconnect" "$platform" "--non-interactive --yes" || true
log INFO "[EXT] 修复后复检 extapi..."
test_ujava_host_services >/dev/null || true
fi
if [[ "$HAS_UPYTHON" -eq 1 && -n "${UPYTHON_CONTAINER:-}" ]]; then
upython_results="$(test_container_ports "$UPYTHON_CONTAINER" "upython(传统平台)" "${UPYTHON_PORTS_OLD[@]}" || true)"
fi
if [[ "$HAS_UPYTHON_VOICE" -eq 1 && -n "${UPYTHON_VOICE_CONTAINER:-}" ]]; then
upython_voice_results="$(test_container_ports "$UPYTHON_VOICE_CONTAINER" "upython_voice" "${UPYTHON_VOICE_PORTS_NEW[@]}" || true)"
fi
fi
report_add ""
report_add "## 服务检测"
if [[ -n "${ujava_container_results:-}" ]]; then
report_add "- ujava 检测结果:"
report_add_block '```' "$ujava_container_results" '```'
else
report_add "- ujava 检测结果:无(未检测或无容器/进程)"
fi
if [[ -n "${ujava_host_results:-}" ]]; then
report_add "- extapi 检测结果:"
report_add_block '```' "$ujava_host_results" '```'
else
report_add "- extapi 检测结果:无"
fi
if [[ -n "${upython_results:-}" ]]; then
report_add "- upython 端口检测结果:"
report_add_block '```' "$upython_results" '```'
else
report_add "- upython 端口检测结果:无"
fi
if [[ -n "${upython_voice_results:-}" ]]; then
report_add "- upython_voice 端口检测结果:"
report_add_block '```' "$upython_voice_results" '```'
fi
# 2) DNS
section "DNS 检测"
local dns_status
dns_status="$(test_dns)"
report_kv_set "dns.status" "$dns_status"
repair_dns_if_needed "$dns_status"
report_add ""
report_add "## DNS 检测"
report_add "- 初次检测结果: $dns_status"
local dns_recheck
dns_recheck="$(report_kv_get "dns.recheck")"
[[ -n "$dns_recheck" ]] && report_add "- 修复后复检结果: $dns_recheck"
report_add "- nameserver: $(report_kv_get "dns.nameservers")"
# 3) 资源
section "资源分析"
test_resources
report_add ""
report_add "## 资源分析"
report_add "- OS: $(report_kv_get "res.os")"
report_add "- CPU: $(report_kv_get "res.cpu_usage") (cores: $(report_kv_get "res.cpu_cores"))"
report_add "- MEM: $(report_kv_get "res.mem_used_gb")/$(report_kv_get "res.mem_total_gb") ($(report_kv_get "res.mem_pct"))"
report_add "- DISK: 详见下方“检测详情 -> 资源分析/磁盘”"
report_add "- 防火墙: type=$(report_kv_get "fw.type") active=$(report_kv_get "fw.active") open=$(report_kv_get "fw.open")"
# 4) 配置文件 IP 检测(✅ 已实现:替换“未实现”占位)
test_config_ip "$platform" "$server_ip"
# (test_config_ip 内部已做 section + report_add 摘要,这里不再重复写)
# 5) NTP
section "NTP 检测"
local ntp_status
ntp_status="$(test_ntp)"
report_kv_set "ntp.status" "$ntp_status"
repair_ntp_if_needed "$ntp_status"
report_add ""
report_add "## NTP 检测"
report_add "- 初次检测结果: $ntp_status"
report_add "- impl: $(report_kv_get "ntp.impl")"
report_add "- diff_seconds: $(report_kv_get "ntp.diff_seconds")"
local ntp_recheck
ntp_recheck="$(report_kv_get "ntp.recheck")"
[[ -n "$ntp_recheck" ]] && report_add "- 修复后复检结果: $ntp_recheck"
# 6) 文件权限
section "文件权限检测"
check_file_permissions "$platform"
report_add ""
report_add "## 文件权限检测"
report_add "- exist_count: $(report_kv_get "perm.exist_count")"
report_add "- miss_count: $(report_kv_get "perm.miss_count")"
# 7) 现场数据备份
section "现场数据备份(可选)"
read -r -p "是否执行现场数据备份? (y/n) [默认:n]: " bak
if [[ "${bak:-n}" =~ ^[Yy]$ ]]; then
log WARN "[BAK] 现场数据备份:sh 版本暂未实现(建议复用 issue_handler.sh 或新增 DataBakup 实现)"
report_add ""
report_add "## 现场数据备份"
report_add "- 用户选择执行,但当前未实现(需补充 DataBakup)"
else
log INFO "跳过现场数据备份"
report_add ""
report_add "## 现场数据备份"
report_add "- 用户选择跳过"
fi
# 8) 日志导出
section "日志导出(可选)"
read -r -p "是否导出服务日志到本地目录? (y/n) [默认:n]: " ex
local export_dir=""
if [[ "${ex:-n}" =~ ^[Yy]$ ]]; then
export_dir="$(export_logs "$platform" "$HAS_UJAVA" "$HAS_UPYTHON" "${UJAVA_CONTAINER:-}" "${UPYTHON_CONTAINER:-}" || true)"
report_add ""
report_add "## 日志导出"
report_add "- 导出目录: $export_dir"
else
log INFO "跳过日志导出"
report_add ""
report_add "## 日志导出"
report_add "- 用户选择跳过"
fi
# 9) 容器信息 + Redis/Emqx
section "容器与中间件"
collect_container_info "$platform"
# ✅ 补齐:Emqx 异常检测(你已经实现但 main 没调用)
check_emqx_container_exception
report_add ""
report_add "## 容器与中间件"
report_add "- docker: $(report_kv_get "docker.available")"
report_add "- redis.log export: $(report_kv_get "redis.export")"
report_add "- emqx.log export: $(report_kv_get "emqx.export")"
report_add "- redis container exception: $(report_kv_get "redis.exception")"
local redis_recheck
redis_recheck="$(report_kv_get "redis.recheck")"
[[ -n "$redis_recheck" ]] && report_add "- redis recheck: $redis_recheck"
# ✅ 新增摘要:Emqx
report_add "- emqx container exception: $(report_kv_get "emqx.exception")"
local emqx_recheck
emqx_recheck="$(report_kv_get "emqx.recheck")"
[[ -n "$emqx_recheck" ]] && report_add "- emqx recheck: $emqx_recheck"
# 10) Android
section "Android 自检(可选)"
android_self_check
report_add ""
report_add "## Android 自检"
report_add "- status: $(report_kv_get "android.status")"
local at
at="$(report_kv_get "android.target")"
[[ -n "$at" ]] && report_add "- target: $at"
local adir
adir="$(report_kv_get "android.export_dir")"
[[ -n "$adir" ]] && report_add "- export_dir: $adir"
# 10.5) 定时任务查询(✅ 补齐,放在报告生成前)
test_scheduled_tasks "$platform"
# (test_scheduled_tasks 内部已 section + report_add 摘要,这里不重复)
# 11) 生成报告
section "生成 Markdown 报告"
local report_file="$REPORT_DIR/health_report_${server_ip//[^0-9A-Za-z\.\-_]/_}_${TS}.md"
write_report "$server_ip" "$platform" "$sys_kv" "$report_file"
log SUCCESS "Markdown 报告已生成: $report_file"
echo "[REPORT_FILE] $report_file"
}
# ✅ 确保文件末尾仍然调用 main
main "$@"
\ No newline at end of file
[
{
"序号": 1,
"功能模块": "通用模块",
"功能类别": "异常场景",
"用例编号": "TC-001",
"功能描述": "权限组管理接口文档",
"用例等级": "中",
"功能编号": "",
"用例名称": "权限组管理接口文档",
"预置条件": "1. 环境已部署; 2. 具备执行权限; 3. 必要依赖/配置已准备",
"操作步骤": "1. ## 1. 分页查询权限组; 2. **接口描述**:根据公司编号、权限组名称、启用状态等条件进行分页查询,支持模糊匹配权限组名称,按创建时间倒序排列; 3. **接口描述**:执行物理删除操作,同时级联删除部门关联、角色关联、用户关联表中的数据,使用事务确保删除操作的原子性。基础权限组(is_base=1)不可删除。; 4. ## 7. 查询绑定关系; 5. **接口描述**:查询权限组已绑定的关联对象,支持按类型筛选或返回全部类型; 6. ## 8. 查询用户最终权限",
"JSON": "",
"预期结果": "**请求头**:需要携带有效的 Token 认证; **响应示例**:; **响应示例**(成功):; **响应示例**(包含基础权限组):; > - departmentIds、roleIds、userIds 至少需要传入一种类型(可为空数组); **响应示例**(不传relationType,返回全部):",
"测试结果": "",
"测试结论": "",
"日志截屏": "",
"备注": ""
},
{
"序号": 2,
"功能模块": "通用模块",
"功能类别": "安全/鉴权",
"用例编号": "TC-002",
"功能描述": "**直接绑定**:查询sys_permission_group_user中user_id=用户ID的权限组",
"用例等级": "中",
"功能编号": "1",
"用例名称": "**直接绑定**:查询sys_permission_group_user中user_id=用户ID的权限组",
"预置条件": "1. 环境已部署; 2. 具备执行权限; 3. 必要依赖/配置已准备",
"操作步骤": "1. 按 PRD 描述执行该功能/流程; 2. 采集相关日志/输出; 3. 记录实际结果",
"JSON": "",
"预期结果": "**直接绑定**:查询sys_permission_group_user中user_id=用户ID的权限组 按 PRD 约定产出正确结果(请补充具体断言点)",
"测试结果": "",
"测试结论": "",
"日志截屏": "",
"备注": ""
},
{
"序号": 3,
"功能模块": "通用模块",
"功能类别": "安全/鉴权",
"用例编号": "TC-003",
"功能描述": "**部门绑定**:查询sys_permission_group_department中department_id=用户所属部门ID的权限组",
"用例等级": "中",
"功能编号": "2",
"用例名称": "**部门绑定**:查询sys_permission_group_department中department_id=用户所属部门ID的权限组",
"预置条件": "1. 环境已部署; 2. 具备执行权限; 3. 必要依赖/配置已准备",
"操作步骤": "1. 按 PRD 描述执行该功能/流程; 2. 采集相关日志/输出; 3. 记录实际结果",
"JSON": "",
"预期结果": "**部门绑定**:查询sys_permission_group_department中department_id=用户所属部门ID的权限组 按 PRD 约定产出正确结果(请补充具体断言点)",
"测试结果": "",
"测试结论": "",
"日志截屏": "",
"备注": ""
},
{
"序号": 4,
"功能模块": "通用模块",
"功能类别": "异常场景",
"用例编号": "TC-004",
"功能描述": "**角色绑定**:查询sys_permission_group_role中role_id=用户所属角色ID的权限组",
"用例等级": "中",
"功能编号": "3",
"用例名称": "**角色绑定**:查询sys_permission_group_role中role_id=用户所属角色ID的权限组",
"预置条件": "1. 环境已部署; 2. 具备执行权限; 3. 必要依赖/配置已准备",
"操作步骤": "1. | A_SELECT_EMPTY | 查询结果为空 |; 2. *文档生成时间:2026年1月12日*",
"JSON": "",
"预期结果": "**响应示例**:; | 200 | 成功 |; | 值 | 说明 | 对应表 |",
"测试结果": "",
"测试结论": "",
"日志截屏": "",
"备注": ""
}
]
\ No newline at end of file
[
{
"序号": 5,
"功能模块": "通用模块",
"功能类别": "安全/鉴权",
"用例编号": "TC-005",
"功能描述": "权限管理页面新增开发",
"用例等级": "中",
"功能编号": "",
"用例名称": "权限管理页面新增开发",
"预置条件": "1. 环境已部署; 2. 具备执行权限; 3. 必要依赖/配置已准备",
"操作步骤": "1. 按 PRD 描述执行该功能/流程; 2. 采集相关日志/输出; 3. 记录实际结果",
"JSON": "",
"预期结果": "权限管理页面新增开发 按 PRD 约定产出正确结果(请补充具体断言点)",
"测试结果": "",
"测试结论": "",
"日志截屏": "",
"备注": ""
},
{
"序号": 6,
"功能模块": "通用模块",
"功能类别": "安全/鉴权",
"用例编号": "TC-006",
"功能描述": "权限管理页面地址: `src\\views\\Backend\\Admin\\PermissionManage\\index.vue`",
"用例等级": "中",
"功能编号": "1",
"用例名称": "权限管理页面地址: `src\\views\\Backend\\Admin\\PermissionManage\\index.vue`",
"预置条件": "1. 环境已部署; 2. 具备执行权限; 3. 必要依赖/配置已准备",
"操作步骤": "1. 按 PRD 描述执行该功能/流程; 2. 采集相关日志/输出; 3. 记录实际结果",
"JSON": "",
"预期结果": "权限管理页面地址: `src\\views\\Backend\\Admin\\PermissionManage\\index.vue` 按 PRD 约定产出正确结果(请补充具体断言点)",
"测试结果": "",
"测试结论": "",
"日志截屏": "",
"备注": ""
},
{
"序号": 7,
"功能模块": "通用模块",
"功能类别": "安全/鉴权",
"用例编号": "TC-007",
"功能描述": "相关API文档地址:`Docs\\Api\\API_权限组管理接口文档.md`",
"用例等级": "中",
"功能编号": "2",
"用例名称": "相关API文档地址:`Docs\\Api\\API_权限组管理接口文档.md`",
"预置条件": "1. 环境已部署; 2. 具备执行权限; 3. 必要依赖/配置已准备",
"操作步骤": "1. 按 PRD 描述执行该功能/流程; 2. 采集相关日志/输出; 3. 记录实际结果",
"JSON": "",
"预期结果": "相关API文档地址:`Docs\\Api\\API_权限组管理接口文档.md` 按 PRD 约定产出正确结果(请补充具体断言点)",
"测试结果": "",
"测试结论": "",
"日志截屏": "",
"备注": ""
},
{
"序号": 8,
"功能模块": "通用模块",
"功能类别": "安全/鉴权",
"用例编号": "TC-008",
"功能描述": "新增/修改权限组弹窗地址:`src\\views\\Backend\\Admin\\PermissionManage\\components\\AddEditDialog\\index.vue`",
"用例等级": "中",
"功能编号": "3",
"用例名称": "新增/修改权限组弹窗地址:`src\\views\\Backend\\Admin\\PermissionManage\\components\\AddEditDialog\\index.vue`",
"预置条件": "1. 环境已部署; 2. 具备执行权限; 3. 必要依赖/配置已准备",
"操作步骤": "1. 按 PRD 描述执行该功能/流程; 2. 采集相关日志/输出; 3. 记录实际结果",
"JSON": "",
"预期结果": "新增/修改权限组弹窗地址:`src\\views\\Backend\\Admin\\PermissionManage\\components\\AddEditDialog\\index.vue` 按 PRD 约定产出正确结果(请补充具体断言点)",
"测试结果": "",
"测试结论": "",
"日志截屏": "",
"备注": ""
},
{
"序号": 9,
"功能模块": "通用模块",
"功能类别": "安全/鉴权",
"用例编号": "TC-009",
"功能描述": "权限绑定弹窗地址:`src\\views\\Backend\\Admin\\PermissionManage\\components\\BindDialog\\index.vue`",
"用例等级": "中",
"功能编号": "4",
"用例名称": "权限绑定弹窗地址:`src\\views\\Backend\\Admin\\PermissionManage\\components\\BindDialog\\index.vue`",
"预置条件": "1. 环境已部署; 2. 具备执行权限; 3. 必要依赖/配置已准备",
"操作步骤": "1. 按 PRD 描述执行该功能/流程; 2. 采集相关日志/输出; 3. 记录实际结果",
"JSON": "",
"预期结果": "权限绑定弹窗地址:`src\\views\\Backend\\Admin\\PermissionManage\\components\\BindDialog\\index.vue` 按 PRD 约定产出正确结果(请补充具体断言点)",
"测试结果": "",
"测试结论": "",
"日志截屏": "",
"备注": ""
},
{
"序号": 10,
"功能模块": "通用模块",
"功能类别": "安全/鉴权",
"用例编号": "TC-010",
"功能描述": "自定义权限控制指令v-permission地址:`src\\utils\\permission.js`",
"用例等级": "中",
"功能编号": "5",
"用例名称": "自定义权限控制指令v-permission地址:`src\\utils\\permission.js`",
"预置条件": "1. 环境已部署; 2. 具备执行权限; 3. 必要依赖/配置已准备",
"操作步骤": "1. 按 PRD 描述执行该功能/流程; 2. 采集相关日志/输出; 3. 记录实际结果",
"JSON": "",
"预期结果": "自定义权限控制指令v-permission地址:`src\\utils\\permission.js` 按 PRD 约定产出正确结果(请补充具体断言点)",
"测试结果": "",
"测试结论": "",
"日志截屏": "",
"备注": ""
},
{
"序号": 11,
"功能模块": "通用模块",
"功能类别": "安全/鉴权",
"用例编号": "TC-011",
"功能描述": "✅ 在权限管理页面遵循`Docs\\PRD\\_PRD_规范文档_新建页面.md`并标准化初始化页面,在`src\\router.js`注册路由",
"用例等级": "中",
"功能编号": "1",
"用例名称": "✅ 在权限管理页面遵循`Docs\\PRD\\_PRD_规范文档_新建页面.md`并标准化初始化页面,在`src\\router.js`注册路由",
"预置条件": "1. 环境已部署; 2. 具备执行权限; 3. 必要依赖/配置已准备",
"操作步骤": "1. 按 PRD 描述执行该功能/流程; 2. 采集相关日志/输出; 3. 记录实际结果",
"JSON": "",
"预期结果": "✅ 在权限管理页面遵循`Docs\\PRD\\_PRD_规范文档_新建页面.md`并标准化初始化页面,在`src\\router.js`注册路由 按 PRD 约定产出正确结果(请补充具体断言点)",
"测试结果": "",
"测试结论": "",
"日志截屏": "",
"备注": ""
},
{
"序号": 12,
"功能模块": "通用模块",
"功能类别": "安全/鉴权",
"用例编号": "TC-012",
"功能描述": "✅ 参考页面`src\\views\\Backend\\Admin\\Role\\index.vue`的设计在初始化后的权限管理页新增功能",
"用例等级": "中",
"功能编号": "2",
"用例名称": "✅ 参考页面`src\\views\\Backend\\Admin\\Role\\index.vue`的设计在初始化后的权限管理页新增功能",
"预置条件": "1. 环境已部署; 2. 具备执行权限; 3. 必要依赖/配置已准备",
"操作步骤": "1. 按 PRD 描述执行该功能/流程; 2. 采集相关日志/输出; 3. 记录实际结果",
"JSON": "",
"预期结果": "✅ 参考页面`src\\views\\Backend\\Admin\\Role\\index.vue`的设计在初始化后的权限管理页新增功能 按 PRD 约定产出正确结果(请补充具体断言点)",
"测试结果": "",
"测试结论": "",
"日志截屏": "",
"备注": ""
},
{
"序号": 13,
"功能模块": "通用模块",
"功能类别": "安全/鉴权",
"用例编号": "TC-013",
"功能描述": "✅ 将权限管理页面的新增/修改权限组弹窗和绑定弹窗分别抽离成组件放置在`src\\views\\Backend\\Admin\\PermissionManage\\components`中,注意数据的传输,并优化一下样式",
"用例等级": "中",
"功能编号": "3",
"用例名称": "✅ 将权限管理页面的新增/修改权限组弹窗和绑定弹窗分别抽离成组件放置在`src\\views\\Backend\\Admin\\PermissionManage\\components`中,注意数据的传输,并优化一下样式",
"预置条件": "1. 环境已部署; 2. 具备执行权限; 3. 必要依赖/配置已准备",
"操作步骤": "1. 按 PRD 描述执行该功能/流程; 2. 采集相关日志/输出; 3. 记录实际结果",
"JSON": "",
"预期结果": "✅ 将权限管理页面的新增/修改权限组弹窗和绑定弹窗分别抽离成组件放置在`src\\views\\Backend\\Admin\\PermissionManage\\components`中,注意数据的传输,并优化一下样式 按 PRD 约定产出正确结果(请补充具体断言点)",
"测试结果": "",
"测试结论": "",
"日志截屏": "",
"备注": ""
},
{
"序号": 14,
"功能模块": "通用模块",
"功能类别": "安全/鉴权",
"用例编号": "TC-014",
"功能描述": "✅ 实现权限组添加权限",
"用例等级": "中",
"功能编号": "4",
"用例名称": "✅ 实现权限组添加权限",
"预置条件": "1. 环境已部署; 2. 具备执行权限; 3. 必要依赖/配置已准备",
"操作步骤": "1. 按 PRD 描述执行该功能/流程; 2. 采集相关日志/输出; 3. 记录实际结果",
"JSON": "",
"预期结果": "✅ 实现权限组添加权限 按 PRD 约定产出正确结果(请补充具体断言点)",
"测试结果": "",
"测试结论": "",
"日志截屏": "",
"备注": ""
},
{
"序号": 15,
"功能模块": "通用模块",
"功能类别": "安全/鉴权",
"用例编号": "TC-015",
"功能描述": "✅ 在新增/修改权限组弹窗继续实现任务4的要求",
"用例等级": "中",
"功能编号": "5",
"用例名称": "✅ 在新增/修改权限组弹窗继续实现任务4的要求",
"预置条件": "1. 环境已部署; 2. 具备执行权限; 3. 必要依赖/配置已准备",
"操作步骤": "1. 按 PRD 描述执行该功能/流程; 2. 采集相关日志/输出; 3. 记录实际结果",
"JSON": "",
"预期结果": "✅ 在新增/修改权限组弹窗继续实现任务4的要求 按 PRD 约定产出正确结果(请补充具体断言点)",
"测试结果": "",
"测试结论": "",
"日志截屏": "",
"备注": ""
},
{
"序号": 16,
"功能模块": "通用模块",
"功能类别": "安全/鉴权",
"用例编号": "TC-016",
"功能描述": "✅ 帮我参考API接口文档,实现权限组的增删改查、禁用/启用功能,绑定功能先不实现",
"用例等级": "中",
"功能编号": "6",
"用例名称": "✅ 帮我参考API接口文档,实现权限组的增删改查、禁用/启用功能,绑定功能先不实现",
"预置条件": "1. 环境已部署; 2. 具备执行权限; 3. 必要依赖/配置已准备",
"操作步骤": "1. 按 PRD 描述执行该功能/流程; 2. 采集相关日志/输出; 3. 记录实际结果",
"JSON": "",
"预期结果": "✅ 帮我参考API接口文档,实现权限组的增删改查、禁用/启用功能,绑定功能先不实现 按 PRD 约定产出正确结果(请补充具体断言点)",
"测试结果": "",
"测试结论": "",
"日志截屏": "",
"备注": ""
},
{
"序号": 17,
"功能模块": "通用模块",
"功能类别": "安全/鉴权",
"用例编号": "TC-017",
"功能描述": "✅ 进入权限管理页面时,帮我请求API接口文档的接口`/permissionGroup/getAllPermissions`获取所有配置项,然后将`src\\views\\Backend\\Admin\\PermissionManage\\components\\PermissionConfig\\index.vue`的permissionList替换为接口请求到的真实数据",
"用例等级": "中",
"功能编号": "7",
"用例名称": "✅ 进入权限管理页面时,帮我请求API接口文档的接口`/permissionGroup/getAllPermissions`获取所有配置项,然后将`src\\views\\Backend\\Admin\\PermissionManage\\components\\PermissionConfig\\index.vue`的permissionList替换为接口请求到的真实数据",
"预置条件": "1. 环境已部署; 2. 具备执行权限; 3. 必要依赖/配置已准备",
"操作步骤": "1. 按 PRD 描述执行该功能/流程; 2. 采集相关日志/输出; 3. 记录实际结果",
"JSON": "",
"预期结果": "✅ 进入权限管理页面时,帮我请求API接口文档的接口`/permissionGroup/getAllPermissions`获取所有配置项,然后将`src\\views\\Backend\\Admin\\PermissionManage\\components\\PermissionConfig\\index.vue`的permissionList替换为接口请求到的真实数据 按 PRD 约定产出正确结果(请补充具体断言点)",
"测试结果": "",
"测试结论": "",
"日志截屏": "",
"备注": ""
},
{
"序号": 18,
"功能模块": "通用模块",
"功能类别": "安全/鉴权",
"用例编号": "TC-018",
"功能描述": "✅ 重构权限绑定弹窗及补充功能实现",
"用例等级": "中",
"功能编号": "8",
"用例名称": "✅ 重构权限绑定弹窗及补充功能实现",
"预置条件": "1. 环境已部署; 2. 具备执行权限; 3. 必要依赖/配置已准备",
"操作步骤": "1. 按 PRD 描述执行该功能/流程; 2. 采集相关日志/输出; 3. 记录实际结果",
"JSON": "",
"预期结果": "✅ 重构权限绑定弹窗及补充功能实现 按 PRD 约定产出正确结果(请补充具体断言点)",
"测试结果": "",
"测试结论": "",
"日志截屏": "",
"备注": ""
},
{
"序号": 19,
"功能模块": "通用模块",
"功能类别": "安全/鉴权",
"用例编号": "TC-019",
"功能描述": "数据结构调整",
"用例等级": "中",
"功能编号": "9",
"用例名称": "数据结构调整",
"预置条件": "1. 环境已部署; 2. 具备执行权限; 3. 必要依赖/配置已准备",
"操作步骤": "1. 按 PRD 描述执行该功能/流程; 2. 采集相关日志/输出; 3. 记录实际结果",
"JSON": "",
"预期结果": "数据结构调整 按 PRD 约定产出正确结果(请补充具体断言点)",
"测试结果": "",
"测试结论": "",
"日志截屏": "",
"备注": ""
},
{
"序号": 20,
"功能模块": "通用模块",
"功能类别": "功能测试",
"用例编号": "TC-020",
"功能描述": "实现功能时提供了API接口,但先不进行对接,先在前端用虚拟数据实现并预留操作方法等,等我要接口对接时再进行接口对接",
"用例等级": "高",
"功能编号": "1",
"用例名称": "实现功能时提供了API接口,但先不进行对接,先在前端用虚拟数据实现并预留操作方法等,等我要接口对接时再进行接口对接",
"预置条件": "1. 环境已部署; 2. 具备执行权限; 3. 必要依赖/配置已准备",
"操作步骤": "1. 按 PRD 描述执行该功能/流程; 2. 采集相关日志/输出; 3. 记录实际结果",
"JSON": "",
"预期结果": "实现功能时提供了API接口,但先不进行对接,先在前端用虚拟数据实现并预留操作方法等,等我要接口对接时再进行接口对接 按 PRD 约定产出正确结果(请补充具体断言点)",
"测试结果": "",
"测试结论": "",
"日志截屏": "",
"备注": ""
},
{
"序号": 21,
"功能模块": "通用模块",
"功能类别": "功能测试",
"用例编号": "TC-021",
"功能描述": "支持模糊搜索,支持基础的增删改查",
"用例等级": "中",
"功能编号": "2",
"用例名称": "支持模糊搜索,支持基础的增删改查",
"预置条件": "1. 环境已部署; 2. 具备执行权限; 3. 必要依赖/配置已准备",
"操作步骤": "1. 按 PRD 描述执行该功能/流程; 2. 采集相关日志/输出; 3. 记录实际结果",
"JSON": "",
"预期结果": "支持模糊搜索,支持基础的增删改查 按 PRD 约定产出正确结果(请补充具体断言点)",
"测试结果": "",
"测试结论": "",
"日志截屏": "",
"备注": ""
},
{
"序号": 22,
"功能模块": "通用模块",
"功能类别": "安全/鉴权",
"用例编号": "TC-022",
"功能描述": "el-table的主要显示字段获取参考相关API文档的\"分页查询权限组\"接口,显示字段groupName、isEnable、createTime,支持多选,支持分页,操作有修改、删除、绑定",
"用例等级": "中",
"功能编号": "3",
"用例名称": "el-table的主要显示字段获取参考相关API文档的\"分页查询权限组\"接口,显示字段groupName、isEnable、createTime,支持多选,支持分页,操作有修改、删除、绑定",
"预置条件": "1. 环境已部署; 2. 具备执行权限; 3. 必要依赖/配置已准备",
"操作步骤": "1. 按 PRD 描述执行该功能/流程; 2. 采集相关日志/输出; 3. 记录实际结果",
"JSON": "",
"预期结果": "el-table的主要显示字段获取参考相关API文档的\"分页查询权限组\"接口,显示字段groupName、isEnable、createTime,支持多选,支持分页,操作有修改、删除、绑定 按 PRD 约定产出正确结果(请补充具体断言点)",
"测试结果": "",
"测试结论": "",
"日志截屏": "",
"备注": ""
},
{
"序号": 23,
"功能模块": "通用模块",
"功能类别": "功能测试",
"用例编号": "TC-023",
"功能描述": "实现功能时提供了API接口,但先不进行对接,先在前端用虚拟数据实现并预留操作方法等,等我要接口对接时再进行接口对接",
"用例等级": "高",
"功能编号": "1",
"用例名称": "实现功能时提供了API接口,但先不进行对接,先在前端用虚拟数据实现并预留操作方法等,等我要接口对接时再进行接口对接",
"预置条件": "1. 环境已部署; 2. 具备执行权限; 3. 必要依赖/配置已准备",
"操作步骤": "1. 按 PRD 描述执行该功能/流程; 2. 采集相关日志/输出; 3. 记录实际结果",
"JSON": "",
"预期结果": "实现功能时提供了API接口,但先不进行对接,先在前端用虚拟数据实现并预留操作方法等,等我要接口对接时再进行接口对接 按 PRD 约定产出正确结果(请补充具体断言点)",
"测试结果": "",
"测试结论": "",
"日志截屏": "",
"备注": ""
},
{
"序号": 24,
"功能模块": "通用模块",
"功能类别": "安全/鉴权",
"用例编号": "TC-024",
"功能描述": "我在`src\\constant\\permissionList.js`模拟了权限数据,帮我使用el-checkbox进行显示",
"用例等级": "中",
"功能编号": "2",
"用例名称": "我在`src\\constant\\permissionList.js`模拟了权限数据,帮我使用el-checkbox进行显示",
"预置条件": "1. 环境已部署; 2. 具备执行权限; 3. 必要依赖/配置已准备",
"操作步骤": "1. 按 PRD 描述执行该功能/流程; 2. 采集相关日志/输出; 3. 记录实际结果",
"JSON": "",
"预期结果": "我在`src\\constant\\permissionList.js`模拟了权限数据,帮我使用el-checkbox进行显示 按 PRD 约定产出正确结果(请补充具体断言点)",
"测试结果": "",
"测试结论": "",
"日志截屏": "",
"备注": ""
},
{
"序号": 25,
"功能模块": "通用模块",
"功能类别": "功能测试",
"用例编号": "TC-025",
"功能描述": "文本显示逻辑:在国际化文件中新建permission子集,通过key值匹配显示文本:例如view=i18n文件中的`permission.view`",
"用例等级": "中",
"功能编号": "3",
"用例名称": "文本显示逻辑:在国际化文件中新建permission子集,通过key值匹配显示文本:例如view=i18n文件中的`permission.view`",
"预置条件": "1. 环境已部署; 2. 具备执行权限; 3. 必要依赖/配置已准备",
"操作步骤": "1. 按 PRD 描述执行该功能/流程; 2. 采集相关日志/输出; 3. 记录实际结果",
"JSON": "",
"预期结果": "文本显示逻辑:在国际化文件中新建permission子集,通过key值匹配显示文本:例如view=i18n文件中的`permission.view` 按 PRD 约定产出正确结果(请补充具体断言点)",
"测试结果": "",
"测试结论": "",
"日志截屏": "",
"备注": ""
},
{
"序号": 26,
"功能模块": "通用模块",
"功能类别": "安全/鉴权",
"用例编号": "TC-026",
"功能描述": "页面显示:要同时显示名称和勾选框,比如",
"用例等级": "中",
"功能编号": "4",
"用例名称": "页面显示:要同时显示名称和勾选框,比如",
"预置条件": "1. 环境已部署; 2. 具备执行权限; 3. 必要依赖/配置已准备",
"操作步骤": "1. 按 PRD 描述执行该功能/流程; 2. 采集相关日志/输出; 3. 记录实际结果",
"JSON": "",
"预期结果": "页面显示:要同时显示名称和勾选框,比如 按 PRD 约定产出正确结果(请补充具体断言点)",
"测试结果": "",
"测试结论": "",
"日志截屏": "",
"备注": ""
},
{
"序号": 27,
"功能模块": "通用模块",
"功能类别": "安全/鉴权",
"用例编号": "TC-027",
"功能描述": "弹窗页面重构,当前的设计不符合产品的功能要求,具体功能要求:权限组支持同时绑定多个用户、角色、部门,取消权限配置功能",
"用例等级": "中",
"功能编号": "1",
"用例名称": "弹窗页面重构,当前的设计不符合产品的功能要求,具体功能要求:权限组支持同时绑定多个用户、角色、部门,取消权限配置功能",
"预置条件": "1. 环境已部署; 2. 具备执行权限; 3. 必要依赖/配置已准备",
"操作步骤": "1. 按 PRD 描述执行该功能/流程; 2. 采集相关日志/输出; 3. 记录实际结果",
"JSON": "",
"预期结果": "弹窗页面重构,当前的设计不符合产品的功能要求,具体功能要求:权限组支持同时绑定多个用户、角色、部门,取消权限配置功能 按 PRD 约定产出正确结果(请补充具体断言点)",
"测试结果": "",
"测试结论": "",
"日志截屏": "",
"备注": ""
},
{
"序号": 28,
"功能模块": "通用模块",
"功能类别": "功能测试",
"用例编号": "TC-028",
"功能描述": "页面样式自由发挥,符合系统主题即可,优先使用UI/UX Pro Max",
"用例等级": "中",
"功能编号": "2",
"用例名称": "页面样式自由发挥,符合系统主题即可,优先使用UI/UX Pro Max",
"预置条件": "1. 环境已部署; 2. 具备执行权限; 3. 必要依赖/配置已准备",
"操作步骤": "1. 按 PRD 描述执行该功能/流程; 2. 采集相关日志/输出; 3. 记录实际结果",
"JSON": "",
"预期结果": "页面样式自由发挥,符合系统主题即可,优先使用UI/UX Pro Max 按 PRD 约定产出正确结果(请补充具体断言点)",
"测试结果": "",
"测试结论": "",
"日志截屏": "",
"备注": ""
},
{
"序号": 29,
"功能模块": "通用模块",
"功能类别": "功能测试",
"用例编号": "TC-029",
"功能描述": "参考`@src/views/Backend/Account/User/index.vue:930-984 `实现用户数据请求获取",
"用例等级": "中",
"功能编号": "3",
"用例名称": "参考`@src/views/Backend/Account/User/index.vue:930-984 `实现用户数据请求获取",
"预置条件": "1. 环境已部署; 2. 具备执行权限; 3. 必要依赖/配置已准备",
"操作步骤": "1. 按 PRD 描述执行该功能/流程; 2. 采集相关日志/输出; 3. 记录实际结果",
"JSON": "",
"预期结果": "参考`@src/views/Backend/Account/User/index.vue:930-984 `实现用户数据请求获取 按 PRD 约定产出正确结果(请补充具体断言点)",
"测试结果": "",
"测试结论": "",
"日志截屏": "",
"备注": ""
},
{
"序号": 30,
"功能模块": "通用模块",
"功能类别": "功能测试",
"用例编号": "TC-030",
"功能描述": "参考`@src/views/Backend/Account/User/index.vue:986-1010 `实现角色数据的请求获取",
"用例等级": "中",
"功能编号": "4",
"用例名称": "参考`@src/views/Backend/Account/User/index.vue:986-1010 `实现角色数据的请求获取",
"预置条件": "1. 环境已部署; 2. 具备执行权限; 3. 必要依赖/配置已准备",
"操作步骤": "1. 按 PRD 描述执行该功能/流程; 2. 采集相关日志/输出; 3. 记录实际结果",
"JSON": "",
"预期结果": "参考`@src/views/Backend/Account/User/index.vue:986-1010 `实现角色数据的请求获取 按 PRD 约定产出正确结果(请补充具体断言点)",
"测试结果": "",
"测试结论": "",
"日志截屏": "",
"备注": ""
},
{
"序号": 31,
"功能模块": "通用模块",
"功能类别": "功能测试",
"用例编号": "TC-031",
"功能描述": "参考`@src/views/Backend/Account/User/index.vue:263-273 `和`@src/views/Backend/Account/User/index.vue:1055-1068 `实现部门数据的请求获取",
"用例等级": "中",
"功能编号": "5",
"用例名称": "参考`@src/views/Backend/Account/User/index.vue:263-273 `和`@src/views/Backend/Account/User/index.vue:1055-1068 `实现部门数据的请求获取",
"预置条件": "1. 环境已部署; 2. 具备执行权限; 3. 必要依赖/配置已准备",
"操作步骤": "1. 按 PRD 描述执行该功能/流程; 2. 采集相关日志/输出; 3. 记录实际结果",
"JSON": "",
"预期结果": "参考`@src/views/Backend/Account/User/index.vue:263-273 `和`@src/views/Backend/Account/User/index.vue:1055-1068 `实现部门数据的请求获取 按 PRD 约定产出正确结果(请补充具体断言点)",
"测试结果": "",
"测试结论": "",
"日志截屏": "",
"备注": ""
},
{
"序号": 32,
"功能模块": "通用模块",
"功能类别": "安全/鉴权",
"用例编号": "TC-032",
"功能描述": "根据权限api文档的`/permissionGroup/bindRelation`实现权限绑定",
"用例等级": "中",
"功能编号": "6",
"用例名称": "根据权限api文档的`/permissionGroup/bindRelation`实现权限绑定",
"预置条件": "1. 环境已部署; 2. 具备执行权限; 3. 必要依赖/配置已准备",
"操作步骤": "1. 按 PRD 描述执行该功能/流程; 2. 采集相关日志/输出; 3. 记录实际结果",
"JSON": "",
"预期结果": "根据权限api文档的`/permissionGroup/bindRelation`实现权限绑定 按 PRD 约定产出正确结果(请补充具体断言点)",
"测试结果": "",
"测试结论": "",
"日志截屏": "",
"备注": ""
},
{
"序号": 33,
"功能模块": "通用模块",
"功能类别": "功能测试",
"用例编号": "TC-033",
"功能描述": "当前接口的数据结构",
"用例等级": "中",
"功能编号": "1",
"用例名称": "当前接口的数据结构",
"预置条件": "1. 环境已部署; 2. 具备执行权限; 3. 必要依赖/配置已准备",
"操作步骤": "1. 按 PRD 描述执行该功能/流程; 2. 采集相关日志/输出; 3. 记录实际结果",
"JSON": "",
"预期结果": "当涉及到接口请求时,需要严格遵循`Docs\\PRD\\_PRD_规范文档_接口请求.md`规范",
"测试结果": "",
"测试结论": "",
"日志截屏": "",
"备注": ""
}
]
\ No newline at end of file
[
{
"序号": 34,
"功能模块": "通用模块",
"功能类别": "安全/鉴权",
"用例编号": "TC-034",
"功能描述": "自定义权限控制指令实现",
"用例等级": "中",
"功能编号": "",
"用例名称": "自定义权限控制指令实现",
"预置条件": "1. 环境已部署; 2. 具备执行权限; 3. 必要依赖/配置已准备",
"操作步骤": "1. 按 PRD 描述执行该功能/流程; 2. 采集相关日志/输出; 3. 记录实际结果",
"JSON": "",
"预期结果": "自定义权限控制指令实现 按 PRD 约定产出正确结果(请补充具体断言点)",
"测试结果": "",
"测试结论": "",
"日志截屏": "",
"备注": ""
},
{
"序号": 35,
"功能模块": "通用模块",
"功能类别": "功能测试",
"用例编号": "TC-035",
"功能描述": "办公室管理页面地址: `src\\views\\Backend\\MeetingRoom\\OfficeManage\\index.vue`",
"用例等级": "中",
"功能编号": "1",
"用例名称": "办公室管理页面地址: `src\\views\\Backend\\MeetingRoom\\OfficeManage\\index.vue`",
"预置条件": "1. 环境已部署; 2. 具备执行权限; 3. 必要依赖/配置已准备",
"操作步骤": "1. 按 PRD 描述执行该功能/流程; 2. 采集相关日志/输出; 3. 记录实际结果",
"JSON": "",
"预期结果": "办公室管理页面地址: `src\\views\\Backend\\MeetingRoom\\OfficeManage\\index.vue` 按 PRD 约定产出正确结果(请补充具体断言点)",
"测试结果": "",
"测试结论": "",
"日志截屏": "",
"备注": ""
},
{
"序号": 36,
"功能模块": "通用模块",
"功能类别": "安全/鉴权",
"用例编号": "TC-036",
"功能描述": "自定义权限控制指令地址:`src\\utils\\permission.js`",
"用例等级": "中",
"功能编号": "2",
"用例名称": "自定义权限控制指令地址:`src\\utils\\permission.js`",
"预置条件": "1. 环境已部署; 2. 具备执行权限; 3. 必要依赖/配置已准备",
"操作步骤": "1. 按 PRD 描述执行该功能/流程; 2. 采集相关日志/输出; 3. 记录实际结果",
"JSON": "",
"预期结果": "自定义权限控制指令地址:`src\\utils\\permission.js` 按 PRD 约定产出正确结果(请补充具体断言点)",
"测试结果": "",
"测试结论": "",
"日志截屏": "",
"备注": ""
},
{
"序号": 37,
"功能模块": "通用模块",
"功能类别": "安全/鉴权",
"用例编号": "TC-037",
"功能描述": "权限数据结构`src\\constant\\permissionList.js`",
"用例等级": "中",
"功能编号": "3",
"用例名称": "权限数据结构`src\\constant\\permissionList.js`",
"预置条件": "1. 环境已部署; 2. 具备执行权限; 3. 必要依赖/配置已准备",
"操作步骤": "1. 按 PRD 描述执行该功能/流程; 2. 采集相关日志/输出; 3. 记录实际结果",
"JSON": "",
"预期结果": "权限数据结构`src\\constant\\permissionList.js` 按 PRD 约定产出正确结果(请补充具体断言点)",
"测试结果": "",
"测试结论": "",
"日志截屏": "",
"备注": ""
},
{
"序号": 38,
"功能模块": "通用模块",
"功能类别": "安全/鉴权",
"用例编号": "TC-038",
"功能描述": "权限控制指令通过`src\\main.js`挂载成`v-permission`使用",
"用例等级": "中",
"功能编号": "4",
"用例名称": "权限控制指令通过`src\\main.js`挂载成`v-permission`使用",
"预置条件": "1. 环境已部署; 2. 具备执行权限; 3. 必要依赖/配置已准备",
"操作步骤": "1. 按 PRD 描述执行该功能/流程; 2. 采集相关日志/输出; 3. 记录实际结果",
"JSON": "",
"预期结果": "权限控制指令通过`src\\main.js`挂载成`v-permission`使用 按 PRD 约定产出正确结果(请补充具体断言点)",
"测试结果": "",
"测试结论": "",
"日志截屏": "",
"备注": ""
},
{
"序号": 39,
"功能模块": "通用模块",
"功能类别": "安全/鉴权",
"用例编号": "TC-039",
"功能描述": "实现自定义权限控制指令",
"用例等级": "中",
"功能编号": "1",
"用例名称": "实现自定义权限控制指令",
"预置条件": "1. 环境已部署; 2. 具备执行权限; 3. 必要依赖/配置已准备",
"操作步骤": "1. 按 PRD 描述执行该功能/流程; 2. 采集相关日志/输出; 3. 记录实际结果",
"JSON": "",
"预期结果": "实现自定义权限控制指令 按 PRD 约定产出正确结果(请补充具体断言点)",
"测试结果": "",
"测试结论": "",
"日志截屏": "",
"备注": ""
},
{
"序号": 40,
"功能模块": "通用模块",
"功能类别": "安全/鉴权",
"用例编号": "TC-040",
"功能描述": "先将权限数据结构存储在localStorage的userPermission中,模拟用户已经获取到当前的拥有权限",
"用例等级": "中",
"功能编号": "1",
"用例名称": "先将权限数据结构存储在localStorage的userPermission中,模拟用户已经获取到当前的拥有权限",
"预置条件": "1. 环境已部署; 2. 具备执行权限; 3. 必要依赖/配置已准备",
"操作步骤": "1. 按 PRD 描述执行该功能/流程; 2. 采集相关日志/输出; 3. 记录实际结果",
"JSON": "",
"预期结果": "先将权限数据结构存储在localStorage的userPermission中,模拟用户已经获取到当前的拥有权限 按 PRD 约定产出正确结果(请补充具体断言点)",
"测试结果": "",
"测试结论": "",
"日志截屏": "",
"备注": ""
},
{
"序号": 41,
"功能模块": "通用模块",
"功能类别": "功能测试",
"用例编号": "TC-041",
"功能描述": "我在办公室管理的页面line:16添加了v-permission=\"'functionType_82.create'\",帮我根据判断实现:如果userPermission的functionType_82.create为1,则显示按钮,并将functionType_82.create作为id赋予给按钮,为0或者不存在则将按钮移出dom",
"用例等级": "中",
"功能编号": "2",
"用例名称": "我在办公室管理的页面line:16添加了v-permission=\"'functionType_82.create'\",帮我根据判断实现:如果userPermission的functionType_82.create为1,则显示按钮,并将functionType_82.create作为id赋予给按钮,为0或者不存在则将按钮移出dom",
"预置条件": "1. 环境已部署; 2. 具备执行权限; 3. 必要依赖/配置已准备",
"操作步骤": "1. 按 PRD 描述执行该功能/流程; 2. 采集相关日志/输出; 3. 记录实际结果",
"JSON": "",
"预期结果": "我在办公室管理的页面line:16添加了v-permission=\"'functionType_82.create'\",帮我根据判断实现:如果userPermission的functionType_82.create为1,则显示按钮,并将functionType_82.create作为id赋予给按钮,为0或者不存在则将按钮移出dom 按 PRD 约定产出正确结果(请补充具体断言点)",
"测试结果": "",
"测试结论": "",
"日志截屏": "",
"备注": ""
},
{
"序号": 42,
"功能模块": "通用模块",
"功能类别": "安全/鉴权",
"用例编号": "TC-042",
"功能描述": "自定义权限控制的生命周期等问题可以参考`E:\\Project\\ubains-unified-platform\\src\\common\\permission.js`",
"用例等级": "中",
"功能编号": "3",
"用例名称": "自定义权限控制的生命周期等问题可以参考`E:\\Project\\ubains-unified-platform\\src\\common\\permission.js`",
"预置条件": "1. 环境已部署; 2. 具备执行权限; 3. 必要依赖/配置已准备",
"操作步骤": "1. 按 PRD 描述执行该功能/流程; 2. 采集相关日志/输出; 3. 记录实际结果",
"JSON": "",
"预期结果": "自定义权限控制的生命周期等问题可以参考`E:\\Project\\ubains-unified-platform\\src\\common\\permission.js` 按 PRD 约定产出正确结果(请补充具体断言点)",
"测试结果": "",
"测试结论": "",
"日志截屏": "",
"备注": ""
}
]
\ No newline at end of file
# -*- coding: utf-8 -*-
"""
根据模板用例 Excel,在同一个工作簿中新建 Sheet,并从 JSON 配置文件中读取用例数据写入。
后续只需要维护 JSON 文件即可复用。
根据模板用例 Excel,在同一个工作簿中新建 Sheet,并从 PRD(Markdown) 自动生成的 JSON 用例写入。
需求对齐(Docs/PRD/_PRD_根据PRD生成用例.md):
- PRD 文档统一路径:Docs/开发PRD目录(仓库实际目录名可能为 Docs/PRD,下面做了兼容)
- 执行脚本时通过交互型输入需要生成测试用例的PRD文档编号
- 若多选,则生成在同一个 sheet 表中;sheet 名 = 交互输入的“测试用例名称”
- 生成的测试用例文件名:交互输入名称 + 时间戳
"""
import os
import re
import json
import argparse
from copy import copy
from datetime import datetime
from dataclasses import dataclass
from typing import List, Dict
from openpyxl import load_workbook
from openpyxl.styles import Alignment
# ===== 1. 配置路径(改为相对当前脚本的路径) =====
# ===== 1) 路径与表头 =====
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
TEMPLATE_PATH = os.path.join(BASE_DIR, "用例文件", "兰州中石化项目测试用例20251203.xlsx")
NEW_SHEET_NAME = "兰州登录MQTT用例" # 新建的 Sheet 名
CASES_FILE = os.path.join(BASE_DIR, "config", "兰州用例.json") # 用例配置文件(JSON)
REPO_ROOT = os.path.abspath(os.path.join(BASE_DIR, "..", ".."))
# ✅ 兼容两种目录:Docs/开发PRD(PRD里写的) & Docs/PRD(仓库里常见)
PRD_DIR_CANDIDATES = [
os.path.join(REPO_ROOT, "Docs", "开发PRD"),
os.path.join(REPO_ROOT, "Docs", "PRD"),
]
TEMPLATE_PATH_DEFAULT = os.path.join(BASE_DIR, "用例文件", "兰州中石化项目测试用例20251203.xlsx")
OUTPUT_JSON_DIR = os.path.join(BASE_DIR, "config")
OUTPUT_XLSX_DIR = os.path.join(BASE_DIR, "用例文件")
# 与你表头对应的顺序(根据截图)
headers_order = [
"序号", "功能模块", "功能类别", "用例编号", "功能描述", "用例等级",
"功能编号", "用例名称", "预置条件", "操作步骤", "JSON", "预期结果",
......@@ -24,79 +43,253 @@ headers_order = [
]
def load_cases():
"""从 JSON 配置文件加载用例列表。"""
if not os.path.exists(CASES_FILE):
raise FileNotFoundError(f"找不到用例配置文件: {CASES_FILE}")
with open(CASES_FILE, "r", encoding="utf-8") as f:
data = json.load(f)
if not isinstance(data, list):
raise ValueError("用例配置文件的根节点必须是列表(List)")
return data
def resolve_prd_dir() -> str:
for d in PRD_DIR_CANDIDATES:
if os.path.isdir(d):
return d
# 默认返回第一个(后续会报错提示)
return PRD_DIR_CANDIDATES[0]
def main():
if not os.path.exists(TEMPLATE_PATH):
print("找不到模板文件:", TEMPLATE_PATH)
return
# ===== 2) PRD 抽取与用例生成(轻量规则)=====
@dataclass
class RequirementItem:
code: str
title: str
detail_lines: List[str]
try:
cases = load_cases()
except Exception as e:
print("加载用例配置失败:", e)
return
wb = load_workbook(TEMPLATE_PATH)
RE_SECTION = re.compile(r"^\s*(?P<code>\d+(?:\.\d+)*)\s*[、\.\-]\s*(?P<title>.+?)\s*[::]?\s*$")
RE_MD_HEADING = re.compile(r"^\s*(#{1,6})\s+(?P<title>.+?)\s*$")
# 以第一个sheet作为模板
template_sheet = wb.worksheets[0]
# 如果新sheet已存在就先删除
if NEW_SHEET_NAME in wb.sheetnames:
ws_new = wb[NEW_SHEET_NAME]
wb.remove(ws_new)
def normalize_md(text: str) -> str:
text = text.replace("\r\n", "\n").replace("\r", "\n")
text = re.sub(r"```.*?```", "", text, flags=re.S)
text = re.sub(r"<!--.*?-->", "", text, flags=re.S)
return text
def clean_line(line: str) -> str:
line = line.strip()
line = re.sub(r"^[\-\*\+]\s+", "", line)
line = re.sub(r"\s+", " ", line)
return line
def extract_requirement_items(md_text: str) -> List[RequirementItem]:
lines = md_text.split("\n")
items: List[RequirementItem] = []
current_code = ""
current_title = ""
current_detail: List[str] = []
def flush():
nonlocal current_code, current_title, current_detail
if current_title:
detail = [clean_line(x) for x in current_detail if clean_line(x)]
items.append(RequirementItem(code=current_code, title=clean_line(current_title), detail_lines=detail))
current_code = ""
current_title = ""
current_detail = []
for raw in lines:
line = raw.rstrip("\n")
m = RE_SECTION.match(line)
if m:
flush()
current_code = m.group("code").strip()
current_title = m.group("title").strip()
continue
mh = RE_MD_HEADING.match(line)
if mh:
if current_title:
current_detail.append(line)
else:
flush()
current_code = ""
current_title = mh.group("title").strip()
continue
if current_title:
current_detail.append(line)
flush()
items = [it for it in items if it.title and len(it.title) >= 2]
return items
def classify_category(title: str, detail_lines: List[str]) -> str:
text = (title + " " + " ".join(detail_lines)).lower()
if any(k in text for k in ["异常", "失败", "错误", "告警", "报警", "超时", "断开", "暴涨"]):
return "异常场景"
if any(k in text for k in ["安全", "权限", "鉴权", "加密", "脱敏"]):
return "安全/鉴权"
if any(k in text for k in ["报告", "输出", "word", "markdown", "邮件", "钉钉", "通知"]):
return "运维可观测"
return "功能测试"
def decide_priority(title: str, detail_lines: List[str]) -> str:
text = title + " " + " ".join(detail_lines)
if any(k in text for k in ["必须", "报警", "告警", "峰值", "暴涨", "发送", "对接"]):
return "高"
if any(k in text for k in ["待实现", "可以", "建议", "优化"]):
return "中"
return "中"
def build_steps(detail_lines: List[str]) -> str:
candidates: List[str] = []
for ln in detail_lines:
s = clean_line(ln)
if not s:
continue
if any(k in s for k in ["检查", "监测", "记录", "输出", "发送", "查询", "进入", "生成", "判定", "调用", "执行"]):
candidates.append(s)
uniq: List[str] = []
for c in candidates:
if c not in uniq:
uniq.append(c)
if not uniq:
return "1. 按 PRD 描述执行该功能/流程; 2. 采集相关日志/输出; 3. 记录实际结果"
uniq = uniq[:6]
return "; ".join([f"{i+1}. {x}" for i, x in enumerate(uniq)])
def build_expected(title: str, detail_lines: List[str]) -> str:
candidates: List[str] = []
for ln in detail_lines:
s = clean_line(ln)
if not s:
continue
if any(k in s for k in ["需要", "应", "必须", "输出", "记录", "发送", "判定", "成功", "失败"]):
candidates.append(s)
uniq: List[str] = []
for c in candidates:
if c not in uniq:
uniq.append(c)
if not uniq:
return f"{title} 按 PRD 约定产出正确结果(请补充具体断言点)"
uniq = uniq[:6]
return "; ".join(uniq)
def make_case_id(prefix: str, idx: int) -> str:
return f"{prefix}-{idx:03d}"
def to_case_record(idx: int, module: str, prefix: str, req: RequirementItem) -> Dict[str, str]:
record: Dict[str, str] = {
"序号": idx,
"功能模块": module,
"功能类别": classify_category(req.title, req.detail_lines),
"用例编号": make_case_id(prefix, idx),
"功能描述": req.title,
"用例等级": decide_priority(req.title, req.detail_lines),
"功能编号": req.code or "",
"用例名称": req.title,
"预置条件": "1. 环境已部署; 2. 具备执行权限; 3. 必要依赖/配置已准备",
"操作步骤": build_steps(req.detail_lines),
"JSON": "",
"预期结果": build_expected(req.title, req.detail_lines),
"测试结果": "",
"测试结论": "",
"日志截屏": "",
"备注": "",
}
for k in headers_order:
record.setdefault(k, "")
return record
def prd_to_cases(prd_path: str, module: str, prefix: str, start_idx: int = 1) -> List[Dict[str, str]]:
with open(prd_path, "r", encoding="utf-8") as f:
md = normalize_md(f.read())
items = extract_requirement_items(md)
cases: List[Dict[str, str]] = []
idx = start_idx
for it in items:
if any(k in it.title for k in ["说明", "目录", "背景", "概述"]):
continue
cases.append(to_case_record(idx=idx, module=module, prefix=prefix, req=it))
idx += 1
return cases
ws_new = wb.create_sheet(NEW_SHEET_NAME)
# ===== 复制表头(仅值 + 简单样式) =====
# 假设模板表头在第3行(根据截图),如有偏差可以改成2或其它
header_row_index = 3
# ===== 3) Excel 写入 =====
def find_header_row(template_sheet) -> int:
must_keys = {"序号", "用例名称", "操作步骤", "预期结果"}
max_scan = min(30, template_sheet.max_row or 30)
for r in range(1, max_scan + 1):
values = []
for c in range(1, min(40, template_sheet.max_column or 40) + 1):
v = template_sheet.cell(row=r, column=c).value
if v is None:
continue
values.append(str(v).strip())
if len(must_keys.intersection(values)) >= 2:
return r
return 3
def safe_sheet_name(name: str) -> str:
name = re.sub(r"[\[\]\:\*\?\/\\]", "_", name)
return name[:31]
def write_cases_to_sheet(wb, template_sheet, sheet_name: str, cases: List[Dict[str, str]]):
# 多选时需求:写入同一个 sheet;如果存在则复用并追加,否则创建
if sheet_name in wb.sheetnames:
ws = wb[sheet_name]
start_row = (ws.max_row or 1) + 1
else:
ws = wb.create_sheet(sheet_name)
start_row = 2
header_row_index = find_header_row(template_sheet)
for col_idx, cell in enumerate(template_sheet[header_row_index], start=1):
new_cell = ws_new.cell(row=1, column=col_idx, value=cell.value)
new_cell = ws.cell(row=1, column=col_idx, value=cell.value)
if cell.has_style:
new_cell.font = copy(cell.font)
new_cell.fill = copy(cell.fill)
new_cell.border = copy(cell.border)
new_cell.alignment = copy(cell.alignment)
new_cell.number_format = cell.number_format
ws.freeze_panes = "B2"
ws_new.freeze_panes = "B2"
# ===== 写入用例数据(预置条件/操作步骤自动换行,JSON列统一留空) =====
row = 2
# 写入/追加数据
row = start_row
for case in cases:
for col_idx, header in enumerate(headers_order, start=1):
val = case.get(header, "")
if header in ("预置条件", "操作步骤") and isinstance(val, str):
# 把分号+空格变成换行,Excel 中会显示为多行
val = val.replace("; ", "\n")
if header == "JSON":
# JSON 列统一留空
val = ""
ws_new.cell(row=row, column=col_idx, value=val)
ws.cell(row=row, column=col_idx, value=val)
row += 1
# ===== 对“预置条件”和“操作步骤”开启自动换行 =====
# 自动换行
col_idx_pre = headers_order.index("预置条件") + 1
col_idx_steps = headers_order.index("操作步骤") + 1
for r in range(1, row):
cell_pre = ws_new.cell(row=r, column=col_idx_pre)
cell_steps = ws_new.cell(row=r, column=col_idx_steps)
cell_pre.alignment = Alignment(wrap_text=True, vertical="top")
cell_steps.alignment = Alignment(wrap_text=True, vertical="top")
ws.cell(row=r, column=col_idx_pre).alignment = Alignment(wrap_text=True, vertical="top")
ws.cell(row=r, column=col_idx_steps).alignment = Alignment(wrap_text=True, vertical="top")
# 调整列宽
for col in ws_new.columns:
# 列宽(仅在首次创建时做一次也可,这里简化:每次都做一次)
for col in ws.columns:
max_len = 0
col_letter = col[0].column_letter
for c in col:
......@@ -106,10 +299,150 @@ def main():
l = len(str(v))
if l > max_len:
max_len = l
ws_new.column_dimensions[col_letter].width = min(max_len + 2, 60)
ws.column_dimensions[col_letter].width = min(max_len + 2, 60)
return ws
# ===== 4) 多选交互 =====
def list_prd_files(prd_dir: str) -> List[str]:
if not os.path.isdir(prd_dir):
return []
fs = [f for f in os.listdir(prd_dir) if f.lower().endswith(".md")]
fs.sort()
return fs
def parse_multi_input(s: str) -> List[str]:
s = (s or "").strip()
if not s:
return []
parts = re.split(r"[,\s]+", s)
return [p for p in (x.strip() for x in parts) if p]
def pick_prds_interactively(prd_dir: str) -> List[str]:
files = list_prd_files(prd_dir)
if not files:
raise RuntimeError(f"PRD目录为空或不存在:{prd_dir}")
print(f"PRD目录:{prd_dir}")
for i, f in enumerate(files, start=1):
print(f"{i:>2}. {f}")
print("支持多选:输入多个序号(如 1,2,5 或 1 2 5)。")
while True:
s = input("请输入PRD序号(可多选):").strip()
parts = parse_multi_input(s)
if not parts or not all(p.isdigit() for p in parts):
print("输入无效,请输入序号(可多选)。")
continue
idxs: List[int] = []
ok = True
for p in parts:
n = int(p)
if not (1 <= n <= len(files)):
ok = False
break
idxs.append(n)
if not ok:
print("序号超出范围,请重试。")
continue
seen = set()
paths: List[str] = []
for n in idxs:
if n in seen:
continue
seen.add(n)
paths.append(os.path.join(prd_dir, files[n - 1]))
return paths
def ask_sheet_name_interactively(default_name: str) -> str:
s = input(f"请输入Sheet名称(回车使用默认:{default_name}):").strip()
return safe_sheet_name(s or default_name)
def ask_output_name_interactively(default_name: str) -> str:
s = input(f"请输入生成的测试用例文件名称(回车使用默认:{default_name}):").strip()
return s or default_name
def main():
parser = argparse.ArgumentParser(description="根据PRD生成用例JSON,并写入测试用例Excel")
parser.add_argument("--template", default=TEMPLATE_PATH_DEFAULT, help="模板Excel路径")
parser.add_argument("--module", default="通用模块", help="功能模块字段值(多PRD时统一使用该值)")
parser.add_argument("--prefix", default="TC", help="用例编号前缀(多PRD时统一使用该值)")
parser.add_argument("--overwrite", action="store_true", help="是否覆盖保存到模板文件(默认另存为新文件)")
args = parser.parse_args()
prd_dir = resolve_prd_dir()
if not os.path.exists(args.template):
print("找不到模板文件:", args.template)
return
# ✅ 需求:交互选择 PRD(可多选)
prd_paths = pick_prds_interactively(prd_dir)
# ✅ 需求:多选时生成在同一个 sheet;sheet 名 = 交互输入的“测试用例名称”
if len(prd_paths) == 1:
default_sheet = f"{os.path.splitext(os.path.basename(prd_paths[0]))[0]}_用例"
else:
default_sheet = "多PRD_用例"
sheet_name = ask_sheet_name_interactively(default_sheet)
# ✅ 需求:输出文件名 = 交互输入名称 + 时间戳
if len(prd_paths) == 1:
default_out_base = os.path.splitext(os.path.basename(prd_paths[0]))[0]
else:
default_out_base = "MultiPRD"
out_base = ask_output_name_interactively(default_out_base)
# 打开模板
wb = load_workbook(args.template)
template_sheet = wb.worksheets[0]
# 多 PRD 合并到同一个 sheet:序号递增,避免重复
os.makedirs(OUTPUT_JSON_DIR, exist_ok=True)
total_cases = 0
next_idx = 1
for prd_path in prd_paths:
prd_base = os.path.splitext(os.path.basename(prd_path))[0]
cases = prd_to_cases(prd_path=prd_path, module=args.module, prefix=args.prefix, start_idx=next_idx)
if not cases:
print(f"未从PRD抽取到可生成用例的条目:{prd_path}")
continue
# 每个 PRD 仍然单独落 JSON(便于追溯)
json_path = os.path.join(OUTPUT_JSON_DIR, f"{prd_base}_用例.json")
with open(json_path, "w", encoding="utf-8") as f:
json.dump(cases, f, ensure_ascii=False, indent=4)
print(f"已生成 JSON:{json_path}({len(cases)}条)")
# 写入同一个 sheet(追加)
write_cases_to_sheet(wb, template_sheet, sheet_name, cases)
total_cases += len(cases)
next_idx += len(cases)
# 保存 Excel
if args.overwrite:
out_xlsx = args.template
else:
os.makedirs(OUTPUT_XLSX_DIR, exist_ok=True)
ts = datetime.now().strftime("%Y%m%d_%H%M%S")
out_xlsx = os.path.join(OUTPUT_XLSX_DIR, f"{out_base}_{ts}.xlsx")
wb.save(TEMPLATE_PATH)
print("已在原文件中创建新Sheet:", NEW_SHEET_NAME)
wb.save(out_xlsx)
print("已生成Excel:", out_xlsx)
print("Sheet:", sheet_name)
print("总用例条数:", total_cases)
if __name__ == "__main__":
......
......@@ -13,14 +13,15 @@
## 🎯 功能实现总览
### 服务自检 (`check_server_health.ps1`)
脚本路径:E:\GithubData\自动化\ubains-module-test\辅助工具\脚本工具\服务自检\check_server_health.ps1
### 服务自检 (`check_server_health.ps1`和`check_server_health.sh`)
脚本路径:AuxiliaryTool\ScriptTool\ServiceSelfInspection\check_server_health.ps1
脚本路径:AuxiliaryTool\ScriptTool\ServiceSelfInspection\check_server_health.sh
#### 检测需求
##### 1、SSH连接(✅ 已实现):
功能描述:
负责与目标服务器建立 SSH 连接,支持多种连接方式,作为所有后续检测和修复操作的基础。
负责与目标服务器建立 SSH 连接,支持多种连接方式,作为所有后续检测和修复操作的基础。(sh脚本不需要做连接!!!)
具体要求:
1)支持两种接入方式:
......
# 根据PRD需求文档生成用例
## 1. 概述
需求文档生成用例,根据PRD需求文档快速生成测试用例,提升测试效率,确保测试覆盖率。
## 2. 需求文档读取
PRD需求文档统一路径为:Docs/开发PRD目录
执行脚本时通过交互型输入需要生成测试用例的PRD文档编号,并且如果是多选时则生成再同一个sheet表中,sheet表的命名为输入的测试用例的名称。
## 3. 需求文档解析
读取开发的PRD文档,解析出各个功能模块及其对应的测试点,重点关注逻辑层面的测试需求。输出测试用例的初步结构。
## 4. 测试用例生成
根据解析出的测试点,再参考测试用例模板,对测试点进行测试用例的生成,生成对应的测试用例,涵盖功能测试、边界测试、异常测试等。生成的用例存放在:AuxiliaryTool/用例生成/用例文件。
生成的测试用例名称通过交互型输入名称后追加时间来命名。
\ No newline at end of file
# 权限组管理接口文档
## 基础信息
- **接口前缀**`/permissionGroup`
- **请求头**:需要携带有效的 Token 认证
- **作者**:Jayson
- **版本**:v1.0
- **日期**:2026-01-12
---
## 1. 分页查询权限组
**接口地址**`POST /permissionGroup/getPage`
**接口描述**:根据公司编号、权限组名称、启用状态等条件进行分页查询,支持模糊匹配权限组名称,按创建时间倒序排列
**请求参数**
| 参数名 | 类型 | 必填 | 默认值 | 说明 |
|--------|------|------|--------|------|
| pageNum | Integer | 否 | 1 | 页码 |
| pageSize | Integer | 否 | 10 | 每页数量 |
| groupName | String | 否 | - | 权限组名称(模糊匹配) |
| isEnable | Integer | 否 | - | 启用状态:0-禁用,1-启用 |
**响应示例**
```json
{
"code": 200,
"message": "success",
"data": {
"records": [
{
"groupId": 1,
"groupName": "管理员权限组",
"permissions": "{\"meetingManage\":{\"create\":1,\"delete\":1,\"edit\":1,\"view\":1}}",
"companyNumber": "COMP001",
"isEnable": 1,
"createTime": "2026-01-12 10:00:00",
"updateTime": "2026-01-12 10:00:00"
}
],
"total": 100,
"size": 10,
"current": 1,
"pages": 10
}
}
```
---
## 2. 获取所有权限配置
**接口地址**`GET /permissionGroup/getAllPermissions`
**接口描述**:从permission.json配置文件读取系统所有可配置的权限项,返回权限配置模板,用于前端权限组配置界面展示
**请求参数**:无
**响应示例**
```json
{
"code": 200,
"message": "success",
"data": {
"meetingManage": {"create": 0, "delete": 0, "edit": 0, "view": 0},
"roomManage": {"create": 0, "delete": 0, "edit": 0, "view": 0},
"userManage": {"create": 0, "delete": 0, "edit": 0, "view": 0},
"departmentManage": {"create": 0, "delete": 0, "edit": 0, "view": 0},
"roleManage": {"create": 0, "delete": 0, "edit": 0, "view": 0},
"deviceManage": {"create": 0, "delete": 0, "edit": 0, "view": 0},
"systemConfig": {"create": 0, "delete": 0, "edit": 0, "view": 0},
"statistics": {"view": 0, "export": 0},
"approval": {"approve": 0, "reject": 0, "view": 0},
"serviceManage": {"create": 0, "delete": 0, "edit": 0, "view": 0}
}
}
```
---
## 3. 新增权限组
**接口地址**`POST /permissionGroup/add`
**接口描述**:创建一个新的权限组,自动设置公司编号、创建时间等信息,权限配置以JSON格式存储
**请求参数**(JSON Body):
| 参数名 | 类型 | 必填 | 说明 |
|--------|------|------|------|
| groupName | String | 是 | 权限组名称 |
| permissions | String | 是 | 权限配置JSON字符串 |
| isEnable | Integer | 否 | 启用状态:0-禁用,1-启用(默认) |
**请求示例**
```json
{
"groupName": "普通员工权限组",
"permissions": "{\"meetingManage\":{\"create\":1,\"delete\":0,\"edit\":1,\"view\":1},\"roomManage\":{\"view\":1}}",
"isEnable": 1
}
```
**响应示例**
```json
{
"code": 200,
"message": "success",
"data": {
"groupId": 2,
"groupName": "普通员工权限组",
"permissions": "{\"meetingManage\":{\"create\":1,\"delete\":0,\"edit\":1,\"view\":1},\"roomManage\":{\"view\":1}}",
"companyNumber": "COMP001",
"isEnable": 1,
"createTime": "2026-01-12 14:30:00",
"updateTime": "2026-01-12 14:30:00"
}
}
```
---
## 4. 修改权限组
**接口地址**`PUT /permissionGroup/update`
**接口描述**:更新指定权限组的名称、权限配置、启用状态等信息,自动更新updateTime字段
**请求参数**(JSON Body):
| 参数名 | 类型 | 必填 | 说明 |
|--------|------|------|------|
| groupId | Long | 是 | 权限组ID |
| groupName | String | 否 | 权限组名称 |
| permissions | String | 否 | 权限配置JSON字符串 |
| isEnable | Integer | 否 | 启用状态:0-禁用,1-启用 |
**请求示例**
```json
{
"groupId": 2,
"groupName": "修改后的权限组名称",
"permissions": "{\"meetingManage\":{\"create\":1,\"delete\":1,\"edit\":1,\"view\":1}}",
"isEnable": 1
}
```
**响应示例**
```json
{
"code": 200,
"message": "success",
"data": {
"groupId": 2,
"groupName": "修改后的权限组名称",
"permissions": "{\"meetingManage\":{\"create\":1,\"delete\":1,\"edit\":1,\"view\":1}}",
"isEnable": 1,
"updateTime": "2026-01-12 15:00:00"
}
}
```
---
## 5. 删除权限组
**接口地址**`POST /permissionGroup/delete`
**接口描述**:执行物理删除操作,同时级联删除部门关联、角色关联、用户关联表中的数据,使用事务确保删除操作的原子性。基础权限组(is_base=1)不可删除。
**请求参数**(JSON Body):
| 参数名 | 类型 | 必填 | 说明 |
|--------|------|------|------|
| - | `List<Long>` | 是 | 要删除的权限组ID列表 |
**请求示例**
```json
[1, 2, 3]
```
**响应示例**(成功):
```json
{
"code": 200,
"message": "success",
"data": "删除成功"
}
```
**响应示例**(包含基础权限组):
```json
{
"code": "A_PARAM_ILLEGAL",
"message": "基础权限组不可删除:超级管理员, 普通用户",
"data": null
}
```
> ⚠️ **注意**:
> - 基础权限组(is_base=1)不可删除,会返回错误信息
> - 删除权限组会级联删除以下关联表中的数据:
> - sys_permission_group_department(部门关联)
> - sys_permission_group_role(角色关联)
> - sys_permission_group_user(用户关联)
---
## 6. 保存绑定关系
**接口地址**`POST /permissionGroup/bindRelation`
**接口描述**:保存权限组与部门/角色/用户的关联关系(全量覆盖模式),支持一次性保存多种类型的关联关系
**请求参数**(JSON Body):
| 参数名 | 类型 | 必填 | 说明 |
|--------|------|------|------|
| groupId | Long | 是 | 权限组ID |
| departmentIds | `List<Long>` | 否 | 要绑定的部门ID列表,传null不处理,传[]清空 |
| roleIds | `List<Long>` | 否 | 要绑定的角色ID列表,传null不处理,传[]清空 |
| userIds | `List<Long>` | 否 | 要绑定的用户ID列表,传null不处理,传[]清空 |
**请求示例**(保存绑定关系):
```
POST /permissionGroup/bindRelation
Content-Type: application/json
{
"groupId": 1,
"departmentIds": [10, 11],
"roleIds": [5, 6],
"userIds": [101, 102, 103]
}
```
**请求示例**(清空部门关联,保留其他):
```json
{
"groupId": 1,
"departmentIds": []
}
```
**响应示例**
```json
{
"code": 200,
"message": "success",
"data": {
"departmentCount": 2,
"roleCount": 2,
"userCount": 3
}
}
```
> 💡 **说明**:
> - **全量覆盖模式**:传入的ID列表将完全替换原有关联(先删后增)
> - 传入 `null`:不处理该类型关联,保持原样
> - 传入 `[]`(空数组):清空该类型的所有关联
> - 传入 `[1,2,3]`:该类型的关联将变为1、2、3
> - departmentIds、roleIds、userIds 至少需要传入一种类型(可为空数组)
> - 适合"编辑后保存"场景:前端回显数据后,用户修改,直接提交最终结果
---
## 7. 查询绑定关系
**接口地址**`GET /permissionGroup/getRelations`
**接口描述**:查询权限组已绑定的关联对象,支持按类型筛选或返回全部类型
**请求参数**
| 参数名 | 类型 | 必填 | 说明 |
|--------|------|------|------|
| groupId | Long | 是 | 权限组ID |
| relationType | Integer | 否 | 关联类型:1-部门,2-角色,3-用户,不传则返回全部 |
**响应示例**(不传relationType,返回全部):
```json
{
"code": 200,
"message": "success",
"data": {
"departments": [
{"id": 1, "groupId": 1, "departmentId": 10, "createTime": "2026-01-12 10:00:00"}
],
"roles": [
{"id": 1, "groupId": 1, "roleId": 5, "createTime": "2026-01-12 10:00:00"}
],
"users": [
{"id": 1, "groupId": 1, "userId": 101, "createTime": "2026-01-12 10:00:00"},
{"id": 2, "groupId": 1, "userId": 102, "createTime": "2026-01-12 10:00:00"}
]
}
}
```
**响应示例**(relationType=3,仅返回用户):
```json
{
"code": 200,
"message": "success",
"data": {
"users": [
{"id": 1, "groupId": 1, "userId": 101, "createTime": "2026-01-12 10:00:00"},
{"id": 2, "groupId": 1, "userId": 102, "createTime": "2026-01-12 10:00:00"}
]
}
}
```
---
## 8. 查询用户最终权限
**接口地址**`GET /permissionGroup/getUserPermissions`
**接口描述**:从三个维度获取用户的权限组并合并权限:
1. **直接绑定**:查询sys_permission_group_user中user_id=用户ID的权限组
2. **部门绑定**:查询sys_permission_group_department中department_id=用户所属部门ID的权限组
3. **角色绑定**:查询sys_permission_group_role中role_id=用户所属角色ID的权限组
**请求参数**
| 参数名 | 类型 | 必填 | 说明 |
|--------|------|------|------|
| userId | Long | 是 | 用户ID |
**响应示例**
```json
{
"code": 200,
"message": "success",
"data": {
"userId": 101,
"permissions": {
"meetingManage": {"create": 1, "delete": 1, "edit": 1, "view": 1},
"roomManage": {"create": 0, "delete": 0, "edit": 0, "view": 1},
"userManage": {"create": 0, "delete": 0, "edit": 0, "view": 1},
"departmentManage": {"create": 0, "delete": 0, "edit": 0, "view": 0},
"roleManage": {"create": 0, "delete": 0, "edit": 0, "view": 0},
"deviceManage": {"create": 0, "delete": 0, "edit": 0, "view": 0},
"systemConfig": {"create": 0, "delete": 0, "edit": 0, "view": 0},
"statistics": {"view": 1, "export": 0},
"approval": {"approve": 0, "reject": 0, "view": 1},
"serviceManage": {"create": 0, "delete": 0, "edit": 0, "view": 0}
},
"groupCount": 3
}
}
```
> 💡 **权限合并规则**:
> - 用户最终权限 = 直接绑定权限 ∪ 所属部门权限 ∪ 所属角色权限
> - 采用**并集策略**:对于同一权限项,有任意一个来源为1则最终为1
> - 仅合并启用状态(is_enable=1)的权限组
---
## 错误码说明
| 错误码 | 说明 |
|--------|------|
| 200 | 成功 |
| A_USER_NOT_EXIST | 用户不存在(Token无效) |
| A_LACK_PARAMS | 缺少必要参数 |
| A_PARAM_ILLEGAL | 参数不合法 |
| A_SELECT_EMPTY | 查询结果为空 |
| B_SERVER_ERR | 服务器内部错误 |
---
## 枚举值定义
### relationType(关联类型)
| 值 | 说明 | 对应表 |
|----|------|--------|
| 1 | 部门 | sys_permission_group_department |
| 2 | 角色 | sys_permission_group_role |
| 3 | 用户 | sys_permission_group_user |
### isEnable(启用状态)
| 值 | 说明 |
|----|------|
| 0 | 禁用 |
| 1 | 启用 |
### isBase(基础权限组标识)
| 值 | 说明 |
|----|------|
| 0 | 普通权限组(可删除) |
| 1 | 基础权限组(不可删除) |
---
## 数据库表结构
### sys_permission_group(权限组主表)
| 字段名 | 类型 | 说明 |
|--------|------|------|
| group_id | bigint | 权限组ID(主键,自增) |
| group_name | varchar(100) | 权限组名称 |
| permissions | text | 权限配置JSON |
| company_number | varchar(64) | 公司编号 |
| is_enable | int | 是否启用(0:禁用,1:启用) |
| is_base | int | 是否基础权限组(0:否,1:是) |
| create_time | datetime | 创建时间 |
| update_time | datetime | 更新时间 |
### sys_permission_group_department(权限组-部门关联表)
| 字段名 | 类型 | 说明 |
|--------|------|------|
| id | bigint | 主键ID(自增) |
| group_id | bigint | 权限组ID |
| department_id | bigint | 部门ID |
| create_time | datetime | 创建时间 |
### sys_permission_group_role(权限组-角色关联表)
| 字段名 | 类型 | 说明 |
|--------|------|------|
| id | bigint | 主键ID(自增) |
| group_id | bigint | 权限组ID |
| role_id | bigint | 角色ID |
| create_time | datetime | 创建时间 |
### sys_permission_group_user(权限组-用户关联表)
| 字段名 | 类型 | 说明 |
|--------|------|------|
| id | bigint | 主键ID(自增) |
| group_id | bigint | 权限组ID |
| user_id | bigint | 用户ID |
| create_time | datetime | 创建时间 |
---
## 权限配置JSON格式说明
权限配置采用JSON格式存储,结构如下:
```json
{
"模块名称": {
"操作类型": 01
}
}
```
- **值为0**:无权限
- **值为1**:有权限
### 示例
```json
{
"meetingManage": {
"create": 1,
"delete": 0,
"edit": 1,
"view": 1
},
"roomManage": {
"create": 0,
"delete": 0,
"edit": 0,
"view": 1
}
}
```
表示:
- 会议管理:可创建、可编辑、可查看,不可删除
- 会议室管理:仅可查看
---
*文档生成时间:2026年1月12日*
# 权限管理页面新增开发
## 📋 概述
1. 权限管理页面地址: `src\views\Backend\Admin\PermissionManage\index.vue`
2. 相关API文档地址:`Docs\Api\API_权限组管理接口文档.md`
3. 新增/修改权限组弹窗地址:`src\views\Backend\Admin\PermissionManage\components\AddEditDialog\index.vue`
4. 权限绑定弹窗地址:`src\views\Backend\Admin\PermissionManage\components\BindDialog\index.vue`
5. 自定义权限控制指令v-permission地址:`src\utils\permission.js`
## 🎯 任务
1. ✅ 在权限管理页面遵循`Docs\PRD\_PRD_规范文档_新建页面.md`并标准化初始化页面,在`src\router.js`注册路由
2. ✅ 参考页面`src\views\Backend\Admin\Role\index.vue`的设计在初始化后的权限管理页新增功能
3. ✅ 将权限管理页面的新增/修改权限组弹窗和绑定弹窗分别抽离成组件放置在`src\views\Backend\Admin\PermissionManage\components`中,注意数据的传输,并优化一下样式
4. ✅ 实现权限组添加权限
5. ✅ 在新增/修改权限组弹窗继续实现任务4的要求
6. ✅ 帮我参考API接口文档,实现权限组的增删改查、禁用/启用功能,绑定功能先不实现
7. ✅ 进入权限管理页面时,帮我请求API接口文档的接口`/permissionGroup/getAllPermissions`获取所有配置项,然后将`src\views\Backend\Admin\PermissionManage\components\PermissionConfig\index.vue`的permissionList替换为接口请求到的真实数据
8. ✅ 重构权限绑定弹窗及补充功能实现
9. 数据结构调整
### 权限页面基础功能
1. 实现功能时提供了API接口,但先不进行对接,先在前端用虚拟数据实现并预留操作方法等,等我要接口对接时再进行接口对接
2. 支持模糊搜索,支持基础的增删改查
3. el-table的主要显示字段获取参考相关API文档的"分页查询权限组"接口,显示字段groupName、isEnable、createTime,支持多选,支持分页,操作有修改、删除、绑定
### 权限组添加权限要求
1. 实现功能时提供了API接口,但先不进行对接,先在前端用虚拟数据实现并预留操作方法等,等我要接口对接时再进行接口对接
2. 我在`src\constant\permissionList.js`模拟了权限数据,帮我使用el-checkbox进行显示
3. 文本显示逻辑:在国际化文件中新建permission子集,通过key值匹配显示文本:例如view=i18n文件中的`permission.view`
4. 页面显示:要同时显示名称和勾选框,比如
```json
functionType_82: {
view: 0,
create: 0,
update: 0,
delete: 0,
},
```
要显示为"办公室管理:查看 新增 编辑 删除"
### 重构权限绑定弹窗及补充功能实现要求
1. 弹窗页面重构,当前的设计不符合产品的功能要求,具体功能要求:权限组支持同时绑定多个用户、角色、部门,取消权限配置功能
2. 页面样式自由发挥,符合系统主题即可,优先使用UI/UX Pro Max
3. 参考`@src/views/Backend/Account/User/index.vue:930-984 `实现用户数据请求获取
4. 参考`@src/views/Backend/Account/User/index.vue:986-1010 `实现角色数据的请求获取
5. 参考`@src/views/Backend/Account/User/index.vue:263-273 ``@src/views/Backend/Account/User/index.vue:1055-1068 `实现部门数据的请求获取
6. 根据权限api文档的`/permissionGroup/bindRelation`实现权限绑定
### 数据结构调整页面相关实现
1. 当前接口的数据结构
## 国际化
遵循`Docs\PRD\_PRD_规范文档_国际化.md`规范
## 接口请求
当涉及到接口请求时,需要严格遵循`Docs\PRD\_PRD_规范文档_接口请求.md`规范
\ No newline at end of file
# 自定义权限控制指令实现
## 📋 概述
1. 办公室管理页面地址: `src\views\Backend\MeetingRoom\OfficeManage\index.vue`
2. 自定义权限控制指令地址:`src\utils\permission.js`
3. 权限数据结构`src\constant\permissionList.js`
4. 权限控制指令通过`src\main.js`挂载成`v-permission`使用
## 🎯 任务
1. 实现自定义权限控制指令
### 自定义权限控制指令实现要求
1. 先将权限数据结构存储在localStorage的userPermission中,模拟用户已经获取到当前的拥有权限
2. 我在办公室管理的页面line:16添加了v-permission="'functionType_82.create'",帮我根据判断实现:如果userPermission的functionType_82.create为1,则显示按钮,并将functionType_82.create作为id赋予给按钮,为0或者不存在则将按钮移出dom
3. 自定义权限控制的生命周期等问题可以参考`E:\Project\ubains-unified-platform\src\common\permission.js`
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论