humanvprojectceo commited on
Commit
e4e412d
·
verified ·
1 Parent(s): 5416ff5

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +649 -285
app.py CHANGED
@@ -12,7 +12,7 @@ from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
12
  from fastapi.responses import JSONResponse, FileResponse
13
  from fastapi.staticfiles import StaticFiles
14
  from pydantic import BaseModel, EmailStr
15
- from sqlalchemy import create_engine, Column, Integer, String, Boolean, Float, DateTime, ForeignKey, Text, and_
16
  from sqlalchemy.orm import relationship, sessionmaker, declarative_base, Session
17
  from jose import JWTError, jwt
18
  from google import genai
@@ -38,17 +38,15 @@ try:
38
  if "sqlite" in DATABASE_URL:
39
  engine = create_engine(DATABASE_URL, connect_args={"check_same_thread": False})
40
  else:
41
- # تنظیمات اتصال ابری پایدار برای جلوگیری از قطعی‌های اتصال موقت Neon
42
  engine = create_engine(
43
  DATABASE_URL,
44
- pool_size=10,
45
- max_overflow=20,
46
  pool_recycle=1800,
47
  pool_pre_ping=True
48
  )
49
  except Exception as db_init_err:
50
  logger.critical(f"Database engine creation failed: {db_init_err}", exc_info=True)
51
- # سوئیچ خودکار به پایگاه داده موقت محلی جهت پایداری ۱۰۰٪ لود کانتینر
52
  engine = create_engine("sqlite:///./fallback.db", connect_args={"check_same_thread": False})
53
 
54
  SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
@@ -62,6 +60,32 @@ security = HTTPBearer()
62
  def get_now():
63
  return datetime.now(timezone.utc).replace(tzinfo=None)
64
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
65
  # ==========================================
66
  # پایگاه داده مرجع محاسبات انتخاب رشته RAG
67
  # ==========================================
@@ -85,10 +109,13 @@ class User(Base):
85
  id = Column(Integer, primary_key=True, index=True)
86
  email = Column(String, unique=True, index=True, nullable=False)
87
  hashed_password = Column(String, nullable=False)
88
- role = Column(String, default="student")
89
  is_verified = Column(Boolean, default=False)
90
  balance = Column(Float, default=0.0)
91
  rating = Column(Float, default=5.0)
 
 
 
92
 
93
  counselor_profile = relationship("CounselorProfile", back_populates="user", uselist=False, cascade="all, delete-orphan")
94
  school_profile = relationship("SchoolProfile", back_populates="user", uselist=False, cascade="all, delete-orphan")
@@ -100,6 +127,7 @@ class CounselorProfile(Base):
100
  user_id = Column(Integer, ForeignKey("users.id"))
101
  specialty = Column(String, nullable=True)
102
  resume = Column(Text, nullable=True)
 
103
  hourly_rate = Column(Float, default=100000.0)
104
 
105
  user = relationship("User", back_populates="counselor_profile")
@@ -112,6 +140,10 @@ class SchoolProfile(Base):
112
  name = Column(String, nullable=True)
113
  subscription_active_until = Column(DateTime, nullable=True)
114
  is_sponsored = Column(Boolean, default=False)
 
 
 
 
115
 
116
  user = relationship("User", back_populates="school_profile")
117
  courses = relationship("SchoolCourse", back_populates="school", cascade="all, delete-orphan")
@@ -128,6 +160,24 @@ class SchoolCourse(Base):
128
  school = relationship("SchoolProfile", back_populates="courses")
129
 
130
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
131
  class TimeSlot(Base):
132
  __tablename__ = "time_slots"
133
  id = Column(Integer, primary_key=True, index=True)
@@ -145,9 +195,32 @@ class Booking(Base):
145
  student_id = Column(Integer, ForeignKey("users.id"))
146
  counselor_id = Column(Integer, ForeignKey("users.id"))
147
  status = Column(String, default="active") # 'active', 'cancelled'
 
 
148
  cost = Column(Float, default=0.0)
149
 
150
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
151
  class TestResult(Base):
152
  __tablename__ = "test_results"
153
  id = Column(Integer, primary_key=True, index=True)
@@ -175,6 +248,18 @@ class MajorSelection(Base):
175
  finalized_list = Column(Text, nullable=True)
176
  status = Column(String, default="draft")
177
 
 
 
 
 
 
 
 
 
 
 
 
 
178
  # ==========================================
179
  # توابع رمزنگاری مستقیم با Bcrypt
180
  # ==========================================
@@ -196,6 +281,7 @@ def verify_password(plain_password: str, hashed_password: str) -> bool:
196
 
197
  def get_db():
198
  db = SessionLocal()
 
199
  try:
200
  yield db
201
  finally:
@@ -228,7 +314,7 @@ def get_current_user(db: Session = Depends(get_db), credentials: HTTPAuthorizati
228
  return user
229
 
230
  # ==========================================
231
- # توابع تحلیل بومی و پردازش ابری (پنهان‌سازی هویت هوش مصنوعی)
232
  # ==========================================
233
 
234
  def _execute_native_matrix_processing(prompt: str, model_type: str = "fast") -> str:
@@ -252,29 +338,30 @@ def core_selection_engine(rank: int, quota: str, favorite_cities: str) -> str:
252
  context_items = []
253
  for u in KNOWLEDGE_BASE:
254
  context_items.append(
255
- f"- {u['university']} (سطح {u['ranking_tier']}): رشته‌های {', '.join(u['fields'])} "
256
- f"با رتبه حدودی منطقه یک زیر {u['minimum_rank_r1']} و منطقه دو زیر {u['minimum_rank_r2']}"
257
  )
258
  context_str = "\n".join(context_items)
259
 
 
260
  prompt = f"""
261
- شما یک هسته بهینه‌سازی انتخاب رشته بر اساس کدهای آماری پذیرش در کنکور ایران هستید.
262
- ورودی داوطلب:
263
- - رتبه: {rank}
264
- - سهمیه: {quota}
265
- - شهرهای درخواستی: {favorite_cities}
266
 
267
- پایگاه داده الگوها:
268
  {context_str}
269
 
270
- یک سناریوی پیشنهادی شامل حداکثر 8 انتخاب خوش‌بینانه و منطقی به صورت ساختار یافته بنویسید.
271
- خروجی را صرفا به صورت یک آرایه معتبر JSON بدون هیچگونه تگ مارک‌داون برگردانید:
272
  [
273
  {{
274
- "college": "دانشگاه پیشنهادی",
275
- "major": "رشته پیشنهادی",
276
  "priority": 1,
277
- "reason": "توضیح محاسباتی در کمال اختصار"
278
  }}
279
  ]
280
  """
@@ -300,6 +387,21 @@ def core_diagnostic_analysis(answers: str) -> str:
300
  return "ارزیابی اولیه: داوطلب تمایل قوی به ساختارهای مفهومی دارد؛ پیشنهاد می‌شود روی زمان‌بندی تست‌های سرعتی متمرکز شود."
301
  return result
302
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
303
  # ==========================================
304
  # تعریف کلاس‌های ورودی Pydantic
305
  # ==========================================
@@ -307,11 +409,13 @@ def core_diagnostic_analysis(answers: str) -> str:
307
  class RegisterRequest(BaseModel):
308
  email: EmailStr
309
  password: str
310
- role: str # 'student', 'counselor', 'school_representative'
311
  name: Optional[str] = None
312
  specialty: Optional[str] = None
313
  resume: Optional[str] = None
314
  hourly_rate: Optional[float] = 100000.0
 
 
315
 
316
  class LoginRequest(BaseModel):
317
  email: EmailStr
@@ -341,6 +445,10 @@ class CourseInput(BaseModel):
341
  class SchoolProfileUpdateRequest(BaseModel):
342
  name: str
343
  courses: List[CourseInput]
 
 
 
 
344
 
345
  class SchoolSubscribeRequest(BaseModel):
346
  months: int
@@ -353,6 +461,47 @@ class CreateSlotInput(BaseModel):
353
  class ApproveSelectionRequest(BaseModel):
354
  finalized_list_json: str
355
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
356
  # ==========================================
357
  # راه‌اندازی و اجرای وب‌سرویس پایتونی FastAPI
358
  # ==========================================
@@ -371,9 +520,9 @@ async def lifespan(app: FastAPI):
371
  yield
372
 
373
  app = FastAPI(
374
- title="پلتفرم خدمات هوشمند تحصیلی",
375
- description="وب‌سرویس عملیاتی لایه‌های اتصال کلاینت ری‌اکت و سرور پایتون",
376
- version="1.2.0",
377
  lifespan=lifespan
378
  )
379
 
@@ -385,56 +534,78 @@ app.add_middleware(
385
  allow_headers=["*"],
386
  )
387
 
 
 
 
388
  def seed_initial_data(db: Session):
389
  try:
390
  test_pwd = get_password_hash("demo1234")
391
 
392
- student = User(email="student@demo.com", hashed_password=test_pwd, role="student", is_verified=True, balance=300000.0)
 
393
  db.add(student)
394
 
395
- counselor = User(email="counselor@demo.com", hashed_password=test_pwd, role="counselor", is_verified=True, rating=4.9)
396
- db.add(counselor)
 
 
 
397
  db.flush()
398
 
399
- c_profile = CounselorProfile(user_id=counselor.id, specialty="مشاوره تخصصی رتبههای برتر", resume="رتبه دو رقمی سال‌های گذشته سراسری", hourly_rate=120000.0)
400
- db.add(c_profile)
401
-
402
- now = get_now()
403
- slot1 = TimeSlot(counselor_id=counselor.id, start_time=now + timedelta(days=1, hours=9), end_time=now + timedelta(days=1, hours=10), is_booked=False)
404
- slot2 = TimeSlot(counselor_id=counselor.id, start_time=now + timedelta(days=2, hours=15), end_time=now + timedelta(days=2, hours=16), is_booked=False)
405
- db.add(slot1)
406
- db.add(slot2)
407
 
408
- school_user = User(email="school@demo.com", hashed_password=test_pwd, role="school_representative", is_verified=True)
 
409
  db.add(school_user)
410
  db.flush()
411
 
412
- s_profile = SchoolProfile(user_id=school_user.id, name="آموزشگاه قلم برتر", subscription_active_until=now + timedelta(days=30), is_sponsored=True)
 
 
 
 
 
 
 
 
 
413
  db.add(s_profile)
414
  db.flush()
415
 
416
- course = SchoolCourse(school_id=s_profile.id, course_name="فیزیک جامع کنکور", teacher_name="استاد رضایی", teaching_style="روش‌های سرعتی تست‌زنی مفهومی")
417
  db.add(course)
418
 
 
 
 
 
 
419
  db.commit()
420
  except Exception as e:
421
  logger.error(f"Seeding process crashed: {e}", exc_info=True)
422
  db.rollback()
423
 
424
- # =======================================================
425
- # سیستم مدیریت استثناهای گلوبال (پنهان‌سازی کدهای خطا از کاربر)
426
- # =======================================================
427
  @app.exception_handler(Exception)
428
  async def global_exception_handler(request: Request, exc: Exception):
429
  logger.error(f"Internal Crash logged on {request.url.path}: {exc}", exc_info=True)
430
  return JSONResponse(
431
  status_code=500,
432
- content={"detail": "سرور موقتاً شلوغ است. لطفاً چند لحظه دیگر مجدداً تلاش کنید."}
433
  )
434
 
435
- # ==========================================
436
- # فرآیند ۱: سیستم احراز هویت (Auth Process)
437
- # ==========================================
 
 
 
 
 
 
438
 
439
  @app.post("/api/auth/register")
440
  def auth_register(data: RegisterRequest, db: Session = Depends(get_db)):
@@ -450,7 +621,9 @@ def auth_register(data: RegisterRequest, db: Session = Depends(get_db)):
450
  email=data.email,
451
  hashed_password=hashed,
452
  role=data.role,
453
- is_verified=is_verified_init
 
 
454
  )
455
  db.add(new_user)
456
  db.flush()
@@ -482,7 +655,7 @@ def auth_register(data: RegisterRequest, db: Session = Depends(get_db)):
482
  except Exception as e:
483
  logger.error(f"Error during registration endpoint: {e}", exc_info=True)
484
  db.rollback()
485
- raise HTTPException(status_code=503, detail="سرور موقتاً شلوغ است؛ لطفاً دقایقی دیگر مجدداً تلاش نمایید.")
486
 
487
 
488
  @app.post("/api/auth/login")
@@ -490,7 +663,7 @@ def auth_login(data: LoginRequest, db: Session = Depends(get_db)):
490
  try:
491
  user = db.query(User).filter(User.email == data.email).first()
492
  if not user or not verify_password(data.password, user.hashed_password):
493
- raise HTTPException(status_code=400, detail="پست الکترونیکی یا رمز عبور اشتباه است.")
494
 
495
  token = create_access_token(data={"sub": user.email})
496
  return {
@@ -506,16 +679,63 @@ def auth_login(data: LoginRequest, db: Session = Depends(get_db)):
506
  logger.error(f"Error during login endpoint: {e}", exc_info=True)
507
  raise HTTPException(status_code=503, detail="سرور موقتاً شلوغ است. لطفاً بعداً تلاش کنید.")
508
 
509
- # ==========================================
510
- # فرآیند ۲: سیستم مشاوران و نوبت‌دهی جلسات حضوری
511
- # ==========================================
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
512
 
513
  @app.get("/api/counselors")
514
- def list_active_counselors(specialty: Optional[str] = None, db: Session = Depends(get_db)):
515
  try:
516
  query = db.query(User).filter(User.role == "counselor", User.is_verified == True)
517
- if specialty:
518
- query = query.join(CounselorProfile).filter(CounselorProfile.specialty.ilike(f"%{specialty}%"))
 
 
 
 
 
 
 
519
 
520
  counselors = query.all()
521
  output = []
@@ -524,8 +744,10 @@ def list_active_counselors(specialty: Optional[str] = None, db: Session = Depend
524
  "counselor_id": c.id,
525
  "email": c.email,
526
  "rating": c.rating,
 
527
  "specialty": c.counselor_profile.specialty if c.counselor_profile else None,
528
  "resume": c.counselor_profile.resume if c.counselor_profile else None,
 
529
  "hourly_rate": c.counselor_profile.hourly_rate if c.counselor_profile else 0.0
530
  })
531
  return output
@@ -533,260 +755,393 @@ def list_active_counselors(specialty: Optional[str] = None, db: Session = Depend
533
  logger.error(f"Error listing counselors: {e}", exc_info=True)
534
  raise HTTPException(status_code=503, detail="سرور موقتاً شلوغ است. لطفاً بعداً تلاش کنید.")
535
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
536
 
537
- @app.get("/api/counselors/{counselor_id}/slots")
538
- def list_counselor_available_slots(counselor_id: int, db: Session = Depends(get_db)):
539
  try:
540
- slots = db.query(TimeSlot).filter(
541
- TimeSlot.counselor_id == counselor_id,
542
- TimeSlot.is_booked == False
 
543
  ).all()
544
- return slots
 
 
 
 
 
 
 
 
 
 
545
  except Exception as e:
546
- logger.error(f"Error listing slots: {e}", exc_info=True)
547
  raise HTTPException(status_code=503, detail="سرور موقتاً شلوغ است. لطفاً بعداً تلاش کنید.")
548
 
549
 
550
- @app.post("/api/bookings")
551
- def book_session(data: BookSlotRequest, current_user: User = Depends(get_current_user), db: Session = Depends(get_db)):
552
  try:
553
- if current_user.role != "student":
554
- raise HTTPException(status_code=403, detail="تنها کاربران با نقش داوطلب مجاز به رزرو هستند.")
 
 
 
 
 
 
555
 
556
- slot = db.query(TimeSlot).filter(TimeSlot.id == data.slot_id).first()
557
- if not slot or slot.is_booked:
558
- raise HTTPException(status_code=400, detail=ین زمان قبلاً رزرو شده است یا در دسترس نیست.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
559
 
560
- counselor = db.query(User).filter(User.id == slot.counselor_id).first()
561
- if not counselor or not counselor.counselor_profile:
562
- raise HTTPException(status_code=404, detail="مشاور انتخابی در سیستم یافت نشد.")
 
 
 
 
563
 
564
- cost = counselor.counselor_profile.hourly_rate
565
- if current_user.balance < cost:
566
- raise HTTPException(status_code=400, detail="موجودی حساب کافی نیست. لطفاً کیف پول خود را شارژ نمایید.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
567
 
568
- current_user.balance -= cost
569
- counselor.balance += cost
570
-
571
- slot.is_booked = True
572
- slot.booked_by_student_id = current_user.id
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
573
 
 
574
  booking = Booking(
575
  slot_id=slot.id,
576
- student_id=current_user.id,
577
- counselor_id=counselor.id,
578
  status="active",
579
- cost=cost
 
 
580
  )
581
  db.add(booking)
582
  db.commit()
583
 
584
  return {
585
- "message": "رزرو جلسه با موفقیت تایید و هزینه از تراز مالی کسر شد.",
586
- "remaining_balance": current_user.balance,
587
- "session_start": slot.start_time
588
  }
589
  except HTTPException:
590
  raise
591
  except Exception as e:
592
- logger.error(f"Error during booking creation: {e}", exc_info=True)
593
  db.rollback()
594
  raise HTTPException(status_code=503, detail="سرور موقتاً شلوغ است. لطفاً بعداً تلاش کنید.")
595
 
596
 
597
- @app.post("/api/bookings/{booking_id}/cancel")
598
- def cancel_session(booking_id: int, current_user: User = Depends(get_current_user), db: Session = Depends(get_db)):
 
599
  try:
600
- booking = db.query(Booking).filter(Booking.id == booking_id).first()
601
- if not booking:
602
- raise HTTPException(status_code=404, detail="رکورد رزرو پیدا نشد.")
603
 
604
- if booking.status == "cancelled":
605
- raise HTTPException(status_code=400, detail="این جلسه قبلاً لغو شده است.")
 
 
 
 
 
606
 
607
- if current_user.role != "student" or booking.student_id != current_user.id:
608
- raise HTTPException(status_code=403, detail="شما مجاز به لغو این نوبت نیستید.")
609
 
610
- slot = db.query(TimeSlot).filter(TimeSlot.id == booking.slot_id).first()
611
- if slot:
612
- slot.is_booked = False
613
- slot.booked_by_student_id = None
614
 
615
- student = db.query(User).filter(User.id == booking.student_id).first()
616
  counselor = db.query(User).filter(User.id == booking.counselor_id).first()
617
 
618
- if student:
619
- student.balance += booking.cost
620
  if counselor:
621
- counselor.balance = max(0.0, counselor.balance - booking.cost)
622
 
623
- booking.status = "cancelled"
624
  db.commit()
625
 
626
  return {
627
- "message": "جلسه با موفقیت لغو گردید و مبلغ به کیف پول شما بازگشت داده شد.",
628
- "new_balance": student.balance if student else 0.0
629
  }
630
  except HTTPException:
631
  raise
632
  except Exception as e:
633
- logger.error(f"Error cancelling booking {booking_id}: {e}", exc_info=True)
634
  db.rollback()
635
  raise HTTPException(status_code=503, detail="سرور موقتاً شلوغ است. لطفاً بعداً تلاش کنید.")
636
 
637
- # ==========================================
638
- # فرآیند ۳: سیستم انتخاب رشته هوشمند (محاسبات)
639
- # ==========================================
640
 
641
- @app.post("/api/selection/select")
642
- def submit_konkur_rag_data(data: SubmitKonkurRequest, current_user: User = Depends(get_current_user), db: Session = Depends(get_db)):
643
  try:
644
- if current_user.role != "student":
645
- raise HTTPException(status_code=403, detail="دسترسی غیرمجاز")
646
-
647
- konkur = db.query(KonkurDetails).filter(KonkurDetails.student_id == current_user.id).first()
648
- if konkur:
649
- konkur.rank = data.rank
650
- konkur.quota = data.quota
651
- konkur.favorite_cities = data.favorite_cities
652
- else:
653
- konkur = KonkurDetails(
654
- student_id=current_user.id,
655
- rank=data.rank,
656
- quota=data.quota,
657
- favorite_cities=data.favorite_cities
658
- )
659
- db.add(konkur)
660
-
661
- selection_list_raw = core_selection_engine(data.rank, data.quota, data.favorite_cities)
662
-
663
- selection = db.query(MajorSelection).filter(
664
- MajorSelection.student_id == current_user.id,
665
- MajorSelection.counselor_id == data.counselor_id
666
- ).first()
667
 
668
- if selection:
669
- selection.generated_list = selection_list_raw
670
- selection.finalized_list = selection_list_raw
671
- selection.status = "draft"
672
- else:
673
- selection = MajorSelection(
674
- student_id=current_user.id,
675
- counselor_id=data.counselor_id,
676
- generated_list=selection_list_raw,
677
- finalized_list=selection_list_raw,
678
- status="draft"
679
- )
680
- db.add(selection)
681
-
682
  db.commit()
683
-
684
- try:
685
- parsed_selections = json.loads(selection_list_raw)
686
- except:
687
- parsed_selections = selection_list_raw
688
-
689
- return {
690
- "message": "پیش‌نویس اولیه با موفقیت آماده و برای مشاور ارسال شد.",
691
- "selections": parsed_selections,
692
- "selection_id": selection.id
693
- }
694
- except HTTPException:
695
- raise
696
  except Exception as e:
697
- logger.error(f"Error submitting konkur details: {e}", exc_info=True)
698
  db.rollback()
699
  raise HTTPException(status_code=503, detail="سرور موقتاً شلوغ است. لطفاً بعداً تلاش کنید.")
700
 
701
 
702
- @app.put("/api/selection/{selection_id}/approve")
703
- def edit_and_approve_selection_list(selection_id: int, data: ApproveSelectionRequest, current_user: User = Depends(get_current_user), db: Session = Depends(get_db)):
704
  try:
705
  if current_user.role != "counselor":
706
- raise HTTPException(status_code=403, detail="تنها مشاوران مجاز به اعمال ویرایش هستند.")
707
 
708
- selection = db.query(MajorSelection).filter(
709
- MajorSelection.id == selection_id,
710
- MajorSelection.counselor_id == current_user.id
711
  ).first()
712
-
713
- if not selection:
714
- raise HTTPException(status_code=404, detail="لیست پیش‌نویس مورد نظر یافت نشد.")
715
-
716
- try:
717
- json.loads(data.finalized_list_json)
718
- except ValueError:
719
- raise HTTPException(status_code=400, detail="فرمت ساختار اولویت‌ها نامعتبر است.")
720
-
721
- selection.finalized_list = data.finalized_list_json
722
- selection.status = "approved_by_counselor"
723
  db.commit()
724
-
725
- return {"message": "لیست نهایی داوطلب ویرایش و تایید گردید."}
726
  except HTTPException:
727
  raise
728
  except Exception as e:
729
- logger.error(f"Error during counselor list approval: {e}", exc_info=True)
730
  db.rollback()
731
  raise HTTPException(status_code=503, detail="سرور موقتاً شلوغ است. لطفاً بعداً تلاش کنید.")
732
 
733
 
734
- @app.get("/api/student/major-selection")
735
- def get_student_selection_finalized_status(current_user: User = Depends(get_current_user), db: Session = Depends(get_db)):
736
  try:
737
  if current_user.role != "student":
738
  raise HTTPException(status_code=403, detail="دسترسی غیرمجاز")
739
 
740
- selection = db.query(MajorSelection).filter(MajorSelection.student_id == current_user.id).order_by(MajorSelection.id.desc()).first()
741
- if not selection:
742
- raise HTTPException(status_code=404, detail="پیش‌نویسی ثبت نشده است.")
743
-
744
- try:
745
- generated = json.loads(selection.generated_list) if selection.generated_list else []
746
- finalized = json.loads(selection.finalized_list) if selection.finalized_list else []
747
- except:
748
- generated = selection.generated_list
749
- finalized = selection.finalized_list
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
750
 
751
- return {
752
- "selection_id": selection.id,
753
- "status": selection.status,
754
- "ai_generated_list": generated,
755
- "counselor_finalized_list": finalized
756
- }
757
  except HTTPException:
758
  raise
759
  except Exception as e:
760
- logger.error(f"Error fetching student selection list: {e}", exc_info=True)
 
761
  raise HTTPException(status_code=503, detail="سرور موقتاً شلوغ است. لطفاً بعداً تلاش کنید.")
762
 
763
- # ==========================================
764
- # فرآیند ۴: دایرکتوری (B2B) آموزشگاه‌ها
765
- # ==========================================
766
 
767
  @app.get("/api/schools")
768
- def search_and_filter_schools(course: Optional[str] = None, teacher: Optional[str] = None, style: Optional[str] = None, db: Session = Depends(get_db)):
769
  try:
770
  now = get_now()
 
771
  query = db.query(SchoolProfile).filter(SchoolProfile.subscription_active_until > now)
772
-
773
- if course or teacher or style:
774
- query = query.join(SchoolCourse)
775
- if course:
776
- query = query.filter(SchoolCourse.course_name.ilike(f"%{course}%"))
777
- if teacher:
778
- query = query.filter(SchoolCourse.teacher_name.ilike(f"%{teacher}%"))
779
- if style:
780
- query = query.filter(SchoolCourse.teaching_style.ilike(f"%{style}%"))
781
-
782
  schools = query.order_by(SchoolProfile.is_sponsored.desc()).all()
783
-
784
  output = []
785
  for s in schools:
786
  output.append({
787
  "school_id": s.id,
788
  "school_name": s.name,
789
  "is_sponsored": s.is_sponsored,
 
 
 
 
 
790
  "courses": [
791
  {
792
  "course_name": c.course_name,
@@ -797,9 +1152,72 @@ def search_and_filter_schools(course: Optional[str] = None, teacher: Optional[st
797
  })
798
  return output
799
  except Exception as e:
800
- logger.error(f"Error querying schools directory: {e}", exc_info=True)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
801
  raise HTTPException(status_code=503, detail="سرور موقتاً شلوغ است. لطفاً بعداً تلاش کنید.")
802
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
803
 
804
  @app.post("/api/schools/update")
805
  def update_school_course_portfolio(data: SchoolProfileUpdateRequest, current_user: User = Depends(get_current_user), db: Session = Depends(get_db)):
@@ -809,11 +1227,22 @@ def update_school_course_portfolio(data: SchoolProfileUpdateRequest, current_use
809
 
810
  profile = db.query(SchoolProfile).filter(SchoolProfile.user_id == current_user.id).first()
811
  if not profile:
812
- profile = SchoolProfile(user_id=current_user.id, name=data.name)
 
 
 
 
 
 
 
813
  db.add(profile)
814
  db.flush()
815
  else:
816
  profile.name = data.name
 
 
 
 
817
 
818
  db.query(SchoolCourse).filter(SchoolCourse.school_id == profile.id).delete()
819
 
@@ -827,7 +1256,7 @@ def update_school_course_portfolio(data: SchoolProfileUpdateRequest, current_use
827
  db.add(db_course)
828
 
829
  db.commit()
830
- return {"message": "رزومه کادر اساتید با موفقیت به روز گردید."}
831
  except HTTPException:
832
  raise
833
  except Exception as e:
@@ -867,71 +1296,6 @@ def purchase_school_subscription(data: SchoolSubscribeRequest, current_user: Use
867
  db.rollback()
868
  raise HTTPException(status_code=503, detail="سرور موقتاً شلوغ است. لطفاً بعداً تلاش کنید.")
869
 
870
- # ==========================================
871
- # سیستم تسویه‌ها و پردازش‌های دوره‌ای خودکار (Cron Jobs)
872
- # ==========================================
873
-
874
- @app.post("/api/cron/deactivate-inactive-schools")
875
- def cron_deactivate_expired_sponsorships(db: Session = Depends(get_db)):
876
- try:
877
- now = get_now()
878
- expired_profiles = db.query(SchoolProfile).filter(
879
- and_(SchoolProfile.subscription_active_until <= now, SchoolProfile.is_sponsored == True)
880
- ).all()
881
-
882
- affected = 0
883
- for profile in expired_profiles:
884
- profile.is_sponsored = False
885
- affected += 1
886
-
887
- db.commit()
888
- return {"status": "success", "deactivated_sponsorship_count": affected}
889
- except Exception as e:
890
- logger.error(f"Cron deactivation task failed: {e}", exc_info=True)
891
- db.rollback()
892
- raise HTTPException(status_code=503, detail="سرور موقتاً شلوغ است. لطفاً بعداً تلاش کنید.")
893
-
894
-
895
- @app.post("/api/cron/settle-counselors")
896
- def cron_settle_payments_and_assess_ratings(db: Session = Depends(get_db)):
897
- try:
898
- weak_profiles = db.query(User).filter(
899
- User.role == "counselor",
900
- User.rating < 2.5,
901
- User.is_verified == True
902
- ).all()
903
-
904
- suspended = 0
905
- for counselor in weak_profiles:
906
- counselor.is_verified = False
907
- suspended += 1
908
-
909
- active_settlements = db.query(User).filter(
910
- User.role == "counselor",
911
- User.balance > 0
912
- ).all()
913
-
914
- settlements = []
915
- for c in active_settlements:
916
- settlements.append({
917
- "id": c.id,
918
- "email": c.email,
919
- "amount": c.balance
920
- })
921
- c.balance = 0.0
922
-
923
- db.commit()
924
- return {
925
- "status": "success",
926
- "suspended_profiles_count": suspended,
927
- "processed_payouts": settlements
928
- }
929
- except Exception as e:
930
- logger.error(f"Cron settlement task failed: {e}", exc_info=True)
931
- db.rollback()
932
- raise HTTPException(status_code=503, detail="سرور موقتاً شلوغ است. لطفاً بعداً تلاش کنید.")
933
-
934
- # --- مدیریت متفرقه دانشجو ---
935
 
936
  @app.post("/api/student/topup")
937
  def top_up_student_wallet(data: TopUpRequest, current_user: User = Depends(get_current_user), db: Session = Depends(get_db)):
 
12
  from fastapi.responses import JSONResponse, FileResponse
13
  from fastapi.staticfiles import StaticFiles
14
  from pydantic import BaseModel, EmailStr
15
+ from sqlalchemy import create_engine, Column, Integer, String, Boolean, Float, DateTime, ForeignKey, Text, and_, or_
16
  from sqlalchemy.orm import relationship, sessionmaker, declarative_base, Session
17
  from jose import JWTError, jwt
18
  from google import genai
 
38
  if "sqlite" in DATABASE_URL:
39
  engine = create_engine(DATABASE_URL, connect_args={"check_same_thread": False})
40
  else:
 
41
  engine = create_engine(
42
  DATABASE_URL,
43
+ pool_size=15,
44
+ max_overflow=25,
45
  pool_recycle=1800,
46
  pool_pre_ping=True
47
  )
48
  except Exception as db_init_err:
49
  logger.critical(f"Database engine creation failed: {db_init_err}", exc_info=True)
 
50
  engine = create_engine("sqlite:///./fallback.db", connect_args={"check_same_thread": False})
51
 
52
  SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
 
60
  def get_now():
61
  return datetime.now(timezone.utc).replace(tzinfo=None)
62
 
63
+ # ==========================================
64
+ # مکانیزم پاک‌سازی اتوماتیک نوبت‌های منقضی شده ۲۴ ساعته
65
+ # ==========================================
66
+ def clean_expired_bookings(db: Session):
67
+ """
68
+ بررسی مداوم و خودکار نوبت‌های رزرو دستی مشاور که داوطلب ظرف ۲۴ ساعت آن‌ها را پرداخت نکرده است.
69
+ آزادسازی زمان کاری بدون نیاز به تنظیمات کرون‌جاب خارجی.
70
+ """
71
+ try:
72
+ now = get_now()
73
+ expired_bookings = db.query(Booking).filter(
74
+ Booking.payment_status == "pending_payment",
75
+ Booking.payment_due_until < now
76
+ ).all()
77
+
78
+ for b in expired_bookings:
79
+ b.payment_status = "expired"
80
+ slot = db.query(TimeSlot).filter(TimeSlot.id == b.slot_id).first()
81
+ if slot:
82
+ slot.is_booked = False
83
+ slot.booked_by_student_id = None
84
+ db.commit()
85
+ except Exception as e:
86
+ logger.error(f"Error during clean_expired_bookings: {e}")
87
+ db.rollback()
88
+
89
  # ==========================================
90
  # پایگاه داده مرجع محاسبات انتخاب رشته RAG
91
  # ==========================================
 
109
  id = Column(Integer, primary_key=True, index=True)
110
  email = Column(String, unique=True, index=True, nullable=False)
111
  hashed_password = Column(String, nullable=False)
112
+ role = Column(String, default="student") # 'student', 'counselor', 'school_representative'
113
  is_verified = Column(Boolean, default=False)
114
  balance = Column(Float, default=0.0)
115
  rating = Column(Float, default=5.0)
116
+ phone_number = Column(String, nullable=True) # جهت فیلتر و جستجوی داوطلبان
117
+ city = Column(String, nullable=True) # جهت فیلترینگ نام و شهر
118
+ profile_pic = Column(String, nullable=True)
119
 
120
  counselor_profile = relationship("CounselorProfile", back_populates="user", uselist=False, cascade="all, delete-orphan")
121
  school_profile = relationship("SchoolProfile", back_populates="user", uselist=False, cascade="all, delete-orphan")
 
127
  user_id = Column(Integer, ForeignKey("users.id"))
128
  specialty = Column(String, nullable=True)
129
  resume = Column(Text, nullable=True)
130
+ bio = Column(Text, nullable=True) # بیوگرافی قابل اصلاح
131
  hourly_rate = Column(Float, default=100000.0)
132
 
133
  user = relationship("User", back_populates="counselor_profile")
 
140
  name = Column(String, nullable=True)
141
  subscription_active_until = Column(DateTime, nullable=True)
142
  is_sponsored = Column(Boolean, default=False)
143
+ bio = Column(Text, nullable=True)
144
+ address = Column(Text, nullable=True)
145
+ phone_number = Column(String, nullable=True)
146
+ founding_year = Column(Integer, nullable=True)
147
 
148
  user = relationship("User", back_populates="school_profile")
149
  courses = relationship("SchoolCourse", back_populates="school", cascade="all, delete-orphan")
 
160
  school = relationship("SchoolProfile", back_populates="courses")
161
 
162
 
163
+ class SchoolTeacher(Base):
164
+ """اساتید بومی مستقیما اضافه شده توسط آموزشگاه"""
165
+ __tablename__ = "school_teachers"
166
+ id = Column(Integer, primary_key=True, index=True)
167
+ school_id = Column(Integer, ForeignKey("school_profiles.id"))
168
+ name = Column(String, nullable=False)
169
+ bio = Column(Text, nullable=True)
170
+
171
+
172
+ class SchoolCounselorAssociation(Base):
173
+ """همبستگی و ریکوئست‌های متقابل مشاوران و آموزشگاه‌ها"""
174
+ __tablename__ = "school_counselor_associations"
175
+ id = Column(Integer, primary_key=True, index=True)
176
+ school_id = Column(Integer, ForeignKey("school_profiles.id"))
177
+ counselor_id = Column(Integer, ForeignKey("users.id"))
178
+ status = Column(String, default="pending") # 'pending', 'approved', 'rejected'
179
+
180
+
181
  class TimeSlot(Base):
182
  __tablename__ = "time_slots"
183
  id = Column(Integer, primary_key=True, index=True)
 
195
  student_id = Column(Integer, ForeignKey("users.id"))
196
  counselor_id = Column(Integer, ForeignKey("users.id"))
197
  status = Column(String, default="active") # 'active', 'cancelled'
198
+ payment_status = Column(String, default="paid") # 'paid', 'pending_payment', 'expired'
199
+ payment_due_until = Column(DateTime, nullable=True) # بازه زمانی ۲۴ ساعت جهت لغو در صورت عدم پرداخت
200
  cost = Column(Float, default=0.0)
201
 
202
 
203
+ class CustomQuiz(Base):
204
+ """طراحی آزمون‌های انتخابی جلسه توسط مشاور"""
205
+ __tablename__ = "custom_quizzes"
206
+ id = Column(Integer, primary_key=True, index=True)
207
+ counselor_id = Column(Integer, ForeignKey("users.id"))
208
+ title = Column(String, nullable=False)
209
+ questions_json = Column(Text, nullable=False) # کدهای چند گزینه‌ای به صورت ساختار یافته
210
+
211
+
212
+ class StudentQuizAssignment(Base):
213
+ """تخصیص آزمون به داوطلب قبل از شروع هر جلسه اختیاری"""
214
+ __tablename__ = "student_quiz_assignments"
215
+ id = Column(Integer, primary_key=True, index=True)
216
+ quiz_id = Column(Integer, ForeignKey("custom_quizzes.id"))
217
+ student_id = Column(Integer, ForeignKey("users.id"))
218
+ booking_id = Column(Integer, ForeignKey("bookings.id"))
219
+ answers_json = Column(Text, nullable=True)
220
+ is_completed = Column(Boolean, default=False)
221
+ score = Column(Float, nullable=True)
222
+
223
+
224
  class TestResult(Base):
225
  __tablename__ = "test_results"
226
  id = Column(Integer, primary_key=True, index=True)
 
248
  finalized_list = Column(Text, nullable=True)
249
  status = Column(String, default="draft")
250
 
251
+
252
+ class Review(Base):
253
+ """سیستم ارزیابی و ثبت نظرات داوطلب برای جلسات مشاوره"""
254
+ __tablename__ = "reviews"
255
+ id = Column(Integer, primary_key=True, index=True)
256
+ student_id = Column(Integer, ForeignKey("users.id"))
257
+ booking_id = Column(Integer, ForeignKey("bookings.id"))
258
+ counselor_id = Column(Integer, ForeignKey("users.id"), nullable=True)
259
+ school_id = Column(Integer, ForeignKey("school_profiles.id"), nullable=True)
260
+ rating = Column(Float, default=5.0)
261
+ comment = Column(Text, nullable=True)
262
+
263
  # ==========================================
264
  # توابع رمزنگاری مستقیم با Bcrypt
265
  # ==========================================
 
281
 
282
  def get_db():
283
  db = SessionLocal()
284
+ clean_expired_bookings(db) # اجرای مکانیزم پاکسازی نوبت‌ها به صورت مستمر
285
  try:
286
  yield db
287
  finally:
 
314
  return user
315
 
316
  # ==========================================
317
+ # توابع پردازش هوشمند کدهای مدل زبانی
318
  # ==========================================
319
 
320
  def _execute_native_matrix_processing(prompt: str, model_type: str = "fast") -> str:
 
338
  context_items = []
339
  for u in KNOWLEDGE_BASE:
340
  context_items.append(
341
+ f"- {u['university']} (رتبه کیفی: {u['ranking_tier']}): مناسب برای حوزه‌های {', '.join(u['fields'])} "
342
+ f"با پذیرش رتبه منطقه یک حدود {u['minimum_rank_r1']} و منطقه دو حدود {u['minimum_rank_r2']}"
343
  )
344
  context_str = "\n".join(context_items)
345
 
346
+ # طراحی پرامپت با زبان صمیمی، ساده و فاقد عبارات پیچیده
347
  prompt = f"""
348
+ شما یک راهنما و مشاور تحصیلی دلسوز برای داوطلبان کنکور سراسری هستید.
349
+ اطلاعات داوطلب:
350
+ - رتبه داوطلب در کارنامه: {rank}
351
+ - سهمیه داوطلب: {quota}
352
+ - شهرهای مورد پسند: {favorite_cities}
353
 
354
+ پایگاه داده دانشگاه‌ها:
355
  {context_str}
356
 
357
+ بر اساس تجربیات مشاوره، یک لیست از منطقی‌ترین و خوش‌بینانه‌ترین انتخابها (حداکثر ۸ مورد) به زبان کاملاً ساده، روان، صمیمی و دانشجویی تولید کنید.
358
+ خروجی را صرفاً و بدون هیچگونه متن توضیحی اضافه در قالب یک آرایه معتبر JSON و فاقد کدهای مارک‌داون برگردانید:
359
  [
360
  {{
361
+ "college": "نام دانشگاه",
362
+ "major": "رشته تحصیلی",
363
  "priority": 1,
364
+ "reason": "دلیل انتخاب با لحنی بسیار صمیمی، صنف دانشجویی و بی‌آلایش"
365
  }}
366
  ]
367
  """
 
387
  return "ارزیابی اولیه: داوطلب تمایل قوی به ساختارهای مفهومی دارد؛ پیشنهاد می‌شود روی زمان‌بندی تست‌های سرعتی متمرکز شود."
388
  return result
389
 
390
+
391
+ def core_bio_extraction(resume_text: str) -> str:
392
+ """
393
+ استخراج خودکار بیوگرافی مشاور از روی فایل رزومه متنی
394
+ """
395
+ prompt = f"""
396
+ بر مبنای متن رزومه مشاور زیر، یک بیوگرافی روان، جذاب، خلاصه و متقاعدکننده (حداکثر ۷۰ کلمه به زبان فارسی و صمیمی) جهت درج در بخش توضیحات پروفایل او بنویسید.
397
+ متن رزومه:
398
+ {resume_text}
399
+ """
400
+ result = _execute_native_matrix_processing(prompt, "fast")
401
+ if result == "MODE_FALLBACK_ACTIVE":
402
+ return "کارشناس تحصیلی با سابقه رتبه‌پروری در کنکور و برنامه‌ریزی تخصصی داوطلبان گروه علوم تجربی و ریاضی."
403
+ return result
404
+
405
  # ==========================================
406
  # تعریف کلاس‌های ورودی Pydantic
407
  # ==========================================
 
409
  class RegisterRequest(BaseModel):
410
  email: EmailStr
411
  password: str
412
+ role: str
413
  name: Optional[str] = None
414
  specialty: Optional[str] = None
415
  resume: Optional[str] = None
416
  hourly_rate: Optional[float] = 100000.0
417
+ phone_number: Optional[str] = None
418
+ city: Optional[str] = None
419
 
420
  class LoginRequest(BaseModel):
421
  email: EmailStr
 
445
  class SchoolProfileUpdateRequest(BaseModel):
446
  name: str
447
  courses: List[CourseInput]
448
+ bio: Optional[str] = None
449
+ address: Optional[str] = None
450
+ phone_number: Optional[str] = None
451
+ founding_year: Optional[int] = None
452
 
453
  class SchoolSubscribeRequest(BaseModel):
454
  months: int
 
461
  class ApproveSelectionRequest(BaseModel):
462
  finalized_list_json: str
463
 
464
+ class ExtractedBioRequest(BaseModel):
465
+ resume_text: str
466
+
467
+ class ManualBookingRequest(BaseModel):
468
+ student_id: int
469
+ start_time: datetime
470
+ end_time: datetime
471
+ cost: float
472
+
473
+ class QuizCreateRequest(BaseModel):
474
+ title: str
475
+ questions_json: str # آرایه چند گزینه‌ای
476
+
477
+ class AssignQuizRequest(BaseModel):
478
+ quiz_id: int
479
+ student_id: int
480
+ booking_id: int
481
+
482
+ class SubmitQuizRequest(BaseModel):
483
+ answers_json: str
484
+
485
+ class ReviewCreateRequest(BaseModel):
486
+ booking_id: int
487
+ counselor_id: Optional[int] = None
488
+ school_id: Optional[int] = None
489
+ rating: float
490
+ comment: Optional[str] = None
491
+
492
+ class CounselorRequestInput(BaseModel):
493
+ counselor_id: int
494
+
495
+ class RequestRespondInput(BaseModel):
496
+ status: str # 'approved' or 'rejected'
497
+
498
+ class UpdateProfileRequest(BaseModel):
499
+ name: Optional[str] = None
500
+ email: Optional[EmailStr] = None
501
+ password: Optional[str] = None
502
+ phone_number: Optional[str] = None
503
+ city: Optional[str] = None
504
+
505
  # ==========================================
506
  # راه‌اندازی و اجرای وب‌سرویس پایتونی FastAPI
507
  # ==========================================
 
520
  yield
521
 
522
  app = FastAPI(
523
+ title="پلتفرم خدمات هوشمند تحصیلی تاپ دانش",
524
+ description="سیستم محاسباتی و ارزیابی تراز تحصیلی بومی",
525
+ version="1.3.0",
526
  lifespan=lifespan
527
  )
528
 
 
534
  allow_headers=["*"],
535
  )
536
 
537
+ # ==========================================
538
+ # ایجاد داده‌های تستی پیش‌فرض پلاتفرم
539
+ # ==========================================
540
  def seed_initial_data(db: Session):
541
  try:
542
  test_pwd = get_password_hash("demo1234")
543
 
544
+ # دانشجویان پیش‌فرض
545
+ student = User(email="student@demo.com", hashed_password=test_pwd, role="student", is_verified=True, balance=500000.0, phone_number="09121111111", city="تهران")
546
  db.add(student)
547
 
548
+ # مشاوران پیش‌فرض
549
+ counselor1 = User(email="counselor1@demo.com", hashed_password=test_pwd, role="counselor", is_verified=True, rating=4.8, city="تهران")
550
+ counselor2 = User(email="counselor2@demo.com", hashed_password=test_pwd, role="counselor", is_verified=True, rating=4.5, city="شیراز")
551
+ db.add(counselor1)
552
+ db.add(counselor2)
553
  db.flush()
554
 
555
+ p1 = CounselorProfile(user_id=counselor1.id, specialty="برنامه‌ریزی کنکور تجربی", resume="رتبه دو رقمی سراسری با سابقه تدریس تخصصی", bio="مشاور ارشد با تخصص رتبه‌پروری تجربی", hourly_rate=120000.0)
556
+ p2 = CounselorProfile(user_id=counselor2.id, specialty="هدایت تحصیلی ریاضی و فیزیک", resume="عضو بنیاد نخبگان ریاضی", bio="متخصص بهینه‌سازی مسیر قبولی رشته ریاضی", hourly_rate=100000.0)
557
+ db.add(p1)
558
+ db.add(p2)
 
 
 
 
559
 
560
+ # آموزشگاه‌های پیش‌فرض
561
+ school_user = User(email="school@demo.com", hashed_password=test_pwd, role="school_representative", is_verified=True, city="اصفهان")
562
  db.add(school_user)
563
  db.flush()
564
 
565
+ s_profile = SchoolProfile(
566
+ user_id=school_user.id,
567
+ name="موسسه کنکور اندیشه",
568
+ subscription_active_until=get_now() + timedelta(days=60),
569
+ is_sponsored=True,
570
+ bio="برترین آکادمی تستی کنکور اصفهان",
571
+ address="اصفهان، خیابان دانشگاه",
572
+ phone_number="03133333333",
573
+ founding_year=1395
574
+ )
575
  db.add(s_profile)
576
  db.flush()
577
 
578
+ course = SchoolCourse(school_id=s_profile.id, course_name="ریاضی تخصصی کنکور", teacher_name="استاد علوی", teaching_style="روش‌های سرعتی و محاسباتی")
579
  db.add(course)
580
 
581
+ # اسلات آزاد مشاور
582
+ now = get_now()
583
+ 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)
584
+ db.add(slot1)
585
+
586
  db.commit()
587
  except Exception as e:
588
  logger.error(f"Seeding process crashed: {e}", exc_info=True)
589
  db.rollback()
590
 
591
+
 
 
592
  @app.exception_handler(Exception)
593
  async def global_exception_handler(request: Request, exc: Exception):
594
  logger.error(f"Internal Crash logged on {request.url.path}: {exc}", exc_info=True)
595
  return JSONResponse(
596
  status_code=500,
597
+ content={"detail": "سرور موقتاً شلوغ است. لطفاً چند لحظه دیگر دوباره تلاش کنید."}
598
  )
599
 
600
+ # --- مدیریت لود مستقیم فایل‌های استاتیک وب‌سایت ---
601
+
602
+ @app.get("/")
603
+ def home_webview():
604
+ if os.path.exists("dist/index.html"):
605
+ return FileResponse("dist/index.html")
606
+ return {"status": "active", "api_docs": "/docs"}
607
+
608
+ # --- احراز هویت (Auth Process) ---
609
 
610
  @app.post("/api/auth/register")
611
  def auth_register(data: RegisterRequest, db: Session = Depends(get_db)):
 
621
  email=data.email,
622
  hashed_password=hashed,
623
  role=data.role,
624
+ is_verified=is_verified_init,
625
+ phone_number=data.phone_number,
626
+ city=data.city
627
  )
628
  db.add(new_user)
629
  db.flush()
 
655
  except Exception as e:
656
  logger.error(f"Error during registration endpoint: {e}", exc_info=True)
657
  db.rollback()
658
+ raise HTTPException(status_code=503, detail="سرور موقتاً شلوغ است. لطفاً بعداً تلاش کنید.")
659
 
660
 
661
  @app.post("/api/auth/login")
 
663
  try:
664
  user = db.query(User).filter(User.email == data.email).first()
665
  if not user or not verify_password(data.password, user.hashed_password):
666
+ raise HTTPException(status_code=400, detail="ایمیل یا رمز عبور اشتباه است.")
667
 
668
  token = create_access_token(data={"sub": user.email})
669
  return {
 
679
  logger.error(f"Error during login endpoint: {e}", exc_info=True)
680
  raise HTTPException(status_code=503, detail="سرور موقتاً شلوغ است. لطفاً بعداً تلاش کنید.")
681
 
682
+ # --- مشخصات و اطلاعات کاربر و تغییر کلمه عبور ---
683
+
684
+ @app.get("/api/auth/me")
685
+ def get_user_profile(current_user: User = Depends(get_current_user)):
686
+ return {
687
+ "id": current_user.id,
688
+ "email": current_user.email,
689
+ "role": current_user.role,
690
+ "balance": current_user.balance,
691
+ "rating": current_user.rating,
692
+ "phone_number": current_user.phone_number,
693
+ "city": current_user.city,
694
+ "profile_pic": current_user.profile_pic
695
+ }
696
+
697
+ @app.put("/api/auth/me/update")
698
+ def update_user_profile(data: UpdateProfileRequest, current_user: User = Depends(get_current_user), db: Session = Depends(get_db)):
699
+ try:
700
+ if data.name:
701
+ if current_user.role == "school_representative" and current_user.school_profile:
702
+ current_user.school_profile.name = data.name
703
+ if data.email:
704
+ existing = db.query(User).filter(User.email == data.email, User.id != current_user.id).first()
705
+ if existing:
706
+ raise HTTPException(status_code=400, detail="این آدرس ایمیل قبلا ثبت شده است.")
707
+ current_user.email = data.email
708
+ if data.password:
709
+ current_user.hashed_password = get_password_hash(data.password)
710
+ if data.phone_number:
711
+ current_user.phone_number = data.phone_number
712
+ if data.city:
713
+ current_user.city = data.city
714
+
715
+ db.commit()
716
+ return {"message": "تغییرات مشخصات کاربری با موفقیت ذخیره شد."}
717
+ except HTTPException:
718
+ raise
719
+ except Exception as e:
720
+ logger.error(f"Error updating profile: {e}")
721
+ db.rollback()
722
+ raise HTTPException(status_code=503, detail="سرور موقتاً شلوغ است. لطفاً بعداً تلاش کنید.")
723
+
724
+ # --- فیلترینگ و جستجو بر اساس نام و شهر (مشاوران و آموزشگاه‌ها) ---
725
 
726
  @app.get("/api/counselors")
727
+ def list_active_counselors(name: Optional[str] = None, city: Optional[str] = None, db: Session = Depends(get_db)):
728
  try:
729
  query = db.query(User).filter(User.role == "counselor", User.is_verified == True)
730
+ if city:
731
+ query = query.filter(User.city.ilike(f"%{city}%"))
732
+ if name:
733
+ query = query.join(CounselorProfile).filter(
734
+ or_(
735
+ User.email.ilike(f"%{name}%"),
736
+ CounselorProfile.specialty.ilike(f"%{name}%")
737
+ )
738
+ )
739
 
740
  counselors = query.all()
741
  output = []
 
744
  "counselor_id": c.id,
745
  "email": c.email,
746
  "rating": c.rating,
747
+ "city": c.city or "تهران",
748
  "specialty": c.counselor_profile.specialty if c.counselor_profile else None,
749
  "resume": c.counselor_profile.resume if c.counselor_profile else None,
750
+ "bio": c.counselor_profile.bio if c.counselor_profile else None,
751
  "hourly_rate": c.counselor_profile.hourly_rate if c.counselor_profile else 0.0
752
  })
753
  return output
 
755
  logger.error(f"Error listing counselors: {e}", exc_info=True)
756
  raise HTTPException(status_code=503, detail="سرور موقتاً شلوغ است. لطفاً بعداً تلاش کنید.")
757
 
758
+ # --- لایه ارتباط مشاور و آموزشگاه (B2B) ---
759
+
760
+ @app.post("/api/schools/request-counselor")
761
+ def school_request_counselor_link(data: CounselorRequestInput, current_user: User = Depends(get_current_user), db: Session = Depends(get_db)):
762
+ """درخواست اتصال آموزشگاه به مشاور بومی پلتفرم"""
763
+ try:
764
+ if current_user.role != "school_representative":
765
+ raise HTTPException(status_code=403, detail="تنها نمایندگان آموزشگاه مجاز به این کار هستند.")
766
+
767
+ school_prof = current_user.school_profile
768
+ if not school_prof:
769
+ raise HTTPException(status_code=404, detail="پروفایل آموزشگاهی شما ثبت نشده است.")
770
+
771
+ existing = db.query(SchoolCounselorAssociation).filter(
772
+ SchoolCounselorAssociation.school_id == school_prof.id,
773
+ SchoolCounselorAssociation.counselor_id == data.counselor_id
774
+ ).first()
775
+ if existing:
776
+ raise HTTPException(status_code=400, detail="درخواست قبلا ثبت شده یا همبستگی برقرار است.")
777
+
778
+ assoc = SchoolCounselorAssociation(
779
+ school_id=school_prof.id,
780
+ counselor_id=data.counselor_id,
781
+ status="pending"
782
+ )
783
+ db.add(assoc)
784
+ db.commit()
785
+ return {"message": "درخواست همبستگی صنف با موفقیت ارسال گردید."}
786
+ except HTTPException:
787
+ raise
788
+ except Exception as e:
789
+ logger.error(f"Error requesting link: {e}")
790
+ db.rollback()
791
+ raise HTTPException(status_code=503, detail="سرور موقتاً شلوغ است. لطفاً بعداً تلاش کنید.")
792
+
793
 
794
+ @app.get("/api/counselor/school-requests")
795
+ def counselor_get_requests(current_user: User = Depends(get_current_user), db: Session = Depends(get_db)):
796
  try:
797
+ if current_user.role != "counselor":
798
+ raise HTTPException(status_code=403, detail="دسترسی غیرمجاز")
799
+ requests_db = db.query(SchoolCounselorAssociation).filter(
800
+ SchoolCounselorAssociation.counselor_id == current_user.id
801
  ).all()
802
+
803
+ output = []
804
+ for r in requests_db:
805
+ school = db.query(SchoolProfile).filter(SchoolProfile.id == r.school_id).first()
806
+ output.append({
807
+ "request_id": r.id,
808
+ "school_id": r.school_id,
809
+ "school_name": school.name if school else "آموزشگاه نمونه",
810
+ "status": r.status
811
+ })
812
+ return output
813
  except Exception as e:
814
+ logger.error(f"Error getting requests: {e}")
815
  raise HTTPException(status_code=503, detail="سرور موقتاً شلوغ است. لطفاً بعداً تلاش کنید.")
816
 
817
 
818
+ @app.post("/api/counselor/school-requests/{request_id}/respond")
819
+ def respond_to_school_request(request_id: int, data: RequestRespondInput, current_user: User = Depends(get_current_user), db: Session = Depends(get_db)):
820
  try:
821
+ if current_user.role != "counselor":
822
+ raise HTTPException(status_code=403, detail="دسترسی غیرمجاز")
823
+ req = db.query(SchoolCounselorAssociation).filter(
824
+ SchoolCounselorAssociation.id == request_id,
825
+ SchoolCounselorAssociation.counselor_id == current_user.id
826
+ ).first()
827
+ if not req:
828
+ raise HTTPException(status_code=404, detail="درخواست یافت نشد.")
829
 
830
+ req.status = data.status
831
+ db.commit()
832
+ return {"message": "پاسخ به درخواست همبستگی با موفقیت ثبت شد."}
833
+ except HTTPException:
834
+ raise
835
+ except Exception as e:
836
+ logger.error(f"Error responding to link: {e}")
837
+ db.rollback()
838
+ raise HTTPException(status_code=503, detail="سرور موقتاً شلوغ است. لطفاً بعداً تلاش کنید.")
839
+
840
+ # --- ماژول استخراج بیوگرافی رزومه با هوش مصنوعی (تنها یک‌بار بارگذاری) ---
841
+
842
+ @app.post("/api/counselor/extract-bio")
843
+ def extract_bio_from_resume_text(data: ExtractedBioRequest, current_user: User = Depends(get_current_user), db: Session = Depends(get_db)):
844
+ """
845
+ استخراج بیوگرافی خلاصه و صمیمی توسط هوش مصنوعی و بروزرسانی پروفایل
846
+ """
847
+ try:
848
+ if current_user.role != "counselor":
849
+ raise HTTPException(status_code=403, detail="تنها مشاوران مجاز به استفاده از این ماژول هستند.")
850
 
851
+ extracted_bio = core_bio_extraction(data.resume_text)
852
+
853
+ profile = current_user.counselor_profile
854
+ if profile:
855
+ profile.bio = extracted_bio
856
+ profile.resume = data.resume_text
857
+ db.commit()
858
 
859
+ return {
860
+ "message": "بیوگرافی بهینه شما بر اساس رزومه استخراج شد و در پروفایل قرار گرفت.",
861
+ "extracted_bio": extracted_bio
862
+ }
863
+ except HTTPException:
864
+ raise
865
+ except Exception as e:
866
+ logger.error(f"Error extracting bio: {e}")
867
+ db.rollback()
868
+ raise HTTPException(status_code=503, detail="سرور موقتاً شلوغ است. لطفاً بعداً تلاش کنید.")
869
+
870
+ # --- مشاهده و فیلترینگ داوطلبان توسط مشاور (محاسبه سود خالص و پورسانت پلاتفرم) ---
871
+
872
+ @app.get("/api/counselor/my-students")
873
+ def get_counselor_students_metrics(search: Optional[str] = None, current_user: User = Depends(get_current_user), db: Session = Depends(get_db)):
874
+ """
875
+ دریافت کاندیداهای تحت مشاوره با فیلترینگ اسم و شماره تماس،
876
+ محاسبه کل مبلغ پرداخت شده و سود خالص مشاور با کسر درصد پورسانت پلتفرم (مثلا ۲۰ درصد)
877
+ """
878
+ try:
879
+ if current_user.role != "counselor":
880
+ raise HTTPException(status_code=403, detail="دسترسی غیرمجاز")
881
 
882
+ query = db.query(User).filter(User.role == "student")
883
+ if search:
884
+ query = query.filter(
885
+ or_(
886
+ User.email.ilike(f"%{search}%"),
887
+ User.phone_number.ilike(f"%{search}%")
888
+ )
889
+ )
890
+
891
+ students = query.all()
892
+ output = []
893
+ for s in students:
894
+ # دریافت رکوردهای رزرو این مشاور برای داوطلب هدف
895
+ bookings_db = db.query(Booking).filter(
896
+ Booking.student_id == s.id,
897
+ Booking.counselor_id == current_user.id,
898
+ Booking.payment_status == "paid"
899
+ ).all()
900
+
901
+ if not bookings_db:
902
+ continue # فقط داوطلبانی که خرید داشته‌اند
903
+
904
+ total_paid = sum([b.cost for b in bookings_db])
905
+ net_profit = total_paid * 0.8 # کسر ۲۰ درصد سهم پلتفرم
906
+
907
+ output.append({
908
+ "student_id": s.id,
909
+ "student_email": s.email,
910
+ "phone_number": s.phone_number or "ثبت‌نشده",
911
+ "city": s.city or "تهران",
912
+ "total_paid_rial": total_paid,
913
+ "net_profit_rial": net_profit,
914
+ "bookings": [
915
+ {
916
+ "booking_id": b.id,
917
+ "cost": b.cost,
918
+ "status": b.status
919
+ } for b in bookings_db
920
+ ]
921
+ })
922
+ return output
923
+ except Exception as e:
924
+ logger.error(f"Error calculating metrics: {e}")
925
+ raise HTTPException(status_code=503, detail="سرور موقتاً شلوغ است. لطفاً بعداً تلاش کنید.")
926
+
927
+ # --- نوبت‌دهی دستی داوطلب اختصاصی توسط مشاور با فرصت پرداخت ۲۴ ساعته ---
928
+
929
+ @app.post("/api/counselor/create-manual-booking")
930
+ def create_manual_booking_by_counselor(data: ManualBookingRequest, current_user: User = Depends(get_current_user), db: Session = Depends(get_db)):
931
+ """
932
+ ثبت نوبت دستی اختصاصی برای داوطلب هدف؛ تولید ریکوئست پرداخت با سررسید ۲۴ ساعته
933
+ """
934
+ try:
935
+ if current_user.role != "counselor":
936
+ raise HTTPException(status_code=403, detail="تنها مشاوران مجاز به ثبت نوبت دستی هستند.")
937
+
938
+ # ایجاد خودکار زمان کاری رزرو شده
939
+ slot = TimeSlot(
940
+ counselor_id=current_user.id,
941
+ start_time=data.start_time,
942
+ end_time=data.end_time,
943
+ is_booked=True,
944
+ booked_by_student_id=data.student_id
945
+ )
946
+ db.add(slot)
947
+ db.flush()
948
 
949
+ due_time = get_now() + timedelta(hours=24)
950
  booking = Booking(
951
  slot_id=slot.id,
952
+ student_id=data.student_id,
953
+ counselor_id=current_user.id,
954
  status="active",
955
+ payment_status="pending_payment", # در انتظار تسویه داوطلب
956
+ payment_due_until=due_time,
957
+ cost=data.cost
958
  )
959
  db.add(booking)
960
  db.commit()
961
 
962
  return {
963
+ "message": "نوبت دستی صادر شد و درخواست تسویه ۲۴ ساعته برای داوطلب ارسال گردید.",
964
+ "booking_id": booking.id,
965
+ "payment_due_until": due_time
966
  }
967
  except HTTPException:
968
  raise
969
  except Exception as e:
970
+ logger.error(f"Error manual booking: {e}")
971
  db.rollback()
972
  raise HTTPException(status_code=503, detail="سرور موقتاً شلوغ است. لطفاً بعداً تلاش کنید.")
973
 
974
 
975
+ @app.post("/api/student/pay-booking/{booking_id}")
976
+ def student_pay_pending_booking(booking_id: int, current_user: User = Depends(get_current_user), db: Session = Depends(get_db)):
977
+ """پرداخت و نهایی‌سازی درخواست تسویه ۲۴ ساعته توسط داوطلب"""
978
  try:
979
+ if current_user.role != "student":
980
+ raise HTTPException(status_code=403, detail="دسترسی غیرمجاز")
 
981
 
982
+ booking = db.query(Booking).filter(
983
+ Booking.id == booking_id,
984
+ Booking.student_id == current_user.id
985
+ ).first()
986
+
987
+ if not booking:
988
+ raise HTTPException(status_code=404, detail="درخواست پرداختی یافت نشد.")
989
 
990
+ if booking.payment_status != "pending_payment":
991
+ raise HTTPException(status_code=400, detail="این صورتحساب قبلاً پرداخت یا منقضی گردیده است.")
992
 
993
+ if current_user.balance < booking.cost:
994
+ raise HTTPException(status_code=400, detail="موجودی کیف پول شما برای این صورتحساب کافی نیست؛ ابتدا شارژ کنید.")
 
 
995
 
 
996
  counselor = db.query(User).filter(User.id == booking.counselor_id).first()
997
 
998
+ current_user.balance -= booking.cost
 
999
  if counselor:
1000
+ counselor.balance += booking.cost
1001
 
1002
+ booking.payment_status = "paid"
1003
  db.commit()
1004
 
1005
  return {
1006
+ "message": "صورتحساب با موفقیت پرداخت گردید و نوبت رزرو شما نهایی شد.",
1007
+ "remaining_balance": current_user.balance
1008
  }
1009
  except HTTPException:
1010
  raise
1011
  except Exception as e:
1012
+ logger.error(f"Error paying booking: {e}")
1013
  db.rollback()
1014
  raise HTTPException(status_code=503, detail="سرور موقتاً شلوغ است. لطفاً بعداً تلاش کنید.")
1015
 
1016
+ # --- طراحی آزمون‌های انتخابی کلاسی توسط مشاور و پاسخ‌دهی داوطلب ---
 
 
1017
 
1018
+ @app.post("/api/counselor/quizzes")
1019
+ def counselor_create_quiz(data: QuizCreateRequest, current_user: User = Depends(get_current_user), db: Session = Depends(get_db)):
1020
  try:
1021
+ if current_user.role != "counselor":
1022
+ raise HTTPException(status_code=403, detail="مخصوص مشاوران")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1023
 
1024
+ quiz = CustomQuiz(
1025
+ counselor_id=current_user.id,
1026
+ title=data.title,
1027
+ questions_json=data.questions_json
1028
+ )
1029
+ db.add(quiz)
 
 
 
 
 
 
 
 
1030
  db.commit()
1031
+ return {"message": "آزمون چندگزینه‌ای کلاسی با موفقیت طراحی شد."}
 
 
 
 
 
 
 
 
 
 
 
 
1032
  except Exception as e:
1033
+ logger.error(f"Error creating quiz: {e}")
1034
  db.rollback()
1035
  raise HTTPException(status_code=503, detail="سرور موقتاً شلوغ است. لطفاً بعداً تلاش کنید.")
1036
 
1037
 
1038
+ @app.post("/api/counselor/assign-quiz")
1039
+ def assign_quiz_to_student(data: AssignQuizRequest, current_user: User = Depends(get_current_user), db: Session = Depends(get_db)):
1040
  try:
1041
  if current_user.role != "counselor":
1042
+ raise HTTPException(status_code=403, detail="مخصوص مشاوران")
1043
 
1044
+ existing = db.query(StudentQuizAssignment).filter(
1045
+ StudentQuizAssignment.booking_id == data.booking_id,
1046
+ StudentQuizAssignment.quiz_id == data.quiz_id
1047
  ).first()
1048
+ if existing:
1049
+ raise HTTPException(status_code=400, detail="این آزمون قبلاً به این جلسه اختصاص داده شده است.")
1050
+
1051
+ assign = StudentQuizAssignment(
1052
+ quiz_id=data.quiz_id,
1053
+ student_id=data.student_id,
1054
+ booking_id=data.booking_id,
1055
+ is_completed=False
1056
+ )
1057
+ db.add(assign)
 
1058
  db.commit()
1059
+ return {"message": "آزمون کلاسی با موفقیت به این جلسه داوطلب تخصیص یافت."}
 
1060
  except HTTPException:
1061
  raise
1062
  except Exception as e:
1063
+ logger.error(f"Error assigning quiz: {e}")
1064
  db.rollback()
1065
  raise HTTPException(status_code=503, detail="سرور موقتاً شلوغ است. لطفاً بعداً تلاش کنید.")
1066
 
1067
 
1068
+ @app.get("/api/student/assigned-quizzes")
1069
+ def student_get_quizzes(current_user: User = Depends(get_current_user), db: Session = Depends(get_db)):
1070
  try:
1071
  if current_user.role != "student":
1072
  raise HTTPException(status_code=403, detail="دسترسی غیرمجاز")
1073
 
1074
+ assigned = db.query(StudentQuizAssignment).filter(
1075
+ StudentQuizAssignment.student_id == current_user.id,
1076
+ StudentQuizAssignment.is_completed == False
1077
+ ).all()
1078
+
1079
+ output = []
1080
+ for item in assigned:
1081
+ quiz = db.query(CustomQuiz).filter(CustomQuiz.id == item.quiz_id).first()
1082
+ output.append({
1083
+ "assignment_id": item.id,
1084
+ "quiz_id": item.quiz_id,
1085
+ "title": quiz.title if quiz else "آزمون کلاسی",
1086
+ "questions": json.loads(quiz.questions_json) if quiz else [],
1087
+ "booking_id": item.booking_id
1088
+ })
1089
+ return output
1090
+ except Exception as e:
1091
+ logger.error(f"Error listing student quizzes: {e}")
1092
+ raise HTTPException(status_code=503, detail="سرور موقتاً شلوغ است. لطفاً بعداً تلاش کنید.")
1093
+
1094
+
1095
+ @app.post("/api/student/quizzes/{assignment_id}/submit")
1096
+ def submit_quiz_answers(assignment_id: int, data: SubmitQuizRequest, current_user: User = Depends(get_current_user), db: Session = Depends(get_db)):
1097
+ try:
1098
+ if current_user.role != "student":
1099
+ raise HTTPException(status_code=403, detail="دسترسی غیرمجاز")
1100
+ assign = db.query(StudentQuizAssignment).filter(
1101
+ StudentQuizAssignment.id == assignment_id,
1102
+ StudentQuizAssignment.student_id == current_user.id
1103
+ ).first()
1104
+ if not assign:
1105
+ raise HTTPException(status_code=404, detail="رکورد آزمون یافت نشد.")
1106
 
1107
+ assign.answers_json = data.answers_json
1108
+ assign.is_completed = True
1109
+ # نمره‌دهی به صورت پیش‌فرض یا شبیه‌ساز تاییدیه
1110
+ assign.score = 100.0
1111
+ db.commit()
1112
+ return {"message": "پاسخ آزمون کلاسی با موفقیت ارسال گردید."}
1113
  except HTTPException:
1114
  raise
1115
  except Exception as e:
1116
+ logger.error(f"Error submitting quiz: {e}")
1117
+ db.rollback()
1118
  raise HTTPException(status_code=503, detail="سرور موقتاً شلوغ است. لطفاً بعداً تلاش کنید.")
1119
 
1120
+ # --- دایرکتوری آموزشگاه‌ها فیلترینگ نام و شهر و ارتقای تبلیغاتی ---
 
 
1121
 
1122
  @app.get("/api/schools")
1123
+ def list_and_filter_schools(name: Optional[str] = None, city: Optional[str] = None, db: Session = Depends(get_db)):
1124
  try:
1125
  now = get_now()
1126
+ # نمایش نتایجی که حق اشتراک فعال دارند
1127
  query = db.query(SchoolProfile).filter(SchoolProfile.subscription_active_until > now)
1128
+ if city:
1129
+ query = query.join(User, User.id == SchoolProfile.user_id).filter(User.city.ilike(f"%{city}%"))
1130
+ if name:
1131
+ query = query.filter(SchoolProfile.name.ilike(f"%{name}%"))
1132
+
 
 
 
 
 
1133
  schools = query.order_by(SchoolProfile.is_sponsored.desc()).all()
 
1134
  output = []
1135
  for s in schools:
1136
  output.append({
1137
  "school_id": s.id,
1138
  "school_name": s.name,
1139
  "is_sponsored": s.is_sponsored,
1140
+ "rating": s.rating,
1141
+ "bio": s.bio,
1142
+ "address": s.address,
1143
+ "phone_number": s.phone_number,
1144
+ "founding_year": s.founding_year,
1145
  "courses": [
1146
  {
1147
  "course_name": c.course_name,
 
1152
  })
1153
  return output
1154
  except Exception as e:
1155
+ logger.error(f"Error querying schools: {e}")
1156
+ raise HTTPException(status_code=503, detail="سرور موقتاً شلوغ است. لطفاً بعداً تلاش کنید.")
1157
+
1158
+
1159
+ @app.post("/api/schools/purchase-ad")
1160
+ def purchase_school_premium_ad(current_user: User = Depends(get_current_user), db: Session = Depends(get_db)):
1161
+ """خرید تبلیغات ویژه جهت نمایش در بالای صف دایرکتوری عمومی"""
1162
+ try:
1163
+ if current_user.role != "school_representative":
1164
+ raise HTTPException(status_code=403, detail="دسترسی غیرمجاز")
1165
+ profile = current_user.school_profile
1166
+ if not profile:
1167
+ raise HTTPException(status_code=404, detail="پروفایل آموزشگاه ثبت نشده است.")
1168
+
1169
+ profile.is_sponsored = True
1170
+ db.commit()
1171
+ return {"message": "تبلیغات ویژه با موفقیت خریداری شد و آموزشگاه به صدر جدول فرستاده شد."}
1172
+ except HTTPException:
1173
+ raise
1174
+ except Exception as e:
1175
+ logger.error(f"Error purchasing school ad: {e}")
1176
+ db.rollback()
1177
  raise HTTPException(status_code=503, detail="سرور موقتاً شلوغ است. لطفاً بعداً تلاش کنید.")
1178
 
1179
+ # --- سیستم ارزیابی و ثبت بازخوردهای داوطلب ---
1180
+
1181
+ @app.post("/api/reviews")
1182
+ def create_session_review(data: ReviewCreateRequest, current_user: User = Depends(get_current_user), db: Session = Depends(get_db)):
1183
+ try:
1184
+ if current_user.role != "student":
1185
+ raise HTTPException(status_code=403, detail="دسترسی غیرمجاز")
1186
+
1187
+ booking = db.query(Booking).filter(Booking.id == data.booking_id, Booking.student_id == current_user.id).first()
1188
+ if not booking:
1189
+ raise HTTPException(status_code=404, detail="جلسه مشاوره یافت نشد.")
1190
+
1191
+ review = Review(
1192
+ student_id=current_user.id,
1193
+ booking_id=data.booking_id,
1194
+ counselor_id=data.counselor_id,
1195
+ school_id=data.school_id,
1196
+ rating=data.rating,
1197
+ comment=data.comment
1198
+ )
1199
+ db.add(review)
1200
+
1201
+ # بروزرسانی میانگین امتیاز مشاور یا آموزشگاه
1202
+ if data.counselor_id:
1203
+ counselor = db.query(User).filter(User.id == data.counselor_id).first()
1204
+ if counselor:
1205
+ counselor.rating = (counselor.rating + data.rating) / 2.0
1206
+ elif data.school_id:
1207
+ school = db.query(SchoolProfile).filter(SchoolProfile.id == data.school_id).first()
1208
+ if school:
1209
+ school.rating = (school.rating + data.rating) / 2.0
1210
+
1211
+ db.commit()
1212
+ return {"message": "امتیاز و نظر شما با موفقیت ثبت شد."}
1213
+ except HTTPException:
1214
+ raise
1215
+ except Exception as e:
1216
+ logger.error(f"Error creating review: {e}")
1217
+ db.rollback()
1218
+ raise HTTPException(status_code=503, detail="سرور موقتاً شلوغ است. لطفاً بعداً تلاش کنید.")
1219
+
1220
+ # --- مدیریت تکمیلی آموزشگاه ---
1221
 
1222
  @app.post("/api/schools/update")
1223
  def update_school_course_portfolio(data: SchoolProfileUpdateRequest, current_user: User = Depends(get_current_user), db: Session = Depends(get_db)):
 
1227
 
1228
  profile = db.query(SchoolProfile).filter(SchoolProfile.user_id == current_user.id).first()
1229
  if not profile:
1230
+ profile = SchoolProfile(
1231
+ user_id=current_user.id,
1232
+ name=data.name,
1233
+ bio=data.bio,
1234
+ address=data.address,
1235
+ phone_number=data.phone_number,
1236
+ founding_year=data.founding_year
1237
+ )
1238
  db.add(profile)
1239
  db.flush()
1240
  else:
1241
  profile.name = data.name
1242
+ profile.bio = data.bio
1243
+ profile.address = data.address
1244
+ profile.phone_number = data.phone_number
1245
+ profile.founding_year = data.founding_year
1246
 
1247
  db.query(SchoolCourse).filter(SchoolCourse.school_id == profile.id).delete()
1248
 
 
1256
  db.add(db_course)
1257
 
1258
  db.commit()
1259
+ return {"message": "رزومه کادر اساتید و آموزشگاه با موفقیت بروز گردید."}
1260
  except HTTPException:
1261
  raise
1262
  except Exception as e:
 
1296
  db.rollback()
1297
  raise HTTPException(status_code=503, detail="سرور موقتاً شلوغ است. لطفاً بعداً تلاش کنید.")
1298
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1299
 
1300
  @app.post("/api/student/topup")
1301
  def top_up_student_wallet(data: TopUpRequest, current_user: User = Depends(get_current_user), db: Session = Depends(get_db)):