حالت تاریک
فصل ۱۱ — سیستم پرداخت (Payment System)
خلاصهٔ تحلیلی — این متن چکیدهٔ مفاهیم فصل به زبان فارسی است، نه ترجمهٔ واژهبهواژه.
گام ۱ — نیازمندیها
بکاند پرداخت برای یک فروشگاه آنلاین شبیه آمازون. نکات کلیدی محدودهٔ مسئله:
- پردازش کارت به PSP های شخص ثالث (Stripe، Braintree و غیره) واگذار میشود؛ اطلاعات کارت را خودمان ذخیره نمیکنیم (بار انطباق سنگین).
- دو جریان: pay-in (دریافت پول از خریدار به حساب پلتفرم) و pay-out (پرداخت به فروشندگان).
- ۱ میلیون تراکنش در روز ≈ فقط ۱۰ TPS — مسئله throughput نیست؛ درستی است: تحمل خطا، مدیریت پرداختهای ناموفق و reconciliation بین سرویسهای داخلی و خارجی.
گام ۲ — طراحی سطح بالا
جریان pay-in و اجزا
پول خریدار ابتدا به حساب بانکی پلتفرم میرود (پلتفرم فقط امین وجه است) و پس از تحویل کالا، در pay-out به فروشنده منتقل میشود. اجزا:
- Payment service: پذیرش رویداد پرداخت و هماهنگی کل فرایند؛ ابتدا risk check (انطباق AML/CFT — معمولاً برونسپاریشده).
- Payment executor: اجرای هر payment order از طریق PSP (یک رویداد پرداخت میتواند چند سفارش داشته باشد — سبد چندفروشنده).
- PSP و card scheme ها: جابهجایی پول و شبکههای کارت (Visa/MasterCard).
- Ledger: ثبت مالی تراکنشها (بدهکار/بستانکار) — پایهٔ تحلیل درآمد.
- Wallet: نگهداری موجودی فروشندهها.
API و مدل داده
POST /v1/payments (buyer_info، checkout_id، credit_card_info و فهرست payment_orders) و GET /v1/payments/{id}. دو نکتهٔ طراحی مهم:
- payment_order_id سراسری-یکتا است و در PSP نقش کلید idempotency (شناسهٔ حذف تکرار) را دارد.
- مبلغ از نوع string است نه double: تفاوت دقت اعداد بین پروتکلها/سختافزارها خطای گردکردن میسازد و دامنهٔ اعداد مالی از satoshi (۱۰⁻⁸) تا GDP ژاپن (~۵×۱۰¹⁴) گسترده است. عدد فقط هنگام نمایش/محاسبه parse شود.
انتخاب storage: نه کارایی، بلکه ثبات اثباتشده، ابزار مانیتورینگ/بازرسی و بازار کار DBA ← دیتابیس رابطهای ACID بر NoSQL ترجیح دارد. دو جدول: payment_event و payment_order (با وضعیت NOT_STARTED → EXECUTING → SUCCESS/FAILED و پرچمهای wallet_updated و ledger_updated). یک job زمانبندیشده سفارشهای معلق بیش از آستانه را هشدار میدهد.
دفترداری دوطرفه (double-entry)
هر تراکنش در دو حساب با مبلغ برابر ثبت میشود (یکی بدهکار، یکی بستانکار)؛ جمع همهٔ ثبتها باید صفر باشد. این اصل، ردیابی سرتاسری و سازگاری چرخهٔ پرداخت را تضمین میکند.
صفحهٔ پرداخت میزبانیشده
برای فرار از PCI DSS، اطلاعات کارت را صفحهٔ میزبانیشدهٔ PSP (ویجت/iframe یا SDK موبایل) مستقیماً جمع میکند و هرگز به سیستم ما نمیرسد.
گام ۳ — بررسی عمیق
یکپارچهسازی با PSP
اتصال مستقیم به بانک/شبکهٔ کارت فقط برای غولهاست. جریان صفحهٔ میزبانیشده: payment service با یک nonce (همان ID سفارش، برای ثبت exactly-once) پرداخت را نزد PSP ثبت میکند ← PSP یک token برمیگرداند که ذخیره میشود ← صفحهٔ PSP با token نمایش داده میشود و کارت را میگیرد ← پس از پرداخت، مرورگر به redirect URL میرود و PSP بهطور async از طریق webhook وضعیت نهایی را به ما اعلام میکند تا payment_order_status بهروزرسانی شود.
Reconciliation — آخرین خط دفاع
در ارتباط async هیچ تضمینی بر تحویل پیام نیست. هر شب PSP/بانک settlement file (موجودی + همهٔ تراکنشهای روز) میفرستد و سیستم reconciliation آن را با ledger مقایسه میکند؛ همچنین سازگاری داخلی ledger و wallet را میسنجد. رفع مغایرتها سه دسته است: قابل دستهبندی با اصلاح خودکار، قابل دستهبندی با اصلاح دستی تیم مالی (خودکارسازی نمیصرفد)، و غیرقابل دستهبندی (بررسی موردی).
تأخیرهای پردازش و ارتباط داخلی
پرداختها ممکن است ساعتها/روزها معلق بمانند (بازبینی انسانی ریسک، 3D Secure). PSP وضعیت pending برمیگرداند، خودش پیگیری میکند و با webhook (یا polling ما) خبر میدهد.
ارتباط داخلی: همگام (HTTP) در مقیاس بزرگ ضعف دارد — زنجیرهٔ وابستگی طولانی، ایزولهٔ خطای ضعیف، جفتشدگی و مقیاسپذیری سخت. ناهمگام دو الگو دارد: تکگیرنده (صف پیام مشترک؛ پیام پس از مصرف حذف میشود) و چندگیرنده (Kafka؛ همان رویداد را پرداخت، تحلیل، اعلان و billing جداگانه مصرف میکنند). برای سیستم پرداخت بزرگ با وابستگیهای شخصثالث فراوان، async انتخاب بهتری است.
مدیریت پرداخت ناموفق
- ثبت وضعیت پرداخت در جدول append-only تا در هر خطا بدانیم retry لازم است یا refund.
- Retry queue برای خطاهای گذرا؛ پس از عبور از آستانهٔ تلاش، پیام به dead letter queue میرود تا جداگانه بررسی شود؛ خطاهای غیرقابلتکرار (ورودی نامعتبر) مستقیم در دیتابیس ثبت میشوند.
تحویل exactly-once
جدیترین خطر: شارژ دوباره. exactly-once = at-least-once (با retry) + at-most-once (با idempotency).
- Retry: استراتژیها از فوری تا فاصلهٔ ثابت/افزایشی و exponential backoff (دو برابر شدن انتظار پس از هر شکست) و لغو. قاعدهٔ کلی: اگر مشکل شبکه زود حل نمیشود backoff نمایی؛ retry تهاجمی سرویس را overload میکند؛ کد خطا + هدر
Retry-Afterبدهید. - Idempotency: کلید یکتای تولیدشده در کلاینت (معمولاً UUID، در هدر HTTP). دو سناریوی پرداخت دوبل:
- دوبار کلیک روی «پرداخت»: کلید idempotency (مثلاً ID سبد خرید) تکراری است ← درخواست دوم وضعیت قبلی را برمیگرداند؛ درخواستهای همزمان با کلید یکسان 429 میگیرند. پیادهسازی با unique key constraint دیتابیس: درج موفق = درخواست جدید؛ شکست درج = تکراری.
- PSP موفق شده اما پاسخ به ما نرسیده و کاربر دوباره میزند: چون سفارش همان است، nonce و در نتیجه token یکسان است و PSP (که token را کلید idempotency خود میداند) پرداخت دوم را رد و وضعیت قبلی را برمیگرداند.
سازگاری و امنیت
چند سرویس stateful (payment، ledger، wallet، PSP) درگیرند. داخلی: پردازش exactly-once؛ با PSP: کلید idempotency یکسان در retry + reconciliation (به سیستم خارجی هم اعتماد مطلق نکنید). برای replication lag: یا خواندن/نوشتن فقط از primary، یا دیتابیسهای مبتنی بر اجماع (Paxos/Raft — مانند CockroachDB و YugabyteDB).
امنیت (خلاصه): HTTPS برای شنود، رمزنگاری/پایش برای دستکاری داده، certificate pinning برای MITM، replication چندناحیهای و snapshot برای از دست رفتن داده، rate limiting و firewall برای DDoS، tokenization بهجای شمارهٔ کارت واقعی برای سرقت کارت، PCI DSS برای انطباق، و AVS/CVV/تحلیل رفتار برای fraud.
نکات کلیدی فصل
- در پرداخت، سؤال مصاحبه throughput نیست؛ درستی در حضور خطا است.
- exactly-once = retry (at-least-once) + idempotency (at-most-once) — این فرمول را حفظ کنید.
- مبلغ پول را string نگه دارید؛ double در سیستم مالی بمب ساعتی است.
- ledger دوطرفه + reconciliation شبانه با settlement file = تور ایمنی نهایی هر مغایرتی.
- nonce/token دوگانهٔ idempotency بین شما و PSP است؛ کلید idempotency + unique constraint دوگانهٔ آن بین کلاینت و شما.
یادداشتهای مترجم
- PSP، nonce، webhook، idempotency، reconciliation و settlement file لاتین حفظ شدهاند.