Files
yarmarka/server.py
2026-03-12 19:23:54 +03:00

1169 lines
41 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# main.py (полная версия с тегами и админкой)
from fastapi import FastAPI, HTTPException, Depends, status, Request, Form
from fastapi.middleware.cors import CORSMiddleware
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
from fastapi.responses import HTMLResponse, FileResponse, RedirectResponse
from fastapi.staticfiles import StaticFiles
from pydantic import BaseModel, EmailStr
from typing import Optional, List
from datetime import datetime, timedelta
import jwt
import sqlite3
import hashlib
import os
from contextlib import contextmanager
import uvicorn
from pathlib import Path
app = FastAPI(title="Rabota.Today API", version="2.0.0")
# Настройки
SECRET_KEY = "your-secret-key-here-change-in-production"
ALGORITHM = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES = 60 * 24 * 7 # 7 дней
# CORS
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
security = HTTPBearer()
# Создаем директории
BASE_DIR = Path(__file__).parent
TEMPLATES_DIR = BASE_DIR / "templates"
STATIC_DIR = BASE_DIR / "static"
TEMPLATES_DIR.mkdir(exist_ok=True)
STATIC_DIR.mkdir(exist_ok=True)
(STATIC_DIR / "css").mkdir(exist_ok=True)
(STATIC_DIR / "js").mkdir(exist_ok=True)
# Подключаем статические файлы
app.mount("/static", StaticFiles(directory=str(STATIC_DIR)), name="static")
# ========== МОДЕЛИ БАЗЫ ДАННЫХ ==========
def init_db():
"""Инициализация базы данных"""
with get_db() as conn:
cursor = conn.cursor()
# Таблица пользователей
cursor.execute("""
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
full_name TEXT NOT NULL,
email TEXT UNIQUE NOT NULL,
phone TEXT NOT NULL,
telegram TEXT,
password_hash TEXT NOT NULL,
role TEXT NOT NULL CHECK(role IN ('employee', 'employer', 'admin')),
is_admin BOOLEAN DEFAULT 0,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
""")
# Таблица вакансий
cursor.execute("""
CREATE TABLE IF NOT EXISTS vacancies (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL,
title TEXT NOT NULL,
salary TEXT,
description TEXT,
contact TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
is_active BOOLEAN DEFAULT 1,
views INTEGER DEFAULT 0,
FOREIGN KEY (user_id) REFERENCES users (id)
)
""")
# Таблица резюме
cursor.execute("""
CREATE TABLE IF NOT EXISTS resumes (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER UNIQUE NOT NULL,
desired_position TEXT,
about_me TEXT,
desired_salary TEXT,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
views INTEGER DEFAULT 0,
FOREIGN KEY (user_id) REFERENCES users (id)
)
""")
# Таблица тегов
cursor.execute("""
CREATE TABLE IF NOT EXISTS tags (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT UNIQUE NOT NULL,
category TEXT CHECK(category IN ('skill', 'industry', 'position', 'other')) DEFAULT 'skill',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
""")
# Связь тегов с вакансиями
cursor.execute("""
CREATE TABLE IF NOT EXISTS vacancy_tags (
vacancy_id INTEGER NOT NULL,
tag_id INTEGER NOT NULL,
PRIMARY KEY (vacancy_id, tag_id),
FOREIGN KEY (vacancy_id) REFERENCES vacancies (id) ON DELETE CASCADE,
FOREIGN KEY (tag_id) REFERENCES tags (id) ON DELETE CASCADE
)
""")
# Связь тегов с резюме
cursor.execute("""
CREATE TABLE IF NOT EXISTS resume_tags (
resume_id INTEGER NOT NULL,
tag_id INTEGER NOT NULL,
PRIMARY KEY (resume_id, tag_id),
FOREIGN KEY (resume_id) REFERENCES resumes (id) ON DELETE CASCADE,
FOREIGN KEY (tag_id) REFERENCES tags (id) ON DELETE CASCADE
)
""")
# Таблица опыта работы
cursor.execute("""
CREATE TABLE IF NOT EXISTS work_experience (
id INTEGER PRIMARY KEY AUTOINCREMENT,
resume_id INTEGER NOT NULL,
position TEXT NOT NULL,
company TEXT NOT NULL,
period TEXT,
FOREIGN KEY (resume_id) REFERENCES resumes (id) ON DELETE CASCADE
)
""")
# Таблица образования
cursor.execute("""
CREATE TABLE IF NOT EXISTS education (
id INTEGER PRIMARY KEY AUTOINCREMENT,
resume_id INTEGER NOT NULL,
institution TEXT NOT NULL,
specialty TEXT,
graduation_year TEXT,
FOREIGN KEY (resume_id) REFERENCES resumes (id) ON DELETE CASCADE
)
""")
# Таблица откликов
cursor.execute("""
CREATE TABLE IF NOT EXISTS applications (
id INTEGER PRIMARY KEY AUTOINCREMENT,
vacancy_id INTEGER NOT NULL,
user_id INTEGER NOT NULL,
message TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
status TEXT DEFAULT 'pending',
FOREIGN KEY (vacancy_id) REFERENCES vacancies (id),
FOREIGN KEY (user_id) REFERENCES users (id)
)
""")
# Создаем админа по умолчанию
admin_password = hash_password("admin123")
cursor.execute("""
INSERT OR IGNORE INTO users (full_name, email, phone, password_hash, role, is_admin)
VALUES ('Admin', 'admin@rabota.today', '+70000000000', ?, 'admin', 1)
""", (admin_password,))
# Добавляем популярные теги
default_tags = [
('Python', 'skill'),
('JavaScript', 'skill'),
('React', 'skill'),
('Vue.js', 'skill'),
('Node.js', 'skill'),
('Java', 'skill'),
('C++', 'skill'),
('PHP', 'skill'),
('HTML/CSS', 'skill'),
('SQL', 'skill'),
('Менеджер', 'position'),
('Разработчик', 'position'),
('Дизайнер', 'position'),
('Маркетолог', 'position'),
('Аналитик', 'position'),
('IT', 'industry'),
('Финансы', 'industry'),
('Медицина', 'industry'),
('Образование', 'industry'),
('Продажи', 'industry'),
]
for tag_name, category in default_tags:
cursor.execute("INSERT OR IGNORE INTO tags (name, category) VALUES (?, ?)", (tag_name, category))
conn.commit()
print("✅ База данных инициализирована")
@contextmanager
def get_db():
"""Контекстный менеджер для работы с БД"""
conn = sqlite3.connect("rabota_today.db")
conn.row_factory = sqlite3.Row
try:
yield conn
finally:
conn.close()
# ========== МОДЕЛИ PYDANTIC ==========
class UserRegister(BaseModel):
full_name: str
email: EmailStr
phone: str
telegram: Optional[str] = None
password: str
role: str
class UserLogin(BaseModel):
email: EmailStr
password: str
class TokenResponse(BaseModel):
access_token: str
token_type: str
user_id: int
full_name: str
role: str
is_admin: bool
class Tag(BaseModel):
id: Optional[int] = None
name: str
category: str = "skill"
class VacancyCreate(BaseModel):
title: str
salary: Optional[str] = None
description: Optional[str] = None
contact: Optional[str] = None
tags: List[str] = [] # список названий тегов
class VacancyResponse(BaseModel):
id: int
title: str
salary: Optional[str]
description: Optional[str]
contact: Optional[str]
created_at: str
is_active: bool
views: int
tags: List[Tag] = []
company_name: Optional[str] = None
class WorkExperience(BaseModel):
position: str
company: str
period: Optional[str] = None
class Education(BaseModel):
institution: str
specialty: Optional[str] = None
graduation_year: Optional[str] = None
class ResumeCreate(BaseModel):
desired_position: Optional[str] = None
about_me: Optional[str] = None
desired_salary: Optional[str] = None
work_experience: List[WorkExperience] = []
education: List[Education] = []
tags: List[str] = [] # список названий тегов
class ResumeResponse(BaseModel):
id: int
user_id: int
desired_position: Optional[str]
about_me: Optional[str]
desired_salary: Optional[str]
work_experience: List[WorkExperience]
education: List[Education]
tags: List[Tag] = []
views: int
updated_at: str
full_name: Optional[str] = None
# ========== ВСПОМОГАТЕЛЬНЫЕ ФУНКЦИИ ==========
def hash_password(password: str) -> str:
"""Хеширование пароля"""
salt = "rabota.today.salt"
return hashlib.sha256((password + salt).encode()).hexdigest()
def verify_password(password: str, hash: str) -> bool:
"""Проверка пароля"""
return hash_password(password) == hash
def create_access_token(data: dict):
"""Создание JWT токена"""
to_encode = data.copy()
expire = datetime.utcnow() + timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
to_encode.update({"exp": expire})
return jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
def get_current_user(credentials: HTTPAuthorizationCredentials = Depends(security)):
"""Получение текущего пользователя из токена"""
token = credentials.credentials
try:
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
user_id = payload.get("sub")
if user_id is None:
raise HTTPException(status_code=401, detail="Невалидный токен")
return int(user_id)
except jwt.PyJWTError:
raise HTTPException(status_code=401, detail="Невалидный токен")
def get_current_user_optional(token: Optional[str] = None):
"""Опциональное получение пользователя (для публичных страниц)"""
if not token:
return None
try:
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
return int(payload.get("sub"))
except:
return None
def get_tags_for_vacancy(cursor, vacancy_id):
"""Получение тегов для вакансии"""
cursor.execute("""
SELECT t.* FROM tags t
JOIN vacancy_tags vt ON t.id = vt.tag_id
WHERE vt.vacancy_id = ?
""", (vacancy_id,))
return [dict(tag) for tag in cursor.fetchall()]
def get_tags_for_resume(cursor, resume_id):
"""Получение тегов для резюме"""
cursor.execute("""
SELECT t.* FROM tags t
JOIN resume_tags rt ON t.id = rt.tag_id
WHERE rt.resume_id = ?
""", (resume_id,))
return [dict(tag) for tag in cursor.fetchall()]
def add_tags_to_vacancy(cursor, vacancy_id, tag_names):
"""Добавление тегов к вакансии"""
for tag_name in tag_names:
# Ищем или создаем тег
cursor.execute("SELECT id FROM tags WHERE name = ?", (tag_name,))
tag = cursor.fetchone()
if tag:
tag_id = tag["id"]
else:
cursor.execute("INSERT INTO tags (name) VALUES (?)", (tag_name,))
tag_id = cursor.lastrowid
# Связываем тег с вакансией
cursor.execute("""
INSERT OR IGNORE INTO vacancy_tags (vacancy_id, tag_id)
VALUES (?, ?)
""", (vacancy_id, tag_id))
def add_tags_to_resume(cursor, resume_id, tag_names):
"""Добавление тегов к резюме"""
for tag_name in tag_names:
cursor.execute("SELECT id FROM tags WHERE name = ?", (tag_name,))
tag = cursor.fetchone()
if tag:
tag_id = tag["id"]
else:
cursor.execute("INSERT INTO tags (name) VALUES (?)", (tag_name,))
tag_id = cursor.lastrowid
cursor.execute("""
INSERT OR IGNORE INTO resume_tags (resume_id, tag_id)
VALUES (?, ?)
""", (resume_id, tag_id))
# ========== HTML СТРАНИЦЫ ==========
@app.get("/", response_class=HTMLResponse)
async def get_index():
"""Главная страница"""
file_path = TEMPLATES_DIR / "index.html"
if not file_path.exists():
return HTMLResponse(
content="<h1>Rabota.Today API</h1><p>Сервер работает. Создайте файл templates/index.html</p>")
return FileResponse(file_path)
@app.get("/login", response_class=HTMLResponse)
async def get_login():
"""Страница входа"""
file_path = TEMPLATES_DIR / "login.html"
return FileResponse(file_path) if file_path.exists() else HTMLResponse(
content="<h1>Страница входа</h1><p>Создайте файл templates/login.html</p>")
@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_detail(vacancy_id: int):
"""Страница конкретной вакансии"""
file_path = TEMPLATES_DIR / "vacancy_detail.html"
return FileResponse(file_path) if file_path.exists() else HTMLResponse(
content=f"<h1>Вакансия #{vacancy_id}</h1><p>Создайте файл templates/vacancy_detail.html</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_detail(resume_id: int):
"""Страница конкретного резюме"""
file_path = TEMPLATES_DIR / "resume_detail.html"
return FileResponse(file_path) if file_path.exists() else HTMLResponse(
content=f"<h1>Резюме #{resume_id}</h1><p>Создайте файл templates/resume_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.post("/api/register", response_model=TokenResponse)
async def register(user: UserRegister):
"""Регистрация нового пользователя"""
with get_db() as conn:
cursor = conn.cursor()
cursor.execute("SELECT id FROM users WHERE email = ?", (user.email,))
if cursor.fetchone():
raise HTTPException(status_code=400, detail="Email уже зарегистрирован")
password_hash = hash_password(user.password)
is_admin = 1 if user.email == "admin@rabota.today" else 0
cursor.execute("""
INSERT INTO users (full_name, email, phone, telegram, password_hash, role, is_admin)
VALUES (?, ?, ?, ?, ?, ?, ?)
""", (user.full_name, user.email, user.phone, user.telegram, password_hash, user.role, is_admin))
user_id = cursor.lastrowid
conn.commit()
token = create_access_token({"sub": str(user_id), "role": user.role, "is_admin": is_admin})
return {
"access_token": token,
"token_type": "bearer",
"user_id": user_id,
"full_name": user.full_name,
"role": user.role,
"is_admin": bool(is_admin)
}
@app.post("/api/login", response_model=TokenResponse)
async def login(user_data: UserLogin):
"""Вход в систему"""
with get_db() as conn:
cursor = conn.cursor()
cursor.execute("SELECT * FROM users WHERE email = ?", (user_data.email,))
user = cursor.fetchone()
if not user or not verify_password(user_data.password, user["password_hash"]):
raise HTTPException(status_code=401, detail="Неверный email или пароль")
token = create_access_token({"sub": str(user["id"]), "role": user["role"], "is_admin": user["is_admin"]})
return {
"access_token": token,
"token_type": "bearer",
"user_id": user["id"],
"full_name": user["full_name"],
"role": user["role"],
"is_admin": bool(user["is_admin"])
}
@app.get("/api/tags")
async def get_all_tags(category: Optional[str] = None):
"""Получение всех тегов"""
with get_db() as conn:
cursor = conn.cursor()
if category:
cursor.execute("SELECT * FROM tags WHERE category = ? ORDER BY name", (category,))
else:
cursor.execute("SELECT * FROM tags ORDER BY category, name")
tags = cursor.fetchall()
return [dict(tag) for tag in tags]
@app.post("/api/tags")
async def create_tag(tag: Tag, user_id: int = Depends(get_current_user)):
"""Создание нового тега (только для админа)"""
with get_db() as conn:
cursor = conn.cursor()
# Проверяем, админ ли пользователь
cursor.execute("SELECT is_admin FROM users WHERE id = ?", (user_id,))
user = cursor.fetchone()
if not user or not user["is_admin"]:
raise HTTPException(status_code=403, detail="Только администратор может создавать теги")
try:
cursor.execute("INSERT INTO tags (name, category) VALUES (?, ?)", (tag.name, tag.category))
conn.commit()
tag_id = cursor.lastrowid
cursor.execute("SELECT * FROM tags WHERE id = ?", (tag_id,))
return dict(cursor.fetchone())
except sqlite3.IntegrityError:
raise HTTPException(status_code=400, detail="Тег с таким названием уже существует")
# ========== ЭНДПОИНТЫ ДЛЯ ВАКАНСИЙ ==========
@app.post("/api/vacancies", response_model=VacancyResponse)
async def create_vacancy(
vacancy: VacancyCreate,
user_id: int = Depends(get_current_user)
):
"""Создание новой вакансии"""
with get_db() as conn:
cursor = conn.cursor()
cursor.execute("SELECT role FROM users WHERE id = ?", (user_id,))
user = cursor.fetchone()
if not user or user["role"] != "employer":
raise HTTPException(status_code=403, detail="Только работодатели могут создавать вакансии")
cursor.execute("""
INSERT INTO vacancies (user_id, title, salary, description, contact)
VALUES (?, ?, ?, ?, ?)
""", (user_id, vacancy.title, vacancy.salary, vacancy.description, vacancy.contact))
vacancy_id = cursor.lastrowid
# Добавляем теги
if vacancy.tags:
add_tags_to_vacancy(cursor, vacancy_id, vacancy.tags)
conn.commit()
cursor.execute("SELECT * FROM vacancies WHERE id = ?", (vacancy_id,))
new_vacancy = dict(cursor.fetchone())
new_vacancy["tags"] = get_tags_for_vacancy(cursor, vacancy_id)
return new_vacancy
@app.get("/api/vacancies", response_model=List[VacancyResponse])
async def get_my_vacancies(user_id: int = Depends(get_current_user)):
"""Получение всех вакансий текущего пользователя"""
with get_db() as conn:
cursor = conn.cursor()
cursor.execute("""
SELECT v.*, u.full_name as company_name
FROM vacancies v
JOIN users u ON v.user_id = u.id
WHERE v.user_id = ? AND v.is_active = 1
ORDER BY v.created_at DESC
""", (user_id,))
vacancies = cursor.fetchall()
result = []
for v in vacancies:
vacancy_dict = dict(v)
vacancy_dict["tags"] = get_tags_for_vacancy(cursor, v["id"])
result.append(vacancy_dict)
return result
@app.get("/api/vacancies/all")
async def get_all_vacancies(
page: int = 1,
search: str = None,
tags: str = None, # теги через запятую
min_salary: int = None,
sort: str = "newest"
):
"""Получение всех вакансий с фильтрацией по тегам"""
with get_db() as conn:
cursor = conn.cursor()
query = """
SELECT DISTINCT v.*, u.full_name as company_name
FROM vacancies v
JOIN users u ON v.user_id = u.id
WHERE v.is_active = 1
"""
params = []
if search:
query += " AND (v.title LIKE ? OR v.description LIKE ?)"
params.extend([f"%{search}%", f"%{search}%"])
if tags:
tag_list = [t.strip() for t in tags.split(',')]
placeholders = ','.join(['?'] * len(tag_list))
query += f"""
AND v.id IN (
SELECT vt.vacancy_id
FROM vacancy_tags vt
JOIN tags t ON vt.tag_id = t.id
WHERE t.name IN ({placeholders})
GROUP BY vt.vacancy_id
HAVING COUNT(DISTINCT t.name) = ?
)
"""
params.extend(tag_list + [len(tag_list)])
if min_salary:
query += " AND v.salary LIKE ?"
params.append(f"%{min_salary}%")
if sort == "salary_desc":
query += " ORDER BY v.salary DESC"
elif sort == "salary_asc":
query += " ORDER BY v.salary ASC"
else:
query += " ORDER BY v.created_at DESC"
# Пагинация
limit = 10
offset = (page - 1) * limit
query += " LIMIT ? OFFSET ?"
params.extend([limit, offset])
cursor.execute(query, params)
vacancies = cursor.fetchall()
result = []
for v in vacancies:
vacancy_dict = dict(v)
vacancy_dict["tags"] = get_tags_for_vacancy(cursor, v["id"])
result.append(vacancy_dict)
# Получаем общее количество
cursor.execute("SELECT COUNT(*) FROM vacancies WHERE is_active = 1")
total = cursor.fetchone()[0]
return {
"vacancies": result,
"total_pages": (total + limit - 1) // limit,
"current_page": page
}
@app.get("/api/vacancies/{vacancy_id}")
async def get_vacancy(vacancy_id: int, token: Optional[str] = None):
"""Получение конкретной вакансии"""
user_id = get_current_user_optional(token)
with get_db() as conn:
cursor = conn.cursor()
# Увеличиваем счетчик просмотров
cursor.execute("UPDATE vacancies SET views = views + 1 WHERE id = ?", (vacancy_id,))
cursor.execute("""
SELECT v.*, u.full_name as company_name, u.email, u.telegram
FROM vacancies v
JOIN users u ON v.user_id = u.id
WHERE v.id = ?
""", (vacancy_id,))
vacancy = cursor.fetchone()
if not vacancy:
raise HTTPException(status_code=404, detail="Вакансия не найдена")
result = dict(vacancy)
result["tags"] = get_tags_for_vacancy(cursor, vacancy_id)
# Проверяем, откликался ли пользователь
if user_id:
cursor.execute("""
SELECT id FROM applications
WHERE vacancy_id = ? AND user_id = ?
""", (vacancy_id, user_id))
result["has_applied"] = cursor.fetchone() is not None
conn.commit()
return result
@app.post("/api/vacancies/{vacancy_id}/apply")
async def apply_to_vacancy(
vacancy_id: int,
user_id: int = Depends(get_current_user)
):
"""Отклик на вакансию"""
with get_db() as conn:
cursor = conn.cursor()
cursor.execute("SELECT id FROM vacancies WHERE id = ?", (vacancy_id,))
if not cursor.fetchone():
raise HTTPException(status_code=404, detail="Вакансия не найдена")
cursor.execute("""
SELECT id FROM applications
WHERE vacancy_id = ? AND user_id = ?
""", (vacancy_id, user_id))
if cursor.fetchone():
raise HTTPException(status_code=400, detail="Вы уже откликались на эту вакансию")
cursor.execute("""
INSERT INTO applications (vacancy_id, user_id)
VALUES (?, ?)
""", (vacancy_id, user_id))
conn.commit()
return {"message": "Отклик отправлен"}
# ========== ЭНДПОИНТЫ ДЛЯ РЕЗЮМЕ ==========
@app.post("/api/resume", response_model=ResumeResponse)
async def create_or_update_resume(
resume: ResumeCreate,
user_id: int = Depends(get_current_user)
):
"""Создание или обновление резюме"""
with get_db() as conn:
cursor = conn.cursor()
cursor.execute("SELECT role FROM users WHERE id = ?", (user_id,))
user = cursor.fetchone()
if not user or user["role"] != "employee":
raise HTTPException(status_code=403, detail="Только соискатели могут создавать резюме")
cursor.execute("SELECT id FROM resumes WHERE user_id = ?", (user_id,))
existing = cursor.fetchone()
if existing:
cursor.execute("""
UPDATE resumes
SET desired_position = ?, about_me = ?, desired_salary = ?, updated_at = CURRENT_TIMESTAMP
WHERE user_id = ?
""", (resume.desired_position, resume.about_me, resume.desired_salary, user_id))
resume_id = existing["id"]
cursor.execute("DELETE FROM work_experience WHERE resume_id = ?", (resume_id,))
cursor.execute("DELETE FROM education WHERE resume_id = ?", (resume_id,))
cursor.execute("DELETE FROM resume_tags WHERE resume_id = ?", (resume_id,))
else:
cursor.execute("""
INSERT INTO resumes (user_id, desired_position, about_me, desired_salary)
VALUES (?, ?, ?, ?)
""", (user_id, resume.desired_position, resume.about_me, resume.desired_salary))
resume_id = cursor.lastrowid
for exp in resume.work_experience:
cursor.execute("""
INSERT INTO work_experience (resume_id, position, company, period)
VALUES (?, ?, ?, ?)
""", (resume_id, exp.position, exp.company, exp.period))
for edu in resume.education:
cursor.execute("""
INSERT INTO education (resume_id, institution, specialty, graduation_year)
VALUES (?, ?, ?, ?)
""", (resume_id, edu.institution, edu.specialty, edu.graduation_year))
# Добавляем теги
if resume.tags:
add_tags_to_resume(cursor, resume_id, resume.tags)
conn.commit()
cursor.execute("SELECT * FROM resumes WHERE id = ?", (resume_id,))
resume_data = dict(cursor.fetchone())
cursor.execute("SELECT position, company, period FROM work_experience WHERE resume_id = ?", (resume_id,))
resume_data["work_experience"] = [dict(exp) for exp in cursor.fetchall()]
cursor.execute("SELECT institution, specialty, graduation_year FROM education WHERE resume_id = ?",
(resume_id,))
resume_data["education"] = [dict(edu) for edu in cursor.fetchall()]
resume_data["tags"] = get_tags_for_resume(cursor, resume_id)
return resume_data
@app.get("/api/resume", response_model=ResumeResponse)
async def get_my_resume(user_id: int = Depends(get_current_user)):
"""Получение резюме текущего пользователя"""
with get_db() as conn:
cursor = conn.cursor()
cursor.execute("SELECT * FROM resumes WHERE user_id = ?", (user_id,))
resume = cursor.fetchone()
if not resume:
raise HTTPException(status_code=404, detail="Резюме не найдено")
resume_data = dict(resume)
cursor.execute("SELECT position, company, period FROM work_experience WHERE resume_id = ?", (resume["id"],))
resume_data["work_experience"] = [dict(exp) for exp in cursor.fetchall()]
cursor.execute("SELECT institution, specialty, graduation_year FROM education WHERE resume_id = ?",
(resume["id"],))
resume_data["education"] = [dict(edu) for edu in cursor.fetchall()]
resume_data["tags"] = get_tags_for_resume(cursor, resume["id"])
return resume_data
@app.get("/api/resumes/all")
async def get_all_resumes(
page: int = 1,
search: str = None,
tags: str = None,
min_salary: int = None
):
"""Получение всех резюме с фильтрацией по тегам"""
with get_db() as conn:
cursor = conn.cursor()
query = """
SELECT DISTINCT r.*, u.full_name
FROM resumes r
JOIN users u ON r.user_id = u.id
WHERE 1=1
"""
params = []
if search:
query += " AND (r.desired_position LIKE ? OR r.about_me LIKE ?)"
params.extend([f"%{search}%", f"%{search}%"])
if tags:
tag_list = [t.strip() for t in tags.split(',')]
placeholders = ','.join(['?'] * len(tag_list))
query += f"""
AND r.id IN (
SELECT rt.resume_id
FROM resume_tags rt
JOIN tags t ON rt.tag_id = t.id
WHERE t.name IN ({placeholders})
GROUP BY rt.resume_id
HAVING COUNT(DISTINCT t.name) = ?
)
"""
params.extend(tag_list + [len(tag_list)])
if min_salary:
query += " AND r.desired_salary LIKE ?"
params.append(f"%{min_salary}%")
# Пагинация
limit = 10
offset = (page - 1) * limit
query += " ORDER BY r.updated_at DESC LIMIT ? OFFSET ?"
params.extend([limit, offset])
cursor.execute(query, params)
resumes = cursor.fetchall()
result = []
for r in resumes:
resume_dict = dict(r)
resume_dict["tags"] = get_tags_for_resume(cursor, r["id"])
cursor.execute("SELECT COUNT(*) FROM work_experience WHERE resume_id = ?", (r["id"],))
resume_dict["experience_count"] = cursor.fetchone()[0]
result.append(resume_dict)
cursor.execute("SELECT COUNT(*) FROM resumes")
total = cursor.fetchone()[0]
return {
"resumes": result,
"total_pages": (total + limit - 1) // limit,
"current_page": page
}
@app.get("/api/resumes/{resume_id}")
async def get_resume(resume_id: int):
"""Получение конкретного резюме"""
with get_db() as conn:
cursor = conn.cursor()
cursor.execute("""
SELECT r.*, u.full_name, u.email, u.phone, u.telegram
FROM resumes r
JOIN users u ON r.user_id = u.id
WHERE r.id = ?
""", (resume_id,))
resume = cursor.fetchone()
if not resume:
raise HTTPException(status_code=404, detail="Резюме не найдено")
# Увеличиваем счетчик просмотров
cursor.execute("UPDATE resumes SET views = views + 1 WHERE id = ?", (resume_id,))
resume_data = dict(resume)
cursor.execute("SELECT position, company, period FROM work_experience WHERE resume_id = ?", (resume_id,))
resume_data["work_experience"] = [dict(exp) for exp in cursor.fetchall()]
cursor.execute("SELECT institution, specialty, graduation_year FROM education WHERE resume_id = ?",
(resume_id,))
resume_data["education"] = [dict(edu) for edu in cursor.fetchall()]
resume_data["tags"] = get_tags_for_resume(cursor, resume_id)
conn.commit()
return resume_data
# ========== АДМИНСКИЕ ЭНДПОИНТЫ ==========
@app.get("/api/admin/stats")
async def get_admin_stats(user_id: int = Depends(get_current_user)):
"""Получение статистики для админки"""
with get_db() as conn:
cursor = conn.cursor()
# Проверяем, админ ли пользователь
cursor.execute("SELECT is_admin FROM users WHERE id = ?", (user_id,))
user = cursor.fetchone()
if not user or not user["is_admin"]:
raise HTTPException(status_code=403, detail="Доступ запрещен")
stats = {}
cursor.execute("SELECT COUNT(*) FROM users")
stats["total_users"] = cursor.fetchone()[0]
cursor.execute("SELECT COUNT(*) FROM users WHERE role = 'employee'")
stats["total_employees"] = cursor.fetchone()[0]
cursor.execute("SELECT COUNT(*) FROM users WHERE role = 'employer'")
stats["total_employers"] = cursor.fetchone()[0]
cursor.execute("SELECT COUNT(*) FROM vacancies WHERE is_active = 1")
stats["active_vacancies"] = cursor.fetchone()[0]
cursor.execute("SELECT COUNT(*) FROM resumes")
stats["total_resumes"] = cursor.fetchone()[0]
cursor.execute("SELECT COUNT(*) FROM applications")
stats["total_applications"] = cursor.fetchone()[0]
cursor.execute("SELECT COUNT(*) FROM tags")
stats["total_tags"] = cursor.fetchone()[0]
# Последние регистрации
cursor.execute("""
SELECT id, full_name, email, role, created_at
FROM users
ORDER BY created_at DESC
LIMIT 10
""")
stats["recent_users"] = [dict(u) for u in cursor.fetchall()]
# Популярные теги
cursor.execute("""
SELECT t.name, COUNT(vt.vacancy_id) as vacancy_count, COUNT(rt.resume_id) as resume_count
FROM tags t
LEFT JOIN vacancy_tags vt ON t.id = vt.tag_id
LEFT JOIN resume_tags rt ON t.id = rt.tag_id
GROUP BY t.id
ORDER BY (COUNT(vt.vacancy_id) + COUNT(rt.resume_id)) DESC
LIMIT 20
""")
stats["popular_tags"] = [dict(t) for t in cursor.fetchall()]
return stats
@app.get("/api/admin/users")
async def get_all_users(user_id: int = Depends(get_current_user)):
"""Получение всех пользователей (для админа)"""
with get_db() as conn:
cursor = conn.cursor()
cursor.execute("SELECT is_admin FROM users WHERE id = ?", (user_id,))
user = cursor.fetchone()
if not user or not user["is_admin"]:
raise HTTPException(status_code=403, detail="Доступ запрещен")
cursor.execute("""
SELECT id, full_name, email, phone, telegram, role, is_admin, created_at
FROM users
ORDER BY created_at DESC
""")
users = cursor.fetchall()
return [dict(u) for u in users]
@app.delete("/api/admin/users/{target_user_id}")
async def delete_user(target_user_id: int, user_id: int = Depends(get_current_user)):
"""Удаление пользователя (для админа)"""
with get_db() as conn:
cursor = conn.cursor()
cursor.execute("SELECT is_admin FROM users WHERE id = ?", (user_id,))
user = cursor.fetchone()
if not user or not user["is_admin"]:
raise HTTPException(status_code=403, detail="Доступ запрещен")
cursor.execute("DELETE FROM users WHERE id = ?", (target_user_id,))
conn.commit()
return {"message": "Пользователь удален"}
@app.get("/api/admin/vacancies")
async def get_all_vacancies_admin(user_id: int = Depends(get_current_user)):
"""Получение всех вакансий (для админа)"""
with get_db() as conn:
cursor = conn.cursor()
cursor.execute("SELECT is_admin FROM users WHERE id = ?", (user_id,))
user = cursor.fetchone()
if not user or not user["is_admin"]:
raise HTTPException(status_code=403, detail="Доступ запрещен")
cursor.execute("""
SELECT v.*, u.full_name as company_name
FROM vacancies v
JOIN users u ON v.user_id = u.id
ORDER BY v.created_at DESC
""")
vacancies = cursor.fetchall()
result = []
for v in vacancies:
vacancy_dict = dict(v)
vacancy_dict["tags"] = get_tags_for_vacancy(cursor, v["id"])
result.append(vacancy_dict)
return result
@app.delete("/api/admin/vacancies/{vacancy_id}")
async def delete_vacancy_admin(vacancy_id: int, user_id: int = Depends(get_current_user)):
"""Удаление вакансии (для админа)"""
with get_db() as conn:
cursor = conn.cursor()
cursor.execute("SELECT is_admin FROM users WHERE id = ?", (user_id,))
user = cursor.fetchone()
if not user or not user["is_admin"]:
raise HTTPException(status_code=403, detail="Доступ запрещен")
cursor.execute("DELETE FROM vacancies WHERE id = ?", (vacancy_id,))
conn.commit()
return {"message": "Вакансия удалена"}
# ========== ПОЛЬЗОВАТЕЛЬСКИЕ ЭНДПОИНТЫ ==========
@app.get("/api/user")
async def get_user_info(user_id: int = Depends(get_current_user)):
"""Получение информации о текущем пользователе"""
with get_db() as conn:
cursor = conn.cursor()
cursor.execute("""
SELECT id, full_name, email, phone, telegram, role, is_admin, created_at
FROM users WHERE id = ?
""", (user_id,))
user = cursor.fetchone()
if not user:
raise HTTPException(status_code=404, detail="Пользователь не найден")
return dict(user)
@app.get("/api/users/{target_user_id}/contact")
async def get_user_contact(
target_user_id: int,
current_user: int = Depends(get_current_user)
):
"""Получение контактных данных пользователя"""
with get_db() as conn:
cursor = conn.cursor()
cursor.execute("SELECT email, phone, telegram FROM users WHERE id = ?", (target_user_id,))
user = cursor.fetchone()
if not user:
raise HTTPException(status_code=404, detail="Пользователь не найден")
return dict(user)
# Инициализация базы данных
init_db()
if __name__ == "__main__":
print("🚀 Запуск сервера на http://localhost:8000")
print("📚 Документация API: http://localhost:8000/docs")
print("👤 Админ: admin@rabota.today / admin123")
print("📁 HTML страницы должны быть в папке templates/")
uvicorn.run(app, host="0.0.0.0", port=8000)