دو سفارش با نام و سبد مشابه الزاماً یک خطا ندارند. ممکن است مشتری دو بار روی دکمه پرداخت کلیک کرده باشد، مرورگر request را پس از timeout تکرار کرده باشد، افزونه سفارشی دوبار سفارش ساخته باشد یا دو callback فقط عملیات پس از پرداخت را تکرار کرده باشند. اول مشخص کنید واقعاً دو رکورد سفارش ایجاد شده، دو پرداخت انجام شده یا فقط دو ایمیل و دو کاهش موجودی رخ داده است.
پاسخ سریع: هیچکدام از سفارشها را فعلاً حذف نکنید. زمان ایجاد با دقت ثانیه، اقلام، مشتری، session/cart hash، transaction ID، noteهای سفارش و وضعیت پنل درگاه را کنار هم بگذارید. سپس request ثبت سفارش و callbackها را در access log همان بازه پیدا کنید. راهحل باید در نقطه ایجاد تکرار اعمال شود، نه با پاککردن رکورد دوم.
سه سناریوی متفاوت را جدا کنید
| آنچه میبینید | احتمال اصلی | شاهد تعیینکننده |
|---|---|---|
| دو شماره سفارش مستقل | دو request یا اجرای دوباره منطق ساخت سفارش | زمان ایجاد، request ID و cart/session |
| یک سفارش و دو تراکنش بانکی | retry پرداخت یا token جدید | transaction ID و پنل درگاه |
| یک سفارش، دو ایمیل یا دو کاهش موجودی | hook/callback تکراری و نبود idempotency | order notes، jobها و log hook |
| دو سفارش با فاصله زیاد | اقدام واقعی مشتری یا بازیابی سبد | IP/session، device و timeline |
این تفکیک مهم است: قفلکردن دکمه Checkout ایجاد دو ایمیل از یک callback را حل نمیکند و deduplicate کردن ایمیل نیز جلوی دو برداشت مالی را نمیگیرد.
یک Timeline قابل اتکا بسازید
برای هر نمونه، زمان بارگذاری Checkout، کلیک، request ثبت سفارش، ایجاد order، دریافت token، redirect، callback، verification و تغییر وضعیت را ثبت کنید. ساعت مرورگر، وردپرس، دیتابیس و سرور ممکن است timezone متفاوت داشته باشند؛ همه را به یک مبنا تبدیل کنید. شماره کارت، cookie، secret و token کامل را در گزارش نگذارید.
Double Click و رابط کاربری
دکمه ثبت سفارش باید بعد از ارسال معتبر موقتاً غیرفعال و وضعیت روشن نمایش داده شود. خطای JavaScript یا سفارشیسازی قالب ممکن است این محافظ را حذف کند. در DevTools بررسی کنید با یک کلیک چند request ایجاد میشود. شبیهسازی را با ابزار خودکار محدود و روی staging انجام دهید؛ کلیک پیاپی روی پرداخت واقعی میتواند تراکنش مالی بسازد.
قفل رابط کاربری فقط یک لایه تجربه کاربری است. کاربر میتواند refresh کند، شبکه retry شود یا درخواست مستقیم برسد؛ بنابراین backend و درگاه نیز باید عملیات حساس را با کلید یکتا و وضعیت فعلی سفارش کنترل کنند.
Timeout و Retry مبهم
اگر پاسخ Checkout دیر برسد، مرورگر یا proxy ممکن است connection را قطع کند درحالیکه PHP سفارش را ساخته است. تلاش بعدی یک سفارش تازه میسازد. در access log، statusهایی مثل قطع ارتباط، مدت upstream و دو POST نزدیک را بررسی کنید. افزایش timeout بدون یافتن Query، API یا hook کند فقط بازه انتظار را بلندتر میکند.
در ارتباط با درگاه، timeout به معنی رد قطعی درخواست نیست. قبل از ساخت token جدید باید در صورت پشتیبانی، وضعیت درخواست قبلی با شناسه یکتا inquiry شود. retry کور میتواند دو تلاش پرداخت معتبر برای یک خرید ایجاد کند.
Callback و Webhook چند بار میرسند
درگاه ممکن است webhook را تا دریافت پاسخ موفق تکرار کند و کاربر نیز صفحه بازگشت را refresh کند. handler باید callback یکسان را بیخطر مدیریت کند: transaction ID را با سفارش و مبلغ تطبیق دهد و اگر پرداخت قبلاً ثبت شده، موجودی، ایمیل و fulfillment را دوباره اجرا نکند. حذف request تکراری در WAF جای idempotency برنامه را نمیگیرد.
اگر دو callback دیده میشود اما دو سفارش پیش از انتقال به درگاه ساخته شدهاند، callback علت ساخت رکورد دوم نیست. ترتیب timestampها جلوی چنین نتیجهگیری اشتباهی را میگیرد. راهنمای تطبیق پرداخت و وضعیت سفارش برای اختلاف مالی مفید است.
Hook سفارشی و افزونههای Checkout
افزونه ثبت سفارش سریع، رزرو، اشتراک، CRM یا کد سفارشی ممکن است هم در hook عمومی و هم در callback درگاه منطق مشابه اجرا کند. stack trace، نام callback و order notes را بررسی کنید. جستوجوی کد باید بر اساس مالک عملیات باشد؛ حذف یک hook بدون شناخت اولویت و مسیر جایگزین ممکن است سفارش را ناقص کند.
Action Scheduler و Jobهای تکراری
ایمیل، webhook، sync و بعضی پردازشهای سفارش در صف اجرا میشوند. چند job لزوماً چند سفارش نیست. hook، args غیرحساس، زمان schedule، attempt و exception را مقایسه کنید. پاککردن صف، تاریخچه علت را از بین میبرد و ممکن است عملیات تجاری لازم را حذف کند. ابتدا producer تکراری یا retry شکستخورده را اصلاح کنید.
Cache، Session و چند Node
Checkout و پاسخهای شخصی نباید page cache عمومی شوند. session ناپایدار ممکن است تلاش دوم را خرید تازه تلقی کند. در معماری چند node، نسخه کد، session store، clock و کلیدهای مشترک باید سازگار باشند. اگر تکرار فقط روی یک node دیده میشود، شناسه داخلی node را در log ثبت و توزیع requestها را مقایسه کنید.
فروشگاه Headless، اپ موبایل و API
اگر سفارش از اپ، رابط Headless یا integration خارجی ساخته میشود، timeout سمت client نباید بدون کلید idempotency به ایجاد دوباره منجر شود. شناسه تلاش باید پیش از ارسال ساخته و در retry همان درخواست حفظ شود. بررسی کنید client بعد از قطع شبکه ابتدا نتیجه درخواست قبلی را query میکند یا فوراً POST تازه میفرستد.
در لاگ API، شناسه client و request را ثبت کنید اما access token و داده شخصی را نه. پاسخ موفقی که به client نرسیده، با request ناموفق یکسان نیست. تست را با قطع کنترلشده پاسخ روی staging انجام دهید و مطمئن شوید یک payload یکسان بیش از یک order نمیسازد.
تمدید اشتراک و سفارش والد/فرزند
در فروشگاه دارای subscription، pre-order یا split shipment ممکن است چند سفارش مرتبط مطابق طراحی ساخته شوند. رابطه parent، نوع سفارش، renewal metadata و زمانبندی را پیش از برچسب «تکراری» بررسی کنید. لغو سفارش renewal واقعی میتواند دسترسی یا تمدید مشتری را قطع کند. معیار تشخیص باید نوع workflow و شناسه تراکنش باشد، نه فقط یکسان بودن اقلام.
آیا شماره سفارش تکراری است یا فقط نمایش آن؟
دو رکورد دیتابیس معمولاً شناسه داخلی جدا دارند، حتی اگر افزونه شمارهگذاری یک شماره نمایشی مشابه تولید کند. شناسه داخلی، order key و transaction ID را با ORM/API ووکامرس بررسی کنید. برای اصلاح sequence یا metadata مستقیماً SQL اجرا نکنید؛ گزارش حسابداری، لینک پرداخت و integrationها ممکن است به همان داده وابسته باشند.
روند تشخیص کمریسک
- نمونهها را حفظ و اثر مالی/موجودی را متوقف یا علامتگذاری کنید.
- تعداد order، payment و side effect را جدا بشمارید.
- timeline مشترک از مرورگر، وردپرس، وبسرور و درگاه بسازید.
- روی staging یک کلیک، دو کلیک، timeout و refresh را کنترلشده تست کنید.
- producer تکرار را در UI، API، hook، job یا callback تعیین کنید.
- محافظ idempotency را در backend و سپس UX دکمه را اصلاح کنید.
- callback تکراری، retry و rollback را با یک سفارش آزمایشی بیازمایید.
با سفارشهای تکراری موجود چه کنیم؟
قبل از لغو یا refund، هر سفارش را با تراکنش رسمی تطبیق دهید. اگر فقط یک پرداخت وجود دارد، مشخص کنید کدام سفارش باید مسیر fulfillment را ادامه دهد. اگر دو پرداخت قطعی است، سیاست refund و وضعیت settlement را دنبال کنید و شناسه اقدام را در note داخلی ثبت کنید. حذف سفارش برای تمیز شدن فهرست، audit و رسیدگی مشتری را دشوار میکند.
پیشگیری و مانیتورینگ
- هشدار برای سفارشهای مشابه در بازه کوتاه، بدون لغو خودکار
- ثبت request ID و transaction ID غیرحساس
- اندازهگیری زمان POST ثبت سفارش و نرخ timeout
- تست callback تکراری در هر release درگاه
- مالک مشخص برای hookهای fulfillment و موجودی
- مانیتور jobهای failed و retry غیرعادی
اشتباههای رایج
- حذف فوری سفارش دوم و از بین بردن شواهد
- نسبت دادن همه موارد به دوبار کلیک
- پرداخت دوباره برای بازتولید روی production
- خاموش کردن callback یا WAF
- افزایش timeout بدون یافتن کندی
- اجرای دوباره hook موجودی و ایمیل
- refund همزمان از پنل و ووکامرس
چه زمانی کمک تخصصی لازم است؟
اگر تکرار با کسر وجه، کاهش دوباره موجودی یا چند callback همراه است، آزمونوخطا روی production ریسک مالی دارد. پشتیبانی فروشگاه ووکامرس میتواند request، سفارش، transaction، job و callback را در یک timeline تطبیق دهد و محافظ idempotency را در نقطه درست طراحی کند.
پرسشهای متداول
آیا غیرفعال کردن دکمه بعد از کلیک کافی است؟
خیر؛ UX بهتر میشود اما retry شبکه، refresh و callback هنوز باید در backend بیخطر باشند.
دو ایمیل سفارش یعنی دو سفارش ساخته شده؟
نه؛ ابتدا شماره و شناسه داخلی سفارشها را بررسی کنید. ممکن است فقط notification دوبار اجرا شده باشد.
میتوان سفارش دوم را خودکار لغو کرد؟
فقط با معیار مالی و تجاری دقیق؛ شباهت نام و مبلغ برای لغو خودکار کافی نیست.
چرا مشکل فقط هنگام کندی درگاه رخ میدهد؟
timeout و تلاش مجدد محتمل است. درگاه و retry را با شناسه تراکنش بررسی کنید.