更新
This commit is contained in:
@@ -3,6 +3,8 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import mimetypes
|
||||
import re
|
||||
import time
|
||||
from collections.abc import Mapping
|
||||
from contextlib import suppress
|
||||
from datetime import date
|
||||
@@ -102,7 +104,7 @@ class DoctorRepository(Protocol):
|
||||
def upload_material(
|
||||
self,
|
||||
path: str | PathLike[str],
|
||||
material_type: Literal["image", "file", "tongue_images", "report_files"],
|
||||
material_type: Literal["image", "video", "file", "tongue_images", "report_files"],
|
||||
cid: int = 0,
|
||||
) -> str:
|
||||
"""Upload one local note material and return its server URI."""
|
||||
@@ -224,6 +226,9 @@ class DoctorRepository(Protocol):
|
||||
def cancel_patient_appointment(self, appointment_id: int) -> Any:
|
||||
"""Cancel an appointment from the patient workspace."""
|
||||
|
||||
def cancel_diagnosis_appointment(self, appointment_id: int) -> Any:
|
||||
"""Cancel a diagnosis-list appointment through the doctor route."""
|
||||
|
||||
def get_diagnosis_detail(self, diagnosis_id: int, *, readonly: bool = False) -> dict[str, Any]:
|
||||
"""Return an editable or permission-aware readonly diagnosis detail."""
|
||||
|
||||
@@ -453,6 +458,39 @@ class DoctorRepository(Protocol):
|
||||
def add_tracking_note(self, diagnosis_id: int, content: str) -> Any:
|
||||
"""Append a diagnosis tracking note."""
|
||||
|
||||
def add_blood_record(self, payload: Mapping[str, Any] | None = None, **fields: Any) -> Any:
|
||||
"""Create one doctor-entered blood glucose / pressure record."""
|
||||
|
||||
def update_blood_record(
|
||||
self,
|
||||
record: int | Mapping[str, Any],
|
||||
changes: Mapping[str, Any] | None = None,
|
||||
**fields: Any,
|
||||
) -> Any:
|
||||
"""Update one blood glucose / pressure record."""
|
||||
|
||||
def add_diet_record(self, payload: Mapping[str, Any] | None = None, **fields: Any) -> Any:
|
||||
"""Create one daily diet record."""
|
||||
|
||||
def update_diet_record(
|
||||
self,
|
||||
record: int | Mapping[str, Any],
|
||||
changes: Mapping[str, Any] | None = None,
|
||||
**fields: Any,
|
||||
) -> Any:
|
||||
"""Update one daily diet record."""
|
||||
|
||||
def add_exercise_record(self, payload: Mapping[str, Any] | None = None, **fields: Any) -> Any:
|
||||
"""Create one daily exercise record."""
|
||||
|
||||
def update_exercise_record(
|
||||
self,
|
||||
record: int | Mapping[str, Any],
|
||||
changes: Mapping[str, Any] | None = None,
|
||||
**fields: Any,
|
||||
) -> Any:
|
||||
"""Update one daily exercise record."""
|
||||
|
||||
def check_diagnosis_phone(self, payload: Mapping[str, Any]) -> Any:
|
||||
"""Check diagnosis phone uniqueness."""
|
||||
|
||||
@@ -479,6 +517,72 @@ class DoctorRepository(Protocol):
|
||||
def cancel_diagnosis_todo(self, todo_id: int) -> Any:
|
||||
"""Cancel a diagnosis follow-up todo."""
|
||||
|
||||
def list_call_records(self, diagnosis_id: int) -> list[dict[str, Any]]:
|
||||
"""Return persisted audio/video call records and replay URLs."""
|
||||
|
||||
def create_manual_call_record(self, diagnosis_id: int) -> dict[str, Any]:
|
||||
"""Create the server record used to hold a manually uploaded replay."""
|
||||
|
||||
def attach_local_call_recording(
|
||||
self,
|
||||
diagnosis_id: int,
|
||||
file_url: str,
|
||||
*,
|
||||
call_record_id: int | None = None,
|
||||
) -> Any:
|
||||
"""Attach one uploaded replay URI to a call record."""
|
||||
|
||||
def upload_call_recording(
|
||||
self,
|
||||
path: str | PathLike[str],
|
||||
diagnosis_id: int,
|
||||
*,
|
||||
call_record_id: int | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""Upload a local video then attach it to a real call record."""
|
||||
|
||||
def list_im_chat_messages(
|
||||
self, diagnosis_id: int, *, only_archived: bool = True
|
||||
) -> dict[str, Any]:
|
||||
"""Return diagnosis IM messages, defaulting to the fast archive-only path."""
|
||||
|
||||
def sync_im_chat_messages(self, diagnosis_id: int) -> Any:
|
||||
"""Queue the server-side IM archive synchronisation job."""
|
||||
|
||||
def list_diagnosis_appointment_logs(self, diagnosis_id: int) -> list[dict[str, Any]]:
|
||||
"""Return per-diagnosis registration / cancellation audit rows."""
|
||||
|
||||
def generate_mini_program_qrcode(
|
||||
self, payload: Mapping[str, Any] | None = None, **fields: Any
|
||||
) -> dict[str, Any]:
|
||||
"""Generate the diagnosis or video-login mini-program QR code."""
|
||||
|
||||
def generate_video_qrcode(
|
||||
self, doctor_id: int, patient_id: int, share_user_id: int
|
||||
) -> dict[str, Any]:
|
||||
"""Generate the video-login QR variant."""
|
||||
|
||||
def generate_diagnosis_qrcode(
|
||||
self, diagnosis_id: int, doctor_id: int, patient_id: int, share_user_id: int
|
||||
) -> dict[str, Any]:
|
||||
"""Generate the diagnosis-confirmation QR variant."""
|
||||
|
||||
def list_appointment_logs(self, diagnosis_id: int) -> list[dict[str, Any]]:
|
||||
"""Compatibility name for diagnosis registration logs."""
|
||||
|
||||
def create_diagnosis_order(
|
||||
self,
|
||||
patient_id: int,
|
||||
order_type: int,
|
||||
amount: float,
|
||||
*,
|
||||
remark: str = "",
|
||||
) -> dict[str, Any]:
|
||||
"""Create the diagnosis-list generic payment order (not a prescription order)."""
|
||||
|
||||
def generate_order_qrcode(self, order_no: str) -> dict[str, Any]:
|
||||
"""Generate the payment mini-program QR code for a generic order."""
|
||||
|
||||
def get_call_ticket(self, patient_id: int, diagnosis_id: int) -> CallTicket:
|
||||
"""Return short-lived call credentials."""
|
||||
|
||||
@@ -796,7 +900,7 @@ class RemoteDoctorRepository:
|
||||
def upload_material(
|
||||
self,
|
||||
path: str | PathLike[str],
|
||||
material_type: Literal["image", "file", "tongue_images", "report_files"],
|
||||
material_type: Literal["image", "video", "file", "tongue_images", "report_files"],
|
||||
cid: int = 0,
|
||||
) -> str:
|
||||
"""Upload a local note material and return only its server reference."""
|
||||
@@ -1437,6 +1541,13 @@ class RemoteDoctorRepository:
|
||||
|
||||
return self.client.post("firstvisit.myPatient/cancelAppointment", {"id": appointment_id})
|
||||
|
||||
def cancel_diagnosis_appointment(self, appointment_id: int) -> Any:
|
||||
"""Cancel through the exact route used by the admin diagnosis list."""
|
||||
|
||||
if appointment_id <= 0:
|
||||
raise ValueError("appointment_id must be positive")
|
||||
return self.client.post("doctor.appointment/cancel", {"id": appointment_id})
|
||||
|
||||
def patient_detail(self, diagnosis_id: int) -> dict[str, Any]:
|
||||
"""Compatibility name for permission-aware readonly diagnosis details."""
|
||||
|
||||
@@ -1516,6 +1627,11 @@ class RemoteDoctorRepository:
|
||||
def set_revisit_slot_start_offset(self, diagnosis_id: int, offset: int) -> Any:
|
||||
"""Set the diagnosis revisit-statistics starting offset."""
|
||||
|
||||
if diagnosis_id <= 0:
|
||||
raise ValueError("diagnosis_id must be positive")
|
||||
if offset not in range(0, 21):
|
||||
raise ValueError("revisit_slot_start_offset must be between 0 and 20")
|
||||
|
||||
return self.client.post(
|
||||
"tcm.diagnosis/setRevisitSlotStartOffset",
|
||||
{"id": diagnosis_id, "revisit_slot_start_offset": offset},
|
||||
@@ -1623,13 +1739,66 @@ class RemoteDoctorRepository:
|
||||
def add_tracking_note(self, diagnosis_id: int, content: str) -> Any:
|
||||
"""Append a textual daily tracking note."""
|
||||
|
||||
if not content.strip():
|
||||
raise ValueError("tracking_content is required")
|
||||
clean_content = content.strip()
|
||||
if diagnosis_id <= 0:
|
||||
raise ValueError("diagnosis_id must be positive")
|
||||
if not clean_content or len(clean_content) > 1000:
|
||||
raise ValueError("tracking_content must contain 1 to 1000 characters")
|
||||
return self.client.post(
|
||||
"tcm.diagnosis/addTrackingNote",
|
||||
{"diagnosis_id": diagnosis_id, "tracking_content": content.strip()},
|
||||
{"diagnosis_id": diagnosis_id, "tracking_content": clean_content},
|
||||
)
|
||||
|
||||
def add_blood_record(self, payload: Mapping[str, Any] | None = None, **fields: Any) -> Any:
|
||||
"""Create a blood record using the fields consumed by ``DailyMatrix``."""
|
||||
|
||||
return self.client.post("tcm.bloodRecord/add", _daily_record_body("blood", payload, fields))
|
||||
|
||||
def update_blood_record(
|
||||
self,
|
||||
record: int | Mapping[str, Any],
|
||||
changes: Mapping[str, Any] | None = None,
|
||||
**fields: Any,
|
||||
) -> Any:
|
||||
"""Update a blood record without weakening add-time validation."""
|
||||
|
||||
body = _identified_body(record, changes, fields)
|
||||
return self.client.post("tcm.bloodRecord/edit", _daily_record_body("blood", body, {}))
|
||||
|
||||
def add_diet_record(self, payload: Mapping[str, Any] | None = None, **fields: Any) -> Any:
|
||||
"""Create a breakfast/lunch/dinner daily record."""
|
||||
|
||||
return self.client.post("tcm.dietRecord/add", _daily_record_body("diet", payload, fields))
|
||||
|
||||
def update_diet_record(
|
||||
self,
|
||||
record: int | Mapping[str, Any],
|
||||
changes: Mapping[str, Any] | None = None,
|
||||
**fields: Any,
|
||||
) -> Any:
|
||||
"""Update a diet record."""
|
||||
|
||||
body = _identified_body(record, changes, fields)
|
||||
return self.client.post("tcm.dietRecord/edit", _daily_record_body("diet", body, {}))
|
||||
|
||||
def add_exercise_record(self, payload: Mapping[str, Any] | None = None, **fields: Any) -> Any:
|
||||
"""Create a validated exercise record."""
|
||||
|
||||
return self.client.post(
|
||||
"tcm.exerciseRecord/add", _daily_record_body("exercise", payload, fields)
|
||||
)
|
||||
|
||||
def update_exercise_record(
|
||||
self,
|
||||
record: int | Mapping[str, Any],
|
||||
changes: Mapping[str, Any] | None = None,
|
||||
**fields: Any,
|
||||
) -> Any:
|
||||
"""Update an exercise record."""
|
||||
|
||||
body = _identified_body(record, changes, fields)
|
||||
return self.client.post("tcm.exerciseRecord/edit", _daily_record_body("exercise", body, {}))
|
||||
|
||||
def list_diagnosis_todos(
|
||||
self,
|
||||
diagnosis_id: int,
|
||||
@@ -1660,11 +1829,19 @@ class RemoteDoctorRepository:
|
||||
def add_diagnosis_todo(self, diagnosis_id: int, content: str, remind_time: int) -> Any:
|
||||
"""Create a follow-up todo using a Unix-seconds reminder."""
|
||||
|
||||
clean_content = content.strip()
|
||||
if diagnosis_id <= 0:
|
||||
raise ValueError("diagnosis_id must be positive")
|
||||
if not clean_content or len(clean_content) > 500:
|
||||
raise ValueError("todo content must contain 1 to 500 characters")
|
||||
if remind_time <= int(time.time()) + 30:
|
||||
raise ValueError("remind_time must be at least 30 seconds in the future")
|
||||
|
||||
return self.client.post(
|
||||
"tcm.diagnosisTodo/add",
|
||||
{
|
||||
"diagnosis_id": diagnosis_id,
|
||||
"content": content.strip(),
|
||||
"content": clean_content,
|
||||
"remind_time": remind_time,
|
||||
},
|
||||
)
|
||||
@@ -1672,8 +1849,204 @@ class RemoteDoctorRepository:
|
||||
def cancel_diagnosis_todo(self, todo_id: int) -> Any:
|
||||
"""Cancel an outstanding diagnosis todo."""
|
||||
|
||||
if todo_id <= 0:
|
||||
raise ValueError("todo_id must be positive")
|
||||
|
||||
return self.client.post("tcm.diagnosisTodo/cancel", {"id": todo_id})
|
||||
|
||||
def list_call_records(self, diagnosis_id: int) -> list[dict[str, Any]]:
|
||||
"""Load the confirmed replay list endpoint."""
|
||||
|
||||
if diagnosis_id <= 0:
|
||||
raise ValueError("diagnosis_id must be positive")
|
||||
return _mapping_rows(
|
||||
self.client.get("tcm.diagnosis/getCallRecords", {"diagnosis_id": diagnosis_id})
|
||||
)
|
||||
|
||||
def create_manual_call_record(self, diagnosis_id: int) -> dict[str, Any]:
|
||||
"""Create a synthetic call record before a toolbar video upload."""
|
||||
|
||||
if diagnosis_id <= 0:
|
||||
raise ValueError("diagnosis_id must be positive")
|
||||
result = self.client.post(
|
||||
"tcm.diagnosis/createManualCallRecord", {"diagnosis_id": diagnosis_id}
|
||||
)
|
||||
return dict(_require_mapping(result, "tcm.diagnosis/createManualCallRecord"))
|
||||
|
||||
def attach_local_call_recording(
|
||||
self,
|
||||
diagnosis_id: int,
|
||||
file_url: str,
|
||||
*,
|
||||
call_record_id: int | None = None,
|
||||
) -> Any:
|
||||
"""Attach an uploaded replay URI to the selected call record."""
|
||||
|
||||
if diagnosis_id <= 0:
|
||||
raise ValueError("diagnosis_id must be positive")
|
||||
clean_url = file_url.strip()
|
||||
if not clean_url:
|
||||
raise ValueError("file_url is required")
|
||||
body: dict[str, Any] = {"diagnosis_id": diagnosis_id, "file_url": clean_url}
|
||||
if call_record_id is not None:
|
||||
if call_record_id <= 0:
|
||||
raise ValueError("call_record_id must be positive")
|
||||
body["call_record_id"] = call_record_id
|
||||
return self.client.post("tcm.diagnosis/attachLocalCallRecording", body)
|
||||
|
||||
def upload_call_recording(
|
||||
self,
|
||||
path: str | PathLike[str],
|
||||
diagnosis_id: int,
|
||||
*,
|
||||
call_record_id: int | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""Perform the same upload/create/attach sequence as ``CallRecordPanel``."""
|
||||
|
||||
if diagnosis_id <= 0:
|
||||
raise ValueError("diagnosis_id must be positive")
|
||||
# Match the admin component exactly: upload first so a failed upload does
|
||||
# not leave behind an empty synthetic call record.
|
||||
file_url = self.upload_material(path, "video")
|
||||
target_id = call_record_id
|
||||
if target_id is None:
|
||||
target_id = int(self.create_manual_call_record(diagnosis_id).get("id") or 0)
|
||||
if target_id <= 0:
|
||||
raise ApiProtocolError("tcm.diagnosis/createManualCallRecord returned no id")
|
||||
self.attach_local_call_recording(diagnosis_id, file_url, call_record_id=target_id)
|
||||
return {
|
||||
"diagnosis_id": diagnosis_id,
|
||||
"call_record_id": target_id,
|
||||
"file_url": file_url,
|
||||
}
|
||||
|
||||
def list_im_chat_messages(
|
||||
self, diagnosis_id: int, *, only_archived: bool = True
|
||||
) -> dict[str, Any]:
|
||||
"""Load archive-only messages unless the caller explicitly requests live merging."""
|
||||
|
||||
if diagnosis_id <= 0:
|
||||
raise ValueError("diagnosis_id must be positive")
|
||||
payload = self.client.get(
|
||||
"tcm.diagnosis/getImChatMessages",
|
||||
{"diagnosis_id": diagnosis_id, "only_archived": int(only_archived)},
|
||||
)
|
||||
return dict(_require_mapping(payload, "tcm.diagnosis/getImChatMessages"))
|
||||
|
||||
def sync_im_chat_messages(self, diagnosis_id: int) -> Any:
|
||||
"""Queue the confirmed shutdown-function archive sync endpoint."""
|
||||
|
||||
if diagnosis_id <= 0:
|
||||
raise ValueError("diagnosis_id must be positive")
|
||||
return self.client.post("tcm.diagnosis/triggerImChatSync", {"diagnosis_id": diagnosis_id})
|
||||
|
||||
def list_diagnosis_appointment_logs(self, diagnosis_id: int) -> list[dict[str, Any]]:
|
||||
"""Return registration/cancellation logs from ``guahaoLogList``."""
|
||||
|
||||
if diagnosis_id <= 0:
|
||||
raise ValueError("diagnosis_id must be positive")
|
||||
return _mapping_rows(self.client.get("tcm.diagnosis/guahaoLogList", {"id": diagnosis_id}))
|
||||
|
||||
def generate_mini_program_qrcode(
|
||||
self, payload: Mapping[str, Any] | None = None, **fields: Any
|
||||
) -> dict[str, Any]:
|
||||
"""Generate either QR variant using the server's shared DTO."""
|
||||
|
||||
body = _body(payload, fields)
|
||||
for key in ("patient_id", "share_user_id"):
|
||||
if int(body.get(key) or 0) <= 0:
|
||||
raise ValueError(f"{key} must be positive")
|
||||
page = str(body.get("mini_program_path") or "").strip()
|
||||
if page == "pages/login/login":
|
||||
if int(body.get("doctor_id") or 0) <= 0:
|
||||
raise ValueError("doctor_id must be positive for a video QR code")
|
||||
# The current server validator mistakenly inspects diagnosis_id for this case;
|
||||
# mirror the admin DTO until that server contract is fixed.
|
||||
body.setdefault("diagnosis_id", body["doctor_id"])
|
||||
elif int(body.get("diagnosis_id") or 0) <= 0:
|
||||
raise ValueError("diagnosis_id must be positive")
|
||||
payload_result = self.client.post("tcm.diagnosis/generateMiniProgramQrcode", body)
|
||||
result = dict(_require_mapping(payload_result, "tcm.diagnosis/generateMiniProgramQrcode"))
|
||||
if not str(result.get("qrcode_url") or "").strip():
|
||||
raise ApiProtocolError(
|
||||
"tcm.diagnosis/generateMiniProgramQrcode returned no qrcode_url",
|
||||
data=result,
|
||||
)
|
||||
return result
|
||||
|
||||
def generate_video_qrcode(
|
||||
self, doctor_id: int, patient_id: int, share_user_id: int
|
||||
) -> dict[str, Any]:
|
||||
"""Generate the video QR with the admin's confirmed login-page DTO."""
|
||||
|
||||
return self.generate_mini_program_qrcode(
|
||||
diagnosis_id=doctor_id,
|
||||
doctor_id=doctor_id,
|
||||
patient_id=patient_id,
|
||||
share_user_id=share_user_id,
|
||||
mini_program_path="pages/login/login",
|
||||
)
|
||||
|
||||
def generate_diagnosis_qrcode(
|
||||
self, diagnosis_id: int, doctor_id: int, patient_id: int, share_user_id: int
|
||||
) -> dict[str, Any]:
|
||||
"""Generate the diagnosis-confirmation QR."""
|
||||
|
||||
return self.generate_mini_program_qrcode(
|
||||
diagnosis_id=diagnosis_id,
|
||||
doctor_id=doctor_id,
|
||||
patient_id=patient_id,
|
||||
share_user_id=share_user_id,
|
||||
)
|
||||
|
||||
def list_appointment_logs(self, diagnosis_id: int) -> list[dict[str, Any]]:
|
||||
"""Compatibility name used by the diagnosis-list menu."""
|
||||
|
||||
return self.list_diagnosis_appointment_logs(diagnosis_id)
|
||||
|
||||
def create_diagnosis_order(
|
||||
self,
|
||||
patient_id: int,
|
||||
order_type: int,
|
||||
amount: float,
|
||||
*,
|
||||
remark: str = "",
|
||||
) -> dict[str, Any]:
|
||||
"""Create the generic order used by the diagnosis-list action."""
|
||||
|
||||
if patient_id <= 0:
|
||||
raise ValueError("patient_id must be positive")
|
||||
if order_type not in range(1, 9):
|
||||
raise ValueError("order_type must be between 1 and 8")
|
||||
if amount <= 0:
|
||||
raise ValueError("amount must be positive")
|
||||
payload = self.client.post(
|
||||
"order.order/create",
|
||||
{
|
||||
"patient_id": patient_id,
|
||||
"order_type": order_type,
|
||||
"amount": round(float(amount), 2),
|
||||
"remark": remark.strip(),
|
||||
},
|
||||
)
|
||||
return dict(_require_mapping(payload, "order.order/create"))
|
||||
|
||||
def generate_order_qrcode(self, order_no: str) -> dict[str, Any]:
|
||||
"""Generate the QR shown immediately after a generic order is created."""
|
||||
|
||||
clean_order_no = order_no.strip()
|
||||
if not clean_order_no:
|
||||
raise ValueError("order_no is required")
|
||||
payload = self.client.post(
|
||||
"tcm.diagnosis/generateOrderQrcode", {"order_no": clean_order_no}
|
||||
)
|
||||
result = dict(_require_mapping(payload, "tcm.diagnosis/generateOrderQrcode"))
|
||||
if not str(result.get("qrcode_url") or "").strip():
|
||||
raise ApiProtocolError(
|
||||
"tcm.diagnosis/generateOrderQrcode returned no qrcode_url", data=result
|
||||
)
|
||||
return result
|
||||
|
||||
def get_call_ticket(self, patient_id: int, diagnosis_id: int) -> CallTicket:
|
||||
"""Obtain short-lived Tencent credentials for a consultation call."""
|
||||
|
||||
@@ -1767,15 +2140,17 @@ def _require_mapping(value: object, endpoint: str) -> Mapping[str, Any]:
|
||||
|
||||
def _material_kind(
|
||||
material_type: str,
|
||||
) -> Literal["image", "file"]:
|
||||
"""Map reception concepts to the two audited upload endpoints."""
|
||||
) -> Literal["image", "video", "file"]:
|
||||
"""Map workstation concepts to the three audited upload endpoints."""
|
||||
|
||||
value = material_type.strip().lower()
|
||||
if value in {"image", "tongue_images"}:
|
||||
return "image"
|
||||
if value == "video":
|
||||
return "video"
|
||||
if value in {"file", "report_files"}:
|
||||
return "file"
|
||||
raise ValueError("material_type must be image/file or tongue_images/report_files")
|
||||
raise ValueError("material_type must be image/video/file or tongue_images/report_files")
|
||||
|
||||
|
||||
def _is_local_material_reference(value: str) -> bool:
|
||||
@@ -1910,6 +2285,111 @@ def _body(
|
||||
return result
|
||||
|
||||
|
||||
_RECORD_DATE_PATTERN = re.compile(r"^\d{4}-\d{2}-\d{2}$")
|
||||
_RECORD_TIME_PATTERN = re.compile(r"^(?:[01]\d|2[0-3]):[0-5]\d$")
|
||||
|
||||
|
||||
def _daily_record_body(
|
||||
kind: Literal["blood", "diet", "exercise"],
|
||||
payload: Mapping[str, Any] | None,
|
||||
fields: Mapping[str, Any],
|
||||
) -> dict[str, Any]:
|
||||
"""Validate the exact DTOs used by the admin ``DailyMatrix`` dialogs."""
|
||||
|
||||
body = _body(payload, fields)
|
||||
for key in ("diagnosis_id", "patient_id"):
|
||||
value = _to_int(body.get(key), 0)
|
||||
if value <= 0:
|
||||
raise ValueError(f"{key} must be positive")
|
||||
body[key] = value
|
||||
record_date = str(body.get("record_date") or "").strip()
|
||||
if not _RECORD_DATE_PATTERN.fullmatch(record_date):
|
||||
raise ValueError("record_date must use YYYY-MM-DD")
|
||||
parsed_date = date.fromisoformat(record_date)
|
||||
if parsed_date > date.today():
|
||||
raise ValueError("record_date cannot be in the future")
|
||||
body["record_date"] = record_date
|
||||
|
||||
if kind == "blood":
|
||||
record_time = str(body.get("record_time") or "").strip()
|
||||
if record_time and not _RECORD_TIME_PATTERN.fullmatch(record_time):
|
||||
raise ValueError("record_time must use HH:MM")
|
||||
body["record_time"] = record_time
|
||||
limits = {
|
||||
"fasting_blood_sugar": 50.0,
|
||||
"postprandial_blood_sugar": 50.0,
|
||||
"other_blood_sugar": 50.0,
|
||||
"systolic_pressure": 300.0,
|
||||
"diastolic_pressure": 200.0,
|
||||
}
|
||||
has_content = False
|
||||
for key, maximum in limits.items():
|
||||
raw = body.get(key)
|
||||
if raw in (None, ""):
|
||||
continue
|
||||
try:
|
||||
number = float(raw)
|
||||
except (TypeError, ValueError) as exc:
|
||||
raise ValueError(f"{key} must be numeric") from exc
|
||||
if number < 0 or number > maximum:
|
||||
raise ValueError(f"{key} must be between 0 and {maximum:g}")
|
||||
body[key] = number
|
||||
has_content = has_content or number > 0
|
||||
for key in ("western_medicine", "insulin", "remark"):
|
||||
clean = str(body.get(key) or "").strip()
|
||||
body[key] = clean
|
||||
has_content = has_content or bool(clean)
|
||||
if not has_content:
|
||||
raise ValueError("a blood record must contain at least one measurement or note")
|
||||
return body
|
||||
|
||||
if kind == "diet":
|
||||
has_content = False
|
||||
for meal in ("breakfast", "lunch", "dinner"):
|
||||
foods_key = f"{meal}_foods"
|
||||
foods = str(body.get(foods_key) or body.get(meal) or "").strip()
|
||||
body[foods_key] = foods
|
||||
has_content = has_content or bool(foods)
|
||||
images_key = f"{meal}_images"
|
||||
raw_images = body.get(images_key) or []
|
||||
if not isinstance(raw_images, (list, tuple)):
|
||||
raise ValueError(f"{images_key} must be an array")
|
||||
if len(raw_images) > 3:
|
||||
raise ValueError(f"{images_key} cannot contain more than 3 images")
|
||||
body[images_key] = _server_materials(raw_images, images_key)
|
||||
has_content = has_content or bool(body[images_key])
|
||||
body["note"] = str(body.get("note") or "").strip()
|
||||
has_content = has_content or bool(body["note"])
|
||||
if not has_content:
|
||||
raise ValueError("a diet record must contain food, an image or a note")
|
||||
return body
|
||||
|
||||
exercise_type = str(body.get("exercise_type") or "").strip()
|
||||
if not exercise_type:
|
||||
raise ValueError("exercise_type is required")
|
||||
duration = _to_int(body.get("duration"), 0)
|
||||
if duration < 1 or duration > 300:
|
||||
raise ValueError("duration must be between 1 and 300 minutes")
|
||||
intensity = _to_int(body.get("intensity"), 0)
|
||||
if intensity not in {1, 2, 3}:
|
||||
raise ValueError("intensity must be 1, 2 or 3")
|
||||
raw_images = body.get("images") or []
|
||||
if not isinstance(raw_images, (list, tuple)):
|
||||
raise ValueError("images must be an array")
|
||||
if len(raw_images) > 3:
|
||||
raise ValueError("images cannot contain more than 3 items")
|
||||
body.update(
|
||||
{
|
||||
"exercise_type": exercise_type,
|
||||
"duration": duration,
|
||||
"intensity": intensity,
|
||||
"images": _server_materials(raw_images, "images"),
|
||||
"note": str(body.get("note") or "").strip(),
|
||||
}
|
||||
)
|
||||
return body
|
||||
|
||||
|
||||
def _identified_body(
|
||||
value: int | Mapping[str, Any],
|
||||
changes: Mapping[str, Any] | None,
|
||||
|
||||
Reference in New Issue
Block a user