9.4 KiB
یکتا بودن کد ملی در پروفایل بیمار (هر کد ملی فقط یک پروفایل)
پروژه
clinicpro (Backend — منبع واحد داده و constraint).
این تغییر cross-repo اثر دارد: سایت عمومی
nobat724_frontو پنل ادمین React (داخل همین پروژه) فقط باید خطای409/422جدید را نمایش دهند. قرارداد پاسخ خطا در «نکات مهم» آمده تا کلاینتها مصرف کنند. تغییر منطق فقط backend است.
زمینه
هر کاربر (User) دقیقاً یک پروفایل (UserProfile, جدول profiles, رابطه OneToOne با کلید یکتای idx_profiles_user) دارد. کد ملی هم روی User و هم روی UserProfile ذخیره میشود ولی در هیچکدام یکتا نیست — فقط یک ایندکس غیر-یکتا روی profiles.national_code هست:
// src/UserProfile/Entity/UserProfile.php:12
#[ORM\Index(columns: ['national_code'], name: 'idx_profiles_national_code')]
// src/UserProfile/Entity/UserProfile.php:36
#[ORM\Column(name: 'national_code', type: 'string', length: 10, nullable: true)]
private ?string $nationalCode = null;
// src/Auth/Entity/User.php:35
#[ORM\Column(name: 'national_code', type: 'string', length: 10, nullable: true)]
private ?string $nationalCode = null;
پرامپت قبلی fix-server-side-mobile-nationalcode-validation.md فقط فرمت کد ملی (رقم کنترلی ایرانی) را سمت سرور اضافه کرد، نه یکتایی را. در نتیجه دو پروفایل مختلف میتوانند یک کد ملی داشته باشند — چه از مسیر پروفایل عمومی، چه نوبتدهی توسط منشی.
مشکل / هدف
کد ملی باید در سطح بیمار یکتا باشد: نباید دو پروفایل جدا با یک کد ملی وجود داشته باشد. وقتی کاربر/منشی/هر مسیرِ نوبتدهی کد ملیای ثبت میکند که قبلاً به پروفایل کاربر دیگری تعلق دارد، باید با خطای 409 رد شود (نه ذخیره).
دامنهی یکتایی: UserProfile.national_code (منبع نهاییِ هویت بیمار، رابطهی ۱:۱ با کاربر). User.nationalCode در حد همگامسازی است و قید یکتایی روی پروفایل کافی است.
فایلهای مرتبط
| فایل | نقش |
|---|---|
src/UserProfile/Entity/UserProfile.php |
افزودن قید یکتای partial روی national_code |
src/UserProfile/Repository/UserProfileRepository.php |
متد findOneByNationalCode() جدید |
src/UserProfile/Controller/UserProfileController.php |
hydrate() / update() — چک یکتایی قبل از set |
src/Shared/Constant/ErrorCodes.php |
کد خطای جدید برای کد ملی تکراری |
migrations/VersionXXXX.php (جدید) |
unique index روی profiles.national_code |
docs/api/user-profile.md |
مستندسازی پاسخ 409 |
وضعیت فعلی (کد واقعی)
UserProfileController::hydrate — بدون چک یکتایی ذخیره میشود:
// src/UserProfile/Controller/UserProfileController.php:211
if (array_key_exists('national_code', $data)) $profile->setNationalCode($data['national_code']);
update() فقط فرمت را چک میکند (از پرامپت قبلی)، یکتایی نه:
// src/UserProfile/Controller/UserProfileController.php:134
if (array_key_exists('national_code', $data) && $data['national_code'] !== null && $data['national_code'] !== '') {
$code = InputValidator::toEnglishDigits((string) $data['national_code']);
if (!InputValidator::isValidIranNationalCode($code)) {
return $this->error(ErrorCodes::ERR_VALIDATION_001, 'کد ملی نامعتبر است', 422, 'national_code');
}
$data['national_code'] = $code;
}
وظایف
۱. قید یکتای دیتابیس (partial — فقط مقادیر non-null)
در UserProfile.php ایندکس فعلی را به unique تبدیل کن. چون national_code nullable است و چند پروفایلِ بدون کد ملی باید مجاز بمانند، در MariaDB رفتار پیشفرضِ unique index این است که چندین NULL مجاز است (دقیقاً همان partial-unique موردنظر) — پس کافی است index را unique کنیم:
// بهجای خط 12
#[ORM\UniqueConstraint(name: 'uniq_profiles_national_code', columns: ['national_code'])]
سپس migration بساز:
ddev exec php bin/console doctrine:migrations:diff --no-interaction
هشدار دادهی موجود: قبل از migrate، تکراریهای فعلی را پیدا کن وگرنه migration روی ALTER TABLE میشکند:
SELECT national_code, COUNT(*) c FROM profiles
WHERE national_code IS NOT NULL AND national_code <> ''
GROUP BY national_code HAVING c > 1;
اگر تکراری بود، در توضیحات خروجی به کاربر گزارش بده (پاکسازی دستی لازم است؛ خودسرانه merge/حذف نکن).
۲. متد repository
در UserProfileRepository.php:
public function findOneByNationalCode(string $nationalCode): ?UserProfile
{
return $this->findOneBy(['nationalCode' => $nationalCode]);
}
۳. چک یکتایی در سطح اپلیکیشن (قبل از flush)
قید DB لایهی آخر است؛ برای پیام تمیز و جلوگیری از UniqueConstraintViolationException، در UserProfileController::update() بعد از validation فرمت و قبل از hydrate، یکتایی را چک کن. اگر کد ملی به پروفایلی با uuid متفاوت تعلق دارد → 409:
// داخل update()، بعد از بلوک normalize/validate فرمت (خط ~140)
if (isset($data['national_code']) && $data['national_code'] !== '') {
$existing = $this->repository->findOneByNationalCode($data['national_code']);
if ($existing !== null && $existing->getUuid() !== $profile->getUuid()) {
return $this->error(
ErrorCodes::ERR_PROFILE_NATIONAL_CODE_TAKEN,
'این کد ملی قبلاً برای کاربر دیگری ثبت شده است',
409,
'national_code'
);
}
}
۴. کد خطا
در src/Shared/Constant/ErrorCodes.php کنار سایر ERR_VALIDATION_* اضافه کن:
public const ERR_PROFILE_NATIONAL_CODE_TAKEN = 'ERR_PROFILE_001';
و پیام فارسی متناظرش را در همان نگاشتِ پیامها (همانجا که بقیه کدها پیام دارند) قرار بده: «این کد ملی قبلاً برای کاربر دیگری ثبت شده است».
۵. مسیرهای نوبتدهی منشی / سایر
هر نقطهای که از مسیرِ غیر-پروفایل کد ملی روی پروفایل یا کاربر مینشاند، باید همین چک findOneByNationalCode را قبل از set اجرا کند. بررسی کن این نقاط را و در صورت وجود set کد ملی روی UserProfile/User، چک را اضافه کن:
src/Patient/Controller/PatientController.php(createPatient— خطوط ~133–160، set رویUser)src/Staff/Controller/StaffController.php(خطوط 64, 91 —ClinicStaff.nationalCode؛ این پرسنل است نه بیمار، فقط در صورتی که staff به پروفایل بیمار map میشود چک لازم است — اگر نه، دست نزن)
برای مسیر منشی/پنل که از UserProfileController::update میگذرد، وظیفهی ۳ کافی است و نیازی به تکرار نیست.
نکات مهم
- partial-unique با NULL: عمداً
NULLرا آزاد میگذاریم تا پروفایلهای ناقصِ بدون کد ملی (که سیستم بهصورت lazy میسازد —resolveProfile، خط 193) نشکنند. هرگز رشتهی خالی''ذخیره نکن؛ همیشهnull. اگر جایی''مینشیند، بهnullنرمالایز کن وگرنه دو پروفایلِ''قید را میشکنند. - دامنهی یکتایی = global (نه per-clinic). یک کد ملی در کل سیستم = یک بیمار.
- چک اپلیکیشنی race-condition دارد (دو request همزمان)؛ قید DB لایهی نهایی است. اگر خواستی،
UniqueConstraintViolationExceptionرا در یک try/catch دورsave()بگیر و همان409را برگردان تا حالت رقابتی هم پیام تمیز بدهد. - همهی پاسخها از
BaseController؛ خطا با$this->error($code, $msg, 409, 'national_code'). - بعد از تغییر
UserProfileController، طبق قانون استاندارد پروژهdocs/api/user-profile.mdرا در همین session بهروز کن (افزودن پاسخ409). - مصرفکنندگان (cross-repo): سایت عمومی و پنل ادمین باید کد
ERR_PROFILE_001/ status409را روی فیلدnational_codeنمایش دهند. این در همین پرامپت پیاده نمیشود؛ فقط قرارداد را در doc ثبت کن تا کلاینتها وصل شوند.