Files
kefu/deploy/patient-session-20260918/before-backend/zyt_patient_client.py
T
2026-09-21 10:34:06 +08:00

171 lines
6.9 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# -*- coding: utf-8 -*-
"""ZYT 患者只读 API 适配器。
这个模块只调用 ZYT 已有的管理端查询接口,不要求 ZYT 增加路由、字段或数据表。
调用方传入桌面端已经取得的 ZYT token;本模块不会持久化账号密码。
"""
from __future__ import annotations
import json
import socket
import urllib.error
import urllib.parse
import urllib.request
from typing import Any
class ZytPatientError(RuntimeError):
"""ZYT 患者查询失败。"""
def mask_mobile(value: Any) -> str:
text = str(value or "").strip()
if len(text) >= 7:
return f"{text[:3]}****{text[-4:]}"
if len(text) >= 3:
return f"{text[:1]}***{text[-1:]}"
return "***" if text else ""
class ZytPatientClient:
def __init__(self, api_url: str, token: str, *, timeout: float = 10.0):
self.api_url = str(api_url or "").strip().rstrip("/")
self.token = str(token or "").strip()
self.timeout = max(2.0, min(float(timeout), 30.0))
parsed = urllib.parse.urlparse(self.api_url)
if parsed.scheme not in {"http", "https"} or not parsed.netloc or parsed.username:
raise ZytPatientError("ZYT 患者接口地址配置不正确")
if not self.token:
raise ZytPatientError("客户端登录已失效,请重新打开软件登录")
def _get(self, path: str, params: dict[str, Any]) -> Any:
query = urllib.parse.urlencode(
{key: value for key, value in params.items() if value not in (None, "")}
)
url = f"{self.api_url}{path}"
if query:
url = f"{url}?{query}"
request = urllib.request.Request(
url,
headers={
"Accept": "application/json",
"User-Agent": "WeCom-RPA-Patient-Bridge/1.0",
# ZYT 管理端中间件读取 token 头,不使用 Bearer。
"token": self.token,
},
method="GET",
)
try:
with urllib.request.urlopen(request, timeout=self.timeout) as response:
payload = json.loads(response.read().decode("utf-8"))
except urllib.error.HTTPError as exc:
if exc.code in {401, 403}:
raise ZytPatientError("ZYT 登录或患者查询权限已失效,请重新登录") from exc
raise ZytPatientError(f"ZYT 患者接口请求失败(HTTP {exc.code})") from exc
except (urllib.error.URLError, TimeoutError, socket.timeout) as exc:
raise ZytPatientError(
f"无法连接 ZYT 患者接口:{getattr(exc, 'reason', exc)}"
) from exc
except (ValueError, TypeError, UnicodeDecodeError) as exc:
raise ZytPatientError("ZYT 患者接口返回了无效数据") from exc
if not isinstance(payload, dict) or int(payload.get("code") or 0) != 1:
message = str(payload.get("msg") if isinstance(payload, dict) else "").strip()
raise ZytPatientError(message or "ZYT 患者查询失败")
return payload.get("data")
@staticmethod
def _list_payload(data: Any) -> tuple[list[dict[str, Any]], int]:
if isinstance(data, list):
return [dict(item) for item in data if isinstance(item, dict)], len(data)
if not isinstance(data, dict):
return [], 0
raw = data.get("lists")
if not isinstance(raw, list):
raw = data.get("items")
if not isinstance(raw, list):
raw = data.get("data")
rows = [dict(item) for item in raw if isinstance(item, dict)] if isinstance(raw, list) else []
total = data.get("count", data.get("total", len(rows)))
try:
total_value = max(0, int(total))
except (TypeError, ValueError):
total_value = len(rows)
return rows, total_value
def search_patients(
self, keyword: str, *, page_no: int = 1, page_size: int = 20
) -> dict[str, Any]:
keyword = str(keyword or "").strip()
if not keyword:
raise ValueError("请输入患者姓名、手机号或患者编号")
if len(keyword) > 64:
raise ValueError("患者搜索关键词不能超过 64 个字符")
page_no = max(1, int(page_no))
page_size = max(1, min(int(page_size), 50))
data = self._get(
"/adminapi/tcm.diagnosis/lists",
{"keyword": keyword, "page_no": page_no, "page_size": page_size},
)
rows, total = self._list_payload(data)
# 诊单列表中同一患者可能出现多次。保留接口返回顺序中的第一条(通常是最新诊单)。
patients: list[dict[str, Any]] = []
seen: set[int] = set()
for row in rows:
try:
diagnosis_id = int(row.get("id") or 0)
patient_id = int(row.get("patient_id") or diagnosis_id)
except (TypeError, ValueError):
continue
if patient_id <= 0 or patient_id in seen:
continue
seen.add(patient_id)
patients.append(
{
"patient_id": patient_id,
"diagnosis_id": max(0, diagnosis_id),
"patient_name": str(row.get("patient_name") or "").strip(),
"phone_masked": mask_mobile(row.get("phone")),
"gender": row.get("gender"),
"age": row.get("age"),
"source_update_time": str(row.get("update_time") or ""),
}
)
return {
"items": patients,
"total": total,
"page_no": page_no,
"page_size": page_size,
}
def get_patient(self, patient_id: int) -> dict[str, Any] | None:
"""按患者主键精确读取,并继续复用 ZYT 列表的数据权限过滤。"""
patient_id = int(patient_id)
if patient_id <= 0:
raise ValueError("患者编号不正确")
data = self._get(
"/adminapi/tcm.diagnosis/lists",
{"patient_id": patient_id, "page_no": 1, "page_size": 20},
)
rows, _total = self._list_payload(data)
for row in rows:
try:
diagnosis_id = int(row.get("id") or 0)
resolved_patient_id = int(row.get("patient_id") or diagnosis_id)
except (TypeError, ValueError):
continue
if resolved_patient_id != patient_id:
continue
return {
"patient_id": resolved_patient_id,
"diagnosis_id": max(0, diagnosis_id),
"patient_name": str(row.get("patient_name") or "").strip(),
"phone_masked": mask_mobile(row.get("phone")),
"gender": row.get("gender"),
"age": row.get("age"),
"source_update_time": str(row.get("update_time") or ""),
}
return None