Files
uniaihealth/tools/test_verify_custom_params.py
T

281 lines
11 KiB
Python

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
UniAI Health (原愈极刻) 定制设备治疗参数功能 - 端到端全链路验证工具
1. 校验 RESTful API (管理员登录鉴权、规则管理、标签打标)
2. 模拟 IoT 设备与后台 UDP 服务 (端口 61100) 双向报文交互
3. 验证四层参数决策引擎:标签定制、单机定制、安全截断兜底、基准兜底
"""
import socket
import sys
import time
import json
import urllib.request
import urllib.error
GREEN = "\033[92m"
RED = "\033[91m"
YELLOW = "\033[93m"
BLUE = "\033[94m"
RESET = "\033[0m"
UDP_HOST = "127.0.0.1"
UDP_PORT = 61100
HTTP_BASE = "http://127.0.0.1:8082"
def calc_crc(content: str) -> str:
"""计算异或校验码 (XOR Checksum)"""
if "$" in content and "*" in content:
raw = content[content.index("$") + 1 : content.rindex("*")]
else:
raw = content
xor_val = 0
for ch in raw.encode("ascii"):
xor_val ^= ch
return f"{xor_val:02X}"
def build_packet(inner_body: str) -> bytes:
"""构建带 $ 与 *{CRC}\\r\\n 的完整报文"""
raw = f"${inner_body}*"
crc = calc_crc(raw)
packet_str = f"${inner_body}*{crc}\r\n"
return packet_str.encode("utf-8")
def parse_kv(packet_str: str) -> dict:
"""解析以管道符分隔的报文内容"""
packet_str = packet_str.strip().strip("\r\n")
if packet_str.startswith("$"):
packet_str = packet_str[1:]
if "*" in packet_str:
packet_str = packet_str[:packet_str.rindex("*")]
parts = packet_str.split("|")
res = {}
if parts:
res["CMD"] = parts[0]
for p in parts[1:]:
if len(p) >= 2:
res[p[:2]] = p[2:]
return res
def test_login() -> str:
"""登录系统获取管理员 JWT Token"""
url = f"{HTTP_BASE}/pc/loginByUsername"
payload = json.dumps({"username": "admin", "password": "admin123"}).encode("utf-8")
req = urllib.request.Request(url, data=payload, headers={"Content-Type": "application/json"})
try:
with urllib.request.urlopen(req, timeout=3) as resp:
raw_data = json.loads(resp.read().decode("utf-8"))
token = raw_data.get("token")
if not token and isinstance(raw_data.get("data"), dict):
token = raw_data["data"].get("token")
if token:
print(f" {GREEN}✓ 管理员身份认证成功 (username: admin){RESET}")
return token
else:
print(f" {RED}✗ 登录成功但未包含 Token: {raw_data}{RESET}")
return ""
except Exception as e:
print(f" {RED}✗ 管理员登录异常: {e}{RESET}")
return ""
def test_rest_api(token: str):
"""测试 REST 接口:规则分页与批量标签"""
print(f"\n{BLUE}▶ 测试用例: [REST API] 验证定制规则管理与标签接口{RESET}")
if not token:
print(f" {RED}✗ 缺少认证 Token,跳过鉴权接口测试{RESET}")
return False
headers = {
"jkzy-auth": token,
"User-Agent": "VerifyScript",
"Content-Type": "application/json"
}
# 1. 查询规则列表
url_rules = f"{HTTP_BASE}/pc/admin/deviceTreatRule/page?pageNo=1&pageSize=10"
try:
req = urllib.request.Request(url_rules, headers=headers)
with urllib.request.urlopen(req, timeout=3) as resp:
data = json.loads(resp.read().decode("utf-8"))
if data.get("code") == 200:
records = data.get("data", {}).get("records", [])
print(f" {GREEN}✓ GET /pc/admin/deviceTreatRule/page 响应成功,已加载 {len(records)} 条定制规则{RESET}")
for r in records[:3]:
print(f" - [{r.get('ruleName')}] 类型:{r.get('targetType')} 目标:{r.get('targetValue')} FR:{r.get('fr')} UL:{r.get('ul')}")
else:
print(f" {RED}✗ 规则接口返回错误: {data}{RESET}")
return False
except Exception as e:
print(f" {RED}✗ 规则列表接口访问失败: {e}{RESET}")
return False
# 2. 批量打标测试
url_tags = f"{HTTP_BASE}/pc/admin/device/setTags"
try:
payload = json.dumps({
"deviceIds": ["DEV_DEF_04"],
"tags": "temp_test_tag,wsl_verify"
}).encode("utf-8")
req = urllib.request.Request(url_tags, data=payload, headers=headers)
with urllib.request.urlopen(req, timeout=3) as resp:
data = json.loads(resp.read().decode("utf-8"))
if data.get("code") == 200:
print(f" {GREEN}✓ POST /pc/admin/device/setTags 批量设备标签更新成功{RESET}")
else:
print(f" {YELLOW}⚠ 打标接口响应: {data.get('message')}{RESET}")
except Exception as e:
print(f" {YELLOW}⚠ 打标接口调用异常: {e}{RESET}")
return True
def test_device_treatment_params(device_id: str, case_name: str, expected_params: dict):
"""通过 UDP 模拟设备请求并断言下发的参数"""
print(f"\n{BLUE}▶ 测试用例: [{device_id}] - {case_name}{RESET}")
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.settimeout(3.0)
try:
# 第一步:设备上报状态 03(就绪等待处方)
req1 = build_packet(f"03|MU{device_id}|CI00|CN00|")
# print(f" [TX -> 61100] {req1.decode().strip()}")
sock.sendto(req1, (UDP_HOST, UDP_PORT))
# 接收服务端的 03 回复(包含生成的处方流水号 CI)
data1, _ = sock.recvfrom(2048)
resp1_str = data1.decode("utf-8", errors="ignore")
# print(f" [RX <- 61100] {resp1_str.strip()}")
kv1 = parse_kv(resp1_str)
ci = kv1.get("CI")
if not ci or ci == "00":
print(f" {RED}✗ 失败: 服务端未返回有效处方流水号 (CI={ci}){RESET}")
return False
# 第二步:设备使用状态 30 请求处方参数分段 TN1
req2 = build_packet(f"30|MU{device_id}|CI{ci}|TN1|")
# print(f" [TX -> 61100] {req2.decode().strip()}")
sock.sendto(req2, (UDP_HOST, UDP_PORT))
# 接收服务端下发的 30 处方参数报文
data2, _ = sock.recvfrom(2048)
resp2_str = data2.decode("utf-8", errors="ignore")
# print(f" [RX <- 61100] {resp2_str.strip()}")
kv2 = parse_kv(resp2_str)
# 断言校验
all_passed = True
actual_details = []
for param, exp_val in expected_params.items():
act_val = kv2.get(param)
# 兼容前导 0
match = (act_val == exp_val) or (act_val and act_val.lstrip("0") == exp_val.lstrip("0"))
if match:
actual_details.append(f"{param}={act_val} {GREEN}✓{RESET}")
else:
actual_details.append(f"{param}={act_val}(预期:{exp_val}) {RED}✗{RESET}")
all_passed = False
print(" 参数下发结果: " + ", ".join(actual_details))
if all_passed:
print(f" {GREEN}✓ PASS: 该设备下发参数完全符合定制/安全/基准规则预期!{RESET}")
return True
else:
print(f" {RED}✗ FAIL: 参数与预期不符!{RESET}")
return False
except socket.timeout:
print(f" {RED}✗ 接收超时: 服务端未及时回复 UDP 报文{RESET}")
return False
except Exception as e:
print(f" {RED}✗ 异常: {e}{RESET}")
return False
finally:
sock.close()
def main():
print(f"{BLUE}=================================================================={RESET}")
print(f"{BLUE} UniAI Health 定制治疗参数功能 WSL 本地全链路验证工具 {RESET}")
print(f"{BLUE}=================================================================={RESET}")
# 检查 HTTP 服务就绪
print(f"\n正在检查后台 HTTP (8082) 与 UDP (61100) 端口可用性...")
http_ready = False
try:
with urllib.request.urlopen(f"{HTTP_BASE}/pc/loginByUsername", timeout=2) as resp:
http_ready = True
except urllib.error.HTTPError as e:
# POST endpoint called with GET will return 405 Method Not Allowed, which proves server is up
http_ready = (e.code in (400, 405))
except Exception:
http_ready = False
if not http_ready:
print(f"{RED}✗ 后台服务尚未就绪或 8082 端口未启动。{RESET}")
print(f"{YELLOW}请先在 WSL-Ubuntu 终端中运行以下命令启动服务:{RESET}")
print(f" cd /mnt/d/Workspace/uniaihealth/server")
print(f" mvn spring-boot:run -pl admin -Dspring-boot.run.profiles=local\n")
sys.exit(1)
print(f"{GREEN}✓ 后台服务连接成功!开始执行测试用例...{RESET}")
# 登录获取 Token
token = test_login()
results = []
# 1. REST API
results.append(("REST API 测试", test_rest_api(token)))
# 2. 标签匹配覆盖 (DEV_GOLF_01 打了 golf_vip 标签)
results.append(("标签定制覆盖 (golf_vip)", test_device_treatment_params(
device_id="DEV_GOLF_01",
case_name="标签定制匹配 (golf_vip 覆盖基准)",
expected_params={"FR": "08", "PW": "55", "PL": "12", "UL": "05", "TL": "025"}
)))
# 3. 单机精确匹配覆盖 (DEV_SPEC_02 配置了单机规则)
results.append(("单机专属定制 (DEV_SPEC_02)", test_device_treatment_params(
device_id="DEV_SPEC_02",
case_name="单机专属规则覆盖 (DEV_SPEC_02 覆盖基准与标签)",
expected_params={"FR": "02", "PW": "40", "PL": "04", "UL": "02", "TL": "010"}
)))
# 4. 非医疗民用版安全极限截断 (DEV_CIVIL_03, cf=2, 规则配置超标 UL=18)
results.append(("民用机安全截断 (DEV_CIVIL_03)", test_device_treatment_params(
device_id="DEV_CIVIL_03",
case_name="硬件安全底线防护 (cf=2 非医疗版强制限幅 UL <= 10)",
expected_params={"UL": "10"}
)))
# 5. 普通设备基准默认参数兜底 (DEV_DEF_04, 无标签无单机规则)
results.append(("基准参数兜底 (DEV_DEF_04)", test_device_treatment_params(
device_id="DEV_DEF_04",
case_name="全局病症基准参数正常下发兜底",
expected_params={"FR": "03", "PW": "50", "PL": "08", "UL": "01", "TL": "017"}
)))
# 汇总
print(f"\n{BLUE}=================================================================={RESET}")
print(f"{BLUE} 测试结果汇总报告 {RESET}")
print(f"{BLUE}=================================================================={RESET}")
total = len(results)
passed = sum(1 for _, ok in results if ok)
for name, ok in results:
status_text = f"{GREEN}PASS ✓{RESET}" if ok else f"{RED}FAIL ✗{RESET}"
print(f" {name: <35} : {status_text}")
print(f"{BLUE}------------------------------------------------------------------{RESET}")
print(f"总计: {total} 项,成功: {GREEN}{passed}{RESET} 项,失败: {RED}{total - passed}{RESET} 项")
if passed == total:
print(f"{GREEN}★ 所有定制治疗参数与调度验证均 100% 成功通过!★{RESET}\n")
sys.exit(0)
else:
print(f"{RED}⚠ 存在未通过的测试项,请检查日志。{RESET}\n")
sys.exit(1)
if __name__ == "__main__":
main()