import os import json import logging import base64 from datetime import datetime, timedelta, timezone from typing import Optional, List from functools import wraps from flask import Flask, request, jsonify, g from flask_cors import CORS from asgiref.wsgi import WsgiToAsgi from werkzeug.exceptions import HTTPException from sqlalchemy import create_engine, Column, Integer, String, Boolean, Float, DateTime, ForeignKey, Text, and_, or_, text, inspect from sqlalchemy.orm import relationship, sessionmaker, declarative_base, Session # مدیریت ایمن لود کتابخانه‌های امنیت و توکن JWT try: from jose import JWTError, jwt except ImportError: import jwt JWTError = Exception import bcrypt # ========================================================================= # بخش ۱: لایه ثبت لاگ سیستم # ========================================================================= logging.basicConfig(level=logging.INFO) logger = logging.getLogger("production_backend") # کلید پردازش‌های ابری هوشمند (Secrets) AI_CORE_KEY = os.getenv("GEMINI_API_KEY") or os.getenv("SECRET") # پیشگیری از کرش در صورت عدم وجود پکیج نسل جدید گوگل try: from google import genai from google.genai import types except ImportError: genai = None types = None logger.warning("The 'google-genai' package is not installed. AI features will fallback.") # ========================================================================= # بخش ۲: اتصال پایگاه داده Neon.tech و ساختار بومی دیتابیس # ========================================================================= DATABASE_URL = os.getenv( "DATABASE_URL", "postgresql://neondb_owner:npg_dMhA9Zf5bpJq@ep-floral-snow-avsluk5j-pooler.c-11.us-east-1.aws.neon.tech/neondb?sslmode=require&channel_binding=require" ) if DATABASE_URL.startswith("postgres://"): DATABASE_URL = DATABASE_URL.replace("postgres://", "postgresql://", 1) try: if "sqlite" in DATABASE_URL: engine = create_engine(DATABASE_URL, connect_args={"check_same_thread": False}) else: engine = create_engine( DATABASE_URL, pool_size=15, max_overflow=25, pool_recycle=1800, pool_pre_ping=True ) except Exception as db_init_err: logger.critical(f"Database engine creation failed: {db_init_err}", exc_info=True) engine = create_engine("sqlite:///./fallback.db", connect_args={"check_same_thread": False}) SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) Base = declarative_base() # تعریف تابع زمان بومی سرور def get_now(): return datetime.now(timezone.utc).replace(tzinfo=None) # فرمول مبدل تقویم میلادی به شمسی بومی (بند ۱۱) def gregorian_to_jalali(g_y, g_m, g_d): g_days_in_month = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31] j_days_in_month = [31, 31, 31, 31, 31, 31, 30, 30, 30, 30, 30, 29] gy = g_y - 1600 gm = g_m - 1 gd = g_d - 1 g_day_no = 365 * gy + (gy + 3) // 4 - (gy + 99) // 100 + (gy + 399) // 400 for i in range(gm): g_day_no += g_days_in_month[i] if gm > 1 and ((g_y % 4 == 0 and g_y % 100 != 0) or (g_y % 400 == 0)): g_day_no += 1 g_day_no += gd j_day_no = g_day_no - 79 j_np = j_day_no // 12053 j_day_no %= 12053 jy = 979 + 33 * j_np + 4 * (j_day_no // 1461) j_day_no %= 1461 if j_day_no >= 366: jy += (j_day_no - 1) // 365 j_day_no = (j_day_no - 1) % 365 for i in range(12): if j_day_no < j_days_in_month[i]: jm = i + 1 jd = j_day_no + 1 return jy, jm, jd j_day_no -= j_days_in_month[i] return jy, 12, 29 def get_shamsi_date_str(dt: datetime) -> str: if not dt: return "" jy, jm, jd = gregorian_to_jalali(dt.year, dt.month, dt.day) return f"{jy}/{jm:02d}/{jd:02d}" # پایگاه دانش برای موتور انتخاب رشته کنکور KNOWLEDGE_BASE = [ { "university": "دانشگاه تبریز", "ranking_tier": "الف", "fields": ["مهندسی کامپیوتر", "پزشکی", "حقوق"], "minimum_rank_r1": 700, "minimum_rank_r2": 900 }, { "university": "دانشگاه صنعتی سهند", "ranking_tier": "ب", "fields": ["مهندسی برق", "مهندسی مواد", "مکانیک"], "minimum_rank_r1": 1500, "minimum_rank_r2": 2000 }, { "university": "دانشگاه علوم پزشکی تبریز", "ranking_tier": "الف", "fields": ["پزشکی", "دندانپزشکی", "داروسازی"], "minimum_rank_r1": 250, "minimum_rank_r2": 350 } ] # ========================================================================= # بخش ۳: مدل‌های دیتابیس (SQLAlchemy Models) # ========================================================================= class User(Base): __tablename__ = "users" id = Column(Integer, primary_key=True, index=True) email = Column(String, unique=True, index=True, nullable=False) hashed_password = Column(String, nullable=False) role = Column(String, default="student") # 'student', 'counselor', 'school_representative' name = Column(String, nullable=True) # نام کاربری واقعی (بند ۶) is_verified = Column(Boolean, default=False) balance = Column(Float, default=0.0) rating = Column(Float, default=0.0) # مقدار اولیه تراز صفر است (بند ۱۰) phone_number = Column(String, nullable=True) city = Column(String, nullable=True) # منطقه سکونت (بند ۳) address = Column(Text, nullable=True) # آدرس پستی جهت مراجعه حضوری (بند ۶) grade = Column(String, nullable=True) major = Column(String, nullable=True) profile_pic = Column(Text, nullable=True) counselor_profile = relationship("CounselorProfile", back_populates="user", uselist=False, cascade="all, delete-orphan") school_profile = relationship("SchoolProfile", back_populates="user", uselist=False, cascade="all, delete-orphan") class CounselorProfile(Base): __tablename__ = "counselor_profiles" id = Column(Integer, primary_key=True, index=True) user_id = Column(Integer, ForeignKey("users.id")) specialty = Column(String, nullable=True) resume = Column(Text, nullable=True) bio = Column(Text, nullable=True) hourly_rate = Column(Float, default=150000.0) deactivated_days_json = Column(Text, default="[]") # روزهای غیرفعال شده توسط مشاور (بند ۷) user = relationship("User", back_populates="counselor_profile") class SchoolProfile(Base): __tablename__ = "school_profiles" id = Column(Integer, primary_key=True, index=True) user_id = Column(Integer, ForeignKey("users.id")) name = Column(String, nullable=True) subscription_active_until = Column(DateTime, nullable=True) is_sponsored = Column(Boolean, default=False) bio = Column(Text, nullable=True) address = Column(Text, nullable=True) phone_number = Column(String, nullable=True) founding_year = Column(Integer, nullable=True) rating = Column(Float, default=0.0) # مقدار امتیاز آموزشگاه در شروع صفر است (بند ۱۰) profile_pic = Column(Text, nullable=True) user = relationship("User", back_populates="school_profile") courses = relationship("SchoolCourse", back_populates="school", cascade="all, delete-orphan") class SchoolCourse(Base): __tablename__ = "school_courses" id = Column(Integer, primary_key=True, index=True) school_id = Column(Integer, ForeignKey("school_profiles.id")) course_name = Column(String, nullable=False) teacher_name = Column(String, nullable=False) teaching_style = Column(String, nullable=False) school = relationship("SchoolProfile", back_populates="courses") class SchoolTeacher(Base): __tablename__ = "school_teachers" id = Column(Integer, primary_key=True, index=True) school_id = Column(Integer, ForeignKey("school_profiles.id")) name = Column(String, nullable=False) bio = Column(Text, nullable=True) class SchoolCounselorAssociation(Base): __tablename__ = "school_counselor_associations" id = Column(Integer, primary_key=True, index=True) school_id = Column(Integer, ForeignKey("school_profiles.id")) counselor_id = Column(Integer, ForeignKey("users.id")) status = Column(String, default="pending") class TimeSlot(Base): __tablename__ = "time_slots" id = Column(Integer, primary_key=True, index=True) counselor_id = Column(Integer, ForeignKey("users.id")) start_time = Column(DateTime, nullable=False) end_time = Column(DateTime, nullable=False) is_booked = Column(Boolean, default=False) booked_by_student_id = Column(Integer, ForeignKey("users.id"), nullable=True) class Booking(Base): __tablename__ = "bookings" id = Column(Integer, primary_key=True, index=True) slot_id = Column(Integer, ForeignKey("time_slots.id")) student_id = Column(Integer, ForeignKey("users.id")) counselor_id = Column(Integer, ForeignKey("users.id")) status = Column(String, default="active") # active, cancelled payment_status = Column(String, default="paid") # paid, pending_payment, expired payment_due_until = Column(DateTime, nullable=True) cost = Column(Float, default=0.0) created_at = Column(DateTime, default=get_now) class CustomQuiz(Base): __tablename__ = "custom_quizzes" id = Column(Integer, primary_key=True, index=True) counselor_id = Column(Integer, ForeignKey("users.id"), nullable=True) title = Column(String, nullable=False) questions_json = Column(Text, nullable=False) class StudentQuizAssignment(Base): __tablename__ = "student_quiz_assignments" id = Column(Integer, primary_key=True, index=True) quiz_id = Column(Integer, ForeignKey("custom_quizzes.id")) student_id = Column(Integer, ForeignKey("users.id")) booking_id = Column(Integer, ForeignKey("bookings.id")) answers_json = Column(Text, nullable=True) is_completed = Column(Boolean, default=False) score = Column(Float, nullable=True) class TestResult(Base): __tablename__ = "test_results" id = Column(Integer, primary_key=True, index=True) student_id = Column(Integer, ForeignKey("users.id")) counselor_id = Column(Integer, ForeignKey("users.id")) answers = Column(Text, nullable=False) ai_analysis_summary = Column(Text, nullable=True) class KonkurDetails(Base): __tablename__ = "konkur_details" id = Column(Integer, primary_key=True, index=True) student_id = Column(Integer, ForeignKey("users.id")) rank = Column(Integer, nullable=False) quota = Column(String, nullable=False) favorite_cities = Column(String, nullable=False) class MajorSelection(Base): __tablename__ = "major_selections" id = Column(Integer, primary_key=True, index=True) student_id = Column(Integer, ForeignKey("users.id")) counselor_id = Column(Integer, ForeignKey("users.id"), nullable=True) generated_list = Column(Text, nullable=True) finalized_list = Column(Text, nullable=True) status = Column(String, default="draft") class Review(Base): __tablename__ = "reviews" id = Column(Integer, primary_key=True, index=True) student_id = Column(Integer, ForeignKey("users.id")) booking_id = Column(Integer, ForeignKey("bookings.id"), nullable=True) counselor_id = Column(Integer, ForeignKey("users.id"), nullable=True) school_id = Column(Integer, ForeignKey("school_profiles.id"), nullable=True) rating = Column(Float, default=5.0) comment = Column(Text, nullable=True) created_at = Column(DateTime, default=get_now) class Notification(Base): __tablename__ = "notifications" id = Column(Integer, primary_key=True, index=True) user_id = Column(Integer, ForeignKey("users.id"), nullable=True) text = Column(Text, nullable=False) role = Column(String, default="all") # 'all', 'student', 'counselor', 'school_representative' is_read = Column(Boolean, default=False) created_at = Column(DateTime, default=get_now) # مدل مدیریت تسویه حساب‌های پلتفرم (بند ۴ و ۵) class SettlementRequest(Base): __tablename__ = "settlement_requests" id = Column(Integer, primary_key=True, index=True) user_id = Column(Integer, ForeignKey("users.id")) amount = Column(Float, nullable=False) status = Column(String, default="pending") # pending, approved, rejected created_at = Column(DateTime, default=get_now) # مدل ردیابی بازدید واقعی آموزشگاه بر اساس حساب کاربری (بند ۹) class PageView(Base): __tablename__ = "page_views" id = Column(Integer, primary_key=True, index=True) school_id = Column(Integer, ForeignKey("school_profiles.id")) student_id = Column(Integer, ForeignKey("users.id")) timestamp = Column(DateTime, default=get_now) # ========================================================================= # بخش ۴: توابع کمکی امنیتی و رمزنگاری # ========================================================================= def get_password_hash(password: str) -> str: salt = bcrypt.gensalt() return bcrypt.hashpw(password.encode('utf-8'), salt).decode('utf-8') def verify_password(plain_password: str, hashed_password: str) -> bool: try: return bcrypt.checkpw(plain_password.encode('utf-8'), hashed_password.encode('utf-8')) except Exception as e: logger.error(f"Error checking hashpw: {e}") return False SECRET_KEY = os.getenv("JWT_SECRET", "8c12fa4870a23049102c89eeffb8a719280abdc0293") ALGORITHM = "HS256" def create_access_token(data: dict, expires_delta: Optional[timedelta] = None) -> str: to_encode = data.copy() if expires_delta: expire = datetime.now(timezone.utc) + expires_delta else: expire = datetime.now(timezone.utc) + timedelta(days=7) to_encode.update({"exp": expire}) return jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM) # ========================================================================= # بخش ۵: مدیریت تراکنش‌ها و احراز هویت در فلاسک # ========================================================================= def clean_expired_bookings(db: Session): try: now = get_now() expired_bookings = db.query(Booking).filter( Booking.payment_status == "pending_payment", Booking.payment_due_until < now ).all() for b in expired_bookings: b.payment_status = "expired" slot = db.query(TimeSlot).filter(TimeSlot.id == b.slot_id).first() if slot: slot.is_booked = False slot.booked_by_student_id = None db.commit() except Exception as e: logger.error(f"Error during clean_expired_bookings: {e}") db.rollback() flask_app = Flask(__name__, static_folder="dist", static_url_path="") CORS(flask_app, resources={r"/*": {"origins": "*"}}) def get_db() -> Session: if 'db' not in g: g.db = SessionLocal() clean_expired_bookings(g.db) return g.db @flask_app.teardown_appcontext def shutdown_session(exception=None): db = g.pop('db', None) if db is not None: db.close() def login_required(f): @wraps(f) def decorated(*args, **kwargs): auth_header = request.headers.get("Authorization") if not auth_header: return jsonify({"detail": "اعتبار ورود شما منقضی شده است؛ لطفاً مجدداً وارد شوید."}), 401 parts = auth_header.split() if len(parts) != 2 or parts[0].lower() != "bearer": return jsonify({"detail": "قالب توکن احراز هویت معتبر نیست."}), 401 token = parts[1] try: payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM]) email = payload.get("sub") if email is None: return jsonify({"detail": "اعتبار ورود شما منقضی شده است؛ لطفاً مجدداً وارد شوید."}), 401 except JWTError: return jsonify({"detail": "اعتبار ورود شما منقضی شده است؛ لطفاً مجدداً وارد شوید."}), 401 db = get_db() user = db.query(User).filter(User.email == email).first() if user is None: return jsonify({"detail": "کاربر یافت نشد."}), 401 g.current_user = user return f(*args, **kwargs) return decorated # ========================================================================= # بخش ۶: توابع پردازش ابری هوشمند بومی تاپ دانش # ========================================================================= def _execute_native_matrix_processing(prompt: str, model_type: str = "fast") -> str: if not AI_CORE_KEY or genai is None: return "MODE_FALLBACK_ACTIVE" try: client = genai.Client(api_key=AI_CORE_KEY) model_name = "gemini-1.5-flash" if model_type == "fast" else "gemini-1.5-pro" response = client.models.generate_content( model=model_name, contents=prompt ) return response.text.strip() except Exception as api_err: logger.error(f"Execution error on core matrix processor: {api_err}", exc_info=True) return "MODE_FALLBACK_ACTIVE" def core_selection_engine(rank: int, quota: str, favorite_cities: str) -> str: context_items = [] for u in KNOWLEDGE_BASE: context_items.append( f"- {u['university']} (رتبه کیفی: {u['ranking_tier']}): مناسب برای حوزه‌های {', '.join(u['fields'])} " f"با پذیرش رتبه منطقه یک حدود {u['minimum_rank_r1']} و منطقه دو حدود {u['minimum_rank_r2']}" ) context_str = "\n".join(context_items) prompt = f""" شما یک راهنما و مشاور تحصیلی دلسوز برای داوطلبان کنکور سراسری هستید. اطلاعات داوطلب: - رتبه داوطلب در کارنامه: {rank} - سهمیه داوطلب: {quota} - شهرهای مورد پسند: {favorite_cities} پایگاه داده دانشگاه‌ها: {context_str} یک سناریوی پیشنهادی شامل حداکثر 8 انتخاب خوش‌بینانه و منطقی به زبان کاملاً ساده, روان, صمیمی و دانشجویی تولید کنید. خروجی را صرفاً و بدون هیچگونه متن توضیحی اضافه در قالب یک آرایه معتبر JSON و فاقد کدهای مارک‌داون برگردانید: [ {{ "college": "نام دانشگاه", "major": "رشته تحصیلی", "priority": 1, "reason": "دلیل انتخاب با لحنی بسیار صمیمی، صنف دانشجویی و بی‌آلایش" }} ] """ result = _execute_native_matrix_processing(prompt, "pro") if result == "MODE_FALLBACK_ACTIVE": return json.dumps([ {"college": "دانشگاه تبریز", "major": "مهندسی کامپیوتر", "priority": 1, "reason": "اولویت تراز اول متناسب با رتبه اعلامی شما در زیرگروه مربوطه."}, {"college": "دانشگاه علوم پزشکی تبریز", "major": "پزشکی عمومی", "priority": 2, "reason": "بر اساس الگوهای انتخابی داوطلبان با سهمیه مشابه."} ], ensure_ascii=False) return result def core_diagnostic_analysis(answers: str) -> str: prompt = f""" یک داوطلب به سوالات آزمون تشخصیصی زیر پاسخ داده است: {answers} یک ارزیابی علمی بسیار فشرده (حداکثر ۲ بند کوتاه) در مورد سبک مطالعه و برطرف کردن نقایص آزمون داوطلب بنویسید تا مشاور مطالعه کند. رسمی و فارسی باشد. """ result = _execute_native_matrix_processing(prompt, "fast") if result == "MODE_FALLBACK_ACTIVE": return "ارزیابی اولیه: داوطلب تمایل قوی به ساختارهای مفهومی دارد؛ پیشنهاد می‌شود روی زمان‌بندی تست‌های سرعتی متمرکز شود." return result def core_bio_extraction(resume_text: str) -> str: prompt = f""" بر مبنای متن رزومه مشاور زیر، یک بیوگرافی روان، جذاب، خلاصه و متقاعدکننده (حداکثر ۷۰ کلمه به زبان فارسی و صمیمی) جهت درج در بخش توضیحات پروفایل او بنویسید. متن رزومه: {resume_text} """ result = _execute_native_matrix_processing(prompt, "fast") if result == "MODE_FALLBACK_ACTIVE": return "کارشناس تحصیلی با سابقه رتبه‌پروری در کنکور و برنامه‌ریزی تخصصی داوطلبان گروه علوم تجربی و ریاضی." return result # ========================================================================= # بخش ۷: لایه همگام‌سازی داده‌های تستی و دمو # ========================================================================= def seed_initial_data(db: Session): try: global_quizzes_seed = [ { "title": "آزمون تشخصیصی سبک‌های مطالعه و یادگیری (عمومی)", "questions": '[{"question":"بیشتر از چه طریقی اطلاعات را بهتر به خاطر می‌سپارید؟","option1":"شنیدن توضیحات دبیر","option2":"خواندن متن کتاب و جزوه","option3":"نوشتن خلاصه و حل تمرین","option4":"ترسیم نمودار و نقشه ذهنی","correct":"2"}]' }, { "title": "تست سنجش میزان استرس و اضطراب آزمون", "questions": '[{"question":"در جلسات شبیه‌ساز کنکور با کمبود وقت چگونه مواجه می‌شوید؟","option1":"دچار دستپاچگی و رها کردن سوالات می‌شوم","option2":"با آرامش سوالات ساده را گلچین می‌کنم","option3":"تلاش می‌کنم سرعت تست‌زنی را بدون دقت بالا ببرم","option4":"تمرکز خود را به طور کامل از دست می‌دهم","correct":"2"}]' }, { "title": "ارزیابی انگیزه تحصیلی و خودکارآمدی کنکور", "questions": '[{"question":"مهم‌ترین محرک شما برای ادامه مسیر فرساینده کنکور چیست؟","option1":"علاقه به رشته دانشگاهی آینده","option2":"فشار خانواده و انتظارات اجتماعی","option3":"رقابت با همکلاسی‌ها","option4":"ترس از شکست و آینده نامعلوم","correct":"1"}]' } ] for gq in global_quizzes_seed: existing_quiz = db.query(CustomQuiz).filter(CustomQuiz.title == gq["title"]).first() if not existing_quiz: new_quiz = CustomQuiz(counselor_id=None, title=gq["title"], questions_json=gq["questions"]) db.add(new_quiz) db.commit() logger.info("Database base check completed.") except Exception as e: logger.error(f"Seeding process crashed completely: {e}", exc_info=True) db.rollback() def run_auto_migrations(engine, Base): inspector = inspect(engine) for table_name, table in Base.metadata.tables.items(): if inspector.has_table(table_name): existing_cols = {c["name"] for c in inspector.get_columns(table_name)} for col in table.columns: if col.name not in existing_cols: col_type = col.type.compile(dialect=engine.dialect) alter_sql = f'ALTER TABLE "{table_name}" ADD COLUMN "{col.name}" {col_type} NULL' try: with engine.begin() as connection: connection.execute(text(alter_sql)) logger.info(f"Auto-migration: Added missing column '{col.name}' to table '{table_name}'") except Exception as e: logger.error(f"Auto-migration: Failed to add column '{col.name}' to table '{table_name}': {e}") else: try: table.create(bind=engine) logger.info(f"Auto-migration: Created missing table '{table_name}'") except Exception as e: logger.error(f"Auto-migration: Failed to create table '{table_name}': {e}") # ========================================================================= # بخش ۸: راه‌اندازی و دیتابیس بومی فلاسک # ========================================================================= with flask_app.app_context(): try: Base.metadata.create_all(bind=engine) try: run_auto_migrations(engine, Base) except Exception as migration_err: logger.warning(f"Auto-migration skipped or handled concurrently: {migration_err}") db = SessionLocal() try: seed_initial_data(db) except Exception as seed_err: logger.error(f"Error seeding database during startup: {seed_err}", exc_info=True) finally: db.close() except Exception as startup_db_err: logger.warning(f"Database setup or seeding skipped: {startup_db_err}") @flask_app.errorhandler(Exception) def global_exception_handler(exc): if isinstance(exc, HTTPException): return exc logger.error(f"Internal Crash logged: {exc}", exc_info=True) return jsonify({"detail": "سرور موقتاً شلوغ است. لطفاً چند لحظه دیگر مجدداً تلاش کنید."}), 500 # ========================================== # بخش ۹: آدرس‌های API مربوط به احراز هویت و حساب کاربری # ========================================== @flask_app.route("/api/auth/register", methods=["POST"]) def auth_register(): db = get_db() data = request.get_json() or {} email = data.get("email") password = data.get("password") role = data.get("role") name = data.get("name") specialty = data.get("specialty") resume = data.get("resume") phone_number = data.get("phone_number") city = data.get("city") address = data.get("address") grade = data.get("grade") major = data.get("major") profile_pic = data.get("profile_pic") if not email or not password or not role: return jsonify({"detail": "وارد کردن ایمیل، رمز عبور و نقش الزامی است."}), 400 try: existing_user = db.query(User).filter(User.email == email).first() if existing_user: return jsonify({"detail": "حسابی با این آدرس ایمیل قبلاً ثبت گردیده است."}), 400 hashed = get_password_hash(password) is_verified_init = True if role == "student" else False new_user = User( email=email, hashed_password=hashed, role=role, name=name, is_verified=is_verified_init, phone_number=phone_number, city=city, address=address, grade=grade, major=major, profile_pic=profile_pic ) db.add(new_user) db.flush() if role == "counselor": c_prof = CounselorProfile( user_id=new_user.id, specialty=specialty, resume=resume, hourly_rate=150000.0, deactivated_days_json="[]" ) db.add(c_prof) elif role == "school_representative": s_prof = SchoolProfile( user_id=new_user.id, name=name or "آموزشگاه جدید ثبت‌شده", subscription_active_until=get_now(), address=address, phone_number=phone_number, profile_pic=profile_pic ) db.add(s_prof) db.commit() return jsonify({ "message": "ثبت‌نام با موفقیت به پایان رسید.", "role": role, "is_verified": is_verified_init }), 200 except Exception as e: logger.error(f"Error during registration: {e}", exc_info=True) db.rollback() return jsonify({"detail": "سرور موقتاً شلوغ است. لطفاً بعداً تلاش کنید."}), 503 @flask_app.route("/api/auth/login", methods=["POST"]) def auth_login(): db = get_db() data = request.get_json() or {} email = data.get("email") password = data.get("password") if not email or not password: return jsonify({"detail": "وارد کردن ایمیل و رمز عبور الزامی است."}), 400 try: user = db.query(User).filter(User.email == email).first() if not user or not verify_password(password, user.hashed_password): return jsonify({"detail": "ایمیل یا رمز عبور اشتباه است."}), 400 existing_login_notif = db.query(Notification).filter( Notification.user_id == user.id, Notification.text.like("%ورود موفقیت‌آمیز به حساب کاربری%") ).first() if not existing_login_notif: notif = Notification( user_id=user.id, text=f"ورود موفقیت‌آمیز به حساب کاربری در تاریخ {get_shamsi_date_str(get_now())}", role=user.role, is_read=False ) db.add(notif) db.commit() token = create_access_token(data={"sub": user.email}) return jsonify({ "access_token": token, "token_type": "bearer", "role": user.role, "is_verified": user.is_verified, "balance": user.balance }), 200 except Exception as e: logger.error(f"Error during login: {e}", exc_info=True) return jsonify({"detail": "سرور موقتاً شلوغ است. لطفاً بعداً تلاش کنید."}), 503 @flask_app.route("/api/auth/me", methods=["GET"]) @login_required def get_user_profile(): current_user = g.current_user requires_rating_warning = False if current_user.role == "counselor" and current_user.rating < 3.0 and current_user.rating > 0.0: requires_rating_warning = True return jsonify({ "id": current_user.id, "email": current_user.email, "role": current_user.role, "name": current_user.name or current_user.email.split("@")[0], # نمایش اولویت بر اساس نام کاربر (بند ۶) "balance": current_user.balance, "rating": current_user.rating, "phone_number": current_user.phone_number, "city": current_user.city, "address": current_user.address, # برگشت دادن فیلد آدرس دقیق (بند ۶) "grade": current_user.grade, "major": current_user.major, "profile_pic": current_user.profile_pic, "is_verified": current_user.is_verified, "requires_rating_warning": requires_rating_warning }), 200 @flask_app.route("/api/auth/me/update", methods=["PUT"]) @login_required def update_user_profile(): db = get_db() current_user = g.current_user data = request.get_json() or {} name = data.get("name") email_val = data.get("email") password_val = data.get("password") phone_number = data.get("phone_number") city = data.get("city") address = data.get("address") grade_val = data.get("grade") major_val = data.get("major") profile_pic = data.get("profile_pic") try: if name: current_user.name = name if current_user.role == "school_representative" and current_user.school_profile: current_user.school_profile.name = name if email_val: existing = db.query(User).filter(User.email == email_val, User.id != current_user.id).first() if existing: return jsonify({"detail": "این آدرس ایمیل قبلا ثبت شده است."}), 400 current_user.email = email_val if password_val: current_user.hashed_password = get_password_hash(password_val) if phone_number: current_user.phone_number = phone_number if current_user.role == "school_representative" and current_user.school_profile: current_user.school_profile.phone_number = phone_number if city: current_user.city = city if address: current_user.address = address if current_user.role == "school_representative" and current_user.school_profile: current_user.school_profile.address = address if grade_val: current_user.grade = grade_val if major_val: current_user.major = major_val if profile_pic: current_user.profile_pic = profile_pic if current_user.role == "school_representative" and current_user.school_profile: current_user.school_profile.profile_pic = profile_pic db.commit() return jsonify({"message": "تغییرات مشخصات کاربری با موفقیت ذخیره شد."}), 200 except Exception as e: logger.error(f"Error updating profile: {e}") db.rollback() return jsonify({"detail": "سرور موقتاً شلوغ است. لطفاً بعداً تلاش کنید."}), 503 # ========================================== # بخش ۱۰: دایرکتوری مشاوران و فیلتر منطقه‌ای هوشمند (بند ۳) # ========================================== @flask_app.route("/api/counselors", methods=["GET"]) def list_active_counselors(): db = get_db() name = request.args.get("name") city = request.args.get("city") student_city = request.args.get("student_city") try: query = db.query(User).filter(User.role == "counselor", User.is_verified == True) target_city = city or student_city is_fallback = False # فال‌بک دایرکتوری به سایر شهرها در صورت نبود مشاور در منطقه داوطلب (بند ۳) if target_city: regional_query = query.filter(User.city.ilike(f"%{target_city}%")) counselors = regional_query.all() if not counselors: is_fallback = True counselors = query.all() else: counselors = query.all() if name: counselors = [c for c in counselors if (name.lower() in (c.name or "").lower() or name.lower() in c.email.lower() or name.lower() in (c.counselor_profile.specialty or "").lower())] output = [] for c in counselors: output.append({ "counselor_id": c.id, "email": c.email, "name": c.name or c.email.split("@")[0], "rating": c.rating, "city": c.city or "تبریز", "address": c.address or "ثبت‌نشده", # ارسال آدرس جهت نمایش در فرانت‌اند "phone_number": c.phone_number or "ثبت‌نشده", # ارسال تلفن کاری جهت نمایش در فرانت‌اند "specialty": c.counselor_profile.specialty if c.counselor_profile else None, "resume": c.counselor_profile.resume if c.counselor_profile else None, "bio": c.counselor_profile.bio if c.counselor_profile else None, "hourly_rate": c.counselor_profile.hourly_rate if c.counselor_profile else 150000.0, "profile_pic": c.profile_pic, "is_fallback": is_fallback }) return jsonify(output), 200 except Exception as e: logger.error(f"Error listing counselors: {e}", exc_info=True) return jsonify({"detail": "سرور موقتاً شلوغ است. لطفاً بعداً تلاش کنید."}), 503 @flask_app.route("/api/counselors//slots", methods=["GET"]) def list_counselor_available_slots(counselor_id): db = get_db() try: slots = db.query(TimeSlot).filter( TimeSlot.counselor_id == counselor_id, TimeSlot.is_booked == False ).all() output = [] for s in slots: output.append({ "id": s.id, "counselor_id": s.counselor_id, "start_time": s.start_time.isoformat(), "end_time": s.end_time.isoformat(), "is_booked": s.is_booked, "booked_by_student_id": s.booked_by_student_id }) return jsonify(output), 200 except Exception as e: logger.error(f"Error listing slots: {e}", exc_info=True) return jsonify({"detail": "سرور موقتاً شلوغ است. لطفاً بعداً تلاش کنید."}), 503 # ========================================== # بخش ۱۱: فرآیند تراکنش رزرو با کسر ۷٪ کارمزد پلتفرم (بند ۴) # ========================================== @flask_app.route("/api/bookings", methods=["POST"]) @login_required def book_session(): db = get_db() current_user = g.current_user data = request.get_json() or {} slot_id = data.get("slot_id") if not slot_id: return jsonify({"detail": "شناسه بازه زمانی الزامی است."}), 400 try: if current_user.role != "student": return jsonify({"detail": "تنها کاربران با نقش داوطلب مجاز به رزرو هستند."}), 403 slot = db.query(TimeSlot).filter(TimeSlot.id == slot_id).first() if not slot or slot.is_booked: return jsonify({"detail": "این زمان قبلاً رزرو شده است یا در دسترس نیست."}), 400 counselor = db.query(User).filter(User.id == slot.counselor_id).first() if not counselor or not counselor.counselor_profile: return jsonify({"detail": "مشاور انتخابی در سیستم یافت نشد."}), 404 cost = counselor.counselor_profile.hourly_rate if current_user.balance < cost: return jsonify({"detail": "موجودی حساب کافی نیست. لطفاً کیف پول خود را شارژ نمایید."}), 400 # کسر کارمزد ۷٪ پلتفرم به صورت خودکار (بند ۴) platform_fee = cost * 0.07 net_to_counselor = cost - platform_fee current_user.balance -= cost counselor.balance += net_to_counselor slot.is_booked = True slot.booked_by_student_id = current_user.id booking = Booking( slot_id=slot.id, student_id=current_user.id, counselor_id=counselor.id, status="active", cost=cost ) db.add(booking) db.commit() return jsonify({ "message": "رزرو جلسه با موفقیت تایید و سهم مشاور به کیف پول واریز شد.", "remaining_balance": current_user.balance, "session_start": slot.start_time.isoformat() }), 200 except Exception as e: logger.error(f"Error during booking creation: {e}", exc_info=True) db.rollback() return jsonify({"detail": "سرور موقتاً شلوغ است. لطفاً بعداً تلاش کنید."}), 503 @flask_app.route("/api/student/bookings", methods=["GET"]) @login_required def get_student_bookings(): db = get_db() current_user = g.current_user try: if current_user.role != "student": return jsonify({"detail": "دسترسی غیرمجاز"}), 403 bookings = db.query(Booking).filter( Booking.student_id == current_user.id, Booking.status == "active" ).order_by(Booking.id.desc()).all() output = [] for b in bookings: counselor = db.query(User).filter(User.id == b.counselor_id).first() slot = db.query(TimeSlot).filter(TimeSlot.id == b.slot_id).first() output.append({ "id": b.id, "counselor_name": counselor.name if counselor else "مشاور تحصیلی", "date": get_shamsi_date_str(slot.start_time) if slot else get_shamsi_date_str(b.created_at), "time": slot.start_time.strftime("%H:%M") if slot else "۱۶:۰۰", "amount": b.cost, "status": "موفق" }) return jsonify(output), 200 except Exception as e: logger.error(f"Error fetching student bookings: {e}") return jsonify({"detail": "خطا در دریافت لیست نوبت‌های داوطلب."}), 500 @flask_app.route("/api/bookings//cancel-by-counselor", methods=["POST"]) @login_required def cancel_session_by_counselor(booking_id): db = get_db() current_user = g.current_user try: if current_user.role != "counselor": return jsonify({"detail": "تنها مشاوران مجاز به استفاده از این متد هستند."}), 403 booking = db.query(Booking).filter( Booking.id == booking_id, Booking.counselor_id == current_user.id ).first() if not booking or booking.status == "cancelled": return jsonify({"detail": "این رزرو قبلاً لغو شده یا وجود ندارد."}), 404 student = db.query(User).filter(User.id == booking.student_id).first() counselor = current_user if student: student.balance += booking.cost student_notif = Notification( user_id=student.id, text=f"پیام سیستم: جلسه مشاوره شما با مشاور {counselor.name} لغو گردید و کل مبلغ {booking.cost:,.0f} ریال به کیف پول شما عودت داده شد.", role="student" ) db.add(student_notif) # جریمه انضباطی ۱۰ درصدی لغو یک‌طرفه مشاور بر اساس قوانین پلتفرم (بند ۴) penalty_fee = booking.cost * 0.10 counselor.balance = counselor.balance - (booking.cost + penalty_fee) counselor_notif = Notification( user_id=counselor.id, text=f"پیام سیستم: رزرو شماره {booking.id} توسط شما لغو گردید و جریمه ۱۰ درصدی ({penalty_fee:,.0f} ریال) در حساب شما ثبت شد.", role="counselor" ) db.add(counselor_notif) slot = db.query(TimeSlot).filter(TimeSlot.id == booking.slot_id).first() if slot: slot.is_booked = False slot.booked_by_student_id = None booking.status = "cancelled" db.commit() return jsonify({"message": "جلسه مشاوره لغو، مبالغ عودت و جریمه کسر گردید."}), 200 except Exception as e: logger.error(f"Error cancelling booking by counselor: {e}", exc_info=True) db.rollback() return jsonify({"detail": "خطا در فرآیند لغو رزرو مشاور."}), 500 # ======================================================== # بخش ۱۲: روت پیشرفته استخراج و تحلیل رزومه با هوش مصنوعی جمینای (بند ۵) # ======================================================== @flask_app.route("/api/counselor/extract-resume-ai", methods=["POST"]) @login_required def extract_resume_ai(): db = get_db() current_user = g.current_user if current_user.role != "counselor": return jsonify({"detail": "دسترسی غیرمجاز"}), 403 data = request.get_json() or {} file_data = data.get("file_data") # رشته رمزگذاری شده به صورت Base64 if not file_data: return jsonify({"detail": "بارگذاری فایل رزومه جهت استخراج الزامی است."}), 400 try: # دیکودر بیس ۶۴ و تفکیک هدر چندرسانه‌ای if "," in file_data: header, base64_str = file_data.split(",", 1) mime_type = header.split(";")[0].split(":")[1] else: base64_str = file_data mime_type = "image/png" # پیش‌فرض در صورت نبود هدر file_bytes = base64.b64decode(base64_str) except Exception as decode_err: logger.error(f"Error decoding base64: {decode_err}") return jsonify({"detail": "فرمت یا کدهای فایل رزومه نامعتبر است."}), 400 extracted_text = "" # اتصال مستقیم به SDK نسل جدید هوش مصنوعی گوگل (گوگل جمینای) if AI_CORE_KEY and genai: try: client = genai.Client(api_key=AI_CORE_KEY) model_name = "gemini-2.5-flash" # استفاده از قوی‌ترین مدل پردازش تصویر و متن contents = [ types.Content( role="user", parts=[ types.Part.from_bytes( mime_type=mime_type, data=file_bytes ), types.Part.from_text(text="شما یک تحلیلگر و پردازشگر رزومه هستید. لطفاً این رزومه علمی مشاور را بررسی کرده و اطلاعات آن شامل سوابق تحصیلی، سوابق کاری، جوایز و حوزه‌های تخصصی او را به زبان فارسی بسیار روان و خلاصه (حداکثر ۱۰۰ کلمه) استخراج کرده و ارائه دهید.") ] ) ] response = client.models.generate_content( model=model_name, contents=contents ) extracted_text = response.text.strip() except Exception as ai_err: logger.error(f"Gemini API processing failed: {ai_err}") extracted_text = "خطا در برقراری ارتباط با جمینای. تحلیل متنی: مشاور هدایت تحصیلی و کنکور در آذربایجان شرقی با رویکرد ارزیابی ترازهای آماری." else: extracted_text = "تحلیل رزومه (حالت آفلاین): مشاور کنکور سراسری، با سابقه رتبه‌پروری و هدایت رتبه‌های تک‌رقمی و دو رقمی در تبریز." return jsonify({ "extracted_text": extracted_text }), 200 # ======================================================== # بخش ۱۳: درگاه‌های مدیریت تسویه حساب و کیف پول (بند ۴) # ======================================================== @flask_app.route("/api/counselor/request-settlement", methods=["POST"]) @login_required def counselor_request_settlement(): db = get_db() current_user = g.current_user try: if current_user.role != "counselor": return jsonify({"detail": "دسترسی غیرمجاز"}), 403 if current_user.balance <= 0: return jsonify({"detail": "موجودی قابل تسویه وجود ندارد."}), 400 req = SettlementRequest( user_id=current_user.id, amount=current_user.balance, status="pending" ) db.add(req) current_user.balance = 0.0 db.commit() return jsonify({"message": "درخواست تسویه با موفقیت ثبت شد."}), 200 except Exception as e: logger.error(f"Error requesting settlement: {e}") db.rollback() return jsonify({"detail": "بروز خطا در ثبت تسویه مالی."}), 500 # ======================================================== # گزارش مالی و تفکیک محاسباتی سه‌گانه (بند ۵) # ======================================================== @flask_app.route("/api/financial/summary", methods=["GET"]) @login_required def get_role_financial_summary(): db = get_db() current_user = g.current_user try: if current_user.role == "student": total_spent = db.query(text("SUM(cost)")).from_statement( text("SELECT cost FROM bookings WHERE student_id = :sid AND status = 'active'") ).params(sid=current_user.id).scalar() or 0.0 refunds = db.query(text("SUM(cost)")).from_statement( text("SELECT cost FROM bookings WHERE student_id = :sid AND status = 'cancelled'") ).params(sid=current_user.id).scalar() or 0.0 return jsonify({ "balance": current_user.balance, "total_spent": total_spent, "refunds": refunds }), 200 elif current_user.role == "counselor": gross = db.query(text("SUM(cost)")).from_statement( text("SELECT cost FROM bookings WHERE counselor_id = :cid") ).params(cid=current_user.id).scalar() or 0.0 platform_fee = gross * 0.07 net_earnings = gross - platform_fee settlements = db.query(text("SUM(amount)")).from_statement( text("SELECT amount FROM settlement_requests WHERE user_id = :uid AND status = 'approved'") ).params(uid=current_user.id).scalar() or 0.0 pending_settlements = db.query(text("SUM(amount)")).from_statement( text("SELECT amount FROM settlement_requests WHERE user_id = :uid AND status = 'pending'") ).params(uid=current_user.id).scalar() or 0.0 return jsonify({ "balance": current_user.balance, "gross_earnings": gross, "platform_fee": platform_fee, "net_earnings": net_earnings, "settled_amount": settlements, "pending_settlements": pending_settlements }), 200 elif current_user.role == "school_representative": spent_ads = db.query(text("SUM(amount)")).from_statement( text("SELECT amount FROM settlement_requests WHERE user_id = :uid") ).params(uid=current_user.id).scalar() or 0.0 return jsonify({ "balance": current_user.balance, "total_spent_ads": spent_ads, "refunds": 0.0 }), 200 except Exception as e: logger.error(f"Error building financial summary: {e}") return jsonify({"detail": "خطا در دریافت وضعیت تراز مالی."}), 500 # ======================================================== # ردیابی ترافیک و آمار بازدید آموزشگاه (بند ۹) # ======================================================== @flask_app.route("/api/schools//visit", methods=["POST"]) @login_required def track_school_page_view(school_id): db = get_db() current_user = g.current_user try: recent = db.query(PageView).filter( PageView.school_id == school_id, PageView.student_id == current_user.id, PageView.timestamp > get_now() - timedelta(minutes=30) ).first() if not recent: view = PageView( school_id=school_id, student_id=current_user.id ) db.add(view) db.commit() return jsonify({"status": "success"}), 200 except Exception as e: logger.error(f"Error tracking visit: {e}") db.rollback() return jsonify({"detail": "خطا در ثبت آمار ترافیک."}), 500 @flask_app.route("/api/schools//analytics", methods=["GET"]) @login_required def get_school_analytics_metrics(school_id): db = get_db() try: total_views = db.query(PageView).filter(PageView.school_id == school_id).count() reviews = db.query(Review).filter(Review.school_id == school_id).all() distribution = [0, 0, 0, 0, 0] avg_rating = 0.0 if reviews: for r in reviews: r_val = int(round(r.rating)) if 1 <= r_val <= 5: distribution[r_val - 1] += 1 avg_rating = sum([r.rating for r in reviews]) / len(reviews) return jsonify({ "page_views": total_views, "average_rating": avg_rating, "ratings_distribution": distribution }), 200 except Exception as e: logger.error(f"Error fetching school analytics: {e}") return jsonify({"detail": "خطا در بارگذاری اطلاعات آموزشگاه."}), 500 # ======================================================== # امتیازدهی اتوماتیک ۲۴ ساعت پس از نوبت (بند ۱۰) # ======================================================== @flask_app.route("/api/student/pending-reviews", methods=["GET"]) @login_required def get_student_pending_reviews_for_counselors(): db = get_db() current_user = g.current_user try: if current_user.role != "student": return jsonify({"detail": "دسترسی غیرمجاز"}), 403 boundary_time = get_now() - timedelta(hours=24) bookings = db.query(Booking).join(TimeSlot).filter( Booking.student_id == current_user.id, Booking.status == "active", TimeSlot.start_time < boundary_time ).all() pending_list = [] for b in bookings: reviewed = db.query(Review).filter(Review.booking_id == b.id).first() if not reviewed: counselor = db.query(User).filter(User.id == b.counselor_id).first() pending_list.append({ "booking_id": b.id, "counselor_id": b.counselor_id, "counselor_name": counselor.name if counselor else "مشاور تحصیلی", "date": get_shamsi_date_str(b.created_at) }) return jsonify(pending_list), 200 except Exception as e: logger.error(f"Error getting pending reviews: {e}") return jsonify({"detail": "خطا در ارزیابی نوبت‌های فعال داوطلب."}), 500 # ========================================== # دایرکتوری آموزشگاه‌های فعال با فیلتر بومی (بند ۳) # ========================================== @flask_app.route("/api/schools", methods=["GET"]) def list_and_filter_schools(): db = get_db() name = request.args.get("name") city = request.args.get("city") student_city = request.args.get("student_city") try: now = get_now() query = db.query(SchoolProfile).filter(SchoolProfile.subscription_active_until > now) target_city = city or student_city is_fallback = False if target_city: regional_query = query.join(User, User.id == SchoolProfile.user_id).filter(User.city.ilike(f"%{target_city}%")) schools = regional_query.all() if not schools: is_fallback = True schools = query.all() else: schools = query.all() if name: schools = [s for s in schools if name.lower() in (s.name or "").lower()] output = [] for s in schools: output.append({ "school_id": s.id, "school_name": s.name, "is_sponsored": s.is_sponsored, "rating": s.rating, "bio": s.bio, "address": s.address, "phone_number": s.phone_number, "founding_year": s.founding_year, "city": s.user.city if s.user else "تبریز", # ارسال شهر به ازای ادرس آموزشگاه (بند ۳) "is_fallback": is_fallback, "courses": [ { "course_name": c.course_name, "teacher_name": c.teacher_name, "teaching_style": c.teaching_style } for c in s.courses ] }) return jsonify(output), 200 except Exception as e: logger.error(f"Error querying schools: {e}") return jsonify({"detail": "سرور موقتاً شلوغ است. لطفاً بعداً تلاش کنید."}), 503 @flask_app.route("/api/reviews", methods=["POST"]) @login_required def create_session_review(): db = get_db() current_user = g.current_user data = request.get_json() or {} booking_id = data.get("booking_id") counselor_id = data.get("counselor_id") school_id = data.get("school_id") rating = data.get("rating", 5.0) comment = data.get("comment") try: if current_user.role != "student": return jsonify({"detail": "دسترسی غیرمجاز"}), 403 review = Review( student_id=current_user.id, booking_id=booking_id, counselor_id=counselor_id, school_id=school_id, rating=rating, comment=comment ) db.add(review) if counselor_id: counselor = db.query(User).filter(User.id == counselor_id).first() if counselor: counselor.rating = (counselor.rating + rating) / 2.0 if counselor.rating > 0.0 else rating elif school_id: school = db.query(SchoolProfile).filter(SchoolProfile.id == school_id).first() if school: school.rating = (school.rating + rating) / 2.0 if school.rating > 0.0 else rating db.commit() return jsonify({"message": "امتیاز با موفقیت در دیتابیس ثبت شد."}), 200 except Exception as e: logger.error(f"Error creating review: {e}") db.rollback() return jsonify({"detail": "سرور موقتاً شلوغ است. لطفاً بعداً تلاش کنید."}), 503 @flask_app.route("/api/schools/update", methods=["POST"]) @login_required def update_school_course_portfolio(): db = get_db() current_user = g.current_user data = request.get_json() or {} name = data.get("name") bio = data.get("bio") address = data.get("address") phone_number = data.get("phone_number") founding_year = data.get("founding_year") courses_list = data.get("courses", []) profile_pic = data.get("profile_pic") if not name: return jsonify({"detail": "نام موسسه یا آموزشگاه الزامی است."}), 400 try: if current_user.role != "school_representative": return jsonify({"detail": "دسترسی غیرمجاز"}), 403 profile = db.query(SchoolProfile).filter(SchoolProfile.user_id == current_user.id).first() if not profile: profile = SchoolProfile( user_id=current_user.id, name=name, bio=bio, address=address, phone_number=phone_number, founding_year=founding_year, profile_pic=profile_pic ) db.add(profile) db.flush() else: profile.name = name profile.bio = bio profile.address = address profile.phone_number = phone_number profile.founding_year = founding_year if profile_pic: profile.profile_pic = profile_pic db.query(SchoolCourse).filter(SchoolCourse.school_id == profile.id).delete() for c in courses_list: db_course = SchoolCourse( school_id=profile.id, course_name=c.get("course_name"), teacher_name=c.get("teacher_name"), teaching_style=c.get("teaching_style") ) db.add(db_course) db.commit() return jsonify({"message": "رزومه کادر اساتید و آموزشگاه با موفقیت بروز گردید."}), 200 except Exception as e: logger.error(f"Error during school portfolio update: {e}", exc_info=True) db.rollback() return jsonify({"detail": "سرور موقتاً شلوغ است. لطفاً بعداً تلاش کنید."}), 503 @flask_app.route("/api/schools/subscribe", methods=["POST"]) @login_required def purchase_school_subscription(): db = get_db() current_user = g.current_user data = request.get_json() or {} months = data.get("months") is_sponsored = data.get("is_sponsored", False) if not months: return jsonify({"detail": "تعداد ماه‌ها معتبر نیست."}), 400 try: if current_user.role != "school_representative": return jsonify({"detail": "دسترسی غیرمجاز"}), 403 profile = db.query(SchoolProfile).filter(SchoolProfile.user_id == current_user.id).first() if not profile: return jsonify({"detail": "مشخصات آموزشگاه یافت نشد."}), 404 now = get_now() if profile.subscription_active_until and profile.subscription_active_until > now: profile.subscription_active_until += timedelta(days=30 * months) else: profile.subscription_active_until = now + timedelta(days=30 * months) profile.is_sponsored = is_sponsored db.commit() return jsonify({ "message": f"عضویت ویژه فعال گردید.", "active_until": profile.subscription_active_until.isoformat(), "is_sponsored": profile.is_sponsored }), 200 except Exception as e: logger.error(f"Error purchasing school subscription: {e}", exc_info=True) db.rollback() return jsonify({"detail": "سرور موقتاً شلوغ است. لطفاً بعداً تلاش کنید."}), 503 @flask_app.route("/api/student/topup", methods=["POST"]) @login_required def top_up_student_wallet(): db = get_db() current_user = g.current_user data = request.get_json() or {} amount = data.get("amount") if amount is None: return jsonify({"detail": "مبلغ تراکنش نامعتبر است."}), 400 try: if current_user.role != "student": return jsonify({"detail": "دسترسی غیرمجاز"}), 403 current_user.balance += amount db.commit() return jsonify({"message": "شارژ حساب کاربری با موفقیت تکمیل شد.", "new_balance": current_user.balance}), 200 except Exception as e: logger.error(f"Error topup wallet: {e}", exc_info=True) db.rollback() return jsonify({"detail": "سرور موقتاً شلوغ است. لطفاً بعداً تلاش کنید."}), 503 @flask_app.route("/api/student/test/submit", methods=["POST"]) @login_required def submit_diagnostic_prep_test(): db = get_db() current_user = g.current_user data = request.get_json() or {} counselor_id = data.get("counselor_id") answers = data.get("answers") if not counselor_id or not answers: return jsonify({"detail": "اطلاعات مشاور و پاسخنامه تشخیصی الزامی است."}), 400 try: if current_user.role != "student": return jsonify({"detail": "دسترسی غیرمجاز"}), 403 analysis_report = core_diagnostic_analysis(answers) test_res = TestResult( student_id=current_user.id, counselor_id=counselor_id, answers=answers, ai_analysis_summary=analysis_report ) db.add(test_res) db.commit() return jsonify({ "message": "پاسخنامه ثبت و برای مشاور ارسال شد.", "evaluation_report": analysis_report }), 200 except Exception as e: logger.error(f"Error submitting prep test: {e}", exc_info=True) db.rollback() return jsonify({"detail": "سرور موقتاً شلوغ است. لطفاً بعداً تلاش کنید."}), 503 @flask_app.route("/api/counselor/slots", methods=["POST"]) @login_required def create_time_slots(): db = get_db() current_user = g.current_user data = request.get_json() or [] if not isinstance(data, list): data = [data] try: if current_user.role != "counselor": return jsonify({"detail": "دسترسی مخصوص مشاورین"}), 403 for slot in data: start_time_str = slot.get("start_time") end_time_str = slot.get("end_time") if not start_time_str or not end_time_str: continue try: start_time = datetime.fromisoformat(start_time_str.replace("Z", "+00:00")) end_time = datetime.fromisoformat(end_time_str.replace("Z", "+00:00")) except ValueError: continue new_slot = TimeSlot( counselor_id=current_user.id, start_time=start_time, end_time=end_time, is_booked=False ) db.add(new_slot) db.commit() return jsonify({"message": "تقویم زمانی ثبت گردید."}), 200 except Exception as e: logger.error(f"Error adding slots: {e}", exc_info=True) db.rollback() return jsonify({"detail": "سرور موقتاً شلوغ است. لطفاً بعداً تلاش کنید."}), 503 @flask_app.route("/api/counselor/students", methods=["GET"]) @login_required def get_counselor_dash_clients(): db = get_db() current_user = g.current_user try: if current_user.role != "counselor": return jsonify({"detail": "دسترسی غیرمجاز"}), 403 booked_slots = db.query(TimeSlot).filter( TimeSlot.counselor_id == current_user.id, TimeSlot.is_booked == True ).all() student_ids = list(set([s.booked_by_student_id for s in booked_slots if s.booked_by_student_id])) results = [] for s_id in student_ids: student_user = db.query(User).filter(User.id == s_id).first() if not student_user: continue tests = db.query(TestResult).filter(TestResult.student_id == s_id, TestResult.counselor_id == current_user.id).all() selections = db.query(MajorSelection).filter(MajorSelection.student_id == s_id, MajorSelection.counselor_id == current_user.id).all() parsed_selections = [] for sel in selections: try: gen_list = json.loads(sel.generated_list) if sel.generated_list else [] fin_list = json.loads(sel.finalized_list) if sel.finalized_list else [] except Exception: gen_list, fin_list = sel.generated_list, sel.finalized_list parsed_selections.append({ "selection_id": sel.id, "status": sel.status, "ai_generated_list": gen_list, "counselor_finalized_list": fin_list }) results.append({ "student_id": s_id, "student_email": student_user.email, "student_name": student_user.name or student_user.email.split("@")[0], "grade": student_user.grade or "یازدهم", # فیلد مقطع تحصیلی داوطلب (بند ۱۲) "major": student_user.major or "علوم تجربی", # فیلد رشته تحصیلی داوطلب (بند ۱۲) "tests": [{"test_id": t.id, "answers": t.answers, "ai_analysis": t.ai_analysis_summary} for t in tests], "selections": parsed_selections }) return jsonify(results), 200 except Exception as e: logger.error(f"Error fetching dashboard clients: {e}", exc_info=True) return jsonify({"detail": "سرور موقتاً شلوغ است. لطفاً بعداً تلاش کنید."}), 503 @flask_app.route("/api/student/my-visited-counselors", methods=["GET"]) @login_required def get_student_visited_counselors(): db = get_db() current_user = g.current_user try: if current_user.role != "student": return jsonify({"detail": "دسترسی غیرمجاز"}), 403 bookings = db.query(Booking).filter( Booking.student_id == current_user.id, Booking.status == "active" ).all() c_ids = list(set([b.counselor_id for b in bookings])) results = [] for cid in c_ids: c = db.query(User).filter(User.id == cid).first() if c: results.append({ "counselor_id": c.id, "name": c.name or c.email.split("@")[0], "specialty": c.counselor_profile.specialty if c.counselor_profile else "کنکور سراسری", "rating": c.rating, "city": c.city or "تبریز" }) return jsonify(results), 200 except Exception as e: logger.error(f"Error getting visited counselors: {e}") return jsonify({"detail": "خطا در دریافت لیست مشاوران من."}), 500 @flask_app.route("/api/notifications//read", methods=["POST"]) @login_required def mark_single_notification_as_read(notif_id): db = get_db() current_user = g.current_user try: notif = db.query(Notification).filter( Notification.id == notif_id, or_( Notification.user_id == current_user.id, Notification.role == current_user.role, Notification.role == "all" ) ).first() if notif: notif.is_read = True db.commit() return jsonify({"status": "success"}), 200 return jsonify({"detail": "اعلان یافت نشد."}), 404 except Exception as e: logger.error(f"Error marking notification: {e}") db.rollback() return jsonify({"detail": "خطا در به‌روزرسانی اعلان."}), 500 @flask_app.route("/api/notifications/read", methods=["POST"]) @login_required def mark_notifications_as_read(): db = get_db() current_user = g.current_user try: notifs = db.query(Notification).filter( or_( Notification.user_id == current_user.id, Notification.role == current_user.role, Notification.role == "all" ) ).all() for n in notifs: n.is_read = True db.commit() return jsonify({"status": "success"}), 200 except Exception as e: logger.error(f"Error marking notifications as read: {e}") db.rollback() return jsonify({"detail": "خطا در به‌روزرسانی اعلان‌ها."}), 500 @flask_app.route("/api/counselor/create-manual-booking", methods=["POST"]) @login_required def create_manual_booking_by_counselor(): db = get_db() current_user = g.current_user data = request.get_json() or {} student_id = data.get("student_id") start_time_str = data.get("start_time") end_time_str = data.get("end_time") cost = data.get("cost") if not student_id or not start_time_str or not end_time_str or cost is None: return jsonify({"detail": "تمام فیلدها الزامی است."}), 400 try: if current_user.role != "counselor": return jsonify({"detail": "تنها مشاوران مجاز به ثبت نوبت هستند."}), 403 try: start_time = datetime.fromisoformat(start_time_str) end_time = datetime.fromisoformat(end_time_str) except ValueError: return jsonify({"detail": "فرمت تاریخ نامعتبر است."}), 400 slot = TimeSlot( counselor_id=current_user.id, start_time=start_time, end_time=end_time, is_booked=True, booked_by_student_id=student_id ) db.add(slot) db.flush() due_time = get_now() + timedelta(hours=24) booking = Booking( slot_id=slot.id, student_id=student_id, counselor_id=current_user.id, status="active", payment_status="pending_payment", payment_due_until=due_time, cost=cost ) db.add(booking) db.commit() return jsonify({ "message": "نوبت صادر و فاکتور ۲۴ ساعته ارسال گردید.", "booking_id": booking.id }), 200 except Exception as e: logger.error(f"Error manual booking: {e}") db.rollback() return jsonify({"detail": "خطا در ایجاد نوبت دستی."}), 503 @flask_app.route("/") def home_webview(): return flask_app.send_static_file("index.html") app = WsgiToAsgi(flask_app) if __name__ == "__main__": flask_app.run(host="0.0.0.0", port=7860, debug=True)