- Add BASE_PATH config, include all routers with prefix
- Inject {{ base }} Jinja2 global for all template URLs
- Add window.BASE_PATH for static JS files
- Update Nginx to proxy /careerbot/ path
- Add OPS_MANUAL.md
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
630 lines
21 KiB
Python
630 lines
21 KiB
Python
import os
|
||
import uuid
|
||
from datetime import datetime
|
||
|
||
from fastapi import APIRouter, Depends, HTTPException, Request, UploadFile, File, Form
|
||
from fastapi.responses import HTMLResponse
|
||
from fastapi.templating import Jinja2Templates
|
||
from pydantic import BaseModel
|
||
from sqlalchemy.orm import Session
|
||
|
||
from app.config import settings
|
||
|
||
BASE = settings.BASE_PATH
|
||
|
||
from app.database import get_db
|
||
from app.models import (
|
||
Profile, Skill, Education, WorkExperience,
|
||
AccessToken, LLMConfig, RecruiterMessage,
|
||
)
|
||
from app.routers.auth import get_current_admin
|
||
|
||
router = APIRouter()
|
||
templates = Jinja2Templates(directory="templates")
|
||
|
||
|
||
# ── Admin Page Routes ──
|
||
|
||
@router.get("/admin/login", response_class=HTMLResponse)
|
||
def admin_login_page(request: Request):
|
||
return templates.TemplateResponse(request, "admin/login.html")
|
||
|
||
|
||
@router.get("/admin/dashboard", response_class=HTMLResponse)
|
||
def admin_dashboard(request: Request):
|
||
return templates.TemplateResponse(request, "admin/dashboard.html")
|
||
|
||
|
||
@router.get("/admin/{page}", response_class=HTMLResponse)
|
||
def admin_page(request: Request, page: str):
|
||
valid_pages = ["profile", "education", "experience", "skills", "tokens", "llm-config", "resume", "messages"]
|
||
if page not in valid_pages:
|
||
raise HTTPException(status_code=404)
|
||
template_name = f"admin/{page}.html"
|
||
return templates.TemplateResponse(request, template_name)
|
||
|
||
|
||
# ── Profile API ──
|
||
|
||
class ProfileUpdate(BaseModel):
|
||
name: str = ""
|
||
phone: str = ""
|
||
location: str = ""
|
||
birthday: str = ""
|
||
party: str = ""
|
||
education_level: str = ""
|
||
email: str = ""
|
||
self_summary: str = ""
|
||
|
||
|
||
@router.get("/api/admin/profile")
|
||
def get_profile(db: Session = Depends(get_db), _=Depends(get_current_admin)):
|
||
p = db.query(Profile).first()
|
||
if not p:
|
||
return {}
|
||
return {
|
||
"id": p.id, "name": p.name, "phone": p.phone, "location": p.location,
|
||
"birthday": p.birthday, "party": p.party, "education_level": p.education_level,
|
||
"email": p.email, "photo_url": p.photo_url, "self_summary": p.self_summary,
|
||
}
|
||
|
||
|
||
@router.put("/api/admin/profile")
|
||
def update_profile(data: ProfileUpdate, db: Session = Depends(get_db), _=Depends(get_current_admin)):
|
||
p = db.query(Profile).first()
|
||
if not p:
|
||
p = Profile()
|
||
db.add(p)
|
||
for key, val in data.model_dump().items():
|
||
setattr(p, key, val)
|
||
db.commit()
|
||
db.refresh(p)
|
||
return {"message": "ok", "id": p.id}
|
||
|
||
|
||
@router.post("/api/admin/profile/photo")
|
||
async def upload_photo(
|
||
file: UploadFile = File(...),
|
||
db: Session = Depends(get_db),
|
||
_=Depends(get_current_admin),
|
||
):
|
||
ext = file.filename.rsplit(".", 1)[-1].lower() if file.filename and "." in file.filename else "jpg"
|
||
filename = f"photo_{uuid.uuid4().hex[:8]}.{ext}"
|
||
filepath = os.path.join(settings.UPLOAD_DIR, filename)
|
||
content = await file.read()
|
||
with open(filepath, "wb") as f:
|
||
f.write(content)
|
||
p = db.query(Profile).first()
|
||
if not p:
|
||
p = Profile()
|
||
db.add(p)
|
||
p.photo_url = f"{BASE}/uploads/{filename}"
|
||
db.commit()
|
||
return {"photo_url": p.photo_url}
|
||
|
||
|
||
# ── Education CRUD ──
|
||
|
||
class EducationData(BaseModel):
|
||
start_date: str = ""
|
||
end_date: str = ""
|
||
school: str = ""
|
||
major: str = ""
|
||
degree: str = ""
|
||
details: str = ""
|
||
sort_order: int = 0
|
||
|
||
|
||
@router.get("/api/admin/education")
|
||
def list_education(db: Session = Depends(get_db), _=Depends(get_current_admin)):
|
||
items = db.query(Education).order_by(Education.sort_order).all()
|
||
return [
|
||
{"id": e.id, "start_date": e.start_date, "end_date": e.end_date,
|
||
"school": e.school, "major": e.major, "degree": e.degree,
|
||
"details": e.details, "sort_order": e.sort_order}
|
||
for e in items
|
||
]
|
||
|
||
|
||
@router.post("/api/admin/education")
|
||
def create_education(data: EducationData, db: Session = Depends(get_db), _=Depends(get_current_admin)):
|
||
item = Education(**data.model_dump())
|
||
db.add(item)
|
||
db.commit()
|
||
db.refresh(item)
|
||
return {"id": item.id, "message": "ok"}
|
||
|
||
|
||
@router.put("/api/admin/education/{item_id}")
|
||
def update_education(item_id: int, data: EducationData, db: Session = Depends(get_db), _=Depends(get_current_admin)):
|
||
item = db.query(Education).filter(Education.id == item_id).first()
|
||
if not item:
|
||
raise HTTPException(status_code=404, detail="Not found")
|
||
for key, val in data.model_dump().items():
|
||
setattr(item, key, val)
|
||
db.commit()
|
||
return {"message": "ok"}
|
||
|
||
|
||
@router.delete("/api/admin/education/{item_id}")
|
||
def delete_education(item_id: int, db: Session = Depends(get_db), _=Depends(get_current_admin)):
|
||
item = db.query(Education).filter(Education.id == item_id).first()
|
||
if not item:
|
||
raise HTTPException(status_code=404, detail="Not found")
|
||
db.delete(item)
|
||
db.commit()
|
||
return {"message": "ok"}
|
||
|
||
|
||
# ── Work Experience CRUD ──
|
||
|
||
class ExperienceData(BaseModel):
|
||
start_date: str = ""
|
||
end_date: str = ""
|
||
company: str = ""
|
||
position: str = ""
|
||
company_intro: str = ""
|
||
responsibilities: str = ""
|
||
achievements: str = ""
|
||
sort_order: int = 0
|
||
|
||
|
||
@router.get("/api/admin/experience")
|
||
def list_experience(db: Session = Depends(get_db), _=Depends(get_current_admin)):
|
||
items = db.query(WorkExperience).order_by(WorkExperience.sort_order).all()
|
||
return [
|
||
{"id": e.id, "start_date": e.start_date, "end_date": e.end_date,
|
||
"company": e.company, "position": e.position, "company_intro": e.company_intro,
|
||
"responsibilities": e.responsibilities, "achievements": e.achievements,
|
||
"sort_order": e.sort_order}
|
||
for e in items
|
||
]
|
||
|
||
|
||
@router.post("/api/admin/experience")
|
||
def create_experience(data: ExperienceData, db: Session = Depends(get_db), _=Depends(get_current_admin)):
|
||
item = WorkExperience(**data.model_dump())
|
||
db.add(item)
|
||
db.commit()
|
||
db.refresh(item)
|
||
return {"id": item.id, "message": "ok"}
|
||
|
||
|
||
@router.put("/api/admin/experience/{item_id}")
|
||
def update_experience(item_id: int, data: ExperienceData, db: Session = Depends(get_db), _=Depends(get_current_admin)):
|
||
item = db.query(WorkExperience).filter(WorkExperience.id == item_id).first()
|
||
if not item:
|
||
raise HTTPException(status_code=404, detail="Not found")
|
||
for key, val in data.model_dump().items():
|
||
setattr(item, key, val)
|
||
db.commit()
|
||
return {"message": "ok"}
|
||
|
||
|
||
@router.delete("/api/admin/experience/{item_id}")
|
||
def delete_experience(item_id: int, db: Session = Depends(get_db), _=Depends(get_current_admin)):
|
||
item = db.query(WorkExperience).filter(WorkExperience.id == item_id).first()
|
||
if not item:
|
||
raise HTTPException(status_code=404, detail="Not found")
|
||
db.delete(item)
|
||
db.commit()
|
||
return {"message": "ok"}
|
||
|
||
|
||
# ── Skills CRUD ──
|
||
|
||
class SkillData(BaseModel):
|
||
category: str = ""
|
||
content: str = ""
|
||
sort_order: int = 0
|
||
|
||
|
||
@router.get("/api/admin/skills")
|
||
def list_skills(db: Session = Depends(get_db), _=Depends(get_current_admin)):
|
||
items = db.query(Skill).order_by(Skill.sort_order).all()
|
||
return [
|
||
{"id": s.id, "category": s.category, "content": s.content, "sort_order": s.sort_order}
|
||
for s in items
|
||
]
|
||
|
||
|
||
@router.post("/api/admin/skills")
|
||
def create_skill(data: SkillData, db: Session = Depends(get_db), _=Depends(get_current_admin)):
|
||
item = Skill(**data.model_dump())
|
||
db.add(item)
|
||
db.commit()
|
||
db.refresh(item)
|
||
return {"id": item.id, "message": "ok"}
|
||
|
||
|
||
@router.put("/api/admin/skills/{item_id}")
|
||
def update_skill(item_id: int, data: SkillData, db: Session = Depends(get_db), _=Depends(get_current_admin)):
|
||
item = db.query(Skill).filter(Skill.id == item_id).first()
|
||
if not item:
|
||
raise HTTPException(status_code=404, detail="Not found")
|
||
for key, val in data.model_dump().items():
|
||
setattr(item, key, val)
|
||
db.commit()
|
||
return {"message": "ok"}
|
||
|
||
|
||
@router.delete("/api/admin/skills/{item_id}")
|
||
def delete_skill(item_id: int, db: Session = Depends(get_db), _=Depends(get_current_admin)):
|
||
item = db.query(Skill).filter(Skill.id == item_id).first()
|
||
if not item:
|
||
raise HTTPException(status_code=404, detail="Not found")
|
||
db.delete(item)
|
||
db.commit()
|
||
return {"message": "ok"}
|
||
|
||
|
||
# ── Access Tokens ──
|
||
|
||
class TokenCreate(BaseModel):
|
||
note: str = ""
|
||
max_questions: int | None = None
|
||
|
||
|
||
class TokenUpdate(BaseModel):
|
||
note: str | None = None
|
||
max_questions: int | None = None
|
||
used_questions: int | None = None
|
||
is_active: bool | None = None
|
||
|
||
|
||
@router.get("/api/admin/tokens")
|
||
def list_tokens(db: Session = Depends(get_db), _=Depends(get_current_admin)):
|
||
items = db.query(AccessToken).order_by(AccessToken.created_at.desc()).all()
|
||
return [
|
||
{"id": t.id, "token": t.token, "created_at": t.created_at.isoformat() if t.created_at else "",
|
||
"is_active": t.is_active, "note": t.note,
|
||
"max_questions": t.max_questions or 10, "used_questions": t.used_questions or 0}
|
||
for t in items
|
||
]
|
||
|
||
|
||
@router.post("/api/admin/tokens")
|
||
def create_token(data: TokenCreate, db: Session = Depends(get_db), _=Depends(get_current_admin)):
|
||
token_str = uuid.uuid4().hex
|
||
# Use LLM config default if max_questions not specified
|
||
max_q = data.max_questions
|
||
if max_q is None:
|
||
llm_config = db.query(LLMConfig).filter(LLMConfig.is_active == True).first()
|
||
max_q = llm_config.max_questions_per_session if llm_config and llm_config.max_questions_per_session else 9
|
||
item = AccessToken(token=token_str, note=data.note, max_questions=max_q, created_at=datetime.utcnow())
|
||
db.add(item)
|
||
db.commit()
|
||
db.refresh(item)
|
||
return {"id": item.id, "token": item.token, "is_active": item.is_active,
|
||
"note": item.note, "max_questions": item.max_questions}
|
||
|
||
|
||
@router.put("/api/admin/tokens/{item_id}")
|
||
def update_token(item_id: int, data: TokenUpdate, db: Session = Depends(get_db), _=Depends(get_current_admin)):
|
||
item = db.query(AccessToken).filter(AccessToken.id == item_id).first()
|
||
if not item:
|
||
raise HTTPException(status_code=404, detail="Not found")
|
||
if data.note is not None:
|
||
item.note = data.note
|
||
if data.max_questions is not None:
|
||
item.max_questions = data.max_questions
|
||
if data.used_questions is not None:
|
||
item.used_questions = data.used_questions
|
||
if data.is_active is not None:
|
||
item.is_active = data.is_active
|
||
db.commit()
|
||
return {"message": "ok", "is_active": item.is_active}
|
||
|
||
|
||
@router.delete("/api/admin/tokens/{item_id}")
|
||
def delete_token(item_id: int, db: Session = Depends(get_db), _=Depends(get_current_admin)):
|
||
item = db.query(AccessToken).filter(AccessToken.id == item_id).first()
|
||
if not item:
|
||
raise HTTPException(status_code=404, detail="Not found")
|
||
db.delete(item)
|
||
db.commit()
|
||
return {"message": "ok"}
|
||
|
||
|
||
# ── LLM Config ──
|
||
|
||
class LLMConfigData(BaseModel):
|
||
api_url: str = ""
|
||
api_key: str = ""
|
||
model_name: str = ""
|
||
max_tokens: int = 2048
|
||
temperature: float = 0.7
|
||
max_questions_per_session: int = 9
|
||
system_prompt: str = ""
|
||
|
||
|
||
@router.get("/api/admin/llm-config")
|
||
def get_llm_config(db: Session = Depends(get_db), _=Depends(get_current_admin)):
|
||
config = db.query(LLMConfig).filter(LLMConfig.is_active == True).first()
|
||
if not config:
|
||
return {}
|
||
return {
|
||
"id": config.id, "api_url": config.api_url, "api_key": config.api_key,
|
||
"model_name": config.model_name, "max_tokens": config.max_tokens,
|
||
"temperature": config.temperature,
|
||
"max_questions_per_session": config.max_questions_per_session or 9,
|
||
"system_prompt": config.system_prompt or "",
|
||
}
|
||
|
||
|
||
@router.put("/api/admin/llm-config")
|
||
def update_llm_config(data: LLMConfigData, db: Session = Depends(get_db), _=Depends(get_current_admin)):
|
||
config = db.query(LLMConfig).filter(LLMConfig.is_active == True).first()
|
||
if not config:
|
||
config = LLMConfig(is_active=True)
|
||
db.add(config)
|
||
for key, val in data.model_dump().items():
|
||
setattr(config, key, val)
|
||
db.commit()
|
||
db.refresh(config)
|
||
return {"message": "ok", "id": config.id}
|
||
|
||
|
||
@router.post("/api/admin/llm-config/test")
|
||
async def test_llm_config(db: Session = Depends(get_db), _=Depends(get_current_admin)):
|
||
config = db.query(LLMConfig).filter(LLMConfig.is_active == True).first()
|
||
if not config or not config.api_url or not config.api_key:
|
||
return {"result": "Error: LLM not configured. Please save API URL and API Key first."}
|
||
|
||
from app.services.llm_service import chat_completion
|
||
messages = [
|
||
{"role": "system", "content": "You are a helpful assistant."},
|
||
{"role": "user", "content": "Please respond with exactly: OK"},
|
||
]
|
||
result = await chat_completion(messages, db)
|
||
return {"result": result}
|
||
|
||
|
||
# ── Resume Generator ──
|
||
|
||
@router.post("/api/admin/resume/generate")
|
||
async def generate_resume(
|
||
jd_text: str = Form(""),
|
||
file: UploadFile | None = File(None),
|
||
db: Session = Depends(get_db),
|
||
_=Depends(get_current_admin),
|
||
):
|
||
"""Generate a tailored resume based on JD, streaming response."""
|
||
import json
|
||
from sse_starlette.sse import EventSourceResponse
|
||
from app.services.file_parser import parse_file
|
||
from app.services.llm_service import chat_completion_stream
|
||
|
||
# Parse JD from file or text
|
||
jd_content = jd_text.strip()
|
||
if file and file.filename:
|
||
parsed = await parse_file(file)
|
||
if parsed.get("text"):
|
||
jd_content = parsed["text"]
|
||
elif parsed.get("error"):
|
||
async def error_gen():
|
||
yield {"data": json.dumps({"content": f"文件解析失败: {parsed['error']}"})}
|
||
yield {"data": json.dumps({"content": "", "done": True})}
|
||
return EventSourceResponse(error_gen())
|
||
|
||
if not jd_content:
|
||
async def empty_gen():
|
||
yield {"data": json.dumps({"content": "请输入或上传职位描述(JD)内容。"})}
|
||
yield {"data": json.dumps({"content": "", "done": True})}
|
||
return EventSourceResponse(empty_gen())
|
||
|
||
# Gather all profile data
|
||
profile = db.query(Profile).first()
|
||
skills = db.query(Skill).order_by(Skill.sort_order).all()
|
||
educations = db.query(Education).order_by(Education.sort_order).all()
|
||
experiences = db.query(WorkExperience).order_by(WorkExperience.sort_order).all()
|
||
|
||
# Build profile context
|
||
parts = []
|
||
if profile:
|
||
parts.append(f"姓名: {profile.name}")
|
||
parts.append(f"电话: {profile.phone}")
|
||
parts.append(f"邮箱: {profile.email}")
|
||
parts.append(f"所在地: {profile.location}")
|
||
parts.append(f"学历: {profile.education_level}")
|
||
if profile.self_summary:
|
||
parts.append(f"个人总结: {profile.self_summary}")
|
||
|
||
if skills:
|
||
parts.append("\n=== 核心技能 ===")
|
||
for s in skills:
|
||
parts.append(f"- {s.category}: {s.content}")
|
||
|
||
if educations:
|
||
parts.append("\n=== 教育经历 ===")
|
||
for e in educations:
|
||
parts.append(f"{e.start_date}-{e.end_date} | {e.school} | {e.major} | {e.degree}")
|
||
if e.details:
|
||
parts.append(f" {e.details}")
|
||
|
||
if experiences:
|
||
parts.append("\n=== 工作经历 ===")
|
||
for exp in experiences:
|
||
parts.append(f"\n{exp.start_date}-{exp.end_date} | {exp.company} | {exp.position}")
|
||
if exp.company_intro:
|
||
parts.append(f"公司简介: {exp.company_intro}")
|
||
if exp.responsibilities:
|
||
parts.append(f"工作职责: {exp.responsibilities}")
|
||
if exp.achievements:
|
||
parts.append(f"工作成就: {exp.achievements}")
|
||
|
||
profile_text = "\n".join(parts)
|
||
|
||
system_prompt = """你是一位专业的简历撰写顾问。请根据提供的职位描述(JD)和候选人的完整背景信息,生成一份针对该职位量身定制的中文简历。
|
||
|
||
要求:
|
||
1. 简历必须包含候选人的全部教育经历和工作经历,不得遗漏任何一段
|
||
2. "个人优势"和"自我评价"部分要针对JD进行定制,突出与JD最匹配的能力和经验
|
||
3. 每段工作经历的描述要重点突出与JD相关的职责和成就,弱化不相关的部分
|
||
4. 技能部分优先展示JD要求的技能
|
||
5. 使用专业、简洁的语言,避免空话套话
|
||
6. 输出格式使用Markdown,结构清晰
|
||
|
||
简历结构:
|
||
# 个人信息
|
||
# 求职意向(根据JD推断)
|
||
# 个人优势(3-5条,针对JD定制)
|
||
# 核心技能
|
||
# 工作经历(全部,按时间倒序)
|
||
# 教育背景(全部,按时间倒序)"""
|
||
|
||
user_prompt = f"""=== 目标职位描述(JD) ===
|
||
{jd_content}
|
||
|
||
=== 候选人完整背景 ===
|
||
{profile_text}
|
||
|
||
请根据以上JD生成一份量身定制的简历:"""
|
||
|
||
messages = [
|
||
{"role": "system", "content": system_prompt},
|
||
{"role": "user", "content": user_prompt},
|
||
]
|
||
|
||
async def event_gen():
|
||
async for chunk in chat_completion_stream(messages, db):
|
||
yield {"data": json.dumps({"content": chunk})}
|
||
yield {"data": json.dumps({"content": "", "done": True})}
|
||
|
||
return EventSourceResponse(event_gen())
|
||
|
||
|
||
@router.post("/api/admin/resume/download")
|
||
async def download_resume(
|
||
content: str = Form(...),
|
||
_=Depends(get_current_admin),
|
||
):
|
||
"""Convert markdown resume content to a Word document for download."""
|
||
from fastapi.responses import Response
|
||
from docx import Document
|
||
from docx.shared import Pt, Inches
|
||
from docx.enum.text import WD_ALIGN_PARAGRAPH
|
||
import re
|
||
|
||
doc = Document()
|
||
|
||
# Page margins
|
||
for section in doc.sections:
|
||
section.top_margin = Inches(0.8)
|
||
section.bottom_margin = Inches(0.8)
|
||
section.left_margin = Inches(0.9)
|
||
section.right_margin = Inches(0.9)
|
||
|
||
# Parse markdown and build document
|
||
lines = content.split("\n")
|
||
for line in lines:
|
||
stripped = line.strip()
|
||
if not stripped:
|
||
continue
|
||
|
||
# H1 heading
|
||
if stripped.startswith("# ") and not stripped.startswith("## "):
|
||
p = doc.add_heading(stripped[2:].strip(), level=1)
|
||
p.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
||
# H2 heading
|
||
elif stripped.startswith("## "):
|
||
doc.add_heading(stripped[3:].strip(), level=2)
|
||
# H3 heading
|
||
elif stripped.startswith("### "):
|
||
doc.add_heading(stripped[4:].strip(), level=3)
|
||
# Bullet list
|
||
elif stripped.startswith("- ") or stripped.startswith("* "):
|
||
text = stripped[2:].strip()
|
||
# Handle **bold** within text
|
||
p = doc.add_paragraph(style="List Bullet")
|
||
_add_rich_text(p, text)
|
||
# Numbered list
|
||
elif re.match(r"^\d+\.\s", stripped):
|
||
text = re.sub(r"^\d+\.\s", "", stripped)
|
||
p = doc.add_paragraph(style="List Number")
|
||
_add_rich_text(p, text)
|
||
# Horizontal rule
|
||
elif stripped in ("---", "***", "___"):
|
||
continue
|
||
# Normal paragraph
|
||
else:
|
||
p = doc.add_paragraph()
|
||
_add_rich_text(p, stripped)
|
||
|
||
import io
|
||
buffer = io.BytesIO()
|
||
doc.save(buffer)
|
||
buffer.seek(0)
|
||
|
||
return Response(
|
||
content=buffer.getvalue(),
|
||
media_type="application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
||
headers={"Content-Disposition": "attachment; filename=resume.docx"},
|
||
)
|
||
|
||
|
||
def _add_rich_text(paragraph, text: str):
|
||
"""Parse **bold** markers in text and add runs to paragraph."""
|
||
import re
|
||
parts = re.split(r"(\*\*.*?\*\*)", text)
|
||
for part in parts:
|
||
if part.startswith("**") and part.endswith("**"):
|
||
run = paragraph.add_run(part[2:-2])
|
||
run.bold = True
|
||
else:
|
||
paragraph.add_run(part)
|
||
|
||
|
||
# ── Recruiter Messages ──
|
||
|
||
@router.get("/api/admin/messages")
|
||
def list_messages(db: Session = Depends(get_db), _=Depends(get_current_admin)):
|
||
items = db.query(RecruiterMessage).order_by(RecruiterMessage.created_at.desc()).all()
|
||
return [
|
||
{
|
||
"id": m.id,
|
||
"session_id": m.session_id,
|
||
"visitor_label": m.visitor_label,
|
||
"company": m.company,
|
||
"contact": m.contact,
|
||
"intent": m.intent,
|
||
"summary": m.summary,
|
||
"is_read": m.is_read,
|
||
"created_at": m.created_at.isoformat() if m.created_at else "",
|
||
}
|
||
for m in items
|
||
]
|
||
|
||
|
||
@router.get("/api/admin/messages/unread-count")
|
||
def unread_message_count(db: Session = Depends(get_db), _=Depends(get_current_admin)):
|
||
count = db.query(RecruiterMessage).filter(RecruiterMessage.is_read == False).count()
|
||
return {"count": count}
|
||
|
||
|
||
@router.put("/api/admin/messages/{item_id}/read")
|
||
def mark_message_read(item_id: int, db: Session = Depends(get_db), _=Depends(get_current_admin)):
|
||
item = db.query(RecruiterMessage).filter(RecruiterMessage.id == item_id).first()
|
||
if not item:
|
||
raise HTTPException(status_code=404, detail="Not found")
|
||
item.is_read = True
|
||
db.commit()
|
||
return {"message": "ok"}
|
||
|
||
|
||
@router.put("/api/admin/messages/read-all")
|
||
def mark_all_read(db: Session = Depends(get_db), _=Depends(get_current_admin)):
|
||
db.query(RecruiterMessage).filter(RecruiterMessage.is_read == False).update({"is_read": True})
|
||
db.commit()
|
||
return {"message": "ok"}
|
||
|
||
|
||
@router.delete("/api/admin/messages/{item_id}")
|
||
def delete_message(item_id: int, db: Session = Depends(get_db), _=Depends(get_current_admin)):
|
||
item = db.query(RecruiterMessage).filter(RecruiterMessage.id == item_id).first()
|
||
if not item:
|
||
raise HTTPException(status_code=404, detail="Not found")
|
||
db.delete(item)
|
||
db.commit()
|
||
return {"message": "ok"}
|