194 lines
8.0 KiB
Python
194 lines
8.0 KiB
Python
# -*- 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 患者查询失败。"""
|
||
|
||
|
||
class ZytPatientSessionExpired(ZytPatientError):
|
||
"""The upstream patient session needs reauthentication, not desktop logout."""
|
||
|
||
|
||
class ZytPatientPermissionDenied(ZytPatientError):
|
||
"""The authenticated ZYT account cannot query patients."""
|
||
|
||
|
||
def _session_expired_message(message: str) -> bool:
|
||
normalized = str(message or "").lower().replace(" ", "")
|
||
return any(part in normalized for part in (
|
||
"登录超时", "登录已失效", "登录失效", "登录已过期", "登录过期",
|
||
"请重新登录", "请先登录", "token过期", "token已过期", "token失效",
|
||
"token已失效", "tokenexpired", "unauthorized",
|
||
))
|
||
|
||
|
||
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 ZytPatientSessionExpired("ZYT 患者查询会话缺失,请重新验证账号")
|
||
|
||
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 == 401:
|
||
raise ZytPatientSessionExpired("ZYT 患者查询登录已过期,请重新验证账号") from exc
|
||
if exc.code == 403:
|
||
raise ZytPatientPermissionDenied("当前 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 str(payload.get("code")) != "1":
|
||
message = str(payload.get("msg") or "").strip() if isinstance(payload, dict) else ""
|
||
if "权限" in message or "无权" in message:
|
||
raise ZytPatientPermissionDenied(message)
|
||
if (isinstance(payload, dict) and str(payload.get("code")) == "-1") or _session_expired_message(message):
|
||
raise ZytPatientSessionExpired(message or "ZYT 患者查询会话已失效")
|
||
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
|