1
0
mirror of https://gitlab.com/MoonTestUse1/AdministrationItDepartmens.git synced 2025-08-14 00:25:46 +02:00

Создание чата

This commit is contained in:
MoonTestUse1
2025-01-05 05:43:45 +06:00
parent 357acd11a1
commit ba0b0ec72b
16 changed files with 1619 additions and 29 deletions

View File

@@ -0,0 +1,141 @@
from fastapi import APIRouter, Depends, HTTPException, WebSocket, UploadFile, File
from sqlalchemy.orm import Session
from typing import List
import os
import aiofiles
import uuid
from app.database import get_db
from app.models.user import User
from app.core.auth import get_current_user
from app.schemas.chat import Chat, Message, ChatFile
from app.websockets.chat import handle_chat_connection
router = APIRouter()
# Путь для сохранения файлов
UPLOAD_DIR = "uploads/chat_files"
os.makedirs(UPLOAD_DIR, exist_ok=True)
@router.websocket("/ws/chat")
async def websocket_endpoint(websocket: WebSocket, db: Session = Depends(get_db)):
await handle_chat_connection(websocket, db)
@router.post("/files/")
async def upload_file(
file: UploadFile = File(...),
current_user: User = Depends(get_current_user),
db: Session = Depends(get_db)
):
try:
# Генерируем уникальное имя файла
file_extension = os.path.splitext(file.filename)[1]
unique_filename = f"{uuid.uuid4()}{file_extension}"
file_path = os.path.join(UPLOAD_DIR, unique_filename)
# Сохраняем файл
async with aiofiles.open(file_path, 'wb') as out_file:
content = await file.read()
await out_file.write(content)
return {
"filename": file.filename,
"saved_path": file_path,
"size": len(content)
}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@router.get("/messages/", response_model=List[Message])
def get_messages(
current_user: User = Depends(get_current_user),
db: Session = Depends(get_db)
):
# Получаем чат пользователя
chat = db.query(Chat).filter(
(Chat.employee_id == current_user.id) |
(Chat.admin_id == current_user.id)
).first()
if not chat:
return []
# Получаем сообщения
messages = db.query(Message).filter(Message.chat_id == chat.id).all()
return messages
@router.get("/unread-count/")
def get_unread_count(
current_user: User = Depends(get_current_user),
db: Session = Depends(get_db)
):
# Получаем чат пользователя
chat = db.query(Chat).filter(
(Chat.employee_id == current_user.id) |
(Chat.admin_id == current_user.id)
).first()
if not chat:
return {"unread_count": 0}
# Считаем непрочитанные сообщения
unread_count = db.query(Message).filter(
Message.chat_id == chat.id,
Message.sender_id != current_user.id,
Message.is_read == False
).count()
return {"unread_count": unread_count}
@router.get("/admin/chats/", response_model=List[Chat])
def get_admin_chats(
current_user: User = Depends(get_current_user),
db: Session = Depends(get_db)
):
if not current_user.is_admin:
raise HTTPException(status_code=403, detail="Not authorized")
# Получаем все чаты с последними сообщениями и количеством непрочитанных
chats = db.query(Chat).all()
# Для каждого чата добавляем дополнительную информацию
for chat in chats:
# Последнее сообщение
last_message = db.query(Message)\
.filter(Message.chat_id == chat.id)\
.order_by(Message.created_at.desc())\
.first()
chat.last_message = last_message
# Количество непрочитанных сообщений
unread_count = db.query(Message)\
.filter(
Message.chat_id == chat.id,
Message.sender_id != current_user.id,
Message.is_read == False
).count()
chat.unread_count = unread_count
return chats
@router.get("/messages/{chat_id}/", response_model=List[Message])
def get_chat_messages(
chat_id: int,
current_user: User = Depends(get_current_user),
db: Session = Depends(get_db)
):
# Проверяем доступ к чату
chat = db.query(Chat).filter(Chat.id == chat_id).first()
if not chat:
raise HTTPException(status_code=404, detail="Chat not found")
if not current_user.is_admin and chat.employee_id != current_user.id:
raise HTTPException(status_code=403, detail="Not authorized")
# Получаем сообщения чата
messages = db.query(Message)\
.filter(Message.chat_id == chat_id)\
.order_by(Message.created_at.asc())\
.all()
return messages

57
backend/app/core/auth.py Normal file
View File

@@ -0,0 +1,57 @@
from fastapi import Depends, HTTPException, status, WebSocket
from fastapi.security import OAuth2PasswordBearer
from jose import JWTError, jwt
from sqlalchemy.orm import Session
from typing import Optional
from app.core.config import settings
from app.database import get_db
from app.models.user import User
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
def get_current_user(token: str = Depends(oauth2_scheme), db: Session = Depends(get_db)) -> User:
credentials_exception = HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Could not validate credentials",
headers={"WWW-Authenticate": "Bearer"},
)
try:
payload = jwt.decode(token, settings.secret_key, algorithms=[settings.algorithm])
email: str = payload.get("sub")
if email is None:
raise credentials_exception
except JWTError:
raise credentials_exception
user = db.query(User).filter(User.email == email).first()
if user is None:
raise credentials_exception
return user
async def get_current_user_ws(websocket: WebSocket, db: Session = Depends(get_db)) -> Optional[User]:
try:
# Получаем токен из параметров запроса
token = websocket.query_params.get("token")
if not token:
await websocket.close(code=status.WS_1008_POLICY_VIOLATION)
return None
# Проверяем токен
payload = jwt.decode(token, settings.secret_key, algorithms=[settings.algorithm])
email: str = payload.get("sub")
if email is None:
await websocket.close(code=status.WS_1008_POLICY_VIOLATION)
return None
# Получаем пользователя
user = db.query(User).filter(User.email == email).first()
if user is None:
await websocket.close(code=status.WS_1008_POLICY_VIOLATION)
return None
return user
except JWTError:
await websocket.close(code=status.WS_1008_POLICY_VIOLATION)
return None

View File

@@ -0,0 +1,19 @@
from apscheduler.schedulers.asyncio import AsyncIOScheduler
from apscheduler.triggers.cron import CronTrigger
from app.tasks.cleanup import cleanup_old_messages
scheduler = AsyncIOScheduler()
def setup_scheduler():
"""Настраивает планировщик задач"""
# Запускаем очистку старых сообщений каждый день в полночь
scheduler.add_job(
cleanup_old_messages,
trigger=CronTrigger(hour=0, minute=0),
id='cleanup_old_messages',
name='Cleanup old messages and files',
replace_existing=True
)
scheduler.start()

View File

@@ -1,40 +1,38 @@
"""Main application module"""
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from . import models
from .routers import admin, employees, requests, auth, statistics
from app.api.api import api_router
from app.core.config import settings
from app.core.scheduler import setup_scheduler
from app.websockets.chat import handle_chat_connection
app = FastAPI(
# Включаем автоматическое перенаправление со слэшем
redirect_slashes=True,
# Добавляем описание API
title="Support System API",
description="API для системы поддержки",
version="1.0.0"
title=settings.project_name,
openapi_url=f"{settings.api_v1_str}/openapi.json"
)
# CORS configuration
origins = [
"http://localhost",
"http://localhost:8080",
"http://localhost:5173",
"http://127.0.0.1:5173",
"http://127.0.0.1:8080",
"http://185.139.70.62", # Добавляем ваш production домен
]
# Настройка CORS
app.add_middleware(
CORSMiddleware,
allow_origins=origins,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
expose_headers=["*"]
)
# Include routers
app.include_router(auth.router, prefix="/api/auth", tags=["auth"])
app.include_router(employees.router, prefix="/api/employees", tags=["employees"])
app.include_router(requests.router, prefix="/api/requests", tags=["requests"])
app.include_router(admin.router, prefix="/api/admin", tags=["admin"])
app.include_router(statistics.router, prefix="/api/statistics", tags=["statistics"])
# Подключаем роутеры
app.include_router(api_router, prefix=settings.api_v1_str)
# WebSocket для чата
app.add_api_websocket_route("/ws/chat", handle_chat_connection)
@app.on_event("startup")
async def startup_event():
"""Действия при запуске приложения"""
setup_scheduler()
@app.on_event("shutdown")
async def shutdown_event():
"""Действия при остановке приложения"""
from app.core.scheduler import scheduler
scheduler.shutdown()

View File

@@ -0,0 +1,46 @@
from sqlalchemy import Column, Integer, String, ForeignKey, DateTime, Boolean, Text
from sqlalchemy.sql import func
from sqlalchemy.orm import relationship
from app.database import Base
class Chat(Base):
__tablename__ = "chats"
id = Column(Integer, primary_key=True, index=True)
employee_id = Column(Integer, ForeignKey("users.id"), nullable=False, unique=True)
admin_id = Column(Integer, ForeignKey("users.id"), nullable=False)
created_at = Column(DateTime(timezone=True), server_default=func.now())
# Отношения
employee = relationship("User", foreign_keys=[employee_id], back_populates="employee_chats")
admin = relationship("User", foreign_keys=[admin_id], back_populates="admin_chats")
messages = relationship("Message", back_populates="chat", cascade="all, delete-orphan")
class Message(Base):
__tablename__ = "messages"
id = Column(Integer, primary_key=True, index=True)
chat_id = Column(Integer, ForeignKey("chats.id", ondelete="CASCADE"), nullable=False)
sender_id = Column(Integer, ForeignKey("users.id"), nullable=False)
content = Column(Text, nullable=False)
is_read = Column(Boolean, default=False)
created_at = Column(DateTime(timezone=True), server_default=func.now())
# Отношения
chat = relationship("Chat", back_populates="messages")
sender = relationship("User", back_populates="sent_messages")
files = relationship("ChatFile", back_populates="message", cascade="all, delete-orphan")
class ChatFile(Base):
__tablename__ = "chat_files"
id = Column(Integer, primary_key=True, index=True)
message_id = Column(Integer, ForeignKey("messages.id", ondelete="CASCADE"), nullable=False)
file_name = Column(String(255), nullable=False)
file_path = Column(String(255), nullable=False)
file_size = Column(Integer, nullable=False)
created_at = Column(DateTime(timezone=True), server_default=func.now())
# Отношения
message = relationship("Message", back_populates="files")

View File

@@ -0,0 +1,19 @@
from sqlalchemy import Boolean, Column, Integer, String
from sqlalchemy.orm import relationship
from app.database import Base
class User(Base):
__tablename__ = "users"
id = Column(Integer, primary_key=True, index=True)
email = Column(String, unique=True, index=True)
full_name = Column(String)
hashed_password = Column(String)
is_active = Column(Boolean, default=True)
is_admin = Column(Boolean, default=False)
# Отношения для чата
employee_chats = relationship("Chat", foreign_keys="[Chat.employee_id]", back_populates="employee")
admin_chats = relationship("Chat", foreign_keys="[Chat.admin_id]", back_populates="admin")
sent_messages = relationship("Message", back_populates="sender")

View File

@@ -0,0 +1,73 @@
from pydantic import BaseModel
from datetime import datetime
from typing import List, Optional
from app.schemas.user import User
class ChatFileBase(BaseModel):
file_name: str
file_size: int
class ChatFileCreate(ChatFileBase):
pass
class ChatFile(ChatFileBase):
id: int
message_id: int
file_path: str
created_at: datetime
class Config:
from_attributes = True
class MessageBase(BaseModel):
content: str
class MessageCreate(MessageBase):
pass
class Message(MessageBase):
id: int
chat_id: int
sender_id: int
is_read: bool
created_at: datetime
files: List[ChatFile] = []
class Config:
from_attributes = True
class ChatBase(BaseModel):
employee_id: int
admin_id: int
class ChatCreate(ChatBase):
pass
class Chat(ChatBase):
id: int
created_at: datetime
employee: User
admin: User
last_message: Optional[Message] = None
unread_count: Optional[int] = 0
class Config:
from_attributes = True
# Схемы для WebSocket сообщений
class WSMessage(BaseModel):
type: str
content: Optional[str] = None
message_ids: Optional[List[int]] = None
files: Optional[List[dict]] = None
class WSResponse(BaseModel):
type: str
id: Optional[int] = None
sender_id: Optional[int] = None
content: Optional[str] = None
created_at: Optional[datetime] = None
is_read: Optional[bool] = None
message_ids: Optional[List[int]] = None
files: Optional[List[ChatFile]] = None
error: Optional[str] = None

View File

@@ -0,0 +1,38 @@
import os
from datetime import datetime, timedelta
from sqlalchemy.orm import Session
from app.database import SessionLocal
from app.models.chat import Message, ChatFile
def cleanup_old_messages():
"""Удаляет сообщения и файлы старше 1 месяца"""
db = SessionLocal()
try:
# Определяем дату, до которой нужно удалить сообщения
cutoff_date = datetime.utcnow() - timedelta(days=30)
# Получаем файлы, которые нужно удалить
files_to_delete = db.query(ChatFile)\
.join(Message)\
.filter(Message.created_at < cutoff_date)\
.all()
# Удаляем физические файлы
for file in files_to_delete:
try:
if os.path.exists(file.file_path):
os.remove(file.file_path)
except Exception as e:
print(f"Error deleting file {file.file_path}: {e}")
# Удаляем старые сообщения (каскадно удалятся и записи о файлах)
db.query(Message)\
.filter(Message.created_at < cutoff_date)\
.delete(synchronize_session=False)
db.commit()
except Exception as e:
print(f"Error during cleanup: {e}")
db.rollback()
finally:
db.close()

View File

@@ -0,0 +1,112 @@
from fastapi import WebSocket, WebSocketDisconnect, Depends
from typing import Dict, List, Optional
from datetime import datetime
import json
from app.core.auth import get_current_user_ws
from app.models.user import User
from app.models.chat import Chat, Message, ChatFile
from app.database import get_db
from sqlalchemy.orm import Session
class ConnectionManager:
def __init__(self):
# Хранение активных соединений: {user_id: WebSocket}
self.active_connections: Dict[int, WebSocket] = {}
async def connect(self, websocket: WebSocket, user_id: int):
await websocket.accept()
self.active_connections[user_id] = websocket
def disconnect(self, user_id: int):
if user_id in self.active_connections:
del self.active_connections[user_id]
async def send_personal_message(self, message: dict, user_id: int):
if user_id in self.active_connections:
await self.active_connections[user_id].send_json(message)
def is_connected(self, user_id: int) -> bool:
return user_id in self.active_connections
manager = ConnectionManager()
async def handle_chat_connection(
websocket: WebSocket,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user_ws)
):
await manager.connect(websocket, current_user.id)
try:
while True:
data = await websocket.receive_json()
# Обработка различных типов сообщений
message_type = data.get('type')
if message_type == 'message':
# Создание нового сообщения
chat = db.query(Chat).filter(
(Chat.employee_id == current_user.id) |
(Chat.admin_id == current_user.id)
).first()
if not chat:
# Создаем новый чат для сотрудника
admin = db.query(User).filter(User.is_admin == True).first()
chat = Chat(employee_id=current_user.id, admin_id=admin.id)
db.add(chat)
db.commit()
message = Message(
chat_id=chat.id,
sender_id=current_user.id,
content=data['content']
)
db.add(message)
db.commit()
# Определяем получателя
recipient_id = chat.admin_id if current_user.id == chat.employee_id else chat.employee_id
# Отправляем сообщение получателю
message_data = {
'type': 'message',
'id': message.id,
'sender_id': current_user.id,
'content': message.content,
'created_at': message.created_at.isoformat(),
'is_read': False
}
if manager.is_connected(recipient_id):
await manager.send_personal_message(message_data, recipient_id)
elif message_type == 'read':
# Отмечаем сообщения как прочитанные
message_ids = data.get('message_ids', [])
db.query(Message).filter(Message.id.in_(message_ids)).update(
{Message.is_read: True},
synchronize_session=False
)
db.commit()
# Отправляем подтверждение прочтения
chat = db.query(Chat).filter(
(Chat.employee_id == current_user.id) |
(Chat.admin_id == current_user.id)
).first()
if chat:
recipient_id = chat.admin_id if current_user.id == chat.employee_id else chat.employee_id
if manager.is_connected(recipient_id):
await manager.send_personal_message({
'type': 'read_confirmation',
'message_ids': message_ids
}, recipient_id)
except WebSocketDisconnect:
manager.disconnect(current_user.id)
except Exception as e:
print(f"Error in WebSocket connection: {e}")
manager.disconnect(current_user.id)