feat: implement advanced reward management and wheel customization

- Add customizable background color
- Add wheel animations and sound effects
- Add reward description field and winner modal
- Change font to Dana
- Add configurable reward probabilities
- Support reward types (Empty, Discount Code, LiveCoin)
- Replace reward title with reward type selector
- Allow wheel page title customization from admin panel
This commit is contained in:
2026-06-10 14:55:07 +03:30
parent b0ff339525
commit ec86cdc528
26 changed files with 1166 additions and 533 deletions

View File

@@ -0,0 +1,28 @@
"""update items table
Revision ID: eb35214a1411
Revises: f71dc2bbdfc6
Create Date: 2026-06-03 02:15:20.584639
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision: str = 'eb35214a1411'
down_revision: Union[str, Sequence[str], None] = 'f71dc2bbdfc6'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
"""Upgrade schema."""
pass
def downgrade() -> None:
"""Downgrade schema."""
pass

View File

@@ -0,0 +1,28 @@
"""update items table
Revision ID: f71dc2bbdfc6
Revises: b55808a86947
Create Date: 2026-06-03 02:12:35.370477
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision: str = 'f71dc2bbdfc6'
down_revision: Union[str, Sequence[str], None] = 'b55808a86947'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
"""Upgrade schema."""
pass
def downgrade() -> None:
"""Downgrade schema."""
pass

View File

@@ -1,9 +1,5 @@
from fastapi import APIRouter
from fastapi import Depends
from fastapi import HTTPException
from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.orm import Session
import schemas
from schemas import ItemResponse
from services import crud
@@ -13,7 +9,6 @@ from core.logger import get_logger
router = APIRouter()
logger = get_logger('admin')
def get_db():
db = SessionLocal()
try:
@@ -21,7 +16,6 @@ def get_db():
finally:
db.close()
# GET ALL ITEMS
@router.get("/items", response_model=list[ItemResponse])
def get_items(db: Session = Depends(get_db)):
@@ -33,28 +27,51 @@ def get_items(db: Session = Depends(get_db)):
logger.critical(f"Unexpected error fetching items: {e}")
raise
# CREATE ITEM
@router.post("/items", response_model=ItemResponse)
def create_item(
item: schemas.ItemCreate,
db: Session = Depends(get_db)
):
def create_item(item: schemas.ItemCreate, db: Session = Depends(get_db)):
try:
result = crud.create_item(db, item.title, item.value) # ✅ value اضافه شد
result = crud.create_item(
db,
item.title,
item.prize,
item.description,
item.prize_type,
item.probability
)
logger.info(f"Item created: {item.title}")
return result
except Exception as e:
logger.critical(f"Unexpected error creating item: {e}")
raise
# UPDATE ITEM
@router.put("/items/{item_id}", response_model=ItemResponse)
def update_item(item_id: int, item: schemas.ItemUpdate, db: Session = Depends(get_db)):
try:
updated = crud.update_item(
db,
item_id,
item.title,
item.prize,
item.description,
item.prize_type,
item.probability
)
if not updated:
logger.warning(f"Item not found: {item_id}")
raise HTTPException(status_code=404, detail="Item not found")
logger.info(f"Item updated: {item_id}")
return updated
except HTTPException:
raise
except Exception as e:
logger.critical(f"Unexpected error updating item: {e}")
raise
# DELETE ITEM
@router.delete("/items/{item_id}")
def delete_item(
item_id: int,
db: Session = Depends(get_db)
):
def delete_item(item_id: int, db: Session = Depends(get_db)):
try:
item = crud.delete_item(db, item_id)
if not item:
@@ -66,28 +83,4 @@ def delete_item(
raise
except Exception as e:
logger.critical(f"Unexpected error deleting item: {e}")
raise
@router.put("/items/{item_id}", response_model=ItemResponse)
def update_item(
item_id: int,
item: schemas.ItemCreate,
db: Session = Depends(get_db)
):
try:
updated = crud.update_item(db, item_id, item.title, item.value)
if not updated:
logger.warning(f"Item not found: {item_id}")
raise HTTPException(status_code=404, detail="Item not found")
logger.info(f"Item updated: {item_id}")
return updated
except HTTPException:
raise
except Exception as e:
logger.critical(f"Unexpected error updating item: {e}")
raise
raise

View File

@@ -51,7 +51,10 @@ def spin(db: Session = Depends(get_db)):
return {
"winner": {
"id": winner.id,
"title": winner.title
"title": winner.title,
"prize": winner.prize,
"prize_type": winner.prize_type,
"description": winner.description or ""
}
}

View File

@@ -1,26 +1,12 @@
from sqlalchemy import Column
from sqlalchemy import Integer
from sqlalchemy import String
from sqlalchemy import Column, Integer, String, Float
from app.db.session import Base
class Item(Base):
__tablename__ = "items"
id = Column(
Integer,
primary_key=True,
index=True
)
title = Column(
String,
nullable=False
)
value = Column(
Integer,
nullable=False
)
id = Column(Integer, primary_key=True, index=True)
title = Column(String, nullable=False)
prize = Column(String, nullable=False)
description = Column(String, nullable=True, default="")
prize_type = Column(String, nullable=False, default="پوچ")
probability = Column(Float, nullable=False, default=1.0)

View File

@@ -287,3 +287,137 @@ FROM items]
2026-05-30 12:50:43 | INFO | admin | Items fetched: 8 items
2026-05-30 12:51:00 | INFO | user | User fetched items: 8 items
2026-05-30 12:53:57 | INFO | user | Spin result: پوچ
2026-05-30 14:30:41 | INFO | admin | Items fetched: 8 items
2026-05-30 14:31:19 | INFO | user | User fetched items: 8 items
2026-05-30 14:31:21 | INFO | user | Spin result: تخفيف يک ماهه
2026-05-30 14:32:54 | INFO | user | Spin result: پوچ
2026-05-30 14:33:18 | INFO | admin | Item updated: 1
2026-05-30 14:33:18 | INFO | admin | Items fetched: 8 items
2026-05-30 14:33:38 | INFO | user | Spin result: تخفيف سه ماهه
2026-06-03 02:01:07 | CRITICAL | user | Unexpected error fetching items: (sqlite3.OperationalError) no such column: items.type
[SQL: SELECT items.id AS items_id, items.title AS items_title, items.type AS items_type, items.description AS items_description, items.probability AS items_probability, items.value AS items_value
FROM items]
(Background on this error at: https://sqlalche.me/e/20/e3q8)
2026-06-03 02:01:11 | CRITICAL | user | Unexpected error fetching items: (sqlite3.OperationalError) no such column: items.type
[SQL: SELECT items.id AS items_id, items.title AS items_title, items.type AS items_type, items.description AS items_description, items.probability AS items_probability, items.value AS items_value
FROM items]
(Background on this error at: https://sqlalche.me/e/20/e3q8)
2026-06-03 02:08:13 | CRITICAL | user | Unexpected error fetching items: (sqlite3.OperationalError) no such column: items.type
[SQL: SELECT items.id AS items_id, items.title AS items_title, items.type AS items_type, items.description AS items_description, items.probability AS items_probability, items.value AS items_value
FROM items]
(Background on this error at: https://sqlalche.me/e/20/e3q8)
2026-06-03 02:30:22 | CRITICAL | admin | Unexpected error fetching items: (sqlite3.OperationalError) no such column: items.prize
[SQL: SELECT items.id AS items_id, items.title AS items_title, items.prize AS items_prize, items.description AS items_description, items.prize_type AS items_prize_type, items.probability AS items_probability
FROM items]
(Background on this error at: https://sqlalche.me/e/20/e3q8)
2026-06-07 00:22:06 | CRITICAL | user | Unexpected error fetching items: (sqlite3.OperationalError) no such column: items.prize
[SQL: SELECT items.id AS items_id, items.title AS items_title, items.prize AS items_prize, items.description AS items_description, items.prize_type AS items_prize_type, items.probability AS items_probability
FROM items]
(Background on this error at: https://sqlalche.me/e/20/e3q8)
2026-06-07 00:22:08 | CRITICAL | user | Unexpected error fetching items: (sqlite3.OperationalError) no such column: items.prize
[SQL: SELECT items.id AS items_id, items.title AS items_title, items.prize AS items_prize, items.description AS items_description, items.prize_type AS items_prize_type, items.probability AS items_probability
FROM items]
(Background on this error at: https://sqlalche.me/e/20/e3q8)
2026-06-07 00:23:31 | CRITICAL | user | Unexpected error fetching items: (sqlite3.OperationalError) no such column: items.prize
[SQL: SELECT items.id AS items_id, items.title AS items_title, items.prize AS items_prize, items.description AS items_description, items.prize_type AS items_prize_type, items.probability AS items_probability
FROM items]
(Background on this error at: https://sqlalche.me/e/20/e3q8)
2026-06-07 00:24:02 | CRITICAL | user | Unexpected error fetching items: (sqlite3.OperationalError) no such column: items.prize
[SQL: SELECT items.id AS items_id, items.title AS items_title, items.prize AS items_prize, items.description AS items_description, items.prize_type AS items_prize_type, items.probability AS items_probability
FROM items]
(Background on this error at: https://sqlalche.me/e/20/e3q8)
2026-06-07 00:26:33 | INFO | user | User fetched items: 0 items
2026-06-07 00:32:56 | INFO | user | User fetched items: 0 items
2026-06-07 00:36:17 | INFO | user | User fetched items: 0 items
2026-06-07 00:36:21 | INFO | user | User fetched items: 0 items
2026-06-07 00:39:11 | INFO | user | User fetched items: 0 items
2026-06-07 00:39:15 | INFO | admin | Items fetched: 0 items
2026-06-07 00:39:33 | INFO | admin | Items fetched: 0 items
2026-06-07 00:49:16 | INFO | admin | Items fetched: 0 items
2026-06-07 00:49:39 | INFO | admin | Item created: jotdt 20
2026-06-07 00:49:39 | INFO | admin | Items fetched: 1 items
2026-06-07 00:49:44 | INFO | user | User fetched items: 1 items
2026-06-07 00:49:47 | INFO | user | Spin result: jotdt 20
2026-06-07 00:50:31 | INFO | admin | Item created: پوچ
2026-06-07 00:50:31 | INFO | admin | Items fetched: 2 items
2026-06-07 00:50:38 | INFO | user | User fetched items: 2 items
2026-06-07 00:50:40 | INFO | user | Spin result: پوچ
2026-06-07 00:51:12 | INFO | admin | Item created: تخفيف 20
2026-06-07 00:51:12 | INFO | admin | Items fetched: 3 items
2026-06-07 00:51:16 | INFO | user | User fetched items: 3 items
2026-06-07 00:52:18 | INFO | user | Spin result: پوچ
2026-06-07 00:53:54 | INFO | admin | Item updated: 1
2026-06-07 00:53:54 | INFO | admin | Items fetched: 3 items
2026-06-07 00:54:22 | INFO | admin | Item created: تخفيف 30
2026-06-07 00:54:22 | INFO | admin | Items fetched: 4 items
2026-06-07 00:54:35 | INFO | user | User fetched items: 4 items
2026-06-07 00:54:38 | INFO | user | Spin result: پوچ
2026-06-07 00:55:39 | INFO | user | Spin result: تخفيف 30
2026-06-07 00:56:16 | INFO | admin | Item created: تخفيف 30
2026-06-07 00:56:16 | INFO | admin | Items fetched: 5 items
2026-06-07 00:56:51 | INFO | admin | Item created: پوچ
2026-06-07 00:56:51 | INFO | admin | Items fetched: 6 items
2026-06-07 00:56:58 | INFO | user | User fetched items: 6 items
2026-06-07 00:57:01 | INFO | user | Spin result: پوچ
2026-06-07 00:57:13 | INFO | user | Spin result: پوچ
2026-06-07 01:03:09 | INFO | user | User fetched items: 6 items
2026-06-07 01:03:12 | INFO | user | Spin result: پوچ
2026-06-07 01:07:35 | INFO | admin | Items fetched: 6 items
2026-06-07 01:07:37 | INFO | user | User fetched items: 6 items
2026-06-07 01:07:39 | INFO | user | Spin result: تخفيف 20
2026-06-07 02:15:44 | INFO | user | User fetched items: 6 items
2026-06-07 02:15:45 | INFO | user | Spin result: پوچ
2026-06-07 02:19:11 | INFO | user | Spin result: پوچ
2026-06-07 02:19:40 | INFO | user | Spin result: پوچ
2026-06-07 13:34:50 | INFO | user | User fetched items: 6 items
2026-06-07 13:34:52 | INFO | user | Spin result: پوچ
2026-06-07 13:35:15 | INFO | admin | Items fetched: 6 items
2026-06-07 13:35:36 | INFO | user | Spin result: پوچ
2026-06-10 11:55:45 | INFO | user | User fetched items: 6 items
2026-06-10 11:56:01 | INFO | admin | Items fetched: 6 items
2026-06-10 11:57:45 | INFO | user | Spin result: پوچ
2026-06-10 11:59:13 | INFO | user | Spin result: پوچ
2026-06-10 11:59:33 | INFO | admin | Item updated: 3
2026-06-10 11:59:33 | INFO | admin | Items fetched: 6 items
2026-06-10 11:59:36 | INFO | user | Spin result: پوچ
2026-06-10 11:59:45 | INFO | user | Spin result: تخفيف 20
2026-06-10 12:01:01 | INFO | user | Spin result: پوچ
2026-06-10 12:02:20 | INFO | user | Spin result: پوچ
2026-06-10 12:07:31 | INFO | user | User fetched items: 6 items
2026-06-10 12:07:31 | INFO | user | User fetched items: 6 items
2026-06-10 12:07:31 | INFO | user | Spin result: تخفيف 20
2026-06-10 12:07:31 | INFO | user | User fetched items: 6 items
2026-06-10 12:07:31 | INFO | user | User fetched items: 6 items
2026-06-10 12:07:31 | INFO | user | User fetched items: 6 items
2026-06-10 12:08:44 | INFO | user | User fetched items: 6 items
2026-06-10 12:08:47 | INFO | user | Spin result: تخفيف 20
2026-06-10 12:17:29 | INFO | user | User fetched items: 6 items
2026-06-10 12:17:32 | INFO | user | Spin result: پوچ
2026-06-10 12:18:03 | INFO | user | Spin result: پوچ
2026-06-10 12:18:44 | INFO | user | Spin result: تخفيف 20
2026-06-10 12:19:16 | INFO | user | User fetched items: 6 items
2026-06-10 12:19:20 | INFO | user | Spin result: تخفيف 20
2026-06-10 12:20:37 | INFO | user | Spin result: پوچ
2026-06-10 12:20:41 | INFO | user | User fetched items: 6 items
2026-06-10 12:20:49 | INFO | admin | Items fetched: 6 items
2026-06-10 12:22:37 | INFO | user | User fetched items: 6 items
2026-06-10 12:22:39 | INFO | user | Spin result: تخفيف 20
2026-06-10 12:22:49 | INFO | user | Spin result: تخفيف 20
2026-06-10 12:23:08 | INFO | user | User fetched items: 6 items
2026-06-10 12:23:09 | INFO | user | Spin result: پوچ
2026-06-10 12:25:32 | INFO | user | User fetched items: 6 items
2026-06-10 12:25:34 | INFO | user | Spin result: تخفيف 20
2026-06-10 12:30:55 | INFO | user | User fetched items: 6 items
2026-06-10 12:30:56 | INFO | user | Spin result: پوچ
2026-06-10 12:36:33 | INFO | user | User fetched items: 6 items
2026-06-10 12:36:35 | INFO | user | Spin result: تخفيف 20
2026-06-10 12:37:57 | INFO | user | User fetched items: 6 items
2026-06-10 12:48:20 | INFO | user | User fetched items: 6 items
2026-06-10 12:51:46 | INFO | user | User fetched items: 6 items
2026-06-10 12:58:36 | INFO | user | User fetched items: 6 items
2026-06-10 12:58:41 | INFO | user | Spin result: تخفيف 30
2026-06-10 12:59:01 | INFO | admin | Items fetched: 6 items
2026-06-10 12:59:04 | INFO | admin | Items fetched: 6 items
2026-06-10 13:00:57 | INFO | user | User fetched items: 6 items
2026-06-10 13:01:44 | INFO | user | User fetched items: 6 items
2026-06-10 13:01:46 | INFO | user | User fetched items: 6 items

View File

@@ -38,3 +38,35 @@ FROM items]
[SQL: INSERT INTO items (title) VALUES (?)]
[parameters: ('جایزه 1',)]
(Background on this error at: https://sqlalche.me/e/20/e3q8)
2026-06-03 02:01:07 | CRITICAL | user | Unexpected error fetching items: (sqlite3.OperationalError) no such column: items.type
[SQL: SELECT items.id AS items_id, items.title AS items_title, items.type AS items_type, items.description AS items_description, items.probability AS items_probability, items.value AS items_value
FROM items]
(Background on this error at: https://sqlalche.me/e/20/e3q8)
2026-06-03 02:01:11 | CRITICAL | user | Unexpected error fetching items: (sqlite3.OperationalError) no such column: items.type
[SQL: SELECT items.id AS items_id, items.title AS items_title, items.type AS items_type, items.description AS items_description, items.probability AS items_probability, items.value AS items_value
FROM items]
(Background on this error at: https://sqlalche.me/e/20/e3q8)
2026-06-03 02:08:13 | CRITICAL | user | Unexpected error fetching items: (sqlite3.OperationalError) no such column: items.type
[SQL: SELECT items.id AS items_id, items.title AS items_title, items.type AS items_type, items.description AS items_description, items.probability AS items_probability, items.value AS items_value
FROM items]
(Background on this error at: https://sqlalche.me/e/20/e3q8)
2026-06-03 02:30:22 | CRITICAL | admin | Unexpected error fetching items: (sqlite3.OperationalError) no such column: items.prize
[SQL: SELECT items.id AS items_id, items.title AS items_title, items.prize AS items_prize, items.description AS items_description, items.prize_type AS items_prize_type, items.probability AS items_probability
FROM items]
(Background on this error at: https://sqlalche.me/e/20/e3q8)
2026-06-07 00:22:06 | CRITICAL | user | Unexpected error fetching items: (sqlite3.OperationalError) no such column: items.prize
[SQL: SELECT items.id AS items_id, items.title AS items_title, items.prize AS items_prize, items.description AS items_description, items.prize_type AS items_prize_type, items.probability AS items_probability
FROM items]
(Background on this error at: https://sqlalche.me/e/20/e3q8)
2026-06-07 00:22:08 | CRITICAL | user | Unexpected error fetching items: (sqlite3.OperationalError) no such column: items.prize
[SQL: SELECT items.id AS items_id, items.title AS items_title, items.prize AS items_prize, items.description AS items_description, items.prize_type AS items_prize_type, items.probability AS items_probability
FROM items]
(Background on this error at: https://sqlalche.me/e/20/e3q8)
2026-06-07 00:23:31 | CRITICAL | user | Unexpected error fetching items: (sqlite3.OperationalError) no such column: items.prize
[SQL: SELECT items.id AS items_id, items.title AS items_title, items.prize AS items_prize, items.description AS items_description, items.prize_type AS items_prize_type, items.probability AS items_probability
FROM items]
(Background on this error at: https://sqlalche.me/e/20/e3q8)
2026-06-07 00:24:02 | CRITICAL | user | Unexpected error fetching items: (sqlite3.OperationalError) no such column: items.prize
[SQL: SELECT items.id AS items_id, items.title AS items_title, items.prize AS items_prize, items.description AS items_description, items.prize_type AS items_prize_type, items.probability AS items_probability
FROM items]
(Background on this error at: https://sqlalche.me/e/20/e3q8)

View File

@@ -1,19 +1,14 @@
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from app.db.session import engine
from app.db.session import Base
from app.db.session import engine, Base
from app.db.models import Item
app = FastAPI()
from api.admin import router as admin_router
from api.user import router as user_router
app.include_router(user_router, prefix="/user")
app = FastAPI()
Base.metadata.create_all(bind=engine)
app.add_middleware(
CORSMiddleware,
@@ -26,4 +21,8 @@ app.add_middleware(
allow_headers=["*"],
)
app.include_router(admin_router, prefix="/admin")
# ✅ بعد router ها
app.include_router(user_router, prefix="/user")
app.include_router(admin_router, prefix="/admin")
Base.metadata.create_all(bind=engine)

View File

@@ -1,20 +1,27 @@
from pydantic import BaseModel
from typing import Optional
class ItemCreate(BaseModel):
title: str
value: int
prize: str
description: Optional[str] = ""
prize_type: str = "پوچ"
probability: float = 1.0
class ItemUpdate(BaseModel):
title: str
value: int
prize: str
description: Optional[str] = ""
prize_type: str = "پوچ"
probability: float = 1.0
class ItemResponse(BaseModel):
id: int
title: str
value: int
prize: str
description: Optional[str] = ""
prize_type: str
probability: float
class Config:
from_attributes = True

View File

@@ -5,18 +5,27 @@ from app.db.models import Item
def get_items(db: Session):
return db.query(Item).all()
def create_item(db: Session, title: str, value: int): # ✅ prize → value
item = Item(title=title, value=value) # ✅ prize → value
def create_item(db: Session, title: str, prize: str, description: str = "", prize_type: str = "پوچ", probability: float = 1.0):
item = Item(
title=title,
prize=prize,
description=description,
prize_type=prize_type,
probability=probability
)
db.add(item)
db.commit()
db.refresh(item)
return item
def update_item(db: Session, item_id: int, title: str, value: int): # ✅ prize → value
def update_item(db: Session, item_id: int, title: str, prize: str, description: str = "", prize_type: str = "پوچ", probability: float = 1.0):
item = db.query(Item).filter(Item.id == item_id).first()
if item:
item.title = title
item.value = value # ✅ prize → value
item.prize = prize
item.description = description
item.prize_type = prize_type
item.probability = probability
db.commit()
db.refresh(item)
return item
@@ -32,4 +41,5 @@ def spin_wheel(db: Session):
items = db.query(Item).all()
if not items:
return None
return random.choice(items)
weights = [item.probability for item in items]
return random.choices(items, weights=weights, k=1)[0]

Binary file not shown.