feat: Freelancer Match — полная продакшн версия с AI-матчингом и escrow

This commit is contained in:
root
2026-07-03 12:12:43 +00:00
commit ec2e9bf508
37 changed files with 1564 additions and 0 deletions
+18
View File
@@ -0,0 +1,18 @@
"""Модели SQLAlchemy."""
from app.models.user import User, FreelancerProfile, ClientProfile
from app.models.project import Project
from app.models.proposal import Proposal
from app.models.ai_match import AIMatch
from app.models.escrow import EscrowTransaction
from app.models.work_session import WorkSession
from app.models.review import Review
from app.models.message import Message
from app.models.notification import Notification
__all__ = [
"User", "FreelancerProfile", "ClientProfile",
"Project", "Proposal", "AIMatch",
"EscrowTransaction", "WorkSession",
"Review", "Message", "Notification",
]
+21
View File
@@ -0,0 +1,21 @@
"""Модель AI-рекомендаций."""
import uuid
from datetime import datetime, timezone
from sqlalchemy import Column, DateTime, Float, ForeignKey, func
from sqlalchemy.dialects.postgresql import UUID as PG_UUID
from sqlalchemy.orm import Mapped, mapped_column, relationship
from app.core.database import Base
class AIMatch(Base):
__tablename__ = "ai_matches"
id: Mapped[uuid.UUID] = mapped_column(PG_UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
project_id: Mapped[uuid.UUID] = mapped_column(PG_UUID(as_uuid=True), ForeignKey("projects.id"), nullable=False)
freelancer_id: Mapped[uuid.UUID] = mapped_column(PG_UUID(as_uuid=True), ForeignKey("users.id"), nullable=False)
match_score: Mapped[float] = mapped_column(Float(precision=5, scale=4), nullable=False)
reasons: Mapped[list] = mapped_column("reasons", postgresql.JSONB, default=list)
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
+23
View File
@@ -0,0 +1,23 @@
"""Модель Escrow-транзакций (гарант)."""
import uuid
from datetime import datetime, timezone
from sqlalchemy import Column, DateTime, Enum, Float, ForeignKey, func
from sqlalchemy.dialects.postgresql import UUID as PG_UUID
from sqlalchemy.orm import Mapped, mapped_column, relationship
from app.core.database import Base
class EscrowTransaction(Base):
__tablename__ = "escrow_transactions"
id: Mapped[uuid.UUID] = mapped_column(PG_UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
project_id: Mapped[uuid.UUID] = mapped_column(PG_UUID(as_uuid=True), ForeignKey("projects.id"), nullable=False)
client_id: Mapped[uuid.UUID] = mapped_column(PG_UUID(as_uuid=True), ForeignKey("users.id"), nullable=False)
freelancer_id: Mapped[uuid.UUID] = mapped_column(PG_UUID(as_uuid=True), ForeignKey("users.id"), nullable=False)
amount: Mapped[float] = mapped_column(Float(precision=10, scale=2), nullable=False)
status: Mapped[str] = mapped_column(Enum("pending", "locked", "released", "disputed", "refunded"), default="pending")
released_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
+21
View File
@@ -0,0 +1,21 @@
"""Модель сообщений чата."""
import uuid
from datetime import datetime, timezone
from sqlalchemy import Column, DateTime, ForeignKey, Text, func
from sqlalchemy.dialects.postgresql import UUID as PG_UUID
from sqlalchemy.orm import Mapped, mapped_column, relationship
from app.core.database import Base
class Message(Base):
__tablename__ = "messages"
id: Mapped[uuid.UUID] = mapped_column(PG_UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
work_session_id: Mapped[uuid.UUID] = mapped_column(PG_UUID(as_uuid=True), ForeignKey("work_sessions.id"), nullable=False)
sender_id: Mapped[uuid.UUID] = mapped_column(PG_UUID(as_uuid=True), ForeignKey("users.id"), nullable=False)
content: Mapped[str] = mapped_column(Text, nullable=False)
attachments: Mapped[list | None] = mapped_column("attachments", postgresql.JSONB, default=list)
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
+22
View File
@@ -0,0 +1,22 @@
"""Модель уведомлений."""
import uuid
from datetime import datetime, timezone
from sqlalchemy import Column, DateTime, Boolean, ForeignKey, String, Text, func
from sqlalchemy.dialects.postgresql import UUID as PG_UUID
from sqlalchemy.orm import Mapped, mapped_column, relationship
from app.core.database import Base
class Notification(Base):
__tablename__ = "notifications"
id: Mapped[uuid.UUID] = mapped_column(PG_UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
user_id: Mapped[uuid.UUID] = mapped_column(PG_UUID(as_uuid=True), ForeignKey("users.id"), nullable=False)
type: Mapped[str] = mapped_column(String(50)) # proposal_received, payment_released, etc.
title: Mapped[str | None] = mapped_column(String(255))
body: Mapped[str | None] = mapped_column(Text)
is_read: Mapped[bool] = mapped_column(default=False)
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
+47
View File
@@ -0,0 +1,47 @@
"""Модели проектов и заявок."""
import uuid
from datetime import datetime, timezone
from sqlalchemy import Column, DateTime, Enum, Float, ForeignKey, Integer, String, Text, func
from sqlalchemy.dialects.postgresql import UUID as PG_UUID
from sqlalchemy.orm import Mapped, mapped_column, relationship
from app.core.database import Base
class Project(Base):
__tablename__ = "projects"
id: Mapped[uuid.UUID] = mapped_column(PG_UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
client_id: Mapped[uuid.UUID] = mapped_column(PG_UUID(as_uuid=True), ForeignKey("users.id"), nullable=False)
title: Mapped[str] = mapped_column(String(255), nullable=False)
description: Mapped[str] = mapped_column(Text, nullable=False)
category: Mapped[str | None] = mapped_column(String(100))
required_skills: Mapped[list] = mapped_column("required_skills", postgresql.JSONB, default=list)
skill_embeddings: Mapped[list | None] = mapped_column("skill_embeddings") # pgvector
budget_min: Mapped[float | None] = mapped_column(Float(precision=10, scale=2))
budget_max: Mapped[float | None] = mapped_column(Float(precision=10, scale=2))
status: Mapped[str] = mapped_column(Enum("open", "in_progress", "completed", "cancelled"), default="open")
deadline: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), onupdate=func.now())
# Relationships
client = relationship("User", foreign_keys=[client_id])
proposals: Mapped[list["Proposal"]] = relationship(back_populates="project")
class Proposal(Base):
__tablename__ = "proposals"
id: Mapped[uuid.UUID] = mapped_column(PG_UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
project_id: Mapped[uuid.UUID] = mapped_column(PG_UUID(as_uuid=True), ForeignKey("projects.id"), nullable=False)
freelancer_id: Mapped[uuid.UUID] = mapped_column(PG_UUID(as_uuid=True), ForeignKey("users.id"), nullable=False)
cover_letter: Mapped[str | None] = mapped_column(Text)
proposed_price: Mapped[float | None] = mapped_column(Float(precision=10, scale=2))
estimated_days: Mapped[int | None] = mapped_column(Integer)
status: Mapped[str] = mapped_column(Enum("pending", "accepted", "rejected"), default="pending")
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
project: Mapped["Project"] = relationship(back_populates="proposals")
+22
View File
@@ -0,0 +1,22 @@
"""Модель отзывов."""
import uuid
from datetime import datetime, timezone
from sqlalchemy import Column, DateTime, Enum, ForeignKey, Integer, String, Text, func
from sqlalchemy.dialects.postgresql import UUID as PG_UUID
from sqlalchemy.orm import Mapped, mapped_column, relationship
from app.core.database import Base
class Review(Base):
__tablename__ = "reviews"
id: Mapped[uuid.UUID] = mapped_column(PG_UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
project_id: Mapped[uuid.UUID] = mapped_column(PG_UUID(as_uuid=True), ForeignKey("projects.id"), nullable=False)
reviewer_id: Mapped[uuid.UUID] = mapped_column(PG_UUID(as_uuid=True), ForeignKey("users.id"), nullable=False)
reviewee_id: Mapped[uuid.UUID] = mapped_column(PG_UUID(as_uuid=True), ForeignKey("users.id"), nullable=False)
rating: Mapped[int] = mapped_column(Integer, nullable=False)
comment: Mapped[str | None] = mapped_column(Text)
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
+61
View File
@@ -0,0 +1,61 @@
"""Модели пользователей."""
import uuid
from datetime import datetime, timezone
from sqlalchemy import Column, DateTime, Enum, Float, Integer, String, Text, func
from sqlalchemy.dialects.postgresql import UUID as PG_UUID
from sqlalchemy.orm import Mapped, mapped_column, relationship
from app.core.database import Base
class User(Base):
__tablename__ = "users"
id: Mapped[uuid.UUID] = mapped_column(PG_UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
email: Mapped[str] = mapped_column(String(255), unique=True, nullable=False, index=True)
password_hash: Mapped[str | None] = mapped_column(String(255))
full_name: Mapped[str | None] = mapped_column(String(100))
avatar_url: Mapped[str | None] = mapped_column(Text)
role: Mapped[str] = mapped_column(Enum("client", "freelancer", "both"), nullable=False, default="freelancer")
is_verified: Mapped[bool] = mapped_column(default=False)
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), onupdate=func.now())
# Relationships
freelancer_profile: Mapped["FreelancerProfile | None"] = relationship(back_populates="user", uselist=False)
client_profile: Mapped["ClientProfile | None"] = relationship(back_populates="user", uselist=False)
class FreelancerProfile(Base):
__tablename__ = "freelancer_profiles"
user_id: Mapped[uuid.UUID] = mapped_column(PG_UUID(as_uuid=True), primary_key=True, nullable=False)
bio: Mapped[str | None] = mapped_column(Text)
skills: Mapped[list] = mapped_column("skills", postgresql.JSONB, default=list)
skill_embeddings: Mapped[list | None] = mapped_column("skill_embeddings") # pgvector VECTOR(1536)
hourly_rate: Mapped[float | None] = mapped_column(Float(precision=10, scale=2))
portfolio_items: Mapped[list] = mapped_column("portfolio_items", postgresql.JSONB, default=list)
experience_years: Mapped[int | None] = mapped_column(Integer)
languages: Mapped[list] = mapped_column("languages", postgresql.JSONB, default=list)
rating: Mapped[float] = mapped_column(Float(precision=3, scale=2), default=5.0)
total_jobs_completed: Mapped[int] = mapped_column(Integer, default=0)
response_time_hours: Mapped[int | None] = mapped_column(Integer)
is_online: Mapped[bool] = mapped_column(default=False)
last_seen: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
user: Mapped["User"] = relationship(back_populates="freelancer_profile")
class ClientProfile(Base):
__tablename__ = "client_profiles"
user_id: Mapped[uuid.UUID] = mapped_column(PG_UUID(as_uuid=True), primary_key=True, nullable=False)
company_name: Mapped[str | None] = mapped_column(String(255))
industry: Mapped[str | None] = mapped_column(String(100))
budget_range: Mapped[dict | None] = mapped_column("budget_range", postgresql.JSONB)
total_spent: Mapped[float] = mapped_column(Float(precision=10, scale=6), default=0.0)
rating: Mapped[float] = mapped_column(Float(precision=3, scale=2), default=5.0)
user: Mapped["User"] = relationship(back_populates="client_profile")
+24
View File
@@ -0,0 +1,24 @@
"""Модель рабочей сессии."""
import uuid
from datetime import datetime, timezone
from sqlalchemy import Column, DateTime, Enum, Float, ForeignKey, func
from sqlalchemy.dialects.postgresql import UUID as PG_UUID
from sqlalchemy.orm import Mapped, mapped_column, relationship
from app.core.database import Base
class WorkSession(Base):
__tablename__ = "work_sessions"
id: Mapped[uuid.UUID] = mapped_column(PG_UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
project_id: Mapped[uuid.UUID] = mapped_column(PG_UUID(as_uuid=True), ForeignKey("projects.id"), nullable=False)
freelancer_id: Mapped[uuid.UUID] = mapped_column(PG_UUID(as_uuid=True), ForeignKey("users.id"), nullable=False)
client_id: Mapped[uuid.UUID] = mapped_column(PG_UUID(as_uuid=True), ForeignKey("users.id"), nullable=False)
start_time: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
end_time: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
hours_worked: Mapped[float] = mapped_column(Float(precision=5, scale=2), default=0.0)
status: Mapped[str] = mapped_column(Enum("active", "paused", "completed"), default="active")
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())