# داکیومنت فنی پیادهسازی سامانه گردونه شانس و قرعهکشی
**نسخه سند:** 2.0 (Implementation Blueprint)
**بر اساس:** Lucky Wheel Lottery Spec v1.0
**نوع سند:** Technical Architecture & Implementation Guide
**Stack:** NestJS + Prisma + PostgreSQL (Backend) | Vue 3 + Vite + Pinia + Ant Design Vue (Admin) | Vue 3 + Vite + Pinia (Client) | BullMQ + Redis (Queue) | raygansms.com (SMS Provider)
---
## فهرست مطالب
0. مقدمه
1. Tech Stack نهایی
2. معماری کلی و ساختار Monorepo
3. مدل داده — Prisma Schema کامل
4. ساختار پروژه Backend (NestJS)
5. ماژول Authentication (OTP)
6. ماژول Campaign
7. ماژول Prize
8. ماژول Chance و Chance Ledger
9. Spin Engine — الگوریتم اجرایی و کنترل Concurrency
10. Idempotency
11. ماژول SMS (raygansms.com) + صف BullMQ
12. ماژول Winner / User Prizes / کد جایزه
13. ماژول File Storage (Local، قابل تعویض به S3)
14. مستندات کامل API — کاربر
15. مستندات کامل API — ادمین
16. ساختار پروژه Admin Panel (Vue 3 + Ant Design Vue + Pinia)
17. ساختار پروژه User Client (Vue 3 + Wheel Component)
18. پنج طرح گردونه (Wheel Designs) قابل انتخاب توسط ادمین
19. Security و Anti-Fraud
20. Rate Limiting
21. Environment Variables
22. Docker / Deployment
23. استراتژی تست و Mapping با Acceptance Criteria
24. جمعبندی و مسیر توسعه آینده
---
## 0. مقدمه
این سند، پیادهسازی فنی مشخصات کسبوکاری موجود در `Lucky_Wheel_Lottery_Spec_v1.0` را با Stack مشخصشده زیر تعریف میکند:
| لایه | تکنولوژی |
|---|---|
| Backend Framework | NestJS (Node.js, TypeScript) |
| ORM | Prisma |
| Database | PostgreSQL |
| Admin Panel Frontend | Vue 3 + Vite + Pinia + Ant Design Vue |
| User Client Frontend | Vue 3 + Vite + Pinia |
| SMS Provider | raygansms.com |
| Queue / Job System | BullMQ + Redis |
| File Storage | Local Disk در فاز فعلی (طراحیشده برای مهاجرت بدون تغییر Business Logic به S3-Compatible مثل لیارا/آروان در آینده) |
| Auth کاربر | OTP + JWT |
| Auth ادمین | Email/Password + JWT (Access + Refresh) |
اصول معماری بدون تغییر نسبت به سند اصلی حفظ میشوند:
- نتیجه Spin همیشه در Backend تعیین میشود.
- عملیات Spin بهصورت Atomic و داخل Database Transaction اجرا میشود.
- تمام Endpointهای حساس نیازمند Authentication هستند.
- تمام تغییرات Chance و اقدامات ادمین Audit میشوند.
---
## 1. Tech Stack نهایی
### Backend
- **Runtime:** Node.js 20 LTS
- **Framework:** NestJS 10.x (Modular Architecture, Dependency Injection)
- **ORM:** Prisma 5.x
- **Database:** PostgreSQL 15+
- **Queue:** BullMQ روی Redis 7+ (برای SMS، و قابل توسعه برای Jobهای آینده مثل گزارشهای سنگین)
- **Validation:** `class-validator` + `class-transformer` (DTO-based)
- **Auth:** `@nestjs/jwt` + `@nestjs/passport` (Strategy جدا برای User و Admin)
- **Rate Limiting:** `@nestjs/throttler`
- **API Docs:** `@nestjs/swagger` (Swagger UI برای مستندسازی زنده API)
- **Logger:** `pino` (nestjs-pino) برای Structured Logging
- **Scheduler:** `@nestjs/schedule` (برای Cron Jobهایی مانند انقضای جوایز)
### Admin Panel
- Vue 3 (Composition API + `
```
### ۱۷.۲ رفتار Spin در Frontend (طبق بخش ۱۴ سند اصلی، بدون تغییر)
1. لمس دکمه «بچرخون» → Disable فوری دکمه.
2. نمایش Loading کوتاه (اسپینر کوچک روی دکمه، نه تمامصفحه).
3. تولید/استفاده از `idempotencyKey` موجود در Store و ارسال `POST /spin`.
4. دریافت پاسخ شامل `prize.segment`.
5. اجرای انیمیشن: چند دور کامل (مثلاً ۵ دور) + توقف دقیق روی زاویه مرکز Segment هدف (محاسبه با `360/segmentsCount * targetIndex + offset تصادفی کوچک داخل همان Segment برای طبیعیتر شدن`).
6. تا پایان انیمیشن (`transitionend` یا تایمر معادل)، دکمه غیرفعال میماند.
7. نمایش Modal نتیجه (`ResultModal.vue`) بعد از پایان انیمیشن، نه همزمان با دریافت پاسخ API.
8. بعد از بستن Modal: بهروزرسانی `remainingChances` در Store؛ اگر باقی مانده، CTA «یک شانس دیگه دارم»، در غیر این صورت «مشاهده جوایز من».
### ۱۷.۳ مدیریت خطای شبکه و Idempotency در Frontend
```ts
// spin.store.ts
async function spin() {
if (isSpinning.value) return; // جلوگیری از دابلکلیک
isSpinning.value = true;
const key = pendingIdempotencyKey.value ?? crypto.randomUUID();
pendingIdempotencyKey.value = key; // Persist در Store (و اختیاری sessionStorage) تا رفرش صفحه هم آن را نبازد
try {
const { data } = await spinApi.spin(campaignId.value, { idempotencyKey: key });
lastResult.value = data;
pendingIdempotencyKey.value = null; // فقط بعد از موفقیت قطعی پاک شود
} catch (err) {
if (isNetworkError(err)) {
// کلید را نگه میداریم؛ کاربر با دکمه "بررسی نتیجه" یا Retry خودکار همان Request را دوباره میفرستد
showNetworkErrorState();
} else {
pendingIdempotencyKey.value = null; // خطای قطعی (مثلا NO_CHANCE_LEFT) → کلید جدید برای تلاش بعدی
handleBusinessError(err);
}
} finally {
isSpinning.value = false;
}
}
```
این الگو دقیقاً پیادهسازی قانون بخش ۲۹ سند اصلی است: «در خطای شبکه، کاربر نباید بدون مشخص شدن وضعیت Request قبلی، Spin جدید ایجاد کند.»
---
## 18. پنج طرح گردونه (Wheel Designs) قابل انتخاب توسط ادمین
هنگام ساخت یا ویرایش کمپین، ادمین از یک `WheelDesignPicker` (Thumbnail هر طرح + پیشنمایش زنده) یکی از پنج طرح زیر را انتخاب میکند؛ مقدار در `campaigns.wheel_design` ذخیره و همان طرح دقیقاً برای همه کاربران آن کمپین نمایش داده میشود. رنگ Segment از `prizes.segment_color` میآید؛ هر طرح فقط چارچوب بصری (Frame، Pointer، Hub، تایپوگرافی، افکتها، پسزمینه) را تعیین میکند.
### طرح ۱ — Classic Casino (کلاسیک کازینویی)
- **حسوحال:** لوکس سنتی، اعتمادساز، مناسب کمپینهای عمومی و فروشگاهی.
- **پالت:** پسزمینه قرمز تیره/طلایی، حاشیه گردونه با حبابهای نور طلایی چشمکزن (LED Bulb Ring).
- **Pointer:** فلش سهبعدی طلایی رو به پایین.
- **Hub مرکزی:** دکمه دایرهای طلایی با متن «SPIN» یا `cta_text` کمپین.
- **تایپوگرافی:** فونت ضخیم Serif برای اعداد/جوایز.
- **افکت برد:** Confetti طلایی + صدای سکه (اختیاری).
- **بهترین کاربرد:** کمپینهای فروشگاهی، جشنوارههای خرید.
### طرح ۲ — Neon Cyber (نئون سایبری)
- **حسوحال:** مدرن، جوانپسند، تکنولوژیک؛ مناسب برندهای دیجیتال، اپلیکیشن، بازی.
- **پالت:** پسزمینه مشکی/بنفش تیره، خطوط Neon Glow (فیروزهای/صورتی) دور گردونه و مرزهای Segment.
- **Pointer:** مثلث نئونی با افکت Glow پالسی.
- **Hub مرکزی:** دایره شیشهای (Glassmorphism) با افکت Pulse هنگام Spin.
- **تایپوگرافی:** فونت Geometric Sans با افکت Glow روی متن برنده.
- **افکت برد:** انفجار ذرات نئونی + لرزش صفحه سبک (Screen Shake).
- **بهترین کاربرد:** اپلیکیشنهای موبایل، گیمینگ، کمپینهای Tech.
### طرح ۳ — Minimal Flat (مینیمال تخت)
- **حسوحال:** تمیز، حرفهای، سریعبارگذاری؛ مناسب برندهایی که هویت بصری ساده و مدرن دارند (فینتک، B2B، خدماتی).
- **پالت:** پسزمینه سفید/خاکستری روشن، Segmentها با رنگهای Flat تعریفشده در تنظیمات جایزه، بدون Gradient/سایه سنگین.
- **Pointer:** مثلث ساده تکرنگ (رنگ اصلی برند از `settings.primaryColor`).
- **Hub مرکزی:** دایره تخت با لوگوی کمپین.
- **تایپوگرافی:** فونت Sans-Serif ظریف، Line-height باز.
- **افکت برد:** Fade + Scale ساده بدون افکت گرافیکی سنگین (Performance بالا در موبایلهای ضعیف).
- **بهترین کاربرد:** کمپینهای Corporate، اپلیکیشنهای بانکی/فینتک، برندهایی با اولویت سرعت بارگذاری.
### طرح ۴ — Luxury Gold (لوکس طلایی/پرمیوم)
- **حسوحال:** خاص، گرانقیمت، VIP؛ مناسب کمپینهای جواهرات، خودرو، جوایز بزرگ.
- **پالت:** پسزمینه مشکی مات با بافت مرمر/طلایی ظریف، Segmentهای مشکی/طلایی متناوب با خطوط باریک طلایی جداکننده.
- **Pointer:** الماس یا تاج کوچک طلایی.
- **Hub مرکزی:** دایره طلایی با افکت درخشش آهسته (Shimmer Animation مداوم، نه فقط هنگام Spin).
- **تایپوگرافی:** فونت Serif ظریف (شبیه برندهای لوکس)، Letter-spacing باز.
- **افکت برد:** درخشش نور از مرکز به بیرون + صدای ملایم (اختیاری).
- **بهترین کاربرد:** کمپینهای VIP، جوایز باارزش بالا، برندهای پرمیوم.
### طرح ۵ — Playful Cartoon (کارتونی و بازیگوش)
- **حسوحال:** شاد، رنگارنگ، خانوادهمحور؛ مناسب کمپینهای کودک، فستفود، سرگرمی، تخفیفهای عمومی.
- **پالت:** رنگهای اشباعشده و شاد (نارنجی، فیروزهای، زرد، صورتی)، Segmentها با الگوی نقطهای/موجی کارتونی.
- **Pointer:** شخصیت کوچک انیمیشنی (مثلاً دست اشارهگر یا موشک) با بونس ساده (Bounce) هنگام توقف.
- **Hub مرکزی:** دکمه بزرگ گرد با افکت Squish (فشردهشدن) هنگام لمس.
- **تایپوگرافی:** فونت گرد و ضخیم (Rounded/Bubbly).
- **افکت برد:** بادکنک/کانفتی رنگی + انیمیشن پرش شخصیت.
- **بهترین کاربرد:** کمپینهای فستفود، سرگرمی، مناسبتی (تولد، اعیاد)، مخاطب خانواده و کودک.
### پیادهسازی فنی مشترک همه طرحها
- همه پنج کامپوننت (`ClassicCasinoWheel.vue`, `NeonCyberWheel.vue`, ...) یک **Contract یکسان Props/Emits** دارند تا `WheelView.vue` بدون شرطگذاری اضافه، هرکدام را Render کند:
```ts
interface WheelDesignProps {
segments: { id: string; title: string; image?: string; color: string }[];
targetSegmentIndex: number | null; // از پاسخ Backend
spinning: boolean;
}
// emits: ['spin-finished']
```
- محاسبه زاویه دقیق توقف (منطق ریاضی چرخش) در `WheelBase.vue` مشترک نگه داشته میشود و بهصورت Composable (`useWheelRotation()`) در تمام ۵ طرح Import میشود؛ فقط SVG/CSS/افکت هر طرح متفاوت است. این کار از تکرار منطق حساس (که مستقیماً روی صحت نمایش نتیجه Backend اثر دارد) در ۵ فایل جلوگیری میکند.
- هر طرح دارای نسخه *Reduced Motion* (برای کاربرانی که `prefers-reduced-motion` را در مرورگر فعال کردهاند) است: افکتهای پرهزینه (Glow پیوسته، Shimmer، ذرات) غیرفعال و فقط چرخش اصلی نمایش داده میشود.
- تمام ۵ طرح باید در viewport موبایل (حداقل عرض ۳۲۰px) بدون Horizontal Scroll و بدون افت کیفیت رندر شوند (الزام بخش ۳۱ سند اصلی)؛ توصیه میشود گردونه بهصورت SVG با `viewBox` نسبی (نه اندازه ثابت پیکسلی) پیادهسازی شود.
---
## 19. Security و Anti-Fraud
| کنترل | پیادهسازی فنی |
|---|---|
| OTP Verification | هششده در دیتابیس، TTL ۲ دقیقه، محدودیت تلاش (بخش ۵) |
| Authentication Token | JWT جدا برای User و Admin با `type` claim متفاوت؛ Guard اختصاصی هرکدام |
| Server-Side Spin | تمام منطق در `SpinEngineService`؛ هیچ مقدار Prize/Winner از Body ورودی خوانده نمیشود |
| Idempotency | `idempotency_key` یکتا در دیتابیس + بررسی Application-level (بخش ۹، ۱۰) |
| IP / User-Agent Logging | ثبت روی هر `Spin` از `req.ip` (پشت Nginx با `X-Forwarded-For` صحیح تنظیمشده) و `req.headers['user-agent']` |
| محدودیت درخواست (Rate Limiting) | `@nestjs/throttler` سراسری + Throttle اختصاصی روی `/spin` و `/send-otp` (بخش ۲۰) |
| جلوگیری از Concurrent Spin | `SELECT ... FOR UPDATE` روی `user_campaigns` (بخش ۹.۳) — عملاً هر کاربر در هر لحظه فقط یک Spin در حال پردازش دارد |
| Transaction برای Prize Inventory | `SELECT ... FOR UPDATE` روی `prizes` + Prisma Interactive Transaction (بخش ۹) |
| Admin Audit Log | Interceptor سراسری (`AuditInterceptor`) روی تمام Routeهای `PATCH/POST/DELETE` پنل ادمین که مقدار قبل/بعد را در `admin_logs` ثبت میکند |
| عدم افشای Weight به کلاینت | Endpoint عمومی گردونه (`/campaigns/:slug/wheel`) فیلد `weight` را Strip میکند (DTO جدا از Entity، بخش ۱۴) |
| Helmet / CORS | `helmet()` فعال، CORS محدود به Origin دقیق دو Frontend (Admin/Client) در Production |
| SQL Injection | استفاده انحصاری از Prisma Client یا `$queryRaw` با Parameterized Query (Tagged Template)؛ هرگز String Concatenation |
| XSS | Escape خودکار Vue در Template؛ عدم استفاده از `v-html` برای محتوای واردشده توسط کاربر/ادمین (مثلاً `description` کمپین با Sanitize مثل `DOMPurify` در صورت نیاز به HTML محدود) |
---
## 20. Rate Limiting
پیکربندی `@nestjs/throttler` بهصورت چندلایه:
```ts
ThrottlerModule.forRoot([
{ name: 'default', ttl: 60000, limit: 60 }, // سراسری: ۶۰ درخواست در دقیقه هر IP
]);
```
Override اختصاصی روی Endpointهای حساس:
| Endpoint | محدودیت |
|---|---|
| `POST /auth/send-otp` | ۱ درخواست در ۶۰ ثانیه هر شماره موبایل + ۵ درخواست در ساعت هر IP |
| `POST /auth/verify-otp` | ۵ تلاش در ۵ دقیقه هر شماره موبایل |
| `POST /campaigns/:id/spin` | ۱۰ درخواست در دقیقه هر کاربر (فراتر از نیاز واقعی UX ولی سد در برابر Script/Bot) |
| `POST /admin/auth/login` | ۵ تلاش در ۱۵ دقیقه هر IP (جلوگیری از Brute-force پنل مدیریت) |
---
## 21. Environment Variables
```env
# App
NODE_ENV=production
PORT=3000
# Database
DATABASE_URL=postgresql://user:pass@localhost:5432/lucky_wheel
# JWT
JWT_USER_SECRET=change-me
JWT_USER_EXPIRES_IN=7d
JWT_ADMIN_ACCESS_SECRET=change-me
JWT_ADMIN_ACCESS_EXPIRES_IN=1h
JWT_ADMIN_REFRESH_SECRET=change-me
JWT_ADMIN_REFRESH_EXPIRES_IN=7d
# Redis / BullMQ
REDIS_HOST=localhost
REDIS_PORT=6379
REDIS_PASSWORD=
# SMS - raygansms.com
RAYGAN_SMS_USERNAME=xxxx
RAYGAN_SMS_PASSWORD=xxxx
RAYGAN_SMS_LINE_NUMBER=xxxx
RAYGAN_SMS_API_BASE_URL=https://api.raygansms.com
# File Storage (Local - فاز فعلی)
STORAGE_DRIVER=local
STORAGE_LOCAL_ROOT=./uploads
STORAGE_PUBLIC_URL=https://api.example.com/uploads
# CORS
CORS_ADMIN_ORIGIN=https://admin.example.com
CORS_CLIENT_ORIGIN=https://wheel.example.com
```
> کلیدهای واقعی سرویس راهگان پیامک (نام کاربری/رمز/شماره خط) باید از پنل `raygansms.com` دریافت و هرگز در Git Commit نشوند (`.env` در `.gitignore`).
---
## 22. Docker / Deployment
```yaml
# docker-compose.yml (Dev)
services:
postgres:
image: postgres:15
environment:
POSTGRES_DB: lucky_wheel
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
ports: ["5432:5432"]
volumes: ["pg_data:/var/lib/postgresql/data"]
redis:
image: redis:7-alpine
ports: ["6379:6379"]
api:
build: ./apps/api
depends_on: [postgres, redis]
env_file: ./apps/api/.env
ports: ["3000:3000"]
volumes: ["./apps/api/uploads:/app/uploads"]
admin:
build: ./apps/admin
ports: ["5173:80"]
client:
build: ./apps/client
ports: ["5174:80"]
volumes:
pg_data:
```
### مراحل Deploy پیشنهادی
1. `pnpm install` در Root (Workspace).
2. `pnpm --filter api prisma migrate deploy` (اجرای Migrationها روی دیتابیس Production).
3. Build سه اپلیکیشن (`pnpm --filter api build`, `--filter admin build`, `--filter client build`).
4. Serve `admin`/`client` بهصورت استاتیک از پشت Nginx (یا هر CDN).
5. اجرای `api` با PM2/Docker با حداقل ۲ Instance پشت Load Balancer برای Zero-downtime Deploy (با توجه به اینکه Session در JWT است، نیازی به Sticky Session نیست).
6. اجرای Worker BullMQ بهصورت Process/Instance جدا از API اصلی (مقیاسپذیری مستقل صف SMS از ترافیک HTTP).
---
## 23. استراتژی تست و Mapping با Acceptance Criteria
### سطوح تست
- **Unit Test:** `WeightedRandomSelector`, `ChanceService`, `PrizeCodeGenerator`, `CampaignAvailabilityService` (Jest، بدون دیتابیس واقعی — Prisma Mock).
- **Integration Test:** `SpinEngineService` روی دیتابیس Test واقعی (Postgres در Docker) شامل سناریوهای Concurrency (اجرای موازی چند Promise برای شبیهسازی Race Condition آیفون بخش ۹ سند اصلی).
- **E2E Test:** با `supertest` روی کل مسیر OTP → Login → Spin → Result برای هر دو نقش.
- **Load Test:** k6 یا Artillery برای اطمینان از پایداری Lock Contention روی `prizes`/`user_campaigns` تحت بار بالا (مثلاً ۵۰۰ Spin همزمان روی یک کمپین با یک جایزه کمموجودی).
### Mapping مستقیم AC-01 تا AC-14 (سند اصلی) به تست
| AC | تست متناظر |
|---|---|
| AC-01 | E2E: تلاش Spin بدون JWT معتبر → 401 |
| AC-02 | Integration: کاربر با `remaining=0` → 400 `NO_CHANCE_LEFT` |
| AC-03 | Integration: بررسی `usedChances` دقیقاً ۱ واحد افزایش بعد از هر Spin موفق |
| AC-04 | E2E: چند بار `GET /me/chances` پشتسرهم بدون تغییر مقدار |
| AC-05 / AC-06 | Unit + Code Review: هیچ Controller مقدار Prize را از Body نمیخواند؛ فقط `idempotencyKey` ورودی است |
| AC-07 | Integration: Prize با `remainingStock=0` هرگز در نتیجه ۱۰۰ اجرای متوالی Selector ظاهر نشود |
| AC-08 | Load Test: ۱۰۰ Request موازی روی Prize با موجودی ۱ → دقیقاً ۱ برنده، `remainingStock` هرگز منفی |
| AC-09 | Integration: ارسال دو Request با یک `idempotencyKey` → پاسخ یکسان، `usedChances` فقط یک بار افزایش |
| AC-10 | بررسی وجود رکورد `Spin` برای هر سناریوی بالا با `ip`/`userAgent` پر |
| AC-11 | Integration: `POST chances` ادمین → بررسی وجود `AdminLog` متناظر |
| AC-12 | Integration: Mock کردن Provider SMS برای پرتاب Exception → بررسی اینکه `Spin`/`UserPrize` همچنان ثبتشده باقی میماند |
| AC-13 | E2E: Login مجدد کاربر → `GET /me/prizes` شامل جوایز قبلی |
| AC-14 | Integration: تلاش Spin روی کمپین با `endAt` گذشته → 400 `CAMPAIGN_FINISHED` |
---
## 24. جمعبندی و مسیر توسعه آینده
این سند، تمام محدوده MVP بخش ۳۴ سند اصلی را با معماری مشخص، Concurrency-Safe و قابل تست پوشش میدهد. طراحی بهگونهای انجام شده که موارد بخش ۳۳ سند اصلی (خارج از MVP) بدون تغییر ساختاری بزرگ قابل افزودن باشند:
- **Referral / دریافت شانس با خرید:** فقط نیازمند یک `ChanceTxnType` جدید و یک Endpoint/Webhook جدید است؛ موتور Chance Ledger از قبل عمومی طراحی شده.
- **اتصال API فروشگاه / Webhook:** با افزودن ماژول `WebhookModule` که رویدادهای `spin.completed`, `prize.won` را Publish میکند (با BullMQ یا یک Event Emitter داخلی NestJS، `@nestjs/event-emitter`).
- **QR Code / Multi-Brand / White Label:** با افزودن سطح `Brand` بالای `Campaign` در مدل داده (یک `brand_id` روی جدول کمپین) بدون نیاز به تغییر Spin Engine.
- **طرحهای گردونه بیشتر از ۵ مورد:** به لطف الگوی `WheelDesignProps` مشترک (بخش ۱۸)، افزودن طرح ششم صرفاً یک کامپوننت Vue جدید + یک مقدار Enum جدید است، بدون تغییر در Backend یا در چهار طرح دیگر.
**مرجع اصلی این سند:** Lucky Wheel Lottery Spec v1.0 (فایل مرجع کارفرما) + تصمیمات فنی این جلسه (NestJS/Prisma/PostgreSQL، Vue3/Pinia/Ant Design Vue، BullMQ+Redis، raygansms.com، Local Storage قابل مهاجرت، ۵ طرح گردونه انتخابی).