# main.py (полная версия с тегами и админкой) from fastapi import FastAPI, HTTPException, Depends, status, Request, Form from fastapi.middleware.cors import CORSMiddleware from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials from fastapi.responses import HTMLResponse, FileResponse, RedirectResponse from fastapi.staticfiles import StaticFiles from pydantic import BaseModel, EmailStr from typing import Optional, List from datetime import datetime, timedelta import jwt import sqlite3 import hashlib import os from contextlib import contextmanager import uvicorn from pathlib import Path app = FastAPI(title="Rabota.Today API", version="2.0.0") # Настройки SECRET_KEY = "your-secret-key-here-change-in-production" ALGORITHM = "HS256" ACCESS_TOKEN_EXPIRE_MINUTES = 60 * 24 * 7 # 7 дней # CORS app.add_middleware( CORSMiddleware, allow_origins=["*"], allow_credentials=True, allow_methods=["*"], allow_headers=["*"], ) security = HTTPBearer() # Создаем директории BASE_DIR = Path(__file__).parent TEMPLATES_DIR = BASE_DIR / "templates" STATIC_DIR = BASE_DIR / "static" TEMPLATES_DIR.mkdir(exist_ok=True) STATIC_DIR.mkdir(exist_ok=True) (STATIC_DIR / "css").mkdir(exist_ok=True) (STATIC_DIR / "js").mkdir(exist_ok=True) # Подключаем статические файлы app.mount("/static", StaticFiles(directory=str(STATIC_DIR)), name="static") # ========== МОДЕЛИ БАЗЫ ДАННЫХ ========== def init_db(): """Инициализация базы данных""" with get_db() as conn: cursor = conn.cursor() # Таблица пользователей cursor.execute(""" CREATE TABLE IF NOT EXISTS users ( id INTEGER PRIMARY KEY AUTOINCREMENT, full_name TEXT NOT NULL, email TEXT UNIQUE NOT NULL, phone TEXT NOT NULL, telegram TEXT, password_hash TEXT NOT NULL, role TEXT NOT NULL CHECK(role IN ('employee', 'employer', 'admin')), is_admin BOOLEAN DEFAULT 0, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ) """) # Таблица вакансий cursor.execute(""" CREATE TABLE IF NOT EXISTS vacancies ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL, title TEXT NOT NULL, salary TEXT, description TEXT, contact TEXT, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, is_active BOOLEAN DEFAULT 1, views INTEGER DEFAULT 0, FOREIGN KEY (user_id) REFERENCES users (id) ) """) # Таблица резюме cursor.execute(""" CREATE TABLE IF NOT EXISTS resumes ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER UNIQUE NOT NULL, desired_position TEXT, about_me TEXT, desired_salary TEXT, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, views INTEGER DEFAULT 0, FOREIGN KEY (user_id) REFERENCES users (id) ) """) # Таблица тегов cursor.execute(""" CREATE TABLE IF NOT EXISTS tags ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT UNIQUE NOT NULL, category TEXT CHECK(category IN ('skill', 'industry', 'position', 'other')) DEFAULT 'skill', created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ) """) # Связь тегов с вакансиями cursor.execute(""" CREATE TABLE IF NOT EXISTS vacancy_tags ( vacancy_id INTEGER NOT NULL, tag_id INTEGER NOT NULL, PRIMARY KEY (vacancy_id, tag_id), FOREIGN KEY (vacancy_id) REFERENCES vacancies (id) ON DELETE CASCADE, FOREIGN KEY (tag_id) REFERENCES tags (id) ON DELETE CASCADE ) """) # Связь тегов с резюме cursor.execute(""" CREATE TABLE IF NOT EXISTS resume_tags ( resume_id INTEGER NOT NULL, tag_id INTEGER NOT NULL, PRIMARY KEY (resume_id, tag_id), FOREIGN KEY (resume_id) REFERENCES resumes (id) ON DELETE CASCADE, FOREIGN KEY (tag_id) REFERENCES tags (id) ON DELETE CASCADE ) """) # Таблица опыта работы cursor.execute(""" CREATE TABLE IF NOT EXISTS work_experience ( id INTEGER PRIMARY KEY AUTOINCREMENT, resume_id INTEGER NOT NULL, position TEXT NOT NULL, company TEXT NOT NULL, period TEXT, FOREIGN KEY (resume_id) REFERENCES resumes (id) ON DELETE CASCADE ) """) # Таблица образования cursor.execute(""" CREATE TABLE IF NOT EXISTS education ( id INTEGER PRIMARY KEY AUTOINCREMENT, resume_id INTEGER NOT NULL, institution TEXT NOT NULL, specialty TEXT, graduation_year TEXT, FOREIGN KEY (resume_id) REFERENCES resumes (id) ON DELETE CASCADE ) """) # Таблица откликов cursor.execute(""" CREATE TABLE IF NOT EXISTS applications ( id INTEGER PRIMARY KEY AUTOINCREMENT, vacancy_id INTEGER NOT NULL, user_id INTEGER NOT NULL, message TEXT, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, status TEXT DEFAULT 'pending', FOREIGN KEY (vacancy_id) REFERENCES vacancies (id), FOREIGN KEY (user_id) REFERENCES users (id) ) """) # Создаем админа по умолчанию admin_password = hash_password("admin123") cursor.execute(""" INSERT OR IGNORE INTO users (full_name, email, phone, password_hash, role, is_admin) VALUES ('Admin', 'admin@rabota.today', '+70000000000', ?, 'admin', 1) """, (admin_password,)) # Добавляем популярные теги default_tags = [ ('Python', 'skill'), ('JavaScript', 'skill'), ('React', 'skill'), ('Vue.js', 'skill'), ('Node.js', 'skill'), ('Java', 'skill'), ('C++', 'skill'), ('PHP', 'skill'), ('HTML/CSS', 'skill'), ('SQL', 'skill'), ('Менеджер', 'position'), ('Разработчик', 'position'), ('Дизайнер', 'position'), ('Маркетолог', 'position'), ('Аналитик', 'position'), ('IT', 'industry'), ('Финансы', 'industry'), ('Медицина', 'industry'), ('Образование', 'industry'), ('Продажи', 'industry'), ] for tag_name, category in default_tags: cursor.execute("INSERT OR IGNORE INTO tags (name, category) VALUES (?, ?)", (tag_name, category)) conn.commit() print("✅ База данных инициализирована") @contextmanager def get_db(): """Контекстный менеджер для работы с БД""" conn = sqlite3.connect("rabota_today.db") conn.row_factory = sqlite3.Row try: yield conn finally: conn.close() # ========== МОДЕЛИ PYDANTIC ========== class UserRegister(BaseModel): full_name: str email: EmailStr phone: str telegram: Optional[str] = None password: str role: str class UserLogin(BaseModel): email: EmailStr password: str class TokenResponse(BaseModel): access_token: str token_type: str user_id: int full_name: str role: str is_admin: bool class Tag(BaseModel): id: Optional[int] = None name: str category: str = "skill" class VacancyCreate(BaseModel): title: str salary: Optional[str] = None description: Optional[str] = None contact: Optional[str] = None tags: List[str] = [] # список названий тегов class VacancyResponse(BaseModel): id: int title: str salary: Optional[str] description: Optional[str] contact: Optional[str] created_at: str is_active: bool views: int tags: List[Tag] = [] company_name: Optional[str] = None class WorkExperience(BaseModel): position: str company: str period: Optional[str] = None class Education(BaseModel): institution: str specialty: Optional[str] = None graduation_year: Optional[str] = None class ResumeCreate(BaseModel): desired_position: Optional[str] = None about_me: Optional[str] = None desired_salary: Optional[str] = None work_experience: List[WorkExperience] = [] education: List[Education] = [] tags: List[str] = [] # список названий тегов class ResumeResponse(BaseModel): id: int user_id: int desired_position: Optional[str] about_me: Optional[str] desired_salary: Optional[str] work_experience: List[WorkExperience] education: List[Education] tags: List[Tag] = [] views: int updated_at: str full_name: Optional[str] = None # ========== ВСПОМОГАТЕЛЬНЫЕ ФУНКЦИИ ========== def hash_password(password: str) -> str: """Хеширование пароля""" salt = "rabota.today.salt" return hashlib.sha256((password + salt).encode()).hexdigest() def verify_password(password: str, hash: str) -> bool: """Проверка пароля""" return hash_password(password) == hash def create_access_token(data: dict): """Создание JWT токена""" to_encode = data.copy() expire = datetime.utcnow() + timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES) to_encode.update({"exp": expire}) return jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM) def get_current_user(credentials: HTTPAuthorizationCredentials = Depends(security)): """Получение текущего пользователя из токена""" token = credentials.credentials try: payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM]) user_id = payload.get("sub") if user_id is None: raise HTTPException(status_code=401, detail="Невалидный токен") return int(user_id) except jwt.PyJWTError: raise HTTPException(status_code=401, detail="Невалидный токен") def get_current_user_optional(token: Optional[str] = None): """Опциональное получение пользователя (для публичных страниц)""" if not token: return None try: payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM]) return int(payload.get("sub")) except: return None def get_tags_for_vacancy(cursor, vacancy_id): """Получение тегов для вакансии""" cursor.execute(""" SELECT t.* FROM tags t JOIN vacancy_tags vt ON t.id = vt.tag_id WHERE vt.vacancy_id = ? """, (vacancy_id,)) return [dict(tag) for tag in cursor.fetchall()] def get_tags_for_resume(cursor, resume_id): """Получение тегов для резюме""" cursor.execute(""" SELECT t.* FROM tags t JOIN resume_tags rt ON t.id = rt.tag_id WHERE rt.resume_id = ? """, (resume_id,)) return [dict(tag) for tag in cursor.fetchall()] def add_tags_to_vacancy(cursor, vacancy_id, tag_names): """Добавление тегов к вакансии""" for tag_name in tag_names: # Ищем или создаем тег cursor.execute("SELECT id FROM tags WHERE name = ?", (tag_name,)) tag = cursor.fetchone() if tag: tag_id = tag["id"] else: cursor.execute("INSERT INTO tags (name) VALUES (?)", (tag_name,)) tag_id = cursor.lastrowid # Связываем тег с вакансией cursor.execute(""" INSERT OR IGNORE INTO vacancy_tags (vacancy_id, tag_id) VALUES (?, ?) """, (vacancy_id, tag_id)) def add_tags_to_resume(cursor, resume_id, tag_names): """Добавление тегов к резюме""" for tag_name in tag_names: cursor.execute("SELECT id FROM tags WHERE name = ?", (tag_name,)) tag = cursor.fetchone() if tag: tag_id = tag["id"] else: cursor.execute("INSERT INTO tags (name) VALUES (?)", (tag_name,)) tag_id = cursor.lastrowid cursor.execute(""" INSERT OR IGNORE INTO resume_tags (resume_id, tag_id) VALUES (?, ?) """, (resume_id, tag_id)) # ========== HTML СТРАНИЦЫ ========== @app.get("/", response_class=HTMLResponse) async def get_index(): """Главная страница""" file_path = TEMPLATES_DIR / "index.html" if not file_path.exists(): return HTMLResponse( content="

Rabota.Today API

Сервер работает. Создайте файл templates/index.html

") return FileResponse(file_path) @app.get("/login", response_class=HTMLResponse) async def get_login(): """Страница входа""" file_path = TEMPLATES_DIR / "login.html" return FileResponse(file_path) if file_path.exists() else HTMLResponse( content="

Страница входа

Создайте файл templates/login.html

") @app.get("/register", response_class=HTMLResponse) async def get_register(): """Страница регистрации""" file_path = TEMPLATES_DIR / "register.html" return FileResponse(file_path) if file_path.exists() else HTMLResponse( content="

Страница регистрации

Создайте файл templates/register.html

") @app.get("/profile", response_class=HTMLResponse) async def get_profile(): """Страница профиля""" file_path = TEMPLATES_DIR / "profile.html" return FileResponse(file_path) if file_path.exists() else HTMLResponse( content="

Профиль

Создайте файл templates/profile.html

") @app.get("/vacancies", response_class=HTMLResponse) async def get_vacancies(): """Страница со списком вакансий""" file_path = TEMPLATES_DIR / "vacancies.html" return FileResponse(file_path) if file_path.exists() else HTMLResponse( content="

Вакансии

Создайте файл templates/vacancies.html

") @app.get("/vacancy/{vacancy_id}", response_class=HTMLResponse) async def get_vacancy_detail(vacancy_id: int): """Страница конкретной вакансии""" file_path = TEMPLATES_DIR / "vacancy_detail.html" return FileResponse(file_path) if file_path.exists() else HTMLResponse( content=f"

Вакансия #{vacancy_id}

Создайте файл templates/vacancy_detail.html

") @app.get("/resumes", response_class=HTMLResponse) async def get_resumes(): """Страница со списком резюме""" file_path = TEMPLATES_DIR / "resumes.html" return FileResponse(file_path) if file_path.exists() else HTMLResponse( content="

Резюме

Создайте файл templates/resumes.html

") @app.get("/resume/{resume_id}", response_class=HTMLResponse) async def get_resume_detail(resume_id: int): """Страница конкретного резюме""" file_path = TEMPLATES_DIR / "resume_detail.html" return FileResponse(file_path) if file_path.exists() else HTMLResponse( content=f"

Резюме #{resume_id}

Создайте файл templates/resume_detail.html

") @app.get("/admin", response_class=HTMLResponse) async def get_admin(): """Страница админки""" file_path = TEMPLATES_DIR / "admin.html" return FileResponse(file_path) if file_path.exists() else HTMLResponse( content="

Админка

Создайте файл templates/admin.html

") # ========== API ЭНДПОИНТЫ ========== @app.post("/api/register", response_model=TokenResponse) async def register(user: UserRegister): """Регистрация нового пользователя""" with get_db() as conn: cursor = conn.cursor() cursor.execute("SELECT id FROM users WHERE email = ?", (user.email,)) if cursor.fetchone(): raise HTTPException(status_code=400, detail="Email уже зарегистрирован") password_hash = hash_password(user.password) is_admin = 1 if user.email == "admin@rabota.today" else 0 cursor.execute(""" INSERT INTO users (full_name, email, phone, telegram, password_hash, role, is_admin) VALUES (?, ?, ?, ?, ?, ?, ?) """, (user.full_name, user.email, user.phone, user.telegram, password_hash, user.role, is_admin)) user_id = cursor.lastrowid conn.commit() token = create_access_token({"sub": str(user_id), "role": user.role, "is_admin": is_admin}) return { "access_token": token, "token_type": "bearer", "user_id": user_id, "full_name": user.full_name, "role": user.role, "is_admin": bool(is_admin) } @app.post("/api/login", response_model=TokenResponse) async def login(user_data: UserLogin): """Вход в систему""" with get_db() as conn: cursor = conn.cursor() cursor.execute("SELECT * FROM users WHERE email = ?", (user_data.email,)) user = cursor.fetchone() if not user or not verify_password(user_data.password, user["password_hash"]): raise HTTPException(status_code=401, detail="Неверный email или пароль") token = create_access_token({"sub": str(user["id"]), "role": user["role"], "is_admin": user["is_admin"]}) return { "access_token": token, "token_type": "bearer", "user_id": user["id"], "full_name": user["full_name"], "role": user["role"], "is_admin": bool(user["is_admin"]) } @app.get("/api/tags") async def get_all_tags(category: Optional[str] = None): """Получение всех тегов""" with get_db() as conn: cursor = conn.cursor() if category: cursor.execute("SELECT * FROM tags WHERE category = ? ORDER BY name", (category,)) else: cursor.execute("SELECT * FROM tags ORDER BY category, name") tags = cursor.fetchall() return [dict(tag) for tag in tags] @app.post("/api/tags") async def create_tag(tag: Tag, user_id: int = Depends(get_current_user)): """Создание нового тега (только для админа)""" with get_db() as conn: cursor = conn.cursor() # Проверяем, админ ли пользователь cursor.execute("SELECT is_admin FROM users WHERE id = ?", (user_id,)) user = cursor.fetchone() if not user or not user["is_admin"]: raise HTTPException(status_code=403, detail="Только администратор может создавать теги") try: cursor.execute("INSERT INTO tags (name, category) VALUES (?, ?)", (tag.name, tag.category)) conn.commit() tag_id = cursor.lastrowid cursor.execute("SELECT * FROM tags WHERE id = ?", (tag_id,)) return dict(cursor.fetchone()) except sqlite3.IntegrityError: raise HTTPException(status_code=400, detail="Тег с таким названием уже существует") # ========== ЭНДПОИНТЫ ДЛЯ ВАКАНСИЙ ========== @app.post("/api/vacancies", response_model=VacancyResponse) async def create_vacancy( vacancy: VacancyCreate, user_id: int = Depends(get_current_user) ): """Создание новой вакансии""" with get_db() as conn: cursor = conn.cursor() cursor.execute("SELECT role FROM users WHERE id = ?", (user_id,)) user = cursor.fetchone() if not user or user["role"] != "employer": raise HTTPException(status_code=403, detail="Только работодатели могут создавать вакансии") cursor.execute(""" INSERT INTO vacancies (user_id, title, salary, description, contact) VALUES (?, ?, ?, ?, ?) """, (user_id, vacancy.title, vacancy.salary, vacancy.description, vacancy.contact)) vacancy_id = cursor.lastrowid # Добавляем теги if vacancy.tags: add_tags_to_vacancy(cursor, vacancy_id, vacancy.tags) conn.commit() cursor.execute("SELECT * FROM vacancies WHERE id = ?", (vacancy_id,)) new_vacancy = dict(cursor.fetchone()) new_vacancy["tags"] = get_tags_for_vacancy(cursor, vacancy_id) return new_vacancy @app.get("/api/vacancies", response_model=List[VacancyResponse]) async def get_my_vacancies(user_id: int = Depends(get_current_user)): """Получение всех вакансий текущего пользователя""" with get_db() as conn: cursor = conn.cursor() cursor.execute(""" SELECT v.*, u.full_name as company_name FROM vacancies v JOIN users u ON v.user_id = u.id WHERE v.user_id = ? AND v.is_active = 1 ORDER BY v.created_at DESC """, (user_id,)) vacancies = cursor.fetchall() result = [] for v in vacancies: vacancy_dict = dict(v) vacancy_dict["tags"] = get_tags_for_vacancy(cursor, v["id"]) result.append(vacancy_dict) return result @app.get("/api/vacancies/all") async def get_all_vacancies( page: int = 1, search: str = None, tags: str = None, # теги через запятую min_salary: int = None, sort: str = "newest" ): """Получение всех вакансий с фильтрацией по тегам""" with get_db() as conn: cursor = conn.cursor() query = """ SELECT DISTINCT v.*, u.full_name as company_name FROM vacancies v JOIN users u ON v.user_id = u.id WHERE v.is_active = 1 """ params = [] if search: query += " AND (v.title LIKE ? OR v.description LIKE ?)" params.extend([f"%{search}%", f"%{search}%"]) if tags: tag_list = [t.strip() for t in tags.split(',')] placeholders = ','.join(['?'] * len(tag_list)) query += f""" AND v.id IN ( SELECT vt.vacancy_id FROM vacancy_tags vt JOIN tags t ON vt.tag_id = t.id WHERE t.name IN ({placeholders}) GROUP BY vt.vacancy_id HAVING COUNT(DISTINCT t.name) = ? ) """ params.extend(tag_list + [len(tag_list)]) if min_salary: query += " AND v.salary LIKE ?" params.append(f"%{min_salary}%") if sort == "salary_desc": query += " ORDER BY v.salary DESC" elif sort == "salary_asc": query += " ORDER BY v.salary ASC" else: query += " ORDER BY v.created_at DESC" # Пагинация limit = 10 offset = (page - 1) * limit query += " LIMIT ? OFFSET ?" params.extend([limit, offset]) cursor.execute(query, params) vacancies = cursor.fetchall() result = [] for v in vacancies: vacancy_dict = dict(v) vacancy_dict["tags"] = get_tags_for_vacancy(cursor, v["id"]) result.append(vacancy_dict) # Получаем общее количество cursor.execute("SELECT COUNT(*) FROM vacancies WHERE is_active = 1") total = cursor.fetchone()[0] return { "vacancies": result, "total_pages": (total + limit - 1) // limit, "current_page": page } @app.get("/api/vacancies/{vacancy_id}") async def get_vacancy(vacancy_id: int, token: Optional[str] = None): """Получение конкретной вакансии""" user_id = get_current_user_optional(token) with get_db() as conn: cursor = conn.cursor() # Увеличиваем счетчик просмотров cursor.execute("UPDATE vacancies SET views = views + 1 WHERE id = ?", (vacancy_id,)) cursor.execute(""" SELECT v.*, u.full_name as company_name, u.email, u.telegram FROM vacancies v JOIN users u ON v.user_id = u.id WHERE v.id = ? """, (vacancy_id,)) vacancy = cursor.fetchone() if not vacancy: raise HTTPException(status_code=404, detail="Вакансия не найдена") result = dict(vacancy) result["tags"] = get_tags_for_vacancy(cursor, vacancy_id) # Проверяем, откликался ли пользователь if user_id: cursor.execute(""" SELECT id FROM applications WHERE vacancy_id = ? AND user_id = ? """, (vacancy_id, user_id)) result["has_applied"] = cursor.fetchone() is not None conn.commit() return result @app.post("/api/vacancies/{vacancy_id}/apply") async def apply_to_vacancy( vacancy_id: int, user_id: int = Depends(get_current_user) ): """Отклик на вакансию""" with get_db() as conn: cursor = conn.cursor() cursor.execute("SELECT id FROM vacancies WHERE id = ?", (vacancy_id,)) if not cursor.fetchone(): raise HTTPException(status_code=404, detail="Вакансия не найдена") cursor.execute(""" SELECT id FROM applications WHERE vacancy_id = ? AND user_id = ? """, (vacancy_id, user_id)) if cursor.fetchone(): raise HTTPException(status_code=400, detail="Вы уже откликались на эту вакансию") cursor.execute(""" INSERT INTO applications (vacancy_id, user_id) VALUES (?, ?) """, (vacancy_id, user_id)) conn.commit() return {"message": "Отклик отправлен"} # ========== ЭНДПОИНТЫ ДЛЯ РЕЗЮМЕ ========== @app.post("/api/resume", response_model=ResumeResponse) async def create_or_update_resume( resume: ResumeCreate, user_id: int = Depends(get_current_user) ): """Создание или обновление резюме""" with get_db() as conn: cursor = conn.cursor() cursor.execute("SELECT role FROM users WHERE id = ?", (user_id,)) user = cursor.fetchone() if not user or user["role"] != "employee": raise HTTPException(status_code=403, detail="Только соискатели могут создавать резюме") cursor.execute("SELECT id FROM resumes WHERE user_id = ?", (user_id,)) existing = cursor.fetchone() if existing: cursor.execute(""" UPDATE resumes SET desired_position = ?, about_me = ?, desired_salary = ?, updated_at = CURRENT_TIMESTAMP WHERE user_id = ? """, (resume.desired_position, resume.about_me, resume.desired_salary, user_id)) resume_id = existing["id"] cursor.execute("DELETE FROM work_experience WHERE resume_id = ?", (resume_id,)) cursor.execute("DELETE FROM education WHERE resume_id = ?", (resume_id,)) cursor.execute("DELETE FROM resume_tags WHERE resume_id = ?", (resume_id,)) else: cursor.execute(""" INSERT INTO resumes (user_id, desired_position, about_me, desired_salary) VALUES (?, ?, ?, ?) """, (user_id, resume.desired_position, resume.about_me, resume.desired_salary)) resume_id = cursor.lastrowid for exp in resume.work_experience: cursor.execute(""" INSERT INTO work_experience (resume_id, position, company, period) VALUES (?, ?, ?, ?) """, (resume_id, exp.position, exp.company, exp.period)) for edu in resume.education: cursor.execute(""" INSERT INTO education (resume_id, institution, specialty, graduation_year) VALUES (?, ?, ?, ?) """, (resume_id, edu.institution, edu.specialty, edu.graduation_year)) # Добавляем теги if resume.tags: add_tags_to_resume(cursor, resume_id, resume.tags) conn.commit() cursor.execute("SELECT * FROM resumes WHERE id = ?", (resume_id,)) resume_data = dict(cursor.fetchone()) cursor.execute("SELECT position, company, period FROM work_experience WHERE resume_id = ?", (resume_id,)) resume_data["work_experience"] = [dict(exp) for exp in cursor.fetchall()] cursor.execute("SELECT institution, specialty, graduation_year FROM education WHERE resume_id = ?", (resume_id,)) resume_data["education"] = [dict(edu) for edu in cursor.fetchall()] resume_data["tags"] = get_tags_for_resume(cursor, resume_id) return resume_data @app.get("/api/resume", response_model=ResumeResponse) async def get_my_resume(user_id: int = Depends(get_current_user)): """Получение резюме текущего пользователя""" with get_db() as conn: cursor = conn.cursor() cursor.execute("SELECT * FROM resumes WHERE user_id = ?", (user_id,)) resume = cursor.fetchone() if not resume: raise HTTPException(status_code=404, detail="Резюме не найдено") resume_data = dict(resume) cursor.execute("SELECT position, company, period FROM work_experience WHERE resume_id = ?", (resume["id"],)) resume_data["work_experience"] = [dict(exp) for exp in cursor.fetchall()] cursor.execute("SELECT institution, specialty, graduation_year FROM education WHERE resume_id = ?", (resume["id"],)) resume_data["education"] = [dict(edu) for edu in cursor.fetchall()] resume_data["tags"] = get_tags_for_resume(cursor, resume["id"]) return resume_data @app.get("/api/resumes/all") async def get_all_resumes( page: int = 1, search: str = None, tags: str = None, min_salary: int = None ): """Получение всех резюме с фильтрацией по тегам""" with get_db() as conn: cursor = conn.cursor() query = """ SELECT DISTINCT r.*, u.full_name FROM resumes r JOIN users u ON r.user_id = u.id WHERE 1=1 """ params = [] if search: query += " AND (r.desired_position LIKE ? OR r.about_me LIKE ?)" params.extend([f"%{search}%", f"%{search}%"]) if tags: tag_list = [t.strip() for t in tags.split(',')] placeholders = ','.join(['?'] * len(tag_list)) query += f""" AND r.id IN ( SELECT rt.resume_id FROM resume_tags rt JOIN tags t ON rt.tag_id = t.id WHERE t.name IN ({placeholders}) GROUP BY rt.resume_id HAVING COUNT(DISTINCT t.name) = ? ) """ params.extend(tag_list + [len(tag_list)]) if min_salary: query += " AND r.desired_salary LIKE ?" params.append(f"%{min_salary}%") # Пагинация limit = 10 offset = (page - 1) * limit query += " ORDER BY r.updated_at DESC LIMIT ? OFFSET ?" params.extend([limit, offset]) cursor.execute(query, params) resumes = cursor.fetchall() result = [] for r in resumes: resume_dict = dict(r) resume_dict["tags"] = get_tags_for_resume(cursor, r["id"]) cursor.execute("SELECT COUNT(*) FROM work_experience WHERE resume_id = ?", (r["id"],)) resume_dict["experience_count"] = cursor.fetchone()[0] result.append(resume_dict) cursor.execute("SELECT COUNT(*) FROM resumes") total = cursor.fetchone()[0] return { "resumes": result, "total_pages": (total + limit - 1) // limit, "current_page": page } @app.get("/api/resumes/{resume_id}") async def get_resume(resume_id: int): """Получение конкретного резюме""" with get_db() as conn: cursor = conn.cursor() cursor.execute(""" SELECT r.*, u.full_name, u.email, u.phone, u.telegram FROM resumes r JOIN users u ON r.user_id = u.id WHERE r.id = ? """, (resume_id,)) resume = cursor.fetchone() if not resume: raise HTTPException(status_code=404, detail="Резюме не найдено") # Увеличиваем счетчик просмотров cursor.execute("UPDATE resumes SET views = views + 1 WHERE id = ?", (resume_id,)) resume_data = dict(resume) cursor.execute("SELECT position, company, period FROM work_experience WHERE resume_id = ?", (resume_id,)) resume_data["work_experience"] = [dict(exp) for exp in cursor.fetchall()] cursor.execute("SELECT institution, specialty, graduation_year FROM education WHERE resume_id = ?", (resume_id,)) resume_data["education"] = [dict(edu) for edu in cursor.fetchall()] resume_data["tags"] = get_tags_for_resume(cursor, resume_id) conn.commit() return resume_data # ========== АДМИНСКИЕ ЭНДПОИНТЫ ========== @app.get("/api/admin/stats") async def get_admin_stats(user_id: int = Depends(get_current_user)): """Получение статистики для админки""" with get_db() as conn: cursor = conn.cursor() # Проверяем, админ ли пользователь cursor.execute("SELECT is_admin FROM users WHERE id = ?", (user_id,)) user = cursor.fetchone() if not user or not user["is_admin"]: raise HTTPException(status_code=403, detail="Доступ запрещен") stats = {} cursor.execute("SELECT COUNT(*) FROM users") stats["total_users"] = cursor.fetchone()[0] cursor.execute("SELECT COUNT(*) FROM users WHERE role = 'employee'") stats["total_employees"] = cursor.fetchone()[0] cursor.execute("SELECT COUNT(*) FROM users WHERE role = 'employer'") stats["total_employers"] = cursor.fetchone()[0] cursor.execute("SELECT COUNT(*) FROM vacancies WHERE is_active = 1") stats["active_vacancies"] = cursor.fetchone()[0] cursor.execute("SELECT COUNT(*) FROM resumes") stats["total_resumes"] = cursor.fetchone()[0] cursor.execute("SELECT COUNT(*) FROM applications") stats["total_applications"] = cursor.fetchone()[0] cursor.execute("SELECT COUNT(*) FROM tags") stats["total_tags"] = cursor.fetchone()[0] # Последние регистрации cursor.execute(""" SELECT id, full_name, email, role, created_at FROM users ORDER BY created_at DESC LIMIT 10 """) stats["recent_users"] = [dict(u) for u in cursor.fetchall()] # Популярные теги cursor.execute(""" SELECT t.name, COUNT(vt.vacancy_id) as vacancy_count, COUNT(rt.resume_id) as resume_count FROM tags t LEFT JOIN vacancy_tags vt ON t.id = vt.tag_id LEFT JOIN resume_tags rt ON t.id = rt.tag_id GROUP BY t.id ORDER BY (COUNT(vt.vacancy_id) + COUNT(rt.resume_id)) DESC LIMIT 20 """) stats["popular_tags"] = [dict(t) for t in cursor.fetchall()] return stats @app.get("/api/admin/users") async def get_all_users(user_id: int = Depends(get_current_user)): """Получение всех пользователей (для админа)""" with get_db() as conn: cursor = conn.cursor() cursor.execute("SELECT is_admin FROM users WHERE id = ?", (user_id,)) user = cursor.fetchone() if not user or not user["is_admin"]: raise HTTPException(status_code=403, detail="Доступ запрещен") cursor.execute(""" SELECT id, full_name, email, phone, telegram, role, is_admin, created_at FROM users ORDER BY created_at DESC """) users = cursor.fetchall() return [dict(u) for u in users] @app.delete("/api/admin/users/{target_user_id}") async def delete_user(target_user_id: int, user_id: int = Depends(get_current_user)): """Удаление пользователя (для админа)""" with get_db() as conn: cursor = conn.cursor() cursor.execute("SELECT is_admin FROM users WHERE id = ?", (user_id,)) user = cursor.fetchone() if not user or not user["is_admin"]: raise HTTPException(status_code=403, detail="Доступ запрещен") cursor.execute("DELETE FROM users WHERE id = ?", (target_user_id,)) conn.commit() return {"message": "Пользователь удален"} @app.get("/api/admin/vacancies") async def get_all_vacancies_admin(user_id: int = Depends(get_current_user)): """Получение всех вакансий (для админа)""" with get_db() as conn: cursor = conn.cursor() cursor.execute("SELECT is_admin FROM users WHERE id = ?", (user_id,)) user = cursor.fetchone() if not user or not user["is_admin"]: raise HTTPException(status_code=403, detail="Доступ запрещен") cursor.execute(""" SELECT v.*, u.full_name as company_name FROM vacancies v JOIN users u ON v.user_id = u.id ORDER BY v.created_at DESC """) vacancies = cursor.fetchall() result = [] for v in vacancies: vacancy_dict = dict(v) vacancy_dict["tags"] = get_tags_for_vacancy(cursor, v["id"]) result.append(vacancy_dict) return result @app.delete("/api/admin/vacancies/{vacancy_id}") async def delete_vacancy_admin(vacancy_id: int, user_id: int = Depends(get_current_user)): """Удаление вакансии (для админа)""" with get_db() as conn: cursor = conn.cursor() cursor.execute("SELECT is_admin FROM users WHERE id = ?", (user_id,)) user = cursor.fetchone() if not user or not user["is_admin"]: raise HTTPException(status_code=403, detail="Доступ запрещен") cursor.execute("DELETE FROM vacancies WHERE id = ?", (vacancy_id,)) conn.commit() return {"message": "Вакансия удалена"} # ========== ПОЛЬЗОВАТЕЛЬСКИЕ ЭНДПОИНТЫ ========== @app.get("/api/user") async def get_user_info(user_id: int = Depends(get_current_user)): """Получение информации о текущем пользователе""" with get_db() as conn: cursor = conn.cursor() cursor.execute(""" SELECT id, full_name, email, phone, telegram, role, is_admin, created_at FROM users WHERE id = ? """, (user_id,)) user = cursor.fetchone() if not user: raise HTTPException(status_code=404, detail="Пользователь не найден") return dict(user) @app.get("/api/users/{target_user_id}/contact") async def get_user_contact( target_user_id: int, current_user: int = Depends(get_current_user) ): """Получение контактных данных пользователя""" with get_db() as conn: cursor = conn.cursor() cursor.execute("SELECT email, phone, telegram FROM users WHERE id = ?", (target_user_id,)) user = cursor.fetchone() if not user: raise HTTPException(status_code=404, detail="Пользователь не найден") return dict(user) # Инициализация базы данных init_db() if __name__ == "__main__": print("🚀 Запуск сервера на http://localhost:8000") print("📚 Документация API: http://localhost:8000/docs") print("👤 Админ: admin@rabota.today / admin123") print("📁 HTML страницы должны быть в папке templates/") uvicorn.run(app, host="0.0.0.0", port=8000)