” اگه بخوایم برای یه سایت سیستم اشتراک بسازیم، باید چه چیزهایی رو در نظر بگیریم؟ توی این مقاله از مدلهای مختلف اشتراک تا طراحی دیتابیس و منطق پرداخت و .. صحبت میکنیم. “
این روزها خیلی از وبسایتها برای ارائه خدماتشون از سیستم اشتراک استفاده میکنن. مثلاً یه سرویس هوش مصنوعی رو در نظر بگیریم که اجازه میده بهصورت رایگان از بعضی امکاناتش استفاده کنیم، اما برای دسترسی به قابلیتهای بیشتر باید اشتراک بخریم. یا وبسایتهایی که با خرید اشتراک، امکان استفاده از مجموعهای از محصولاتشون رو در اختیارمون قرار میدن.
اگه بخوایم همچین قابلیتی رو به سایت خودمون اضافه کنیم، احتمالاً اولین چیزی که به ذهنمون میرسه اینه که چند تا پلن تعریف کنیم، یه درگاه پرداخت به سایت وصل کنیم و بعد از پرداخت، دسترسی کاربر رو فعال کنیم.
اما وقتی بخوایم چنین سیستمی رو پیادهسازی کنیم، باید به چند تا موضوع دیگه هم فکر کنیم. مثلاً اشتراکها قراره بهصورت خودکار تمدید بشن یا کاربر باید هر بار خودش برای تمدید اقدام کنه؟ اگه کاربر وسط دوره اشتراکش تصمیم بگیره پلن حرفهایتری بخره، از چه زمانی باید به امکانات جدید دسترسی داشته باشه؟ یا اگه اشتراکش رو لغو کنه، دسترسیش همون لحظه قطع میشه یا تا پایان دوره اشتراکش باقی میمونه و..
از طرف دیگه، ممکنه بخوایم پلنهای مختلفی با امکانات و محدودیتهای متفاوت داشته باشیم؛
مثلاً یه پلن رایگان، یه پلن حرفهای و یه پلن سازمانی.
باید مشخص کنیم هر پلن چه امکاناتی داره و چطور میتونیم دسترسی کاربران رو بر اساس اشتراکشون مدیریت کنیم.
برای پیادهسازی این موارد، اول باید مشخص کنیم چه مدلهایی از اشتراک رو میخوایم پشتیبانی کنیم و هرکدوم چه قوانینی دارن. بعد میتونیم سراغ طراحی دیتابیس بریم و ببینیم اطلاعات پلنها، اشتراک کاربران و پرداختها رو چطور ذخیره کنیم تا مدیریت تمدید، لغو و تغییر پلن هم سادهتر بشه.
برای اینکه موضوع ملموستر باشه، فرض کنیم میخوایم چنین سیستمی رو برای یه سرویس آنلاین پیادهسازی کنیم.
در ادامه، همین مثال رو جلو میبریم و قدمبهقدم بخشهای مختلف سیستم رو بررسی میکنیم.
قبل از اینکه سراغ دیتابیس بریم، باید بدونیم قراره محصولمون رو چطور به کاربر ارائه بدیم،چون ساختار سیستمی که اشتراک ماهانه میفروشه، لزوماً با سیستمی که دسترسی یکباره یا مادامالعمر ارائه میده یکی نیست.
این مدل رو احتمالاً بیشتر از بقیه دیدیم. کاربر برای استفاده از خدمات، هزینه یک دوره مشخص رو پرداخت میکنه و بعد از پایان اون دوره باید اشتراکش رو تمدید کنه.
مثلاً فرض کنیم سه پلن داریم:
رایگان: دسترسی به امکانات پایه.
حرفهای (Pro): ماهانه ۳۰۰ هزار تومان.
حرفهای سالانه: یک میلیون و ۸۰۰ هزار تومان.
کاربری که اشتراک ماهانه میخره، تا پایان دورهای که هزینهش رو پرداخت کرده به امکانات پلن حرفهای دسترسی داره. بعد از پایان دوره هم باید مشخص کنیم که اشتراکش چطور تمدید میشه.
ممکنه تمدید بهصورت خودکار انجام بشه یا کاربر هر بار خودش برای خرید دوره بعد اقدام کنه.
این دو حالت رو باید از هم جدا کنیم، چون روی منطق پرداخت و مدیریت اشتراک تأثیر میذارن.
بعضی محصولات بهجای اشتراک دورهای، با یک بار پرداخت خریداری میشن. این مدل معمولاً به دو شکل ارائه میشه: دسترسی مادامالعمر (Lifetime Access) یا دسترسی محدود به یک محصول یا نسخه مشخص.
مثلاً فرض کنیم یه وبسایت مجموعهای از محصولات رو ارائه میده و کاربر با پرداخت یه مبلغ مشخص، برای همیشه به اون مجموعه دسترسی پیدا میکنه. یا یه محصول رو یک بار میخره و فقط به همون نسخهای که خریداری کرده دسترسی داره.
توی این مدل، برخلاف اشتراک ماهانه یا سالانه، پرداخت دوبارهای برای تمدید وجود نداره. بنابراین، باید منطق دسترسی رو طوری طراحی کنیم که خرید کاربر ثبت بشه و بر اساس نوع محصول، دسترسی لازم رو داشته باشه.
بعضی سرویسها علاوه بر زمان اشتراک، محدودیتهای دیگهای هم دارن.
مثلاً در یک سرویس هوش مصنوعی، پلن رایگان اجازه ارسال ۱۰ درخواست در روز رو میده، پلن حرفهای ۱۰۰ درخواست و پلن سازمانی ۱۰۰۰ درخواست.
اینجا باید مصرف کاربر رو هم ثبت کنیم و قبل از انجام هر درخواست، بررسی کنیم که به محدودیت استفاده از پلنش رسیده یا نه.
پس قبل از طراحی دیتابیس، بهتره این موارد رو مشخص کنیم. قرار نیست از همون ابتدا همه مدلها رو پشتیبانی کنیم؛ فقط باید بدونیم محصولمون به چه چیزهایی نیاز داره تا ساختار رو متناسب با همون نیازها طراحی کنیم.
حالا فرض کنیم تصمیم گرفتیم اشتراک ماهانه و سالانه داشته باشیم، امکان خرید یکباره هم در آینده اضافه بشه و بعضی پلنها محدودیت مصرف داشته باشن.
برای پیادهسازی این سیستم، چند مفهوم اصلی داریم که بهتره از هم جداشون کنیم.
پلن مشخص میکنه چه خدماتی ارائه میدیم، قیمت مشخص میکنه کاربر برای خرید اون خدمات چقدر باید پرداخت کنه. اشتراک اطلاعات خرید و وضعیت دسترسی کاربر رو نگه میداره و پرداخت مشخص میکنه چه اتفاقی برای تراکنش مالی افتاده.
اینها چهار مفهوم متفاوتن و بهتره از ابتدا اونها رو با هم یکی در نظر نگیریم.
بیایم جدولها رو یکییکی طراحی کنیم.
plans)اولین جدول، اطلاعات پلنهایی رو نگه میداره که برای فروش تعریف کردیم.
مثلاً پلن رایگان، حرفهای و سازمانی هر کدوم یک رکورد در این جدول دارن.
برای این جدول میتونیم فیلدهای زیر رو در نظر بگیریم:
id: شناسه پلن.
name: نام پلن.
slug: شناسه متنی پلن که در آدرسها یا برنامه استفاده میکنیم.
description: توضیحات پلن.
is_active: مشخص میکنه که پلن هنوز برای خرید قابل ارائه هست یا نه.
created_at و updated_at: زمان ایجاد و آخرین تغییر.
مثلاً اطلاعات یک پلن حرفهای میتونه به این شکل باشه:
{
"id": 2,
"name": "حرفهای",
"slug": "pro",
"description": "مناسب برای استفاده حرفهای",
"is_active": true
}اینجا فقط اطلاعات خود پلن رو نگه میداریم و هنوز کاری به قیمت و مدت اعتبارش نداریم.
چرا؟ چون ممکنه همین پلن رو هم بهصورت ماهانه بفروشیم و هم سالانه.
prices)حالا باید مشخص کنیم هر پلن با چه قیمت و شرایطی فروخته میشه.
برای همین یک جدول به اسم prices در نظر میگیریم که به جدول plans متصل میشه.
فیلدهای این جدول میتونن شامل موارد زیر باشن:
id: شناسه قیمت.
plan_id: شناسه پلنی که این قیمت بهش تعلق داره.
amount: مبلغ.
currency: واحد پول.
billing_type: نوع پرداخت؛ مثلاً دورهای یا یکباره.
billing_interval: بازه پرداخت، مثل ماه یا سال.
interval_count: تعداد واحدهای بازه.
is_active: مشخص میکنه که این قیمت هنوز برای خرید قابل استفاده هست یا نه.
فرض کنیم برای پلن حرفهای دو قیمت تعریف کردیم:
[
{
"id": 1,
"plan_id": 2,
"amount": 3000000,
"currency": "IRR",
"billing_type": "recurring",
"billing_interval": "month",
"interval_count": 1,
"is_active": true
},
{
"id": 2,
"plan_id": 2,
"amount": 18000000,
"currency": "IRR",
"billing_type": "recurring",
"billing_interval": "year",
"interval_count": 1,
"is_active": true
}
]تو این مثال، قیمت ماهانه رو ۳۰۰ هزار تومان و قیمت سالانه رو یک میلیون و ۸۰۰ هزار تومان در نظر گرفتیم. چون مبلغها رو با واحد ریال ذخیره میکنیم، مثلا مقدار amount برای قیمت ماهانه باید 3000000 یا سه میلیون ریال باشه و ..
از طرف دیگه، فیلد billing_type مشخص میکنه که پرداخت بهصورت دورهای انجام میشه یا یکباره. مثلاً برای محصولی که فقط یک بار فروخته میشه، میتونیم قیمت رو با نوع پرداخت one_time ثبت کنیم.
البته یکباره بودن پرداخت لزوماً به معنی دسترسی مادامالعمر نیست. ممکنه کاربر یک بار هزینه رو پرداخت کنه، اما فقط برای مدت مشخصی به محصول دسترسی داشته باشه. پس نوع پرداخت و مدت دسترسی دو موضوع جدا هستن و باید هر کدوم رو در جای خودش مدیریت کنیم.
یه نکته دیگه هم اینه که قیمتها ممکنه در آینده تغییر کنن. مثلاً فرض کنیم قیمت ماهانه پلن حرفهای از ۳۰۰ هزار تومان به ۴۰۰ هزار تومان افزایش پیدا کنه. بهتره قیمت قبلی رو تغییر ندیم؛ کافیه اون رو غیرفعال کنیم و یک رکورد جدید برای قیمت تازه بسازیم. اینجوری هم خریدهای قبلی با قیمت زمان خودشون قابل پیگیری میمونن و هم کاربران جدید میتونن با قیمت جدید خرید کنن.
subscriptions)تا اینجا پلنها و قیمتها رو تعریف کردیم. حالا باید اطلاعات کاربری رو نگه داریم که یکی از این قیمتها رو خریده.
برای این کار به جدول subscriptions نیاز داریم.
این جدول مشخص میکنه اشتراک متعلق به کدوم کاربره، چه پلنی خریده، دوره فعلیش از چه زمانی شروع شده و چه زمانی به پایان میرسه.
فیلدهای اصلی این جدول میتونن اینها باشن:
id: شناسه اشتراک.
user_id: شناسه کاربر.
price_id: شناسه قیمتی که کاربر خریداری کرده.
status: وضعیت اشتراک.
current_period_start: شروع دوره فعلی.
current_period_end: پایان دوره فعلی.
cancel_at_period_end: مشخص میکنه که اشتراک در پایان دوره لغو بشه یا نه.
canceled_at: زمان ثبت لغو.
created_at و updated_at: زمان ایجاد و آخرین تغییر.
فرض کنیم کاربری با شناسه ۴۲، اشتراک ماهانه حرفهای خریده:
{
"id": 15,
"user_id": 42,
"price_id": 1,
"status": "active",
"current_period_start": "2026-10-01",
"current_period_end": "2026-11-01",
"cancel_at_period_end": false,
"canceled_at": null
}یعنی کاربر ۴۲ یک اشتراک فعال داره که بر اساس قیمت شماره ۱ ایجاد شده و دوره فعلیش تا اول آبان اعتبار داره.
اینجا یک سؤال پیش میاد: چرا بهجای ذخیره plan_id، شناسه price_id رو نگه داشتیم؟
چون کاربر فقط یک پلن کلی رو نخریده؛ یک گزینه قیمتگذاری مشخص رو انتخاب کرده. با داشتن price_id میتونیم با روابطی که بعدا تعریف میکنیم بفهمیم خرید بر اساس کدوم قیمت و دوره انجام شده.
در صورت نیاز، میتونیم اطلاعات پلن رو از طریق همین ارتباط به دست بیاریم.
برای فیلد status باید چند وضعیت مشخص تعریف کنیم.
مثلاً:
incomplete: فرایند ایجاد اشتراک هنوز کامل نشده؛ مثلاً منتظر پرداخت هستیم.
trialing: کاربر در دوره آزمایشی قرار داره.
active: اشتراک فعاله.
past_due: موعد یا زمان پرداخت رسیده، اما پرداخت هنوز انجام نشده یا ناموفق بوده.
canceled: اشتراک لغو شده.
expired: اعتبار اشتراک به پایان رسیده.
این وضعیتها یک نمونه پیشنهادی هستن و باید متناسب با مدل محصول تعریف بشن. برای مثال، اگه اشتراک در پایان دوره لغو شده باشه، ممکنه تا پایان همون دوره همچنان دسترسی داشته باشه؛ پس نباید صرفاً با ثبت درخواست لغو، دسترسیش رو قطع کنیم.
در ادامه مقاله به این موضوع برمیگردیم :)
payments)حالا که پلنها و اشتراکها رو مشخص کردیم، به جدولی برای ثبت پرداختهای کاربران هم نیاز داریم. این جدول باید اطلاعات هر تراکنش رو ذخیره کنه تا بتونیم وضعیت پرداختها رو بررسی کنیم و سابقه خریدهای هر کاربر رو داشته باشیم.
مثلاً وقتی کاربری اشتراک حرفهای میخره، اطلاعات پرداختش در این جدول ثبت میشه. اگه ماه بعد اشتراکش رو تمدید کنه، پرداخت جدیدی ثبت میکنیم و اطلاعات پرداخت قبلی هم باقی میمونه.
فیلدهای اصلی جدول payments میتونن شامل این موارد باشن:
id: شناسه پرداخت.
user_id: شناسه کاربر.
subscription_id: شناسه اشتراک مرتبط، در صورت وجود.
amount: مبلغ پرداخت.
currency: واحد پول.
status: وضعیت پرداخت.
provider: نام سرویس پرداخت.
provider_reference: شناسه تراکنش در سرویس پرداخت.
paid_at: زمان پرداخت موفق.
created_at: زمان ایجاد رکورد.
مثلاً اطلاعات یک پرداخت موفق میتونه این شکلی باشه:
{
"id": 101,
"user_id": 42,
"subscription_id": 15,
"amount": 3000000,
"currency": "IRR",
"status": "succeeded",
"provider": "example_gateway",
"provider_reference": "TXN-845921",
"paid_at": "2026-10-01T10:30:00"
}در این مثال، کاربر ۴۲ مبلغ ۳ میلیون ریال، معادل ۳۰۰ هزار تومان، پرداخت کرده و تراکنش با موفقیت انجام شده.
فیلد provider_reference هم شناسهایه که از سرویس پرداخت دریافت میکنیم و برای پیگیری تراکنش به کار میاد.
وضعیتهای پرداخت هم باید جداگانه تعریف بشن. مثلاً pending برای در انتظار پرداخت، succeeded برای پرداخت موفق و failed برای پرداخت ناموفق.
بعداً اگه کاربر اشتراکش رو تمدید کنه، یک رکورد پرداخت جدید ایجاد میکنیم. در نتیجه سابقه پرداختهای قبلی از بین نمیره و میتونیم تمام تراکنشهای مربوط به یک اشتراک رو بررسی کنیم.
تا اینجا اطلاعات اشتراک کاربر، از جمله تاریخ شروع و پایان، رو در جدول subscriptions ذخیره کردیم. اما این جدول بهتنهایی نمیتونه سابقه تمام دورههایی رو که کاربر پشت سر گذاشته، نگه داره؛ چون با هر بار تمدید، تاریخ پایان اشتراک تغییر میکنه.
مثلاً فرض کنیم کاربری اشتراک ماهانه خریده و سه بار اون رو تمدید کرده. در جدول subscriptions میتونیم وضعیت فعلی اشتراک و تاریخ پایان دوره فعلی رو نگه داریم، اما اگه بخوایم بدونیم هر دوره دقیقاً از چه تاریخی شروع شده، چه زمانی تموم شده و چه مبلغی بابتش پرداخت شده، به اطلاعات جداگانهای نیاز داریم.
اینجاست که جدول subscription_periods به کارمون میاد. هر رکورد از این جدول، نماینده یک دوره از اشتراکه؛ یعنی با هر بار تمدید، یک رکورد جدید ایجاد میکنیم و اطلاعات دوره قبلی هم دستنخورده باقی میمونه.
فیلدهای این جدول میتونن شامل موارد زیر باشن:
id: شناسه دوره.
subscription_id: شناسه اشتراک مربوطه.
starts_at: زمان شروع دوره.
ends_at: زمان پایان دوره.
amount: مبلغ مربوط به دوره.
currency: واحد پول.
status: وضعیت دوره.
payment_id: شناسه پرداخت مرتبط، در صورت وجود.
مثلاً یک دوره از اشتراک میتونه به این شکل ثبت بشه:
{
"id": 201,
"subscription_id": 15,
"starts_at": "2026-10-01",
"ends_at": "2026-11-01",
"amount": 3000000,
"currency": "IRR",
"status": "completed",
"payment_id": 101
}این رکورد نشون میده که یکی از دورههای اشتراک شماره ۱۵، از اول مهر تا اول آبان ادامه داشته و پرداخت شماره ۱۰۱ به اون مربوط بوده.
در نتیجه، این دو جدول اطلاعات متفاوتی رو نگه میدارن: جدول subscriptions وضعیت کلی و فعلی اشتراک رو مشخص میکنه، در حالی که subscription_periods سابقه دورههای مختلف اون رو نگه میداره.
البته برای نسخه اولیه سیستم، اگه فقط به وضعیت فعلی اشتراک نیاز داریم و قرار نیست سوابق هر دوره رو جداگانه بررسی کنیم، میتونیم فعلاً از جدول subscriptions استفاده کنیم و بعداً جدول دورهها رو اضافه کنیم.
فرض کنیم پلن رایگان اجازه ساخت ۳ پروژه رو میده، پلن حرفهای اجازه ساخت ۱۰۰ پروژه و پلن سازمانی اجازه ساخت ۵۰۰ پروژه.
برای کنترل این محدودیتها باید اطلاعات مربوط به ویژگیهای پلن رو هم ذخیره کنیم.
اگه تعداد ویژگیها کمه و از ابتدا مشخصه، میتونیم فیلدهایی مثل max_projects رو مستقیماً در جدول پلن قرار بدیم.
اما اگه قراره ویژگیهای زیادی داشته باشیم که در پلنهای مختلف تغییر میکنن، میتونیم ساختار جداگانهای طراحی کنیم.
در این روش، یک جدول به اسم plan_features برای تعریف ویژگیها داریم.
مثلاً یک رکورد از این جدول:
{
"id": 1,
"key": "max_projects",
"name": "حداکثر تعداد پروژه",
"description": "تعداد پروژههایی که کاربر میتواند ایجاد کند"
}این رکورد فقط خود ویژگی رو تعریف میکنه. حالا باید مشخص کنیم مقدار این ویژگی برای هر پلن چقدره.
برای این کار از جدول plan_feature_values استفاده میکنیم:
{
"id": 1,
"plan_id": 2,
"feature_id": 1,
"value": "100"
}یعنی برای پلن شماره ۲، مقدار ویژگی شماره ۱ برابر با ۱۰۰ است.
در این مثال فرض کردیم پلن شماره ۲ همون پلن حرفهایه.
با همین روش میتونیم ویژگیهایی مثل سقف مصرف روزانه، تعداد اعضای تیم یا امکان استفاده از قابلیتهای خاص رو تعریف کنیم.
البته باید مشخص کنیم که هر ویژگی چه نوع مقداری داره. مثلاً max_projects یک عدد محسوب میشه، در حالی که مثلاً priority_support مشخص میکنه کاربر به پشتیبانی ویژه دسترسی داره یا نه.
این موضوع در زمان بررسی دسترسی اهمیت پیدا میکنه؛ چون نمیتونیم همه ویژگیها رو با یک منطق یکسان بررسی کنیم.
اگه پلنها محدودیت مصرف داشته باشن، باید میزان استفاده هر کاربر رو هم نگه داریم.
برای مثال، فرض کنیم کاربر پلن حرفهای داره و اجازه ارسال ۱۰۰ درخواست در روز رو داره. هر بار که یک درخواست ارسال میکنه، باید مشخص کنیم چقدر از سهمیه استفادهش باقی مونده.
یکی از روشها اینه که تمام رویدادهای مصرف رو در جدول usage_records ذخیره کنیم.
این جدول میتونه فیلدهای زیر رو داشته باشه:
id: شناسه رکورد.
user_id: شناسه کاربر.
subscription_id: اشتراک مرتبط، در صورت وجود.
feature_key: نوع مصرف.
quantity: مقدار مصرف.
created_at: زمان ثبت مصرف.
مثلاً:
{
"id": 501,
"user_id": 42,
"subscription_id": 15,
"feature_key": "ai_requests",
"quantity": 1,
"created_at": "2026-10-16T09:15:00"
}این رکورد نشون میده که کاربر ۴۲ یک درخواست به سرویس هوش مصنوعی ارسال کرده. با ثبت این اطلاعات میتونیم تعداد درخواستهای کاربر رو در یک بازه مشخص، مثلاً در طول یک روز، محاسبه کنیم.
فیلد subscription_id هم مشخص میکنه این مصرف مربوط به کدوم اشتراک بوده. این فیلد میتونه خالی باشه؛ مثلاً وقتی کاربر از امکانات رایگان سرویس استفاده میکنه و برای اون دسترسی، رکوردی در جدول اشتراکها نداریم.
برای بررسی میزان مصرف، میتونیم تعداد رکوردهای مربوط به درخواستهای کاربر در طول روز رو بشماریم. اما اگه سرویس تعداد زیادی درخواست داشته باشه، انجام این محاسبه از روی تمام رکوردها در هر بار درخواست ممکنه فشار زیادی به دیتابیس وارد کنه و باعث کند شدن پاسخگویی بشه.
در چنین شرایطی میتونیم تعداد درخواستهای هر کاربر رو جداگانه نگه داریم و با هر درخواست، اون رو بهروزرسانی کنیم. به این ترتیب، برای بررسی محدودیت مصرف لازم نیست هر بار تمام رکوردهای قبلی رو بشماریم.
در نهایت، باید مشخص کنیم که محدودیت مصرف چه زمانی از نو محاسبه میشه؛ مثلاً ابتدای هر روز. همچنین اگه چند درخواست همزمان ارسال بشن، باید مراقب باشیم که همگی با دیدن یک مقدار قدیمی از مصرف، از محدودیت پلن عبور نکنن.
بیایم ارتباط بین جدولهایی که ساختیم رو مرور کنیم.
هر پلن میتونه چند قیمت داشته باشه؛ بنابراین جدول prices از طریق plan_id به جدول plans متصل میشه.
هر کاربر هم میتونه در طول زمان چند اشتراک داشته باشه. پس جدول subscriptions با user_id به جدول کاربران و با price_id به جدول قیمتها متصل میشه.
هر اشتراک میتونه چند پرداخت و چند دوره داشته باشه. برای همین جدولهای payments و subscription_periods هم شناسه اشتراک رو نگه میدارن.
از طرف دیگه، ویژگیهای پلنها از طریق plan_feature_values به پلن و تعریف ویژگی متصل میشن. رکوردهای مصرف هم به کاربر و در صورت نیاز به اشتراک مربوطه وصل میشن.
این ارتباطها رو باید در دیتابیس با کلیدهای خارجی (Foreign Keys) تعریف کنیم تا مثلاً نتونیم اشتراکی بسازیم که به کاربر یا قیمتی اشاره کنه که وجود نداره.
یه نکته دیگه هم هست. فرض کنیم کاربری پلن حرفهای رو با قیمت ۳۰۰ هزار تومان خریده و چند ماه بعد قیمت پلن به ۴۰۰ هزار تومان افزایش پیدا میکنه.
نباید اطلاعات پرداختهای قبلی کاربر با این تغییر قیمت عوض بشن. به همین دلیل، مبلغ پرداخت و اطلاعات مالی دورههای قبلی باید به شکل مستقل ثبت بشن.
همچنین غیرفعال کردن یک پلن نباید باعث حذف اشتراکهای قبلی یا سوابق پرداخت کاربران بشه. غیرفعال کردن برای جلوگیری از خریدهای جدید کافیه؛ اطلاعات تاریخی باید همچنان قابل پیگیری بمونن.
فرض کنیم کاربری وارد صفحه پلنها شده و میخواد اشتراک حرفهای رو برای یک ماه بخره. از لحظهای که روی دکمه خرید کلیک میکنه تا زمانی که اشتراکش فعال بشه، چند تا کار باید بهترتیب انجام بشن.
اول، سرور بررسی میکنه که پلن و قیمت انتخابشده معتبر باشن و هنوز امکان خریدشون وجود داشته باشه. مبلغ رو هم از اطلاعاتی که مرورگر میفرسته قبول نمیکنیم؛ قیمت باید از اطلاعات خودمون در دیتابیس به دست بیاد. وگرنه کاربر میتونه درخواست رو تغییر بده و مبلغ کمتری پرداخت کنه.
بعد، اطلاعات پرداخت رو ثبت میکنیم و کاربر رو به درگاه میفرستیم. وقتی پرداخت انجام شد، باید نتیجه رو از طریق درگاه بررسی کنیم. صرفاً اینکه کاربر به صفحه پرداخت موفق برگرده کافی نیست؛ چون رسیدن به این صفحه لزوماً به معنی تأیید پرداخت نیست.
اگه پرداخت تأیید بشه، پرداخت رو در سیستم موفق ثبت میکنیم و اشتراک کاربر رو فعال میکنیم. اما اینجا یه مشکل احتمالی وجود داره: فرض کنیم پرداخت با موفقیت انجام شده، ولی درست وقتی میخوایم اشتراک رو فعال کنیم، خطایی در سیستم رخ میده. در این حالت، ممکنه پول از کاربر گرفته شده باشه، اما اشتراکش هنوز فعال نشده باشه.
برای جلوگیری از ناسازگاری اطلاعات، میتونیم از قابلیتی در دیتابیس به اسم تراکنش (Database Transaction) استفاده کنیم. این قابلیت اجازه میده چند تغییر مرتبط رو با هم انجام بدیم؛ یعنی اگه یکی از تغییرات با خطا مواجه بشه، تغییرات قبلی هم به حالت قبل برگردن. مثلاً نمیذاریم وضعیت پرداخت و اطلاعات اشتراک، نصفهنیمه بهروزرسانی بشن.
البته این قابلیت فقط روی تغییرات دیتابیس اثر داره و نمیتونه پولی رو که در درگاه پرداخت شده برگردونه. برای همین، باید حالتی رو هم در نظر بگیریم که نتیجه پرداخت مشخص نیست؛ مثلاً ارتباط با درگاه قطع شده و نمیدونیم پرداخت نهایی شده یا نه. در چنین شرایطی، سیستم باید بتونه وضعیت پرداخت رو دوباره بررسی کنه و اطلاعات خودش رو با نتیجه واقعی درگاه هماهنگ کنه.
فرض کنیم کاربر اشتراک ماهانه خریده و دوره فعلیش اول آبان به پایان میرسه.
اگه تمدید خودکار فعال باشه، سیستم باید برای دریافت هزینه دوره بعد اقدام کنه. اگه پرداخت موفق باشه، دوره جدید ثبت میشه و تاریخ پایان اشتراک بهروزرسانی میشه.
مثلاً دوره قبلی از اول مهر تا اول آبان بوده و دوره بعدی از اول آبان تا اول آذر خواهد بود.
اما اگه پرداخت ناموفق باشه، نباید اشتراک رو به شکل عادی تمدید کنیم. تو این شرایط میتونیم وضعیت اشتراک رو به past_due تغییر بدیم و بر اساس سیاست محصول، فرصت دوبارهای برای پرداخت در نظر بگیریم.
به این فرصت معمولاً مهلت پرداخت (Grace Period) گفته میشه. مثلاً ممکنه تصمیم بگیریم کاربر تا سه روز بعد از ناموفق بودن پرداخت همچنان به خدمات دسترسی داشته باشه.
تعداد دفعات تلاش مجدد و فاصله بین اونها هم باید مشخص باشه. این کار معمولاً با وظایف زمانبندیشده یا صف انجام میشه؛ نه اینکه منتظر بمونیم کاربر وارد سایت بشه.
اگه سرویس پرداخت خودش تمدید خودکار رو مدیریت میکنه، باید مشخص کنیم کدوم بخش مسئول تمدیده تا سیستم ما و سرویس پرداخت، یک عملیات رو دوبار انجام ندن.
لغو اشتراک همیشه به معنی قطع فوری دسترسی نیست.
فرض کنیم کاربر اشتراک ماهانه خریده و ۱۰ روز از دورهش باقی مونده. حالا تصمیم میگیره اشتراکش رو لغو کنه.
ممکنه فقط بخواد تمدید خودکار غیرفعال بشه و تا پایان همین دوره از خدمات استفاده کنه. در این حالت، مقدار cancel_at_period_end رو برابر true قرار میدیم، اما تاریخ پایان دوره رو تغییر نمیدیم.
حالت دیگه اینه که محصول ما اجازه لغو فوری بده. در این صورت باید وضعیت اشتراک و زمان لغو رو ثبت کنیم و مشخص کنیم دسترسی از چه زمانی قطع میشه.
پس موقع طراحی سیستم باید این دو رفتار رو از هم جدا کنیم:
لغو تمدید خودکار در پایان دوره.
لغو فوری اشتراک و قطع دسترسی، در صورت پشتیبانی محصول.
بازگشت وجه هم موضوع جداگانهایه. اگه کاربر اشتراکش رو لغو کرد، لزوماً نباید مبلغ پرداختشده رو بهش برگردونیم. این موضوع به شرایط فروش و سیاست محصول بستگی داره.
ممکنه کاربر بعد از مدتی بخواد پلن خودش رو تغییر بده.
مثلاً کاربری که اشتراک حرفهای داره، تصمیم میگیره به پلن سازمانی ارتقا پیدا کنه. یا برعکس، میخواد به پلن پایینتر برگرده.
در زمان ارتقا باید مشخص کنیم که تغییر از چه زمانی اعمال میشه و هزینه دوره باقیمونده چطور محاسبه میشه.
برای مثال، اگه کاربر وسط دوره پلن خودش رو ارتقا بده، میتونیم تغییر رو فوراً اعمال کنیم و هزینه رو متناسب با زمان باقیمونده محاسبه کنیم. به این روش تسهیم هزینه بر اساس زمان (Proration) گفته میشه.
روش دیگه اینه که تغییر پلن رو به ابتدای دوره بعد موکول کنیم.
برای تنزل پلن هم باید وضعیت فعلی کاربر رو در نظر بگیریم. فرض کنیم کاربر در پلن حرفهای اجازه ساخت ۱۰۰ پروژه رو داره، اما پلن پایینتر فقط ۱۰ پروژه رو پشتیبانی میکنه. اگه کاربر ۵۰ پروژه ساخته باشه، با تغییر پلن چه اتفاقی باید بیفته؟
ممکنه تغییر رو به ابتدای دوره بعد موکول کنیم یا دسترسی به امکانات اضافه رو محدود کنیم. تصمیم مناسب به رفتار محصول بستگی داره.
همچنین باید مشخص کنیم تغییر پلن باعث ایجاد اشتراک جدید میشه یا اطلاعات اشتراک فعلی رو بهروزرسانی میکنیم. در هر دو حالت، نباید سوابق پرداخت و دورههای قبلی رو از بین ببریم.
تا اینجا درباره خرید و مدیریت اشتراک صحبت کردیم. حالا باید مشخص کنیم که کاربر در هر لحظه اجازه استفاده از چه امکاناتی رو داره.
فرض کنیم کاربر وارد صفحهای میشه که فقط برای مشترکهای حرفهای قابل استفاده است. برنامه باید قبل از نمایش اطلاعات یا اجرای عملیات، دسترسی اون کاربر رو بررسی کنه.
برای این کار بهتره منطق بررسی دسترسی رو در یک بخش مشخص از برنامه قرار بدیم. مثلاً متدی مثل hasAccessTo() میتونه بررسی کنه که کاربر به یک قابلیت مشخص دسترسی داره یا نه.
این بررسی ممکنه شامل وضعیت اشتراک، تاریخ شروع و پایان، نوع دسترسی و محدودیتهای پلن باشه.
نباید فقط بیایم بررسی کنیم که کاربر یک رکورد اشتراک در دیتابیس داره یا نه؛ چون ممکنه اشتراکش منقضی شده باشه یا هنوز پرداخت اولیه انجام نشده باشه.
از طرف دیگه، کنترل دسترسی نباید فقط به رابط کاربری محدود بشه. مخفی کردن یک دکمه در صفحه کافی نیست؛ چون کاربر ممکنه مستقیماً درخواست مربوط به اون قابلیت رو به سرور ارسال کنه.
پس بررسی دسترسی باید هم در سمت سرور و هم در رابط کاربری انجام بشه؛ مثلاً اگه لاراول کار میکنید با استفاده از Middleware یا Policy میتونید اینکارو انجام بدید.
اگه کاربر یک محصول رو بهصورت یکباره خریده باشه، نباید برای بررسی دسترسی حتماً به وجود یک اشتراک فعال با تاریخ پایان مشخص وابسته باشیم.
در چنین شرایطی میتونیم مفهوم مجوز دسترسی (Entitlement) رو در نظر بگیریم. مجوز دسترسی مشخص میکنه کاربر اجازه استفاده از چه قابلیت یا محصولی رو داره.
مثلاً ممکنه کاربر از طریق اشتراک ماهانه به یک قابلیت دسترسی داشته باشه، اما کاربر دیگه همون قابلیت رو با خرید یکباره دریافت کرده باشه.
در این معماری، اشتراک تنها راه ایجاد دسترسی نیست؛ خرید یکباره یا حتی اعطای دسترسی توسط مدیر هم میتونه مجوزی برای استفاده از یک قابلیت ایجاد کنه.
این مدل برای محصولاتی که چند نوع روش فروش دارن، انعطاف بیشتری ایجاد میکنه.
فرض کنیم کاربری اشتراک حرفهای رو میخره و پرداختش هم با موفقیت انجام میشه. اما بهخاطر قطعی اینترنت، سرور ما متوجه نتیجه پرداخت نمیشه.
کاربر دوباره تلاش میکنه یا درگاه نتیجه پرداخت رو دوباره برای سرور میفرسته. حالا اگه سیستم بدون بررسی، این درخواست رو دوباره پردازش کنه، ممکنه اشتراک کاربر دو بار تمدید بشه.
برای جلوگیری از این مشکل، باید بتونیم تشخیص بدیم که یک پرداخت قبلاً پردازش شده یا نه. مثلاً شناسه تراکنش درگاه رو ذخیره میکنیم و قبل از ثبت نتیجه، بررسی میکنیم که قبلاً با این شناسه پرداختی ثبت شده یا نه. به این ترتیب، دریافت دوباره یک نتیجه نباید باعث تمدید دوباره اشتراک بشه. به این رفتار در برنامهنویسی ایدمپوتنسی (Idempotency) میگیم.
البته همیشه هم مشکل از درخواست تکراری نیست. گاهی پرداخت واقعاً انجام شده، اما سرور ما به هر دلیلی نتیجه رو دریافت نکرده. در این حالت، نباید صرفاً بهخاطر نبودن پاسخ، پرداخت رو ناموفق در نظر بگیریم. باید بتونیم وضعیت تراکنش رو دوباره از درگاه بپرسیم و بعد اطلاعات پرداخت و اشتراک رو بهروز کنیم.
از طرف دیگه، اگه کاربر پرداخت رو نیمهکاره رها کنه یا پرداختش ناموفق باشه، نباید اشتراکش فعال بشه. پس باید وضعیت هر پرداخت رو نگه داریم و فقط بعد از تأیید معتبر درگاه، فرایند فعالسازی اشتراک رو انجام بدیم.
بسته به نیاز محصول، ممکنه بخوایم کد تخفیف یا دوره آزمایشی هم داشته باشیم.
کد تخفیف میتونه درصدی یا مبلغ ثابت باشه. مثلاً کاربر با یک کد تخفیف، ۲۰ درصد از هزینه اشتراک رو کمتر پرداخت کنه.
برای مدیریت این قابلیت میتونیم جدول coupons رو داشته باشیم که اطلاعات کد تخفیف، مقدار تخفیف، تاریخ اعتبار و محدودیت استفاده رو نگه میداره.
مثلاً یک رکورد از این جدول میتونه اینطور باشه:
{
"id": 10,
"code": "AUTUMN20",
"discount_type": "percentage",
"discount_value": 20,
"max_redemptions": 100,
"is_active": true
}در این مثال، کد AUTUMN20 یک تخفیف ۲۰ درصدی داره و حداکثر ۱۰۰ بار قابل استفاده است.
اگه بخوایم بدونیم هر کاربر چه زمانی از کد تخفیف استفاده کرده، میتونیم جدول coupon_redemptions رو هم اضافه کنیم.
مثلاً:
{
"id": 31,
"coupon_id": 10,
"user_id": 42,
"payment_id": 101,
"discount_amount": 600000
}این رکورد مشخص میکنه که کاربر ۴۲ در پرداخت شماره ۱۰۱ از کد تخفیف شماره ۱۰ استفاده کرده و ۶۰۰ هزار ریال تخفیف گرفته.
برای دوره آزمایشی هم باید مشخص کنیم چه کسانی واجد شرایط هستن، مدت دوره چقدره و بعد از پایان اون چه اتفاقی میافته.
اگه محصول ما نیاز به صدور فاکتور داشته باشه، بهتره اطلاعات فاکتور رو هم از پرداخت جدا کنیم.
فاکتور (Invoice) مشخص میکنه بابت چه خدماتی چه مبلغی از مشتری درخواست شده؛ پرداخت (Payment) مشخص میکنه چه مبلغی واقعاً دریافت شده.
ممکنه فاکتور صادر شده باشه، اما پرداخت هنوز انجام نشده باشه. برای همین این دو مفهوم رو نباید یکی در نظر بگیریم.
در صورت نیاز میتونیم دو جدول invoices و invoice_items داشته باشیم.
برای مثال، یک فاکتور میتونه این اطلاعات رو داشته باشه:
{
"id": 7001,
"user_id": 42,
"number": "INV-7001",
"subtotal": 3000000,
"discount_amount": 0,
"total": 3000000,
"currency": "IRR",
"status": "paid"
}این فاکتور برای کاربر ۴۲ صادر شده و مبلغ نهایی اون ۳ میلیون ریاله.
حالا اگه بخوایم جزئیات خدمات داخل فاکتور رو نگه داریم، میتونیم از جدول invoice_items استفاده کنیم.
مثلاً:
{
"id": 1,
"invoice_id": 7001,
"description": "اشتراک ماهانه حرفهای",
"quantity": 1,
"unit_amount": 3000000,
"total": 3000000
}این رکورد مشخص میکنه که فاکتور شامل یک اشتراک ماهانه حرفهای بوده.
البته برای یک محصول ساده، ممکنه در نسخه اول نیازی به این دو جدول نداشته باشیم. بهتره امکانات مالی رو متناسب با نیاز واقعی محصول اضافه کنیم.
بعضی کارها در سیستم اشتراک باید بدون دخالت مستقیم کاربر انجام بشن.
مثلاً ارسال یادآوری قبل از پایان اشتراک، تلاش مجدد برای پرداخت ناموفق یا بررسی تراکنشهایی که وضعیت مشخصی ندارن.
برای این کارها میتونیم از زمانبندی وظایف (Scheduler) و صف (Queue) استفاده کنیم.
اما نباید فرض کنیم که یک وظیفه زمانبندیشده همیشه دقیقاً در لحظه موردنظر اجرا میشه. ممکنه اجرای اون با تأخیر مواجه بشه یا به دلیل خطای موقت انجام نشه.
برای همین، کنترل دسترسی نباید فقط به تغییر وضعیت اشتراک توسط یک وظیفه زمانبندیشده وابسته باشه. هنگام بررسی دسترسی باید تاریخ اعتبار هم بررسی بشه.
مثلاً اگه دوره اشتراک تموم شده، سیستم باید بتونه این موضوع رو تشخیص بده؛ حتی اگه وظیفه مربوط به بهروزرسانی وضعیت هنوز اجرا نشده باشه.
بعد از پیادهسازی سیستم، نباید فقط مسیر خرید موفق رو تست کنیم. خیلی از خطاهای مهم زمانی اتفاق میافتن که پرداخت یا وضعیت اشتراک مطابق انتظار پیش نمیره.
چند موردی که بهتره براشون تست بنویسیم:
پرداخت موفق باعث فعال شدن اشتراک میشه.
پرداخت ناموفق باعث فعال شدن دسترسی پولی نمیشه.
اشتراک منقضیشده اجازه استفاده از امکانات محدودشده رو نمیده.
پرداخت تکراری باعث تمدید چندباره اشتراک نمیشه.
تغییر قیمت پلن، اطلاعات پرداختهای قبلی رو تغییر نمیده.
کاربر نمیتونه اطلاعات اشتراک کاربران دیگه رو مشاهده یا تغییر بده.
محدودیت مصرف در درخواستهای همزمان بهدرستی اعمال میشه.
دسترسی یکباره یا مادامالعمر بهخاطر نداشتن تاریخ پایان اشتباه تشخیص داده نمیشه.
در صورت خطای موقت درگاه، امکان پیگیری وضعیت پرداخت وجود داره.
این تستها کمک میکنن مطمئن بشیم سیستم فقط در شرایط عادی درست کار نمیکنه، بلکه برای اتفاقات رایج و خطاهای احتمالی هم آماده است.
سیستم اشتراک فقط به خرید و تمدید محدود نمیشه. باید بتونیم وضعیت پرداختها رو مدیریت کنیم، اعتبار اشتراک رو بررسی کنیم، دسترسی کاربران رو کنترل کنیم و برای حالتهایی مثل لغو، تغییر پلن و پرداخت ناموفق هم رفتار مشخصی داشته باشیم.
برای شروع، بهتره مدل فروش محصول رو مشخص کنیم و بعد موجودیتهای اصلی مثل پلن، قیمت، اشتراک و پرداخت رو از هم جدا کنیم. در ادامه هم میتونیم بر اساس نیاز محصول، جدولهای مربوط به دورهها، ویژگیهای پلن، مصرف کاربران و سایر قابلیتها رو اضافه کنیم.
لازم نیست از همون ابتدا یک سیستم بزرگ و پیچیده بسازیم. اگه مسئولیت هر بخش مشخص باشه و رفتار سیستم رو در شرایط مختلف درست طراحی کنیم، میتونیم از یک نسخه ساده شروع کنیم و بهمرور قابلیتهای بیشتری بهش اضافه کنیم.
هدف نهایی اینه که کاربر بتونه بهراحتی خرید انجام بده و از خدمات استفاده کنه؛ در حالی که سیستم هم در تمام مراحل، از اولین پرداخت تا تمدید و پایان دسترسی، رفتار قابل پیشبینی و درستی داشته باشه.
خب، اینم از بررسی ایده ساخت سیستم اشتراک :)
اگه سؤال یا نظری دارید، توی کامنتها بنویسید.
تا مقاله بعدی، بدرود ❤️👋
دیدگاه و یا پرسش خود را برای ما ارسال کنید.
هنوز دیدگاه یا پرسشی ایجاد نشده است :/
تجربهها، دیدگاهها و نکات الهامبخشی که با شما به اشتراک میگذاریم.