3199 lines
119 KiB
Python
3199 lines
119 KiB
Python
# 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, timezone
|
||
from jose import jwt
|
||
from jose.exceptions import JWTError
|
||
import sqlite3
|
||
import hashlib
|
||
import os
|
||
from contextlib import contextmanager
|
||
import uvicorn
|
||
from pathlib import Path
|
||
import traceback
|
||
from fastapi.middleware.trustedhost import TrustedHostMiddleware
|
||
from seo_helpers import generate_resume_seo_tags, generate_vacancy_seo_tags, inject_seo_tags
|
||
|
||
from fastapi.middleware.cors import CORSMiddleware
|
||
from fastapi.responses import JSONResponse
|
||
import re
|
||
|
||
app = FastAPI(title="Rabota.Today API", version="2.0.0")
|
||
|
||
# Настройки JWT
|
||
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
|
||
)
|
||
""")
|
||
|
||
# Таблица опыта работы (ОБНОВЛЕНА - добавлено поле description)
|
||
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,
|
||
description 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,
|
||
status TEXT DEFAULT 'pending' CHECK(status IN ('pending', 'viewed', 'accepted', 'rejected')),
|
||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||
viewed_at TIMESTAMP,
|
||
response_message TEXT,
|
||
response_at TIMESTAMP,
|
||
FOREIGN KEY (vacancy_id) REFERENCES vacancies (id) ON DELETE CASCADE,
|
||
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE
|
||
)
|
||
""")
|
||
|
||
# Таблица избранного
|
||
cursor.execute("""
|
||
CREATE TABLE IF NOT EXISTS favorites (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
user_id INTEGER NOT NULL,
|
||
item_type TEXT NOT NULL CHECK(item_type IN ('vacancy', 'resume')),
|
||
item_id INTEGER NOT NULL,
|
||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||
UNIQUE(user_id, item_type, item_id),
|
||
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE
|
||
)
|
||
""")
|
||
|
||
# Таблица компаний
|
||
cursor.execute("""
|
||
CREATE TABLE IF NOT EXISTS companies (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
user_id INTEGER UNIQUE NOT NULL,
|
||
name TEXT NOT NULL,
|
||
description TEXT,
|
||
website TEXT,
|
||
logo TEXT,
|
||
address TEXT,
|
||
phone TEXT,
|
||
email TEXT,
|
||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE
|
||
)
|
||
""")
|
||
|
||
# Создаем индексы для быстрого поиска
|
||
cursor.execute("CREATE INDEX IF NOT EXISTS idx_applications_vacancy ON applications(vacancy_id)")
|
||
cursor.execute("CREATE INDEX IF NOT EXISTS idx_applications_user ON applications(user_id)")
|
||
cursor.execute("CREATE INDEX IF NOT EXISTS idx_applications_status ON applications(status)")
|
||
cursor.execute("CREATE INDEX IF NOT EXISTS idx_favorites_user ON favorites(user_id)")
|
||
cursor.execute("CREATE INDEX IF NOT EXISTS idx_favorites_item ON favorites(item_type, item_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("✅ База данных инициализирована")
|
||
|
||
|
||
def set_initial_sequence_ids():
|
||
"""Установка начальных ID для таблиц при первом запуске"""
|
||
with get_db() as conn:
|
||
cursor = conn.cursor()
|
||
|
||
# Проверяем, есть ли уже записи в таблицах
|
||
cursor.execute("SELECT COUNT(*) FROM vacancies")
|
||
vacancies_count = cursor.fetchone()[0]
|
||
|
||
cursor.execute("SELECT COUNT(*) FROM resumes")
|
||
resumes_count = cursor.fetchone()[0]
|
||
|
||
# Если таблицы пустые, устанавливаем начальные ID
|
||
if vacancies_count == 0:
|
||
cursor.execute("INSERT OR REPLACE INTO sqlite_sequence (name, seq) VALUES ('vacancies', 9999)")
|
||
print("✅ Установлен начальный ID для вакансий: 10000")
|
||
|
||
if resumes_count == 0:
|
||
cursor.execute("INSERT OR REPLACE INTO sqlite_sequence (name, seq) VALUES ('resumes', 9999)")
|
||
print("✅ Установлен начальный ID для резюме: 10000")
|
||
|
||
conn.commit()
|
||
|
||
|
||
@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 # больше не Optional
|
||
telegram: str # больше не Optional
|
||
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 VacancyUpdate(BaseModel):
|
||
title: Optional[str] = None
|
||
salary: Optional[str] = None
|
||
description: Optional[str] = None
|
||
contact: Optional[str] = None
|
||
tags: Optional[List[str]] = None
|
||
is_active: Optional[bool] = None
|
||
|
||
|
||
class WorkExperience(BaseModel):
|
||
position: str
|
||
company: str
|
||
period: Optional[str] = None
|
||
description: 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
|
||
|
||
class CompanyCreate(BaseModel):
|
||
name: str
|
||
description: Optional[str] = None
|
||
website: Optional[str] = None
|
||
logo: Optional[str] = None
|
||
address: Optional[str] = None
|
||
phone: Optional[str] = None
|
||
email: Optional[str] = None
|
||
|
||
class CompanyResponse(BaseModel):
|
||
id: int
|
||
user_id: int
|
||
name: str
|
||
description: Optional[str]
|
||
website: Optional[str]
|
||
logo: Optional[str]
|
||
address: Optional[str]
|
||
phone: Optional[str]
|
||
email: Optional[str]
|
||
created_at: str
|
||
updated_at: str
|
||
|
||
|
||
class FavoriteCreate(BaseModel):
|
||
item_type: str # 'vacancy' или 'resume'
|
||
item_id: int
|
||
|
||
|
||
class FavoriteResponse(BaseModel):
|
||
id: int
|
||
user_id: int
|
||
item_type: str
|
||
item_id: int
|
||
created_at: str
|
||
|
||
# Для расширенной информации
|
||
item_data: Optional[dict] = None
|
||
|
||
|
||
class FavoriteItem(BaseModel):
|
||
id: int
|
||
type: str
|
||
title: str
|
||
subtitle: str
|
||
salary: Optional[str] = None
|
||
company: Optional[str] = None
|
||
name: Optional[str] = None
|
||
tags: List[str] = []
|
||
created_at: str
|
||
url: str
|
||
|
||
|
||
class ApplicationCreate(BaseModel):
|
||
vacancy_id: int
|
||
message: Optional[str] = None
|
||
|
||
|
||
class ApplicationResponse(BaseModel):
|
||
id: int
|
||
vacancy_id: int
|
||
user_id: int
|
||
message: Optional[str]
|
||
status: str
|
||
created_at: str
|
||
viewed_at: Optional[str]
|
||
response_message: Optional[str]
|
||
response_at: Optional[str]
|
||
|
||
# Данные о вакансии
|
||
vacancy_title: Optional[str] = None
|
||
company_name: Optional[str] = None
|
||
|
||
# Данные о соискателе
|
||
user_name: Optional[str] = None
|
||
user_email: Optional[str] = None
|
||
user_phone: Optional[str] = None
|
||
user_telegram: Optional[str] = None
|
||
|
||
|
||
class ApplicationStatusUpdate(BaseModel):
|
||
status: str # 'viewed', 'accepted', 'rejected'
|
||
response_message: Optional[str] = None
|
||
|
||
|
||
class ApplicationStats(BaseModel):
|
||
total: int
|
||
pending: int
|
||
viewed: int
|
||
accepted: int
|
||
rejected: int
|
||
|
||
|
||
# ========== ВСПОМОГАТЕЛЬНЫЕ ФУНКЦИИ ==========
|
||
|
||
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()
|
||
# Исправление предупреждения о datetime.utcnow()
|
||
expire = datetime.now(timezone.utc) + timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
|
||
to_encode.update({"exp": expire})
|
||
|
||
# Убеждаемся, что user_id сохраняется как строка
|
||
if "sub" in to_encode:
|
||
to_encode["sub"] = str(to_encode["sub"])
|
||
|
||
# Используем jose.jwt.encode
|
||
return jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
|
||
|
||
|
||
def get_current_user(credentials: HTTPAuthorizationCredentials = Depends(security)):
|
||
"""Получение текущего пользователя из токена"""
|
||
token = credentials.credentials
|
||
|
||
try:
|
||
# Декодируем токен с помощью jose.jwt.decode
|
||
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
|
||
|
||
# Получаем user_id из поля sub
|
||
user_id = payload.get("sub")
|
||
|
||
if user_id is None:
|
||
print("❌ No sub in token payload")
|
||
raise HTTPException(status_code=401, detail="Невалидный токен: отсутствует идентификатор пользователя")
|
||
|
||
# Пробуем преобразовать в int
|
||
try:
|
||
user_id_int = int(user_id)
|
||
except (ValueError, TypeError) as e:
|
||
print(f"❌ Cannot convert user_id to int: {user_id}, error: {e}")
|
||
raise HTTPException(status_code=401, detail=f"Невалидный формат ID пользователя: {user_id}")
|
||
|
||
print(f"✅ User authenticated: {user_id_int}")
|
||
return user_id_int
|
||
|
||
except jwt.ExpiredSignatureError:
|
||
print("❌ Token expired")
|
||
raise HTTPException(status_code=401, detail="Токен истек")
|
||
except jwt.JWTError as e:
|
||
print(f"❌ Invalid token: {e}")
|
||
raise HTTPException(status_code=401, detail=f"Невалидный токен: {str(e)}")
|
||
except Exception as e:
|
||
print(f"❌ Unexpected error in get_current_user: {e}")
|
||
traceback.print_exc()
|
||
raise HTTPException(status_code=401, detail=f"Ошибка авторизации: {str(e)}")
|
||
|
||
|
||
def get_current_user_optional(token: Optional[str] = None):
|
||
"""Опциональное получение пользователя (для публичных страниц)"""
|
||
if not token:
|
||
return None
|
||
|
||
# Пытаемся получить токен из заголовка Authorization
|
||
auth_header = token
|
||
if isinstance(token, str) and token.startswith("Bearer "):
|
||
token = token.replace("Bearer ", "")
|
||
|
||
try:
|
||
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
|
||
user_id = payload.get("sub")
|
||
if user_id is None:
|
||
return None
|
||
return int(user_id)
|
||
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(request: Request):
|
||
"""Главная страница с определением устройства"""
|
||
|
||
# Определяем мобильное устройство по User-Agent
|
||
user_agent = request.headers.get("user-agent", "").lower()
|
||
is_mobile = any([
|
||
"mobile" in user_agent,
|
||
"android" in user_agent,
|
||
"iphone" in user_agent,
|
||
"ipad" in user_agent
|
||
])
|
||
|
||
if is_mobile:
|
||
file_path = TEMPLATES_DIR / "index_mobile.html"
|
||
else:
|
||
file_path = TEMPLATES_DIR / "index.html"
|
||
|
||
if file_path.exists():
|
||
with open(file_path, "r", encoding="utf-8") as f:
|
||
return HTMLResponse(content=f.read())
|
||
|
||
return HTMLResponse(content="<h1>Rabota.Today</h1><p>Страница не найдена</p>")
|
||
|
||
|
||
@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="<h1>Страница входа</h1><p>Создайте файл templates/login.html</p>")
|
||
|
||
|
||
@app.get("/companies", response_class=HTMLResponse)
|
||
async def get_companies_page():
|
||
"""Страница со списком компаний"""
|
||
file_path = TEMPLATES_DIR / "companies.html"
|
||
if file_path.exists():
|
||
with open(file_path, "r", encoding="utf-8") as f:
|
||
return HTMLResponse(content=f.read())
|
||
return HTMLResponse(content="<h1>Компании</h1><p>Страница в разработке</p>")
|
||
|
||
|
||
@app.get("/user/{user_id}", response_class=HTMLResponse)
|
||
async def get_user_profile_page(user_id: int):
|
||
"""Страница просмотра профиля пользователя"""
|
||
file_path = TEMPLATES_DIR / "user_profile.html"
|
||
if file_path.exists():
|
||
with open(file_path, "r", encoding="utf-8") as f:
|
||
return HTMLResponse(content=f.read())
|
||
return HTMLResponse(content="<h1>Профиль пользователя</h1><p>Страница в разработке</p>")
|
||
|
||
|
||
@app.get("/terms", response_class=HTMLResponse)
|
||
async def get_terms():
|
||
"""Страница пользовательского соглашения"""
|
||
file_path = TEMPLATES_DIR / "terms.html"
|
||
if file_path.exists():
|
||
with open(file_path, "r", encoding="utf-8") as f:
|
||
return HTMLResponse(content=f.read())
|
||
return HTMLResponse(content="<h1>Пользовательское соглашение</h1><p>Страница в разработке</p>")
|
||
|
||
@app.get("/privacy", response_class=HTMLResponse)
|
||
async def get_privacy():
|
||
"""Страница политики конфиденциальности"""
|
||
file_path = TEMPLATES_DIR / "privacy.html"
|
||
if file_path.exists():
|
||
with open(file_path, "r", encoding="utf-8") as f:
|
||
return HTMLResponse(content=f.read())
|
||
return HTMLResponse(content="<h1>Политика конфиденциальности</h1><p>Страница в разработке</p>")
|
||
|
||
|
||
@app.get("/company/{company_id}", response_class=HTMLResponse)
|
||
async def get_company_page(company_id: int):
|
||
"""Страница компании"""
|
||
file_path = TEMPLATES_DIR / "company_detail.html"
|
||
if file_path.exists():
|
||
with open(file_path, "r", encoding="utf-8") as f:
|
||
return HTMLResponse(content=f.read())
|
||
return HTMLResponse(content="<h1>Страница компании</h1><p>Файл не найден</p>")
|
||
|
||
|
||
@app.get("/debug_simple", response_class=HTMLResponse)
|
||
async def debug():
|
||
"""Страница debug"""
|
||
file_path = TEMPLATES_DIR / "debug.html"
|
||
return FileResponse(file_path) if file_path.exists() else HTMLResponse(
|
||
content="<h1>Страница входа</h1><p>Создайте файл templates/debug.html</p>")
|
||
|
||
@app.get("/debug", response_class=HTMLResponse)
|
||
async def get_debug_page():
|
||
"""Страница диагностики для мобильных устройств"""
|
||
file_path = "templates/mobile_debug_simple.html"
|
||
if os.path.exists(file_path):
|
||
with open(file_path, "r", encoding="utf-8") as f:
|
||
return HTMLResponse(content=f.read())
|
||
return HTMLResponse(content="""
|
||
<!DOCTYPE html>
|
||
<html>
|
||
<head><title>Debug</title></head>
|
||
<body>
|
||
<h1>Debug page not found</h1>
|
||
<p>Create file: templates/mobile_debug_simple.html</p>
|
||
</body>
|
||
</html>
|
||
""")
|
||
|
||
|
||
@app.get("/register", response_class=HTMLResponse)
|
||
async def get_register_page():
|
||
"""Отдельная страница регистрации"""
|
||
file_path = TEMPLATES_DIR / "register.html"
|
||
if not file_path.exists():
|
||
return HTMLResponse(content="<h1>Страница регистрации</h1><p>Создайте файл templates/register.html</p>")
|
||
return FileResponse(file_path)
|
||
|
||
|
||
@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="<h1>Страница регистрации</h1><p>Создайте файл templates/register.html</p>")
|
||
|
||
|
||
@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="<h1>Профиль</h1><p>Создайте файл templates/profile.html</p>")
|
||
|
||
|
||
@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="<h1>Вакансии</h1><p>Создайте файл templates/vacancies.html</p>")
|
||
|
||
|
||
@app.get("/vacancy/{vacancy_id}", response_class=HTMLResponse)
|
||
async def get_vacancy_page(request: Request, vacancy_id: int):
|
||
"""Страница детального просмотра вакансии с SEO-тегами на сервере"""
|
||
try:
|
||
with get_db() as conn:
|
||
cursor = conn.cursor()
|
||
|
||
cursor.execute("""
|
||
SELECT v.*,
|
||
COALESCE(c.name, u.full_name) as company_name,
|
||
c.description as company_description,
|
||
c.website as company_website,
|
||
c.address as company_address,
|
||
u.full_name as user_name,
|
||
u.email as user_email,
|
||
u.telegram as user_telegram
|
||
FROM vacancies v
|
||
JOIN users u ON v.user_id = u.id
|
||
LEFT JOIN companies c ON v.user_id = c.user_id
|
||
WHERE v.id = ?
|
||
""", (vacancy_id,))
|
||
|
||
vacancy = cursor.fetchone()
|
||
if not vacancy:
|
||
return HTMLResponse(content="<h1>Вакансия не найдена</h1>")
|
||
|
||
# Получаем теги
|
||
cursor.execute("""
|
||
SELECT t.name FROM tags t
|
||
JOIN vacancy_tags vt ON t.id = vt.tag_id
|
||
WHERE vt.vacancy_id = ?
|
||
""", (vacancy_id,))
|
||
tags = [t["name"] for t in cursor.fetchall()]
|
||
|
||
vacancy_data = dict(vacancy)
|
||
vacancy_data["tags"] = tags
|
||
|
||
# Генерируем SEO-теги
|
||
seo_tags = generate_vacancy_seo_tags(vacancy_data, vacancy_id)
|
||
|
||
# Читаем HTML шаблон
|
||
file_path = TEMPLATES_DIR / "vacancy_detail.html"
|
||
if not file_path.exists():
|
||
return HTMLResponse(content="<h1>Страница не найдена</h1>")
|
||
|
||
with open(file_path, "r", encoding="utf-8") as f:
|
||
html = f.read()
|
||
|
||
# Внедряем SEO-теги в HTML
|
||
html = inject_seo_tags(html, seo_tags)
|
||
|
||
return HTMLResponse(content=html)
|
||
|
||
except Exception as e:
|
||
print(f"❌ Ошибка: {e}")
|
||
traceback.print_exc()
|
||
return HTMLResponse(content=f"<h1>Ошибка</h1><p>{str(e)}</p>")
|
||
|
||
|
||
@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="<h1>Резюме</h1><p>Создайте файл templates/resumes.html</p>")
|
||
|
||
|
||
@app.get("/resume/{resume_id}", response_class=HTMLResponse)
|
||
async def get_resume_page(request: Request, resume_id: int):
|
||
"""Страница детального просмотра резюме с SEO-тегами на сервере"""
|
||
try:
|
||
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:
|
||
file_path = TEMPLATES_DIR / "resume_detail.html"
|
||
if file_path.exists():
|
||
with open(file_path, "r", encoding="utf-8") as f:
|
||
html = f.read()
|
||
html = html.replace('<title id="pageTitle">Резюме | Rabota.Today</title>',
|
||
'<title>Резюме не найдено | Rabota.Today</title>')
|
||
return HTMLResponse(content=html)
|
||
return HTMLResponse(content="<h1>Резюме не найдено</h1>")
|
||
|
||
# Получаем теги
|
||
cursor.execute("""
|
||
SELECT t.name FROM tags t
|
||
JOIN resume_tags rt ON t.id = rt.tag_id
|
||
WHERE rt.resume_id = ?
|
||
""", (resume_id,))
|
||
tags = [t["name"] for t in cursor.fetchall()]
|
||
|
||
# Получаем опыт работы
|
||
cursor.execute("""
|
||
SELECT position, company, period, description
|
||
FROM work_experience
|
||
WHERE resume_id = ?
|
||
ORDER BY period DESC
|
||
""", (resume_id,))
|
||
work_experience = cursor.fetchall()
|
||
|
||
# Получаем образование
|
||
cursor.execute("""
|
||
SELECT institution, specialty, graduation_year
|
||
FROM education
|
||
WHERE resume_id = ?
|
||
ORDER BY graduation_year DESC
|
||
""", (resume_id,))
|
||
education = cursor.fetchall()
|
||
|
||
# Формируем данные для SEO
|
||
resume_data = dict(resume)
|
||
resume_data["tags"] = tags
|
||
resume_data["work_experience"] = [dict(exp) for exp in work_experience]
|
||
resume_data["education"] = [dict(edu) for edu in education]
|
||
|
||
# Генерируем SEO-теги
|
||
seo_tags = generate_resume_seo_tags(resume_data, resume_id)
|
||
|
||
# Читаем HTML шаблон
|
||
file_path = TEMPLATES_DIR / "resume_detail.html"
|
||
if not file_path.exists():
|
||
return HTMLResponse(content="<h1>Страница не найдена</h1>")
|
||
|
||
with open(file_path, "r", encoding="utf-8") as f:
|
||
html = f.read()
|
||
|
||
# Внедряем SEO-теги в HTML
|
||
html = inject_seo_tags(html, seo_tags)
|
||
|
||
return HTMLResponse(content=html)
|
||
|
||
except Exception as e:
|
||
print(f"❌ Ошибка при загрузке страницы резюме: {e}")
|
||
traceback.print_exc()
|
||
return HTMLResponse(content=f"<h1>Ошибка</h1><p>{str(e)}</p>")
|
||
|
||
|
||
@app.get("/favorites", response_class=HTMLResponse)
|
||
async def get_favorites_page():
|
||
"""Страница избранного"""
|
||
file_path = TEMPLATES_DIR / "favorites.html"
|
||
return FileResponse(file_path) if file_path.exists() else HTMLResponse(content="<h1>Избранное</h1><p>Создайте файл templates/favorites.html</p>")
|
||
|
||
|
||
@app.get("/applications", response_class=HTMLResponse)
|
||
async def get_applications_page():
|
||
"""Страница со списком откликов"""
|
||
file_path = TEMPLATES_DIR / "applications.html"
|
||
return FileResponse(file_path) if file_path.exists() else HTMLResponse(content="<h1>Отклики</h1><p>Создайте файл templates/applications.html</p>")
|
||
|
||
@app.get("/application/{application_id}", response_class=HTMLResponse)
|
||
async def get_application_page(application_id: int):
|
||
"""Детальная страница отклика"""
|
||
file_path = TEMPLATES_DIR / "application_detail.html"
|
||
return FileResponse(file_path) if file_path.exists() else HTMLResponse(content=f"<h1>Отклик #{application_id}</h1><p>Создайте файл templates/application_detail.html</p>")
|
||
|
||
@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="<h1>Админка</h1><p>Создайте файл templates/admin.html</p>")
|
||
|
||
|
||
# ========== API ЭНДПОИНТЫ ==========
|
||
|
||
@app.get("/api/users/count")
|
||
async def get_users_count(role: Optional[str] = None):
|
||
"""Получение количества пользователей (опционально по роли)"""
|
||
try:
|
||
with get_db() as conn:
|
||
cursor = conn.cursor()
|
||
|
||
if role:
|
||
cursor.execute("SELECT COUNT(*) FROM users WHERE role = ?", (role,))
|
||
else:
|
||
cursor.execute("SELECT COUNT(*) FROM users")
|
||
|
||
count = cursor.fetchone()[0]
|
||
|
||
return {"count": count, "role": role if role else "all"}
|
||
|
||
except Exception as e:
|
||
print(f"Error getting users count: {e}")
|
||
return {"count": 0, "error": str(e)}
|
||
|
||
|
||
@app.get("/api/public/stats")
|
||
async def get_public_stats():
|
||
"""Публичная статистика для главной страницы"""
|
||
try:
|
||
with get_db() as conn:
|
||
cursor = conn.cursor()
|
||
|
||
# Активные вакансии
|
||
cursor.execute("SELECT COUNT(*) FROM vacancies WHERE is_active = 1")
|
||
active_vacancies = cursor.fetchone()[0]
|
||
|
||
# Соискатели (пользователи с ролью employee)
|
||
cursor.execute("SELECT COUNT(*) FROM users WHERE role = 'employee'")
|
||
total_employees = cursor.fetchone()[0]
|
||
|
||
# Работодатели (компании) - используем количество работодателей
|
||
cursor.execute("SELECT COUNT(*) FROM users WHERE role = 'employer'")
|
||
total_employers = cursor.fetchone()[0]
|
||
|
||
# Все пользователи
|
||
cursor.execute("SELECT COUNT(*) FROM users")
|
||
total_users = cursor.fetchone()[0]
|
||
|
||
return {
|
||
"active_vacancies": active_vacancies,
|
||
"total_employees": total_employees, # Изменено с total_resumes
|
||
"total_employers": total_employers,
|
||
"total_users": total_users
|
||
}
|
||
except Exception as e:
|
||
print(f"Error getting public stats: {e}")
|
||
return {
|
||
"active_vacancies": 1234,
|
||
"total_employees": 5678, # Изменено с total_resumes
|
||
"total_employers": 500,
|
||
"total_users": 10000
|
||
}
|
||
|
||
|
||
@app.post("/api/register", response_model=TokenResponse)
|
||
async def register(user: UserRegister):
|
||
"""Регистрация нового пользователя"""
|
||
try:
|
||
print(f"📝 Регистрация: {user.email}")
|
||
|
||
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()
|
||
|
||
print(f"✅ Пользователь создан: ID {user_id}")
|
||
|
||
token = create_access_token({"sub": str(user_id), "role": user.role})
|
||
|
||
return {
|
||
"access_token": token,
|
||
"token_type": "bearer",
|
||
"user_id": user_id,
|
||
"full_name": user.full_name,
|
||
"role": user.role,
|
||
"is_admin": bool(is_admin)
|
||
}
|
||
|
||
except HTTPException:
|
||
raise
|
||
except Exception as e:
|
||
print(f"❌ Ошибка регистрации: {e}")
|
||
raise HTTPException(status_code=500, detail=f"Внутренняя ошибка: {str(e)}")
|
||
|
||
|
||
@app.post("/api/login", response_model=TokenResponse)
|
||
async def login(user_data: UserLogin):
|
||
"""Вход в систему"""
|
||
try:
|
||
print(f"🔑 Login attempt: {user_data.email}")
|
||
|
||
with get_db() as conn:
|
||
cursor = conn.cursor()
|
||
cursor.execute("SELECT * FROM users WHERE email = ?", (user_data.email,))
|
||
user = cursor.fetchone()
|
||
|
||
if not user:
|
||
print(f"❌ User not found: {user_data.email}")
|
||
raise HTTPException(status_code=401, detail="Неверный email или пароль")
|
||
|
||
if not verify_password(user_data.password, user["password_hash"]):
|
||
print(f"❌ Wrong password for: {user_data.email}")
|
||
raise HTTPException(status_code=401, detail="Неверный email или пароль")
|
||
|
||
print(f"✅ Login successful for user ID: {user['id']}")
|
||
|
||
# Создаем данные для токена
|
||
token_data = {
|
||
"sub": str(user["id"]), # Явно преобразуем в строку
|
||
"role": user["role"],
|
||
"is_admin": bool(user["is_admin"])
|
||
}
|
||
token = create_access_token(token_data)
|
||
|
||
response_data = {
|
||
"access_token": token,
|
||
"token_type": "bearer",
|
||
"user_id": user["id"],
|
||
"full_name": user["full_name"],
|
||
"role": user["role"],
|
||
"is_admin": bool(user["is_admin"])
|
||
}
|
||
|
||
return response_data
|
||
|
||
except HTTPException:
|
||
raise
|
||
except Exception as e:
|
||
print(f"❌ Login error: {e}")
|
||
traceback.print_exc()
|
||
raise HTTPException(status_code=500, detail=f"Внутренняя ошибка: {str(e)}")
|
||
|
||
|
||
@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 name FROM companies WHERE user_id = ?", (user_id,))
|
||
company = cursor.fetchone()
|
||
company_name = company["name"] if company else None
|
||
|
||
cursor.execute("""
|
||
SELECT * FROM vacancies
|
||
WHERE user_id = ? AND is_active = 1
|
||
ORDER BY created_at DESC
|
||
""", (user_id,))
|
||
vacancies = cursor.fetchall()
|
||
|
||
result = []
|
||
for v in vacancies:
|
||
vacancy_dict = dict(v)
|
||
# Добавляем название компании (либо из таблицы companies, либо имя пользователя)
|
||
if company_name:
|
||
vacancy_dict["company_name"] = company_name
|
||
else:
|
||
cursor.execute("SELECT full_name FROM users WHERE id = ?", (user_id,))
|
||
user = cursor.fetchone()
|
||
vacancy_dict["company_name"] = user["full_name"] if user else None
|
||
|
||
# Добавляем теги
|
||
cursor.execute("""
|
||
SELECT t.* FROM tags t
|
||
JOIN vacancy_tags vt ON t.id = vt.tag_id
|
||
WHERE vt.vacancy_id = ?
|
||
""", (v["id"],))
|
||
vacancy_dict["tags"] = [dict(tag) for tag in cursor.fetchall()]
|
||
|
||
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",
|
||
company_id: int = None
|
||
):
|
||
"""Получение всех вакансий с фильтрацией"""
|
||
with get_db() as conn:
|
||
cursor = conn.cursor()
|
||
|
||
query = """
|
||
SELECT DISTINCT
|
||
v.*,
|
||
COALESCE(c.name, u.full_name) as company_name,
|
||
c.id as company_id -- Добавляем ID компании
|
||
FROM vacancies v
|
||
JOIN users u ON v.user_id = u.id
|
||
LEFT JOIN companies c ON v.user_id = c.user_id
|
||
WHERE v.is_active = 1
|
||
"""
|
||
params = []
|
||
|
||
# Фильтр по компании
|
||
if company_id:
|
||
query += " AND c.id = ?"
|
||
params.append(company_id)
|
||
|
||
# Остальные фильтры...
|
||
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)
|
||
|
||
# Добавляем теги
|
||
cursor.execute("""
|
||
SELECT t.* FROM tags t
|
||
JOIN vacancy_tags vt ON t.id = vt.tag_id
|
||
WHERE vt.vacancy_id = ?
|
||
""", (v["id"],))
|
||
vacancy_dict["tags"] = [dict(tag) for tag in cursor.fetchall()]
|
||
|
||
result.append(vacancy_dict)
|
||
|
||
# Получаем общее количество
|
||
count_query = "SELECT COUNT(*) FROM vacancies WHERE is_active = 1"
|
||
if company_id:
|
||
# Находим user_id по company_id
|
||
cursor.execute("SELECT user_id FROM companies WHERE id = ?", (company_id,))
|
||
company_user = cursor.fetchone()
|
||
if company_user:
|
||
count_query += " AND user_id = ?"
|
||
cursor.execute(count_query, (company_user["user_id"],))
|
||
else:
|
||
cursor.execute(count_query)
|
||
else:
|
||
cursor.execute(count_query)
|
||
total = cursor.fetchone()[0]
|
||
|
||
return {
|
||
"vacancies": result,
|
||
"total_pages": (total + limit - 1) // limit,
|
||
"current_page": page,
|
||
"total": total
|
||
}
|
||
|
||
|
||
@app.get("/api/vacancies/my-all")
|
||
async def get_my_all_vacancies(
|
||
user_id: int = Depends(get_current_user)
|
||
):
|
||
"""Получение ВСЕХ вакансий ТЕКУЩЕГО пользователя (включая неактивные)"""
|
||
with get_db() as conn:
|
||
cursor = conn.cursor()
|
||
|
||
# Получаем компанию пользователя
|
||
cursor.execute("SELECT name FROM companies WHERE user_id = ?", (user_id,))
|
||
company = cursor.fetchone()
|
||
company_name = company["name"] if company else None
|
||
|
||
# Получаем ВСЕ вакансии пользователя без фильтра по is_active
|
||
cursor.execute("""
|
||
SELECT * FROM vacancies
|
||
WHERE user_id = ?
|
||
ORDER BY
|
||
is_active DESC, -- сначала активные
|
||
created_at DESC -- потом по дате
|
||
""", (user_id,))
|
||
|
||
vacancies = cursor.fetchall()
|
||
|
||
result = []
|
||
for v in vacancies:
|
||
vacancy_dict = dict(v)
|
||
if company_name:
|
||
vacancy_dict["company_name"] = company_name
|
||
else:
|
||
cursor.execute("SELECT full_name FROM users WHERE id = ?", (user_id,))
|
||
user = cursor.fetchone()
|
||
vacancy_dict["company_name"] = user["full_name"] if user else None
|
||
|
||
# Добавляем теги
|
||
cursor.execute("""
|
||
SELECT t.* FROM tags t
|
||
JOIN vacancy_tags vt ON t.id = vt.tag_id
|
||
WHERE vt.vacancy_id = ?
|
||
""", (v["id"],))
|
||
vacancy_dict["tags"] = [dict(tag) for tag in cursor.fetchall()]
|
||
|
||
result.append(vacancy_dict)
|
||
|
||
return result
|
||
|
||
|
||
@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,))
|
||
|
||
# Получаем вакансию с данными компании - ДОБАВЛЯЕМ company_id
|
||
cursor.execute("""
|
||
SELECT
|
||
v.*,
|
||
COALESCE(c.name, u.full_name) as company_name,
|
||
c.id as company_id, -- ВАЖНО: добавляем ID компании
|
||
c.description as company_description,
|
||
c.website as company_website,
|
||
c.logo as company_logo,
|
||
c.address as company_address,
|
||
c.phone as company_phone,
|
||
c.email as company_email,
|
||
u.email as user_email,
|
||
u.telegram as user_telegram
|
||
FROM vacancies v
|
||
JOIN users u ON v.user_id = u.id
|
||
LEFT JOIN companies c ON v.user_id = c.user_id
|
||
WHERE v.id = ?
|
||
""", (vacancy_id,))
|
||
|
||
vacancy = cursor.fetchone()
|
||
if not vacancy:
|
||
raise HTTPException(status_code=404, detail="Вакансия не найдена")
|
||
|
||
result = dict(vacancy)
|
||
|
||
# Добавляем теги
|
||
cursor.execute("""
|
||
SELECT t.* FROM tags t
|
||
JOIN vacancy_tags vt ON t.id = vt.tag_id
|
||
WHERE vt.vacancy_id = ?
|
||
""", (vacancy_id,))
|
||
result["tags"] = [dict(tag) for tag in cursor.fetchall()]
|
||
|
||
# Проверяем, откликался ли пользователь
|
||
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.put("/api/vacancies/{vacancy_id}")
|
||
async def update_vacancy(
|
||
vacancy_id: int,
|
||
vacancy_update: VacancyUpdate,
|
||
user_id: int = Depends(get_current_user)
|
||
):
|
||
"""Обновление существующей вакансии"""
|
||
with get_db() as conn:
|
||
cursor = conn.cursor()
|
||
|
||
# Проверяем, что вакансия принадлежит пользователю
|
||
cursor.execute("""
|
||
SELECT v.*, u.role
|
||
FROM vacancies v
|
||
JOIN users u ON v.user_id = u.id
|
||
WHERE v.id = ? AND v.user_id = ?
|
||
""", (vacancy_id, user_id))
|
||
|
||
vacancy = cursor.fetchone()
|
||
if not vacancy:
|
||
raise HTTPException(status_code=404, detail="Вакансия не найдена")
|
||
|
||
# Проверяем роль (только работодатель может редактировать)
|
||
if vacancy["role"] != "employer":
|
||
raise HTTPException(status_code=403, detail="Только работодатели могут редактировать вакансии")
|
||
|
||
# Обновляем основные поля
|
||
update_fields = []
|
||
params = []
|
||
|
||
if vacancy_update.title is not None:
|
||
update_fields.append("title = ?")
|
||
params.append(vacancy_update.title)
|
||
|
||
if vacancy_update.salary is not None:
|
||
update_fields.append("salary = ?")
|
||
params.append(vacancy_update.salary)
|
||
|
||
if vacancy_update.description is not None:
|
||
update_fields.append("description = ?")
|
||
params.append(vacancy_update.description)
|
||
|
||
if vacancy_update.contact is not None:
|
||
update_fields.append("contact = ?")
|
||
params.append(vacancy_update.contact)
|
||
|
||
if vacancy_update.is_active is not None:
|
||
update_fields.append("is_active = ?")
|
||
params.append(1 if vacancy_update.is_active else 0)
|
||
|
||
if update_fields:
|
||
query = f"UPDATE vacancies SET {', '.join(update_fields)} WHERE id = ?"
|
||
params.append(vacancy_id)
|
||
cursor.execute(query, params)
|
||
|
||
# Обновляем теги
|
||
if vacancy_update.tags is not None:
|
||
# Удаляем старые теги
|
||
cursor.execute("DELETE FROM vacancy_tags WHERE vacancy_id = ?", (vacancy_id,))
|
||
|
||
# Добавляем новые теги
|
||
for tag_name in vacancy_update.tags:
|
||
# Ищем или создаем тег
|
||
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 INTO vacancy_tags (vacancy_id, tag_id)
|
||
VALUES (?, ?)
|
||
""", (vacancy_id, tag_id))
|
||
|
||
conn.commit()
|
||
|
||
# Возвращаем обновленную вакансию
|
||
cursor.execute("""
|
||
SELECT v.*, COALESCE(c.name, u.full_name) as company_name
|
||
FROM vacancies v
|
||
JOIN users u ON v.user_id = u.id
|
||
LEFT JOIN companies c ON v.user_id = c.user_id
|
||
WHERE v.id = ?
|
||
""", (vacancy_id,))
|
||
|
||
updated_vacancy = dict(cursor.fetchone())
|
||
|
||
# Добавляем теги
|
||
cursor.execute("""
|
||
SELECT t.* FROM tags t
|
||
JOIN vacancy_tags vt ON t.id = vt.tag_id
|
||
WHERE vt.vacancy_id = ?
|
||
""", (vacancy_id,))
|
||
updated_vacancy["tags"] = [dict(tag) for tag in cursor.fetchall()]
|
||
|
||
return updated_vacancy
|
||
|
||
|
||
@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
|
||
|
||
# Добавление опыта работы (ОБНОВЛЕНО - добавили description)
|
||
for exp in resume.work_experience:
|
||
cursor.execute("""
|
||
INSERT INTO work_experience (resume_id, position, company, period, description)
|
||
VALUES (?, ?, ?, ?, ?)
|
||
""", (resume_id, exp.position, exp.company, exp.period, exp.description))
|
||
|
||
# Добавление образования
|
||
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:
|
||
for tag_name in resume.tags:
|
||
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))
|
||
|
||
conn.commit()
|
||
|
||
# Получение обновленного резюме
|
||
cursor.execute("SELECT * FROM resumes WHERE id = ?", (resume_id,))
|
||
resume_data = dict(cursor.fetchone())
|
||
|
||
cursor.execute("""
|
||
SELECT position, company, period, description
|
||
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()]
|
||
|
||
cursor.execute("""
|
||
SELECT t.* FROM tags t
|
||
JOIN resume_tags rt ON t.id = rt.tag_id
|
||
WHERE rt.resume_id = ?
|
||
""", (resume_id,))
|
||
resume_data["tags"] = [dict(tag) for tag in cursor.fetchall()]
|
||
|
||
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)
|
||
|
||
# Получаем опыт работы (ОБНОВЛЕНО - добавили поле description)
|
||
cursor.execute("""
|
||
SELECT position, company, period, description
|
||
FROM work_experience
|
||
WHERE resume_id = ?
|
||
ORDER BY
|
||
CASE
|
||
WHEN period IS NULL THEN 1
|
||
ELSE 0
|
||
END,
|
||
period DESC
|
||
""", (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 = ?
|
||
ORDER BY graduation_year DESC
|
||
""", (resume["id"],))
|
||
resume_data["education"] = [dict(edu) for edu in cursor.fetchall()]
|
||
|
||
# Получаем теги
|
||
cursor.execute("""
|
||
SELECT t.* FROM tags t
|
||
JOIN resume_tags rt ON t.id = rt.tag_id
|
||
WHERE rt.resume_id = ?
|
||
""", (resume["id"],))
|
||
resume_data["tags"] = [dict(tag) for tag in cursor.fetchall()]
|
||
|
||
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/admin/resumes")
|
||
async def get_all_resumes_admin(
|
||
user_id: int = Depends(get_current_user),
|
||
page: int = 1,
|
||
limit: int = 10,
|
||
search: str = None
|
||
):
|
||
"""Получение всех резюме для админки с пагинацией и поиском"""
|
||
try:
|
||
print(f"👑 Админ запрашивает резюме: страница {page}, поиск '{search}'")
|
||
|
||
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"]:
|
||
print(f"❌ Доступ запрещен для пользователя {user_id}")
|
||
raise HTTPException(status_code=403, detail="Доступ запрещен")
|
||
|
||
# Базовый запрос для подсчета общего количества
|
||
count_query = """
|
||
SELECT COUNT(*)
|
||
FROM resumes r
|
||
JOIN users u ON r.user_id = u.id
|
||
WHERE 1=1
|
||
"""
|
||
count_params = []
|
||
|
||
# Базовый запрос для получения данных
|
||
query = """
|
||
SELECT
|
||
r.id,
|
||
r.user_id,
|
||
r.desired_position,
|
||
r.about_me,
|
||
r.desired_salary,
|
||
r.updated_at,
|
||
r.views,
|
||
u.full_name,
|
||
u.email,
|
||
u.phone,
|
||
u.telegram
|
||
FROM resumes r
|
||
JOIN users u ON r.user_id = u.id
|
||
WHERE 1=1
|
||
"""
|
||
params = []
|
||
|
||
# Добавляем поиск, если есть
|
||
if search:
|
||
search_term = f"%{search}%"
|
||
search_condition = """ AND (
|
||
u.full_name LIKE ? OR
|
||
u.email LIKE ? OR
|
||
r.desired_position LIKE ? OR
|
||
r.about_me LIKE ?
|
||
)"""
|
||
query += search_condition
|
||
count_query += search_condition
|
||
|
||
search_params = [search_term, search_term, search_term, search_term]
|
||
params.extend(search_params)
|
||
count_params.extend(search_params)
|
||
|
||
# Сортировка
|
||
query += " ORDER BY r.updated_at DESC"
|
||
|
||
# Пагинация
|
||
offset = (page - 1) * limit
|
||
query += " LIMIT ? OFFSET ?"
|
||
params.extend([limit, offset])
|
||
|
||
# Получаем общее количество
|
||
cursor.execute(count_query, count_params)
|
||
total = cursor.fetchone()[0]
|
||
|
||
# Получаем резюме
|
||
cursor.execute(query, params)
|
||
resumes = cursor.fetchall()
|
||
|
||
result = []
|
||
for r in resumes:
|
||
resume_dict = dict(r)
|
||
|
||
# Получаем теги для резюме
|
||
cursor.execute("""
|
||
SELECT t.name, t.category
|
||
FROM tags t
|
||
JOIN resume_tags rt ON t.id = rt.tag_id
|
||
WHERE rt.resume_id = ?
|
||
""", (resume_dict["id"],))
|
||
resume_dict["tags"] = [dict(tag) for tag in cursor.fetchall()]
|
||
|
||
# Получаем опыт работы
|
||
cursor.execute("""
|
||
SELECT position, company, period, description
|
||
FROM work_experience
|
||
WHERE resume_id = ?
|
||
ORDER BY
|
||
CASE
|
||
WHEN period IS NULL THEN 1
|
||
ELSE 0
|
||
END,
|
||
period DESC
|
||
""", (resume_dict["id"],))
|
||
resume_dict["work_experience"] = [dict(exp) for exp in cursor.fetchall()]
|
||
|
||
# Получаем образование
|
||
cursor.execute("""
|
||
SELECT institution, specialty, graduation_year
|
||
FROM education
|
||
WHERE resume_id = ?
|
||
ORDER BY graduation_year DESC
|
||
""", (resume_dict["id"],))
|
||
resume_dict["education"] = [dict(edu) for edu in cursor.fetchall()]
|
||
|
||
# Количество опыта для быстрого отображения
|
||
resume_dict["experience_count"] = len(resume_dict["work_experience"])
|
||
|
||
result.append(resume_dict)
|
||
|
||
print(f"✅ Загружено {len(result)} резюме из {total}")
|
||
|
||
return {
|
||
"resumes": result,
|
||
"total": total,
|
||
"page": page,
|
||
"total_pages": (total + limit - 1) // limit,
|
||
"limit": limit
|
||
}
|
||
|
||
except HTTPException:
|
||
raise
|
||
except Exception as e:
|
||
print(f"❌ Ошибка при загрузке резюме для админки: {e}")
|
||
traceback.print_exc()
|
||
raise HTTPException(status_code=500, detail=f"Внутренняя ошибка: {str(e)}")
|
||
|
||
|
||
@app.get("/api/admin/resumes/{resume_id}")
|
||
async def get_resume_admin(
|
||
resume_id: int,
|
||
user_id: int = Depends(get_current_user)
|
||
):
|
||
"""Получение детальной информации о резюме для админки"""
|
||
try:
|
||
print(f"👑 Админ {user_id} запрашивает детали резюме {resume_id}")
|
||
|
||
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
|
||
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="Резюме не найдено")
|
||
|
||
resume_dict = dict(resume)
|
||
|
||
# Получаем теги
|
||
cursor.execute("""
|
||
SELECT t.* FROM tags t
|
||
JOIN resume_tags rt ON t.id = rt.tag_id
|
||
WHERE rt.resume_id = ?
|
||
""", (resume_id,))
|
||
resume_dict["tags"] = [dict(tag) for tag in cursor.fetchall()]
|
||
|
||
# Получаем опыт работы
|
||
cursor.execute("""
|
||
SELECT * FROM work_experience
|
||
WHERE resume_id = ?
|
||
ORDER BY
|
||
CASE
|
||
WHEN period IS NULL THEN 1
|
||
ELSE 0
|
||
END,
|
||
period DESC
|
||
""", (resume_id,))
|
||
resume_dict["work_experience"] = [dict(exp) for exp in cursor.fetchall()]
|
||
|
||
# Получаем образование
|
||
cursor.execute("""
|
||
SELECT * FROM education
|
||
WHERE resume_id = ?
|
||
ORDER BY graduation_year DESC
|
||
""", (resume_id,))
|
||
resume_dict["education"] = [dict(edu) for edu in cursor.fetchall()]
|
||
|
||
return resume_dict
|
||
|
||
except HTTPException:
|
||
raise
|
||
except Exception as e:
|
||
print(f"❌ Ошибка при загрузке деталей резюме {resume_id}: {e}")
|
||
traceback.print_exc()
|
||
raise HTTPException(status_code=500, detail=f"Внутренняя ошибка: {str(e)}")
|
||
|
||
|
||
@app.delete("/api/admin/resumes/{resume_id}")
|
||
async def delete_resume_admin(
|
||
resume_id: int,
|
||
user_id: int = Depends(get_current_user)
|
||
):
|
||
"""Удаление резюме (только для админа)"""
|
||
try:
|
||
print(f"👑 Админ {user_id} пытается удалить резюме {resume_id}")
|
||
|
||
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"]:
|
||
print(f"❌ Доступ запрещен для пользователя {user_id}")
|
||
raise HTTPException(status_code=403, detail="Доступ запрещен")
|
||
|
||
# Проверяем существование резюме
|
||
cursor.execute("SELECT id FROM resumes WHERE id = ?", (resume_id,))
|
||
resume = cursor.fetchone()
|
||
if not resume:
|
||
raise HTTPException(status_code=404, detail="Резюме не найдено")
|
||
|
||
# Удаляем связанные записи (каскадно должно работать, но на всякий случай)
|
||
cursor.execute("DELETE FROM resume_tags WHERE resume_id = ?", (resume_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 resumes WHERE id = ?", (resume_id,))
|
||
|
||
conn.commit()
|
||
print(f"✅ Резюме {resume_id} успешно удалено")
|
||
|
||
return {"message": "Резюме успешно удалено"}
|
||
|
||
except HTTPException:
|
||
raise
|
||
except Exception as e:
|
||
print(f"❌ Ошибка при удалении резюме {resume_id}: {e}")
|
||
traceback.print_exc()
|
||
raise HTTPException(status_code=500, detail=f"Внутренняя ошибка: {str(e)}")
|
||
|
||
|
||
@app.get("/api/resumes/{resume_id}")
|
||
async def get_resume(resume_id: int):
|
||
"""Получение конкретного резюме"""
|
||
with get_db() as conn:
|
||
cursor = conn.cursor()
|
||
|
||
# Увеличиваем счетчик просмотров
|
||
cursor.execute("UPDATE resumes SET views = views + 1 WHERE id = ?", (resume_id,))
|
||
|
||
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="Резюме не найдено")
|
||
|
||
resume_data = dict(resume)
|
||
|
||
# Получаем опыт работы (ОБНОВЛЕНО - добавили поле description)
|
||
cursor.execute("""
|
||
SELECT position, company, period, description
|
||
FROM work_experience
|
||
WHERE resume_id = ?
|
||
ORDER BY
|
||
CASE
|
||
WHEN period IS NULL THEN 1
|
||
ELSE 0
|
||
END,
|
||
period DESC
|
||
""", (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 = ?
|
||
ORDER BY graduation_year DESC
|
||
""", (resume_id,))
|
||
resume_data["education"] = [dict(edu) for edu in cursor.fetchall()]
|
||
|
||
# Получаем теги
|
||
cursor.execute("""
|
||
SELECT t.* FROM tags t
|
||
JOIN resume_tags rt ON t.id = rt.tag_id
|
||
WHERE rt.resume_id = ?
|
||
""", (resume_id,))
|
||
resume_data["tags"] = [dict(tag) for tag in cursor.fetchall()]
|
||
|
||
conn.commit()
|
||
return resume_data
|
||
|
||
|
||
@app.get("/api/company", response_model=CompanyResponse)
|
||
async def get_my_company(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("SELECT * FROM companies WHERE user_id = ?", (user_id,))
|
||
company = cursor.fetchone()
|
||
|
||
if not company:
|
||
# Если компании нет, возвращаем пустой объект с 404
|
||
raise HTTPException(status_code=404, detail="Компания не найдена")
|
||
|
||
return dict(company)
|
||
|
||
|
||
@app.post("/api/company", response_model=CompanyResponse)
|
||
async def create_or_update_company(
|
||
company: CompanyCreate,
|
||
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("SELECT id FROM companies WHERE user_id = ?", (user_id,))
|
||
existing = cursor.fetchone()
|
||
|
||
if existing:
|
||
# Обновление существующей компании
|
||
cursor.execute("""
|
||
UPDATE companies
|
||
SET name = ?, description = ?, website = ?, logo = ?,
|
||
address = ?, phone = ?, email = ?, updated_at = CURRENT_TIMESTAMP
|
||
WHERE user_id = ?
|
||
""", (company.name, company.description, company.website, company.logo,
|
||
company.address, company.phone, company.email, user_id))
|
||
|
||
company_id = existing["id"]
|
||
else:
|
||
# Создание новой компании
|
||
cursor.execute("""
|
||
INSERT INTO companies (user_id, name, description, website, logo, address, phone, email)
|
||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
||
""", (user_id, company.name, company.description, company.website, company.logo,
|
||
company.address, company.phone, company.email))
|
||
|
||
company_id = cursor.lastrowid
|
||
|
||
conn.commit()
|
||
|
||
cursor.execute("SELECT * FROM companies WHERE id = ?", (company_id,))
|
||
return dict(cursor.fetchone())
|
||
|
||
|
||
@app.get("/api/companies/{company_id}")
|
||
async def get_company_by_id(company_id: int):
|
||
"""Получение информации о компании по ID (публичный эндпоинт)"""
|
||
with get_db() as conn:
|
||
cursor = conn.cursor()
|
||
cursor.execute("""
|
||
SELECT c.*, u.full_name as owner_name
|
||
FROM companies c
|
||
JOIN users u ON c.user_id = u.id
|
||
WHERE c.id = ?
|
||
""", (company_id,))
|
||
|
||
company = cursor.fetchone()
|
||
if not company:
|
||
raise HTTPException(status_code=404, detail="Компания не найдена")
|
||
|
||
return dict(company)
|
||
|
||
|
||
@app.get("/api/resumes/{resume_id}")
|
||
async def get_resume(resume_id: int):
|
||
"""Получение конкретного резюме"""
|
||
with get_db() as conn:
|
||
cursor = conn.cursor()
|
||
|
||
# Увеличиваем счетчик просмотров
|
||
cursor.execute("UPDATE resumes SET views = views + 1 WHERE id = ?", (resume_id,))
|
||
|
||
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="Резюме не найдено")
|
||
|
||
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()]
|
||
|
||
# Получаем теги
|
||
cursor.execute("""
|
||
SELECT t.* FROM tags t
|
||
JOIN resume_tags rt ON t.id = rt.tag_id
|
||
WHERE rt.resume_id = ?
|
||
""", (resume_id,))
|
||
resume_data["tags"] = [dict(tag) for tag in cursor.fetchall()]
|
||
|
||
conn.commit()
|
||
return resume_data
|
||
|
||
|
||
# ========== ЭНДПОИНТЫ ДЛЯ ИЗБРАННОГО ==========
|
||
|
||
@app.post("/api/favorites")
|
||
async def add_to_favorites(
|
||
favorite: FavoriteCreate,
|
||
user_id: int = Depends(get_current_user)
|
||
):
|
||
"""Добавление в избранное"""
|
||
with get_db() as conn:
|
||
cursor = conn.cursor()
|
||
|
||
# Проверяем существование элемента
|
||
if favorite.item_type == 'vacancy':
|
||
cursor.execute("SELECT id FROM vacancies WHERE id = ? AND is_active = 1", (favorite.item_id,))
|
||
else: # resume
|
||
cursor.execute("SELECT id FROM resumes WHERE id = ?", (favorite.item_id,))
|
||
|
||
if not cursor.fetchone():
|
||
raise HTTPException(status_code=404, detail=f"{favorite.item_type} не найден")
|
||
|
||
# Добавляем в избранное
|
||
try:
|
||
cursor.execute("""
|
||
INSERT INTO favorites (user_id, item_type, item_id)
|
||
VALUES (?, ?, ?)
|
||
""", (user_id, favorite.item_type, favorite.item_id))
|
||
conn.commit()
|
||
return {"message": "Добавлено в избранное"}
|
||
except sqlite3.IntegrityError:
|
||
raise HTTPException(status_code=400, detail="Уже в избранном")
|
||
|
||
|
||
@app.delete("/api/favorites")
|
||
async def remove_from_favorites(
|
||
favorite: FavoriteCreate,
|
||
user_id: int = Depends(get_current_user)
|
||
):
|
||
"""Удаление из избранного"""
|
||
with get_db() as conn:
|
||
cursor = conn.cursor()
|
||
|
||
cursor.execute("""
|
||
DELETE FROM favorites
|
||
WHERE user_id = ? AND item_type = ? AND item_id = ?
|
||
""", (user_id, favorite.item_type, favorite.item_id))
|
||
|
||
if cursor.rowcount == 0:
|
||
raise HTTPException(status_code=404, detail="Не найдено в избранном")
|
||
|
||
conn.commit()
|
||
return {"message": "Удалено из избранного"}
|
||
|
||
|
||
@app.get("/api/favorites")
|
||
async def get_favorites(
|
||
user_id: int = Depends(get_current_user),
|
||
item_type: Optional[str] = None
|
||
):
|
||
"""Получение списка избранного"""
|
||
with get_db() as conn:
|
||
cursor = conn.cursor()
|
||
|
||
query = "SELECT * FROM favorites WHERE user_id = ?"
|
||
params = [user_id]
|
||
|
||
if item_type:
|
||
query += " AND item_type = ?"
|
||
params.append(item_type)
|
||
|
||
query += " ORDER BY created_at DESC"
|
||
|
||
cursor.execute(query, params)
|
||
favorites = cursor.fetchall()
|
||
|
||
result = []
|
||
for fav in favorites:
|
||
fav_dict = dict(fav)
|
||
|
||
# Получаем дополнительные данные о элементе
|
||
if fav["item_type"] == 'vacancy':
|
||
cursor.execute("""
|
||
SELECT v.*, COALESCE(c.name, u.full_name) as company_name
|
||
FROM vacancies v
|
||
JOIN users u ON v.user_id = u.id
|
||
LEFT JOIN companies c ON v.user_id = c.user_id
|
||
WHERE v.id = ?
|
||
""", (fav["item_id"],))
|
||
item = cursor.fetchone()
|
||
|
||
if item:
|
||
item_dict = dict(item)
|
||
# Получаем теги вакансии
|
||
cursor.execute("""
|
||
SELECT t.name FROM tags t
|
||
JOIN vacancy_tags vt ON t.id = vt.tag_id
|
||
WHERE vt.vacancy_id = ?
|
||
""", (fav["item_id"],))
|
||
tags = [t["name"] for t in cursor.fetchall()]
|
||
|
||
fav_dict["item_data"] = {
|
||
"id": item["id"],
|
||
"title": item["title"],
|
||
"company": item_dict.get("company_name"),
|
||
"salary": item["salary"],
|
||
"tags": tags,
|
||
"url": f"/vacancy/{item['id']}"
|
||
}
|
||
|
||
else: # resume
|
||
cursor.execute("""
|
||
SELECT r.*, u.full_name
|
||
FROM resumes r
|
||
JOIN users u ON r.user_id = u.id
|
||
WHERE r.id = ?
|
||
""", (fav["item_id"],))
|
||
item = cursor.fetchone()
|
||
|
||
if item:
|
||
item_dict = dict(item)
|
||
# Получаем теги резюме
|
||
cursor.execute("""
|
||
SELECT t.name FROM tags t
|
||
JOIN resume_tags rt ON t.id = rt.tag_id
|
||
WHERE rt.resume_id = ?
|
||
""", (fav["item_id"],))
|
||
tags = [t["name"] for t in cursor.fetchall()]
|
||
|
||
fav_dict["item_data"] = {
|
||
"id": item["id"],
|
||
"title": item_dict.get("desired_position", "Резюме"),
|
||
"name": item["full_name"],
|
||
"salary": item["desired_salary"],
|
||
"tags": tags,
|
||
"url": f"/resume/{item['id']}"
|
||
}
|
||
|
||
result.append(fav_dict)
|
||
|
||
return result
|
||
|
||
|
||
@app.get("/api/favorites/check/{item_type}/{item_id}")
|
||
async def check_favorite(
|
||
item_type: str,
|
||
item_id: int,
|
||
user_id: int = Depends(get_current_user)
|
||
):
|
||
"""Проверка, находится ли элемент в избранном"""
|
||
with get_db() as conn:
|
||
cursor = conn.cursor()
|
||
cursor.execute("""
|
||
SELECT id FROM favorites
|
||
WHERE user_id = ? AND item_type = ? AND item_id = ?
|
||
""", (user_id, item_type, item_id))
|
||
|
||
return {"is_favorite": cursor.fetchone() is not None}
|
||
|
||
|
||
@app.get("/api/user/stats")
|
||
async def get_user_stats(user_id: int = Depends(get_current_user)):
|
||
"""Получение статистики пользователя (количество избранного, просмотров и т.д.)"""
|
||
with get_db() as conn:
|
||
cursor = conn.cursor()
|
||
|
||
stats = {}
|
||
|
||
# Количество избранных вакансий
|
||
cursor.execute("""
|
||
SELECT COUNT(*) FROM favorites
|
||
WHERE user_id = ? AND item_type = 'vacancy'
|
||
""", (user_id,))
|
||
stats["favorite_vacancies"] = cursor.fetchone()[0]
|
||
|
||
# Количество избранных резюме
|
||
cursor.execute("""
|
||
SELECT COUNT(*) FROM favorites
|
||
WHERE user_id = ? AND item_type = 'resume'
|
||
""", (user_id,))
|
||
stats["favorite_resumes"] = cursor.fetchone()[0]
|
||
|
||
# Общее количество избранного
|
||
stats["total_favorites"] = stats["favorite_vacancies"] + stats["favorite_resumes"]
|
||
|
||
# Количество просмотров профиля (если есть резюме)
|
||
cursor.execute("""
|
||
SELECT SUM(views) FROM resumes WHERE user_id = ?
|
||
""", (user_id,))
|
||
stats["profile_views"] = cursor.fetchone()[0] or 0
|
||
|
||
# Количество созданных вакансий (для работодателя)
|
||
cursor.execute("""
|
||
SELECT COUNT(*) FROM vacancies
|
||
WHERE user_id = ? AND is_active = 1
|
||
""", (user_id,))
|
||
stats["active_vacancies"] = cursor.fetchone()[0]
|
||
|
||
return stats
|
||
|
||
|
||
@app.get("/api/users/{target_user_id}")
|
||
async def get_public_user_info(
|
||
target_user_id: int,
|
||
request: Request,
|
||
token: Optional[str] = None
|
||
):
|
||
"""Получение публичной информации о пользователе с контактами (если есть права)"""
|
||
try:
|
||
print(f"📥 Запрос информации о пользователе {target_user_id}")
|
||
|
||
# Получаем текущего пользователя из токена (если есть)
|
||
current_user_id = None
|
||
is_admin = False
|
||
|
||
auth_header = request.headers.get("Authorization")
|
||
if auth_header and auth_header.startswith("Bearer "):
|
||
token = auth_header.replace("Bearer ", "")
|
||
try:
|
||
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
|
||
current_user_id = int(payload.get("sub"))
|
||
# Проверяем в базе, является ли пользователь администратором
|
||
with get_db() as conn:
|
||
cursor = conn.cursor()
|
||
cursor.execute("SELECT is_admin FROM users WHERE id = ?", (current_user_id,))
|
||
user_data = cursor.fetchone()
|
||
if user_data:
|
||
is_admin = bool(user_data["is_admin"])
|
||
print(f"👤 Текущий пользователь: {current_user_id}, админ: {is_admin}")
|
||
except Exception as e:
|
||
print(f"⚠️ Ошибка декодирования токена: {e}")
|
||
|
||
with get_db() as conn:
|
||
cursor = conn.cursor()
|
||
|
||
# Получаем базовую информацию о пользователе
|
||
cursor.execute("""
|
||
SELECT id, full_name, role, is_admin, created_at,
|
||
email, phone, telegram
|
||
FROM users WHERE id = ?
|
||
""", (target_user_id,))
|
||
|
||
user = cursor.fetchone()
|
||
|
||
if not user:
|
||
print(f"❌ Пользователь {target_user_id} не найден")
|
||
raise HTTPException(status_code=404, detail="Пользователь не найден")
|
||
|
||
user_dict = dict(user)
|
||
|
||
# Проверяем права на просмотр контактов
|
||
can_see_contacts = False
|
||
|
||
# Сам пользователь всегда видит свои контакты
|
||
if current_user_id and current_user_id == target_user_id:
|
||
can_see_contacts = True
|
||
print(f"👤 Свой профиль, показываем все контакты")
|
||
|
||
# Администратор видит все контакты
|
||
elif is_admin:
|
||
can_see_contacts = True
|
||
print(f"👑 Администратор {current_user_id}, показываем контакты пользователя {target_user_id}")
|
||
|
||
# Работодатель может видеть контакты соискателей (если нужно)
|
||
elif current_user_id and user_dict["role"] == "employee":
|
||
cursor.execute("SELECT role FROM users WHERE id = ?", (current_user_id,))
|
||
current_user_role = cursor.fetchone()
|
||
if current_user_role and current_user_role["role"] == "employer":
|
||
can_see_contacts = True
|
||
print(f"🏢 Работодатель просматривает соискателя, показываем контакты")
|
||
|
||
if not can_see_contacts:
|
||
# Скрываем контактные данные
|
||
user_dict["email"] = None
|
||
user_dict["phone"] = None
|
||
user_dict["telegram"] = None
|
||
user_dict["contacts_hidden"] = True
|
||
print(f"🔒 Контакты скрыты для пользователя {current_user_id}")
|
||
else:
|
||
user_dict["contacts_hidden"] = False
|
||
print(
|
||
f"✅ Контакты доступны: email={user_dict['email']}, phone={user_dict['phone']}, telegram={user_dict['telegram']}")
|
||
|
||
return user_dict
|
||
|
||
except HTTPException:
|
||
raise
|
||
except Exception as e:
|
||
print(f"❌ Ошибка при загрузке пользователя {target_user_id}: {e}")
|
||
traceback.print_exc()
|
||
raise HTTPException(status_code=500, detail=f"Внутренняя ошибка: {str(e)}")
|
||
|
||
|
||
@app.get("/api/users/{target_user_id}/company")
|
||
async def get_public_company(target_user_id: int):
|
||
"""Получение публичной информации о компании пользователя"""
|
||
with get_db() as conn:
|
||
cursor = conn.cursor()
|
||
|
||
cursor.execute("SELECT * FROM companies WHERE user_id = ?", (target_user_id,))
|
||
company = cursor.fetchone()
|
||
|
||
if not company:
|
||
raise HTTPException(status_code=404, detail="Компания не найдена")
|
||
|
||
return dict(company)
|
||
|
||
|
||
@app.get("/api/users/{target_user_id}/vacancies")
|
||
async def get_public_vacancies(target_user_id: int):
|
||
"""Получение публичных вакансий пользователя"""
|
||
with get_db() as conn:
|
||
cursor = conn.cursor()
|
||
|
||
cursor.execute("""
|
||
SELECT * FROM vacancies
|
||
WHERE user_id = ? AND is_active = 1
|
||
ORDER BY created_at DESC
|
||
""", (target_user_id,))
|
||
|
||
vacancies = cursor.fetchall()
|
||
return [dict(v) for v in vacancies]
|
||
|
||
|
||
@app.get("/api/users/{target_user_id}/resume")
|
||
async def get_public_resume(target_user_id: int):
|
||
"""Получение публичного резюме пользователя"""
|
||
with get_db() as conn:
|
||
cursor = conn.cursor()
|
||
|
||
cursor.execute("SELECT id FROM resumes WHERE user_id = ?", (target_user_id,))
|
||
resume_record = cursor.fetchone()
|
||
|
||
if not resume_record:
|
||
raise HTTPException(status_code=404, detail="Резюме не найдено")
|
||
|
||
return await get_resume(resume_record["id"])
|
||
|
||
|
||
# ========== ЭНДПОИНТЫ ДЛЯ ОТКЛИКОВ ==========
|
||
|
||
@app.post("/api/applications")
|
||
async def create_application(
|
||
application: ApplicationCreate,
|
||
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 v.*, u.id as employer_id
|
||
FROM vacancies v
|
||
JOIN users u ON v.user_id = u.id
|
||
WHERE v.id = ? AND v.is_active = 1
|
||
""", (application.vacancy_id,))
|
||
|
||
vacancy = cursor.fetchone()
|
||
if not vacancy:
|
||
raise HTTPException(status_code=404, detail="Вакансия не найдена")
|
||
|
||
# Проверяем, не откликался ли уже
|
||
cursor.execute("""
|
||
SELECT id FROM applications
|
||
WHERE vacancy_id = ? AND user_id = ?
|
||
""", (application.vacancy_id, user_id))
|
||
|
||
if cursor.fetchone():
|
||
raise HTTPException(status_code=400, detail="Вы уже откликались на эту вакансию")
|
||
|
||
# Создаем отклик
|
||
cursor.execute("""
|
||
INSERT INTO applications (vacancy_id, user_id, message)
|
||
VALUES (?, ?, ?)
|
||
""", (application.vacancy_id, user_id, application.message))
|
||
|
||
application_id = cursor.lastrowid
|
||
conn.commit()
|
||
|
||
# Получаем созданный отклик
|
||
cursor.execute("SELECT * FROM applications WHERE id = ?", (application_id,))
|
||
new_application = dict(cursor.fetchone())
|
||
|
||
return new_application
|
||
|
||
|
||
@app.get("/api/applications/received")
|
||
async def get_received_applications(
|
||
user_id: int = Depends(get_current_user),
|
||
status: Optional[str] = None,
|
||
page: int = 1,
|
||
limit: int = 20
|
||
):
|
||
"""Получение откликов на вакансии работодателя (входящие)"""
|
||
try:
|
||
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="Только работодатели могут просматривать отклики")
|
||
|
||
# Исправлено: убрали лишние поля, добавили правильные алиасы
|
||
query = """
|
||
SELECT
|
||
a.*,
|
||
v.title as vacancy_title,
|
||
v.salary as vacancy_salary,
|
||
u.full_name as user_name,
|
||
u.email as user_email,
|
||
u.phone as user_phone,
|
||
u.telegram as user_telegram
|
||
FROM applications a
|
||
JOIN vacancies v ON a.vacancy_id = v.id
|
||
JOIN users u ON a.user_id = u.id
|
||
WHERE v.user_id = ?
|
||
"""
|
||
params = [user_id]
|
||
|
||
if status:
|
||
query += " AND a.status = ?"
|
||
params.append(status)
|
||
|
||
# Пагинация
|
||
offset = (page - 1) * limit
|
||
query += " ORDER BY a.created_at DESC LIMIT ? OFFSET ?"
|
||
params.extend([limit, offset])
|
||
|
||
cursor.execute(query, params)
|
||
applications = cursor.fetchall()
|
||
|
||
# Получаем общее количество
|
||
count_query = "SELECT COUNT(*) FROM applications a JOIN vacancies v ON a.vacancy_id = v.id WHERE v.user_id = ?"
|
||
count_params = [user_id]
|
||
|
||
if status:
|
||
count_query += " AND a.status = ?"
|
||
count_params.append(status)
|
||
|
||
cursor.execute(count_query, count_params)
|
||
total = cursor.fetchone()[0]
|
||
|
||
result = []
|
||
for app in applications:
|
||
app_dict = dict(app)
|
||
result.append(app_dict)
|
||
|
||
return {
|
||
"applications": result,
|
||
"total": total,
|
||
"page": page,
|
||
"total_pages": (total + limit - 1) // limit
|
||
}
|
||
|
||
except HTTPException:
|
||
raise
|
||
except Exception as e:
|
||
print(f"Error in get_received_applications: {e}")
|
||
raise HTTPException(status_code=500, detail=str(e))
|
||
|
||
|
||
@app.get("/api/applications/stats")
|
||
async def get_application_stats(user_id: int = Depends(get_current_user)):
|
||
"""Получение статистики по откликам"""
|
||
try:
|
||
with get_db() as conn:
|
||
cursor = conn.cursor()
|
||
|
||
# Проверяем существование пользователя
|
||
cursor.execute("SELECT id, role FROM users WHERE id = ?", (user_id,))
|
||
user = cursor.fetchone()
|
||
|
||
if not user:
|
||
raise HTTPException(status_code=404, detail="Пользователь не найден")
|
||
|
||
# Базовая структура статистики
|
||
stats = {
|
||
"total": 0,
|
||
"pending": 0,
|
||
"viewed": 0,
|
||
"accepted": 0,
|
||
"rejected": 0
|
||
}
|
||
|
||
if user["role"] == "employer":
|
||
# Статистика для работодателя (полученные отклики)
|
||
cursor.execute("""
|
||
SELECT
|
||
COUNT(*) as total,
|
||
SUM(CASE WHEN a.status = 'pending' THEN 1 ELSE 0 END) as pending,
|
||
SUM(CASE WHEN a.status = 'viewed' THEN 1 ELSE 0 END) as viewed,
|
||
SUM(CASE WHEN a.status = 'accepted' THEN 1 ELSE 0 END) as accepted,
|
||
SUM(CASE WHEN a.status = 'rejected' THEN 1 ELSE 0 END) as rejected
|
||
FROM applications a
|
||
JOIN vacancies v ON a.vacancy_id = v.id
|
||
WHERE v.user_id = ?
|
||
""", (user_id,))
|
||
|
||
row = cursor.fetchone()
|
||
if row:
|
||
stats = {
|
||
"total": row[0] or 0,
|
||
"pending": row[1] or 0,
|
||
"viewed": row[2] or 0,
|
||
"accepted": row[3] or 0,
|
||
"rejected": row[4] or 0
|
||
}
|
||
|
||
elif user["role"] == "employee":
|
||
# Статистика для соискателя (отправленные отклики)
|
||
cursor.execute("""
|
||
SELECT
|
||
COUNT(*) as total,
|
||
SUM(CASE WHEN status = 'pending' THEN 1 ELSE 0 END) as pending,
|
||
SUM(CASE WHEN status = 'viewed' THEN 1 ELSE 0 END) as viewed,
|
||
SUM(CASE WHEN status = 'accepted' THEN 1 ELSE 0 END) as accepted,
|
||
SUM(CASE WHEN status = 'rejected' THEN 1 ELSE 0 END) as rejected
|
||
FROM applications
|
||
WHERE user_id = ?
|
||
""", (user_id,))
|
||
|
||
row = cursor.fetchone()
|
||
if row:
|
||
stats = {
|
||
"total": row[0] or 0,
|
||
"pending": row[1] or 0,
|
||
"viewed": row[2] or 0,
|
||
"accepted": row[3] or 0,
|
||
"rejected": row[4] or 0
|
||
}
|
||
|
||
# Добавляем информацию о роли для отладки
|
||
stats["role"] = user["role"]
|
||
|
||
return stats
|
||
|
||
except Exception as e:
|
||
print(f"❌ Error in get_application_stats: {e}")
|
||
raise HTTPException(status_code=500, detail=f"Internal server error: {str(e)}")
|
||
|
||
|
||
@app.get("/api/applications/sent")
|
||
async def get_sent_applications(
|
||
user_id: int = Depends(get_current_user),
|
||
status: Optional[str] = None,
|
||
page: int = 1,
|
||
limit: int = 20
|
||
):
|
||
"""Получение отправленных откликов (для соискателя)"""
|
||
try:
|
||
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="Только соискатели могут просматривать свои отклики")
|
||
|
||
query = """
|
||
SELECT
|
||
a.*,
|
||
v.title as vacancy_title,
|
||
v.salary as vacancy_salary,
|
||
COALESCE(c.name, u_emp.full_name) as company_name,
|
||
u_emp.email as employer_email,
|
||
u_emp.telegram as employer_telegram
|
||
FROM applications a
|
||
JOIN vacancies v ON a.vacancy_id = v.id
|
||
JOIN users u_emp ON v.user_id = u_emp.id
|
||
LEFT JOIN companies c ON u_emp.id = c.user_id
|
||
WHERE a.user_id = ?
|
||
"""
|
||
params = [user_id]
|
||
|
||
if status:
|
||
query += " AND a.status = ?"
|
||
params.append(status)
|
||
|
||
# Пагинация
|
||
offset = (page - 1) * limit
|
||
query += " ORDER BY a.created_at DESC LIMIT ? OFFSET ?"
|
||
params.extend([limit, offset])
|
||
|
||
cursor.execute(query, params)
|
||
applications = cursor.fetchall()
|
||
|
||
# Получаем общее количество
|
||
cursor.execute("SELECT COUNT(*) FROM applications WHERE user_id = ?", (user_id,))
|
||
total = cursor.fetchone()[0]
|
||
|
||
result = []
|
||
for app in applications:
|
||
app_dict = dict(app)
|
||
result.append(app_dict)
|
||
|
||
return {
|
||
"applications": result,
|
||
"total": total,
|
||
"page": page,
|
||
"total_pages": (total + limit - 1) // limit
|
||
}
|
||
|
||
except HTTPException:
|
||
raise
|
||
except Exception as e:
|
||
print(f"Error in get_sent_applications: {e}")
|
||
raise HTTPException(status_code=500, detail=str(e))
|
||
|
||
|
||
@app.put("/api/applications/{application_id}/status")
|
||
async def update_application_status(
|
||
application_id: int,
|
||
status_update: ApplicationStatusUpdate,
|
||
user_id: int = Depends(get_current_user)
|
||
):
|
||
"""Обновление статуса отклика (для работодателя)"""
|
||
with get_db() as conn:
|
||
cursor = conn.cursor()
|
||
|
||
# Проверяем, что отклик существует и пользователь - владелец вакансии
|
||
cursor.execute("""
|
||
SELECT a.*, v.user_id as employer_id
|
||
FROM applications a
|
||
JOIN vacancies v ON a.vacancy_id = v.id
|
||
WHERE a.id = ?
|
||
""", (application_id,))
|
||
|
||
application = cursor.fetchone()
|
||
if not application:
|
||
raise HTTPException(status_code=404, detail="Отклик не найден")
|
||
|
||
if application["employer_id"] != user_id:
|
||
raise HTTPException(status_code=403, detail="Только владелец вакансии может изменять статус")
|
||
|
||
# Обновляем статус
|
||
cursor.execute("""
|
||
UPDATE applications
|
||
SET status = ?, response_message = ?, response_at = CURRENT_TIMESTAMP
|
||
WHERE id = ?
|
||
""", (status_update.status, status_update.response_message, application_id))
|
||
|
||
conn.commit()
|
||
|
||
return {"message": "Статус обновлен"}
|
||
|
||
|
||
@app.get("/api/applications/{application_id}")
|
||
async def get_application(
|
||
application_id: int,
|
||
user_id: int = Depends(get_current_user)
|
||
):
|
||
"""Получение детальной информации об отклике"""
|
||
try:
|
||
print(f"📄 Getting application details for ID: {application_id}")
|
||
|
||
with get_db() as conn:
|
||
cursor = conn.cursor()
|
||
|
||
# Сначала проверим, какие колонки есть в таблице
|
||
cursor.execute("PRAGMA table_info(applications)")
|
||
columns = [col[1] for col in cursor.fetchall()]
|
||
print(f"📊 Доступные колонки: {columns}")
|
||
|
||
# Динамически строим запрос в зависимости от наличия колонок
|
||
select_fields = """
|
||
a.*,
|
||
v.title as vacancy_title,
|
||
v.description as vacancy_description,
|
||
v.salary as vacancy_salary,
|
||
v.contact as vacancy_contact,
|
||
v.user_id as employer_id,
|
||
u_applicant.full_name as applicant_name,
|
||
u_applicant.email as applicant_email,
|
||
u_applicant.phone as applicant_phone,
|
||
u_applicant.telegram as applicant_telegram,
|
||
u_employer.full_name as employer_name,
|
||
u_employer.email as employer_email,
|
||
u_employer.telegram as employer_telegram,
|
||
c.name as company_name,
|
||
c.description as company_description
|
||
"""
|
||
|
||
cursor.execute(f"""
|
||
SELECT
|
||
{select_fields}
|
||
FROM applications a
|
||
JOIN vacancies v ON a.vacancy_id = v.id
|
||
JOIN users u_applicant ON a.user_id = u_applicant.id
|
||
JOIN users u_employer ON v.user_id = u_employer.id
|
||
LEFT JOIN companies c ON u_employer.id = c.user_id
|
||
WHERE a.id = ?
|
||
""", (application_id,))
|
||
|
||
application = cursor.fetchone()
|
||
if not application:
|
||
raise HTTPException(status_code=404, detail="Application not found")
|
||
|
||
app_dict = dict(application)
|
||
|
||
# Проверка прав доступа
|
||
if app_dict["user_id"] != user_id and app_dict["employer_id"] != user_id:
|
||
raise HTTPException(status_code=403, detail="Нет доступа к этому отклику")
|
||
|
||
# Если просматривает работодатель и статус 'pending', меняем на 'viewed'
|
||
# Проверяем, существует ли колонка viewed_at
|
||
if 'viewed_at' in columns and app_dict["employer_id"] == user_id and app_dict["status"] == 'pending':
|
||
cursor.execute("""
|
||
UPDATE applications
|
||
SET status = 'viewed', viewed_at = CURRENT_TIMESTAMP
|
||
WHERE id = ?
|
||
""", (application_id,))
|
||
conn.commit()
|
||
app_dict["status"] = 'viewed'
|
||
|
||
return app_dict
|
||
|
||
except HTTPException:
|
||
raise
|
||
except Exception as e:
|
||
print(f"❌ Error in get_application: {e}")
|
||
import traceback
|
||
traceback.print_exc()
|
||
raise HTTPException(status_code=500, detail=f"Internal server error: {str(e)}")
|
||
|
||
|
||
# ========== АДМИНСКИЕ ЭНДПОИНТЫ ==========
|
||
|
||
@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)
|
||
|
||
|
||
@app.get("/api/health")
|
||
async def health_check():
|
||
"""Проверка работоспособности API"""
|
||
return {
|
||
"status": "ok",
|
||
"time": datetime.now().isoformat(),
|
||
"server": "Rabota.Today API"
|
||
}
|
||
|
||
|
||
# Улучшенная настройка CORS для мобильных устройств
|
||
# Настройка CORS - максимально разрешительная для мобильных
|
||
app.add_middleware(
|
||
CORSMiddleware,
|
||
allow_origins=["*"], # Разрешить всем
|
||
allow_credentials=True,
|
||
allow_methods=["*"],
|
||
allow_headers=["*"],
|
||
)
|
||
|
||
app.add_middleware(
|
||
TrustedHostMiddleware,
|
||
allowed_hosts=["yarmarka.rabota.today", "localhost", "127.0.0.1"]
|
||
)
|
||
|
||
|
||
# Добавьте middleware для обработки мобильных User-Agent
|
||
@app.middleware("http")
|
||
async def detect_mobile(request: Request, call_next):
|
||
"""Определение мобильных устройств и добавление заголовков"""
|
||
user_agent = request.headers.get("user-agent", "").lower()
|
||
|
||
# Определяем мобильное устройство
|
||
is_mobile = any([
|
||
"mobile" in user_agent,
|
||
"android" in user_agent,
|
||
"iphone" in user_agent,
|
||
"ipad" in user_agent,
|
||
"windows phone" in user_agent
|
||
])
|
||
|
||
# Добавляем информацию о мобильности в request.state
|
||
request.state.is_mobile = is_mobile
|
||
|
||
response = await call_next(request)
|
||
|
||
# Добавляем заголовки для мобильных устройств
|
||
if is_mobile:
|
||
response.headers["X-Device-Type"] = "mobile"
|
||
response.headers["Cache-Control"] = "no-cache, no-store, must-revalidate"
|
||
|
||
return response
|
||
|
||
|
||
# Инициализация базы данных
|
||
init_db()
|
||
set_initial_sequence_ids()
|
||
|
||
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) |