10 KiB
سختسازی ضدتکرار ایمپورت پزشک با شناسهٔ پروفایل نظام پزشکی
پروژه
clinicpro (backend). این پرامپت گارد اجرایی ضدتکرار است؛ پرامپت همتای خزنده
clinicpro-crawler/.claude/prompt/crawler-dedup-and-data-audit.md منبع داده را اصلاح میکند.
این یکی اول اجرا شود — خزنده به این قرارداد تکیه میکند.
زمینه
خزندهٔ نظام پزشکی (clinicpro-crawler) پزشکان را با POST /api/v1/admin/doctors/import
وارد میکند. idempotency فعلی فقط روی (source, medical_system_code) است
(DoctorImportService::doImport + یونیکایندکس uniq_doctors_source_code). اما شناسهٔ
authoritative واقعی irimc، آیدیِ پروفایل است که در source_ref ذخیره میشود
(https://membersearch.irimc.org/member/profile?id=<uuid>) — نه medical_system_code.
بررسی دادهٔ فعلی (۲۳۳۷ رکورد source='irimc'): همه source_ref یکتا دارند و همه
medical_system_code یکتا. تکرار دقیق فعلاً وجود ندارد. ولی همنامهایی مثل «محمد صالحی»
با کدهای متفاوت (42507, آ-1469 — هر دو «آسیبشناسی») وجود دارند که profile id متمایز
دارند؛ ممکن است یک نفر باشند ولی از دید irimc دو پروفایل جدا هستند.
مشکل / هدف
هدف: هیچوقت یک پروفایلِ irimc دو رکورد نسازد — حتی اگر در جستوجوهای مختلف با
medical_system_code متفاوت برگردد. کلید idempotency باید علاوه بر
(source, medical_system_code)، روی (source, source_ref) (profile id) هم باشد.
ضدهدف: ادغام خودکار پروفایلهای متمایز. همنامها با profile id متفاوت نباید خودکار ادغام شوند — فقط برای بازبینی دستی گزارش شوند.
فایلهای مرتبط
| فایل | نقش |
|---|---|
src/Doctor/Service/DoctorImportService.php |
منطق idempotency ایمپورت |
src/Doctor/Entity/Doctor.php |
فیلد sourceRef + یونیکایندکسها |
src/Doctor/Repository/DoctorRepository.php |
کوئریهای پیدا کردن پزشک |
migrations/ |
یونیکایندکس جدید روی (source, source_ref) |
src/Doctor/Command/RepairImportedDoctorsCommand.php |
جای مناسب برای گام گزارش تکراری |
src/Doctor/Service/Repair/ |
گامهای ترمیم (الگوی DoctorRepairStep) |
docs/api/doctor-import.md |
مستند idempotency |
وضعیت فعلی
idempotency فقط با medical_system_code (DoctorImportService::doImport)
$code = trim((string) ($data['medical_system_code'] ?? $data['medicalSystemCode']));
$source = trim((string) ($data['source'] ?? 'irimc')) ?: 'irimc';
// idempotency: همان پزشکِ منبع → بهروزرسانی، نه ساخت تکراری
$doctor = $doctorRepo->findOneBy(['source' => $source, 'medicalSystemCode' => $code]);
if ($doctor === null) {
// جلوگیری از تکرار بین منابع بر پایهٔ کد
$existing = $doctorRepo->findOneBy(['medicalSystemCode' => $code]);
if ($existing !== null) {
return new DoctorImportResult($existing, false, 'duplicate');
}
}
// ...
// source_ref فقط ذخیره میشود، کلید dedup نیست:
if (array_key_exists('source_ref', $data) || array_key_exists('profile_url', $data)) {
$doctor->setSourceRef($data['source_ref'] ?? $data['profile_url'] ?? null);
}
یونیکایندکس فعلی
uniq_doctors_source_code → (source, medical_system_code)
source_ref هیچ ایندکس/قید یکتایی ندارد.
وظایف
۱. dedup روی profile id نظام پزشکی
استخراج آیدی پایدار از source_ref. خودِ URL کامل ممکن است تغییر قالب بدهد
(query param اضافه/کم)، پس آیدیِ UUID داخل آن استخراج و نرمالایز شود. یک متد در
DoctorImportService (یا یک VO کوچک):
/** آیدی پایدار پروفایل نظام پزشکی از داخل source_ref/profile_url. */
private function extractProfileId(?string $ref): ?string
{
if ($ref === null || trim($ref) === '') {
return null;
}
// .../member/profile?id=<uuid> → <uuid> (بدون توجه به دامنه/پارامترهای دیگر)
if (preg_match('/[?&]id=([0-9a-f-]{8,})/i', $ref, $m)) {
return strtolower($m[1]);
}
return null; // قالب ناشناخته → روی همان medical_system_code fallback کن
}
اولویت dedup: اول با profile id، بعد با کد.
$profileId = $this->extractProfileId($data['source_ref'] ?? $data['profile_url'] ?? null);
$doctor = null;
if ($profileId !== null) {
// همان پروفایل irimc → همیشه همان رکورد، حتی اگر کد فرق کرده باشد
$doctor = $doctorRepo->findOneByProfileId($source, $profileId);
}
if ($doctor === null) {
$doctor = $doctorRepo->findOneBy(['source' => $source, 'medicalSystemCode' => $code]);
}
تصمیم لازم: برای مطابقت با profile id، یا ستون
source_refرا عیناً کوئری بزن (اگر خزنده URL پایدار میفرستد) یا یک ستون نرمالِsource_profile_idاضافه کن (بهتر، چون ایندکس یکتا روی UUID خالص تمیزتر است). گزینهٔ دوم پیشنهاد میشود: ستونsource_profile_id VARCHAR(36) NULL+ پرکردنش در ایمپورت + یونیکایندکس(source, source_profile_id). برای دادهٔ موجود در گام backfill (وظیفهٔ ۳) پر شود.
۲. یونیکایندکس + migration
- ستون
source_profile_idبهDoctor(اگر گزینهٔ دوم را گرفتی). - یونیکایندکس جزئی:
(source, source_profile_id)کهNULLرا نادیده میگیرد (پزشکانmanual/seedبدون profile id نباید تداخل کنند — MariaDB روی چندNULLدر یونیکایندکس تداخل نمیگیرد، پس امن است). doctrine:migrations:diff→migrate.
#[ORM\Column(name: 'source_profile_id', type: 'string', length: 36, nullable: true)]
private ?string $sourceProfileId = null;
#[ORM\UniqueConstraint(name: 'uniq_doctors_source_profile', columns: ['source', 'source_profile_id'])]
نکتهٔ همزمانی: الگوی موجود doImport روی UniqueConstraintViolationException رفت
(resetManager + تلاش مجدد → مسیر update). این را برای ایندکس جدید هم حفظ کن.
۳. گام گزارش تکراری مشکوک (بدون ادغام خودکار)
یک DoctorRepairStep جدید report-suspected-duplicates در
src/Doctor/Service/Repair/ اضافه کن (خودکار کشف میشود؛ الگوی گامهای موجود).
این گام هیچچیز تغییر نمیدهد — فقط خوشههای مشکوک را چاپ میکند:
- معیار: نام نرمالشده یکسان (
PersianText::normalize) و حداقل یکspecialty_idمشترک و همان شهر، ولیsource_profile_id(یاsource_ref) متفاوت. - خروجی: جدول
[نام، تعداد، profile_idها، تخصص مشترک، شهر]برای بازبینی دستی. - در همین گام،
source_profile_idخالیِ رکوردهای قدیمی را ازsource_refbackfill کن (اینجا idempotent و قابل--dry-run).
چرا ادغام خودکار نه: profile id در irimc authoritative است و متمایز بودن آن یعنی irimc آنها را دو نفر میداند. «محمد صالحیِ آسیبشناس» میتواند دو نفر واقعی باشد. ادغام اشتباه، دادهٔ دو پزشک را یکی میکند و برگشتناپذیر است.
۴. مستندسازی
docs/api/doctor-import.md: بخش idempotency را بهروزرسانی کن — اولویت
source_profile_id بر medical_system_code، رفتار پزشکان بدون profile id، و گام
report-suspected-duplicates.
نکات مهم
- پزشکان
manual/seedبدون profile id نباید بشکنند:source_profile_id = NULLو یونیکایندکس چندNULLرا میپذیرد. مسیر ایمپورت فقط وقتی profile id دارد از آن استفاده میکند؛ وگرنه دقیقاً مثل قبل رویmedical_system_codeکار میکند. - پروفایل
claimedدستنخورده: منطق فعلی که ایمپورت مجددِ پروفایل تصاحبشده را بازنویسی نمیکند (return ... 'claimed') باید حفظ شود. - backfill قبل از افزودن یونیکایندکس: اگر دادهٔ موجود پس از پرکردن
source_profile_idتکراری داشته باشد، migration یونیکایندکس میشکند. اول در گام ترمیم backfill کن و تکراریهای دقیق (همان profile id، دو رکورد) را — که الان صفرند ولی باید چک شوند — گزارش/حل کن، بعد ایندکس را بزن. - الگوها: Controller از
BaseController؛ خطا با$this->error()/AppException؛ تایماستمپ Unix؛ گام ترمیم باDoctorRepairStepو tagapp.doctor_repair_step. - تست الزامی (موفق + خطا + مرزی):
- همان profile id با
medical_system_codeمتفاوت → یک رکورد (update، نه create). - profile id متفاوت با همان نام/تخصص → دو رکورد (ادغام نشود).
- رکورد بدون profile id (manual) → رفتار قبلی، بدون تداخل یونیک.
- idempotency: ایمپورت دوبارهٔ همان payload → صفر رکورد جدید.
- گام گزارش: خوشهٔ مشکوک را مییابد ولی هیچ رکوردی را تغییر نمیدهد.
- همان profile id با
- بعد از هر تغییر:
ddev exec php bin/phpunitسبز،docs/api/doctor-import.mdبهروز.