Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
|
@@ -1,6 +1,7 @@
|
|
| 1 |
import os
|
| 2 |
import json
|
| 3 |
import logging
|
|
|
|
| 4 |
from datetime import datetime, timedelta, timezone
|
| 5 |
from typing import Optional, List
|
| 6 |
from contextlib import asynccontextmanager
|
|
@@ -18,7 +19,7 @@ from google import genai
|
|
| 18 |
import bcrypt
|
| 19 |
|
| 20 |
# ==========================================
|
| 21 |
-
# بخش اول: سیستم ثبت لاگ خطاهای
|
| 22 |
# ==========================================
|
| 23 |
logging.basicConfig(level=logging.INFO)
|
| 24 |
logger = logging.getLogger("production_backend")
|
|
@@ -27,7 +28,7 @@ logger = logging.getLogger("production_backend")
|
|
| 27 |
AI_CORE_KEY = os.getenv("GEMINI_API_KEY") or os.getenv("SECRET")
|
| 28 |
|
| 29 |
# ==========================================
|
| 30 |
-
# بخش دوم: اتصال پایگاه داده Neon.tech و س
|
| 31 |
# ==========================================
|
| 32 |
DATABASE_URL = os.getenv("DATABASE_URL", "sqlite:///./test.db")
|
| 33 |
if DATABASE_URL.startswith("postgres://"):
|
|
@@ -51,15 +52,12 @@ except Exception as db_init_err:
|
|
| 51 |
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
|
| 52 |
Base = declarative_base()
|
| 53 |
|
| 54 |
-
#
|
| 55 |
-
# بخش سوم: توابع همزمانی پایه (حل قطعی NameError)
|
| 56 |
-
# ==========================================
|
| 57 |
def get_now():
|
| 58 |
-
"""تولید زمان بومی و دقیق کنونی جهت هماهنگی با فرآیندهای دیتابیس"""
|
| 59 |
return datetime.now(timezone.utc).replace(tzinfo=None)
|
| 60 |
|
| 61 |
# ==========================================
|
| 62 |
-
# بخش
|
| 63 |
# ==========================================
|
| 64 |
|
| 65 |
class User(Base):
|
|
@@ -214,7 +212,7 @@ class Review(Base):
|
|
| 214 |
comment = Column(Text, nullable=True)
|
| 215 |
|
| 216 |
# ==========================================
|
| 217 |
-
# بخش
|
| 218 |
# ==========================================
|
| 219 |
|
| 220 |
def get_password_hash(password: str) -> str:
|
|
@@ -232,14 +230,8 @@ SECRET_KEY = os.getenv("JWT_SECRET", "8c12fa4870a23049102c89eeffb8a719280abdc029
|
|
| 232 |
ALGORITHM = "HS256"
|
| 233 |
security = HTTPBearer()
|
| 234 |
|
| 235 |
-
def create_access_token(data: dict, expires_delta: Optional[timedelta] = None):
|
| 236 |
-
to_encode = data.copy()
|
| 237 |
-
expire = get_now() + (expires_delta or timedelta(minutes=1440))
|
| 238 |
-
to_encode.update({"exp": expire})
|
| 239 |
-
return jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
|
| 240 |
-
|
| 241 |
# ==========================================
|
| 242 |
-
# بخش
|
| 243 |
# ==========================================
|
| 244 |
|
| 245 |
def clean_expired_bookings(db: Session):
|
|
@@ -264,7 +256,7 @@ def clean_expired_bookings(db: Session):
|
|
| 264 |
|
| 265 |
def get_db():
|
| 266 |
db = SessionLocal()
|
| 267 |
-
clean_expired_bookings(db) # اجرای آزادسازی
|
| 268 |
try:
|
| 269 |
yield db
|
| 270 |
finally:
|
|
@@ -292,7 +284,7 @@ def get_current_user(db: Session = Depends(get_db), credentials: HTTPAuthorizati
|
|
| 292 |
return user
|
| 293 |
|
| 294 |
# ==========================================
|
| 295 |
-
# بخش
|
| 296 |
# ==========================================
|
| 297 |
|
| 298 |
def _execute_native_matrix_processing(prompt: str, model_type: str = "fast") -> str:
|
|
@@ -376,7 +368,7 @@ def core_bio_extraction(resume_text: str) -> str:
|
|
| 376 |
return result
|
| 377 |
|
| 378 |
# ==========================================
|
| 379 |
-
# بخش ه
|
| 380 |
# ==========================================
|
| 381 |
|
| 382 |
class RegisterRequest(BaseModel):
|
|
@@ -476,68 +468,15 @@ class UpdateProfileRequest(BaseModel):
|
|
| 476 |
city: Optional[str] = None
|
| 477 |
|
| 478 |
# ==========================================
|
| 479 |
-
# بخش
|
| 480 |
# ==========================================
|
| 481 |
|
| 482 |
-
def seed_initial_data(db: Session):
|
| 483 |
-
try:
|
| 484 |
-
test_pwd = get_password_hash("demo1234")
|
| 485 |
-
|
| 486 |
-
# دانشجویان پیشفرض
|
| 487 |
-
student = User(email="student@demo.com", hashed_password=test_pwd, role="student", is_verified=True, balance=500000.0, phone_number="09121111111", city="تهران")
|
| 488 |
-
db.add(student)
|
| 489 |
-
|
| 490 |
-
# مشاوران پیشفرض
|
| 491 |
-
counselor1 = User(email="counselor1@demo.com", hashed_password=test_pwd, role="counselor", is_verified=True, rating=4.8, city="تهران")
|
| 492 |
-
counselor2 = User(email="counselor2@demo.com", hashed_password=test_pwd, role="counselor", is_verified=True, rating=4.5, city="شیراز")
|
| 493 |
-
db.add(counselor1)
|
| 494 |
-
db.add(counselor2)
|
| 495 |
-
db.flush()
|
| 496 |
-
|
| 497 |
-
p1 = CounselorProfile(user_id=counselor1.id, specialty="برنامهریزی کنکور تجربی", resume="رتبه دو رقمی سراسری با سابقه تدریس تخصصی", bio="مشاور ارشد با تخصص رتبهپروری تجربی", hourly_rate=120000.0)
|
| 498 |
-
p2 = CounselorProfile(user_id=counselor2.id, specialty="هدایت تحصیلی ریاضی و فیزیک", resume="عضو بنیاد نخبگان ریاضی", bio="متخصص بهینهسازی مسیر قبولی رشته ریاضی", hourly_rate=100000.0)
|
| 499 |
-
db.add(p1)
|
| 500 |
-
db.add(p2)
|
| 501 |
-
|
| 502 |
-
# آموزشگاههای پیشفرض
|
| 503 |
-
school_user = User(email="school@demo.com", hashed_password=test_pwd, role="school_representative", is_verified=True, city="اصفهان")
|
| 504 |
-
db.add(school_user)
|
| 505 |
-
db.flush()
|
| 506 |
-
|
| 507 |
-
s_profile = SchoolProfile(
|
| 508 |
-
user_id=school_user.id,
|
| 509 |
-
name="موسسه کنکور اندیشه",
|
| 510 |
-
subscription_active_until=get_now() + timedelta(days=60),
|
| 511 |
-
is_sponsored=True,
|
| 512 |
-
bio="برترین آکادمی تستی کنکور اصفهان",
|
| 513 |
-
address="اصفهان، خیابان دانشگاه",
|
| 514 |
-
phone_number="03133333333",
|
| 515 |
-
founding_year=1395
|
| 516 |
-
)
|
| 517 |
-
db.add(s_profile)
|
| 518 |
-
db.flush()
|
| 519 |
-
|
| 520 |
-
course = SchoolCourse(school_id=s_profile.id, course_name="ریاضی تخصصی کنکور", teacher_name="استاد علوی", teaching_style="روشهای سرعتی و محاسباتی")
|
| 521 |
-
db.add(course)
|
| 522 |
-
|
| 523 |
-
# اسلات آزاد مشاور
|
| 524 |
-
now = get_now()
|
| 525 |
-
slot1 = TimeSlot(counselor_id=counselor1.id, start_time=now + timedelta(days=1, hours=10), end_time=now + timedelta(days=1, hours=11), is_booked=False)
|
| 526 |
-
db.add(slot1)
|
| 527 |
-
|
| 528 |
-
db.commit()
|
| 529 |
-
except Exception as e:
|
| 530 |
-
logger.error(f"Seeding process crashed: {e}", exc_info=True)
|
| 531 |
-
db.rollback()
|
| 532 |
-
|
| 533 |
-
|
| 534 |
@asynccontextmanager
|
| 535 |
async def lifespan(app: FastAPI):
|
| 536 |
Base.metadata.create_all(bind=engine)
|
| 537 |
db = SessionLocal()
|
| 538 |
try:
|
| 539 |
-
|
| 540 |
-
seed_initial_data(db)
|
| 541 |
except Exception as seed_err:
|
| 542 |
logger.error(f"Error seeding database during startup: {seed_err}", exc_info=True)
|
| 543 |
finally:
|
|
@@ -559,6 +498,89 @@ app.add_middleware(
|
|
| 559 |
allow_headers=["*"],
|
| 560 |
)
|
| 561 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 562 |
|
| 563 |
@app.exception_handler(Exception)
|
| 564 |
async def global_exception_handler(request: Request, exc: Exception):
|
|
@@ -569,7 +591,7 @@ async def global_exception_handler(request: Request, exc: Exception):
|
|
| 569 |
)
|
| 570 |
|
| 571 |
# ==========================================
|
| 572 |
-
# بخش دهم: آدرسهای API
|
| 573 |
# ==========================================
|
| 574 |
|
| 575 |
@app.post("/api/auth/register")
|
|
|
|
| 1 |
import os
|
| 2 |
import json
|
| 3 |
import logging
|
| 4 |
+
import datetime
|
| 5 |
from datetime import datetime, timedelta, timezone
|
| 6 |
from typing import Optional, List
|
| 7 |
from contextlib import asynccontextmanager
|
|
|
|
| 19 |
import bcrypt
|
| 20 |
|
| 21 |
# ==========================================
|
| 22 |
+
# بخش اول: سیستم ثبت لاگ خطارهای سرور
|
| 23 |
# ==========================================
|
| 24 |
logging.basicConfig(level=logging.INFO)
|
| 25 |
logger = logging.getLogger("production_backend")
|
|
|
|
| 28 |
AI_CORE_KEY = os.getenv("GEMINI_API_KEY") or os.getenv("SECRET")
|
| 29 |
|
| 30 |
# ==========================================
|
| 31 |
+
# بخش دوم: اتصال پایگاه داده Neon.tech و ساختار پایه
|
| 32 |
# ==========================================
|
| 33 |
DATABASE_URL = os.getenv("DATABASE_URL", "sqlite:///./test.db")
|
| 34 |
if DATABASE_URL.startswith("postgres://"):
|
|
|
|
| 52 |
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
|
| 53 |
Base = declarative_base()
|
| 54 |
|
| 55 |
+
# تعریف تابع زمان بومی سرور
|
|
|
|
|
|
|
| 56 |
def get_now():
|
|
|
|
| 57 |
return datetime.now(timezone.utc).replace(tzinfo=None)
|
| 58 |
|
| 59 |
# ==========================================
|
| 60 |
+
# بخش سوم: مدلهای دیتابیس (SQLAlchemy Models)
|
| 61 |
# ==========================================
|
| 62 |
|
| 63 |
class User(Base):
|
|
|
|
| 212 |
comment = Column(Text, nullable=True)
|
| 213 |
|
| 214 |
# ==========================================
|
| 215 |
+
# بخش چهارم: توابع کمکی امنیتی و رمزنگاری
|
| 216 |
# ==========================================
|
| 217 |
|
| 218 |
def get_password_hash(password: str) -> str:
|
|
|
|
| 230 |
ALGORITHM = "HS256"
|
| 231 |
security = HTTPBearer()
|
| 232 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 233 |
# ==========================================
|
| 234 |
+
# بخش پنجم: توابع وابستگی تزریق پایگاه داده و احراز هویت
|
| 235 |
# ==========================================
|
| 236 |
|
| 237 |
def clean_expired_bookings(db: Session):
|
|
|
|
| 256 |
|
| 257 |
def get_db():
|
| 258 |
db = SessionLocal()
|
| 259 |
+
clean_expired_bookings(db) # اجرای مکانیزم آزادسازی نوبتها در لحظه لود دیتابیس
|
| 260 |
try:
|
| 261 |
yield db
|
| 262 |
finally:
|
|
|
|
| 284 |
return user
|
| 285 |
|
| 286 |
# ==========================================
|
| 287 |
+
# بخش ششم: توابع پردازش ابری هوشمند بومی تاپ دانش
|
| 288 |
# ==========================================
|
| 289 |
|
| 290 |
def _execute_native_matrix_processing(prompt: str, model_type: str = "fast") -> str:
|
|
|
|
| 368 |
return result
|
| 369 |
|
| 370 |
# ==========================================
|
| 371 |
+
# بخش هفتم: کلاسهای ورودی Pydantic (صحتسنجی)
|
| 372 |
# ==========================================
|
| 373 |
|
| 374 |
class RegisterRequest(BaseModel):
|
|
|
|
| 468 |
city: Optional[str] = None
|
| 469 |
|
| 470 |
# ==========================================
|
| 471 |
+
# بخش هشتم: راهاندازی و اجرای وبسرور FastAPI
|
| 472 |
# ==========================================
|
| 473 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 474 |
@asynccontextmanager
|
| 475 |
async def lifespan(app: FastAPI):
|
| 476 |
Base.metadata.create_all(bind=engine)
|
| 477 |
db = SessionLocal()
|
| 478 |
try:
|
| 479 |
+
seed_initial_data(db)
|
|
|
|
| 480 |
except Exception as seed_err:
|
| 481 |
logger.error(f"Error seeding database during startup: {seed_err}", exc_info=True)
|
| 482 |
finally:
|
|
|
|
| 498 |
allow_headers=["*"],
|
| 499 |
)
|
| 500 |
|
| 501 |
+
# ==========================================
|
| 502 |
+
# بخش نهم: فرآیند ثبت و همگامسازی دادههای تستی و دمو (Check-Seeding)
|
| 503 |
+
# ==========================================
|
| 504 |
+
|
| 505 |
+
def seed_initial_data(db: Session):
|
| 506 |
+
try:
|
| 507 |
+
# لیست جامع اکانتهای پیشفرض قدیمی و جدید با کلمات عبور هماهنگ با کلاینت اندروید
|
| 508 |
+
seed_users = [
|
| 509 |
+
{"email": "student@demo.com", "password": "demo1234", "role": "student", "balance": 500000.0, "phone": "09121111111", "city": "تهران"},
|
| 510 |
+
{"email": "student@test.com", "password": "student123", "role": "student", "balance": 300000.0, "phone": "09122222222", "city": "تهران"},
|
| 511 |
+
{"email": "counselor1@demo.com", "password": "demo1234", "role": "counselor", "rating": 4.8, "city": "تهران"},
|
| 512 |
+
{"email": "counselor2@demo.com", "password": "demo1234", "role": "counselor", "rating": 4.5, "city": "شیراز"},
|
| 513 |
+
{"email": "counselor@test.com", "password": "counselor123", "role": "counselor", "rating": 4.7, "city": "تهران"},
|
| 514 |
+
{"email": "school@demo.com", "password": "demo1234", "role": "school_representative", "city": "اصفهان"},
|
| 515 |
+
{"email": "school@test.com", "password": "school123", "role": "school_representative", "city": "تهران"}
|
| 516 |
+
]
|
| 517 |
+
|
| 518 |
+
for u_data in seed_users:
|
| 519 |
+
# بررسی تک تک کاندیداها به صورت پویا (سازگار با دادههای قبلی Neon)
|
| 520 |
+
existing = db.query(User).filter(User.email == u_data["email"]).first()
|
| 521 |
+
if not existing:
|
| 522 |
+
new_user = User(
|
| 523 |
+
email=u_data["email"],
|
| 524 |
+
hashed_password=get_password_hash(u_data["password"]),
|
| 525 |
+
role=u_data["role"],
|
| 526 |
+
is_verified=True,
|
| 527 |
+
balance=u_data.get("balance", 0.0),
|
| 528 |
+
rating=u_data.get("rating", 5.0),
|
| 529 |
+
phone_number=u_data.get("phone"),
|
| 530 |
+
city=u_data.get("city")
|
| 531 |
+
)
|
| 532 |
+
db.add(new_user)
|
| 533 |
+
db.flush()
|
| 534 |
+
|
| 535 |
+
# ثبت دقیق پروفایلهای همبسته
|
| 536 |
+
if u_data["role"] == "counselor":
|
| 537 |
+
p = CounselorProfile(
|
| 538 |
+
user_id=new_user.id,
|
| 539 |
+
specialty="برنامهریزی کنکور و هدایت تحصیلی",
|
| 540 |
+
resume="رتبه برتر کنکور و کارشناس باسابقه پلتفرم تراز تحصیلی بومی تاپ دانش",
|
| 541 |
+
bio="متخصص افزایش راندمان مطالعه داوطلبان با رویکردهای نوین مشاوره کلاسی",
|
| 542 |
+
hourly_rate=120000.0
|
| 543 |
+
)
|
| 544 |
+
db.add(p)
|
| 545 |
+
|
| 546 |
+
# ایجاد اسلات زمانی آزاد عمومی پیشفرض
|
| 547 |
+
now = get_now()
|
| 548 |
+
slot = TimeSlot(
|
| 549 |
+
counselor_id=new_user.id,
|
| 550 |
+
start_time=now + timedelta(days=1, hours=10),
|
| 551 |
+
end_time=now + timedelta(days=1, hours=11),
|
| 552 |
+
is_booked=False
|
| 553 |
+
)
|
| 554 |
+
db.add(slot)
|
| 555 |
+
|
| 556 |
+
elif u_data["role"] == "school_representative":
|
| 557 |
+
s = SchoolProfile(
|
| 558 |
+
user_id=new_user.id,
|
| 559 |
+
name="موسسه اندیشه نوین",
|
| 560 |
+
subscription_active_until=get_now() + timedelta(days=60),
|
| 561 |
+
is_sponsored=True,
|
| 562 |
+
bio="برترین آکادمی تستی کنکور با کادر اساتید برجسته بومی",
|
| 563 |
+
address="خیابان ولیعصر، پلاک ۱۲",
|
| 564 |
+
phone_number="02122222222",
|
| 565 |
+
founding_year=1395
|
| 566 |
+
)
|
| 567 |
+
db.add(s)
|
| 568 |
+
db.flush()
|
| 569 |
+
|
| 570 |
+
course = SchoolCourse(
|
| 571 |
+
school_id=s.id,
|
| 572 |
+
course_name="شیمی تخصصی جامع",
|
| 573 |
+
teacher_name="استاد احمدی",
|
| 574 |
+
teaching_style="روشهای تستی سرعتی"
|
| 575 |
+
)
|
| 576 |
+
db.add(course)
|
| 577 |
+
|
| 578 |
+
db.commit()
|
| 579 |
+
logger.info("Check-Seeding process successfully verified and updated!")
|
| 580 |
+
except Exception as e:
|
| 581 |
+
logger.error(f"Seeding process crashed: {e}", exc_info=True)
|
| 582 |
+
db.rollback()
|
| 583 |
+
|
| 584 |
|
| 585 |
@app.exception_handler(Exception)
|
| 586 |
async def global_exception_handler(request: Request, exc: Exception):
|
|
|
|
| 591 |
)
|
| 592 |
|
| 593 |
# ==========================================
|
| 594 |
+
# بخش دهم: کلیه آدرسهای APIها (مراحل ۱ تا ۴)
|
| 595 |
# ==========================================
|
| 596 |
|
| 597 |
@app.post("/api/auth/register")
|