feat(chat): 聊天单向删除
This commit is contained in:
@@ -27,6 +27,30 @@ from core.db.models import BaseModel # 假设你的 SQLAlchemy 模型在 models
|
|||||||
target_metadata = BaseModel.metadata
|
target_metadata = BaseModel.metadata
|
||||||
|
|
||||||
|
|
||||||
|
def run_migrations_offline() -> None:
|
||||||
|
"""Run migrations in 'offline' mode.
|
||||||
|
|
||||||
|
This configures the context with just a URL
|
||||||
|
and not an Engine, though an Engine is acceptable
|
||||||
|
here as well. By skipping the Engine creation
|
||||||
|
we don't even need a DBAPI to be available.
|
||||||
|
|
||||||
|
Calls to context.execute() here emit the given string to the
|
||||||
|
script output.
|
||||||
|
|
||||||
|
"""
|
||||||
|
url = config.get_main_option("sqlalchemy.url")
|
||||||
|
context.configure(
|
||||||
|
url=url,
|
||||||
|
target_metadata=target_metadata,
|
||||||
|
literal_binds=True,
|
||||||
|
dialect_opts={"paramstyle": "named"},
|
||||||
|
)
|
||||||
|
|
||||||
|
with context.begin_transaction():
|
||||||
|
context.run_migrations()
|
||||||
|
|
||||||
|
|
||||||
def do_run_migrations(connection):
|
def do_run_migrations(connection):
|
||||||
context.configure(
|
context.configure(
|
||||||
connection=connection,
|
connection=connection,
|
||||||
|
|||||||
@@ -24,4 +24,4 @@ def upgrade() -> None:
|
|||||||
|
|
||||||
|
|
||||||
def downgrade() -> None:
|
def downgrade() -> None:
|
||||||
pass
|
op.drop_column("system_cards", 'diy_source')
|
||||||
|
|||||||
@@ -31,8 +31,8 @@ def upgrade() -> None:
|
|||||||
def downgrade() -> None:
|
def downgrade() -> None:
|
||||||
# ### commands auto generated by Alembic - please adjust! ###
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
op.alter_column('system_products', 'action',
|
op.alter_column('system_products', 'action',
|
||||||
existing_type=mysql.ENUM('VIP_RENEWAL', 'RECHARGE_POINTS', 'FREE_GIFT'),
|
existing_type=mysql.ENUM('VIP_RENEWAL', 'RECHARGE_POINTS'),
|
||||||
comment='付款后的动作,会员续期:vip_renewal 积分:recharge_points 赠送产品:free_gift',
|
comment='付款后的动作,会员续期:vip_renewal 积分:recharge_points',
|
||||||
existing_comment='付款后的动作,会员续期:vip_renewal 积分:recharge_points',
|
existing_comment='付款后的动作,会员续期:vip_renewal 积分:recharge_points',
|
||||||
existing_nullable=False)
|
existing_nullable=False)
|
||||||
# ### end Alembic commands ###
|
# ### end Alembic commands ###
|
||||||
|
|||||||
62
alembic/versions/e9466c397aa2_add_chat_participant_to_orm.py
Normal file
62
alembic/versions/e9466c397aa2_add_chat_participant_to_orm.py
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
"""add chat_participant to orm
|
||||||
|
|
||||||
|
Revision ID: e9466c397aa2
|
||||||
|
Revises: 4a03470b2c09
|
||||||
|
Create Date: 2025-05-15 17:22:26.087396
|
||||||
|
|
||||||
|
"""
|
||||||
|
from typing import Sequence, Union
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
from sqlalchemy.dialects import mysql
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision: str = 'e9466c397aa2'
|
||||||
|
down_revision: Union[str, None] = '4a03470b2c09'
|
||||||
|
branch_labels: Union[str, Sequence[str], None] = None
|
||||||
|
depends_on: Union[str, Sequence[str], None] = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.create_table('system_chat_participant',
|
||||||
|
sa.Column('user_id', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('chat_id', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('is_deleted', sa.Boolean, default=False, nullable=False),
|
||||||
|
sa.ForeignKeyConstraint(['chat_id'], ['system_chats.id'], ),
|
||||||
|
sa.ForeignKeyConstraint(['user_id'], ['system_users.user_id'], ),
|
||||||
|
sa.PrimaryKeyConstraint('user_id', 'chat_id'),
|
||||||
|
comment='聊天参与者关联表'
|
||||||
|
)
|
||||||
|
op.drop_table('chat_participant')
|
||||||
|
op.alter_column('system_orders', 'status',
|
||||||
|
existing_type=mysql.ENUM('INIT', 'FAIL', 'OK', 'WAIT_PAYMENT', 'PAID', 'CANCEL'),
|
||||||
|
comment='订单状态,init:初始化 fail:失败 ok:完成 wait payment:待支付 paid:已支付 cancel. 取消',
|
||||||
|
existing_comment='订单状态,init:初始化 fail:失败 ok:完成 wait payment:待支付 cancel. 取消',
|
||||||
|
existing_nullable=False,
|
||||||
|
existing_server_default=sa.text("'INIT'"))
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.alter_column('system_orders', 'status',
|
||||||
|
existing_type=mysql.ENUM('INIT', 'FAIL', 'OK', 'WAIT_PAYMENT', 'PAID', 'CANCEL'),
|
||||||
|
comment='订单状态,init:初始化 fail:失败 ok:完成 wait payment:待支付 cancel. 取消',
|
||||||
|
existing_comment='订单状态,init:初始化 fail:失败 ok:完成 wait payment:待支付 paid:已支付 cancel. 取消',
|
||||||
|
existing_nullable=False,
|
||||||
|
existing_server_default=sa.text("'INIT'"))
|
||||||
|
op.create_table('chat_participant',
|
||||||
|
sa.Column('user_id', mysql.INTEGER(display_width=11), autoincrement=False, nullable=False),
|
||||||
|
sa.Column('chat_id', mysql.INTEGER(display_width=11), autoincrement=False, nullable=False),
|
||||||
|
sa.ForeignKeyConstraint(['chat_id'], ['system_chats.id'], name='chat_participant_ibfk_1'),
|
||||||
|
sa.ForeignKeyConstraint(['user_id'], ['system_users.user_id'], name='chat_participant_ibfk_2'),
|
||||||
|
sa.PrimaryKeyConstraint('user_id', 'chat_id'),
|
||||||
|
comment='聊天参与者关联表',
|
||||||
|
mysql_comment='聊天参与者关联表',
|
||||||
|
mysql_default_charset='utf8mb4',
|
||||||
|
mysql_engine='InnoDB'
|
||||||
|
)
|
||||||
|
op.drop_table('system_chat_participant')
|
||||||
|
# ### end Alembic commands ###
|
||||||
@@ -97,7 +97,7 @@ class User(BaseModel, DateModel):
|
|||||||
foreign_keys='UserFollows.followed_id',
|
foreign_keys='UserFollows.followed_id',
|
||||||
cascade='all, delete-orphan'
|
cascade='all, delete-orphan'
|
||||||
)
|
)
|
||||||
chats: Mapped[List['Chat']] = relationship(secondary='chat_participant', back_populates='users')
|
chats: Mapped[List['Chat']] = relationship(secondary='system_chat_participant', back_populates='users')
|
||||||
messages: Mapped[List['Message']] = relationship(back_populates='user')
|
messages: Mapped[List['Message']] = relationship(back_populates='user')
|
||||||
read_statuses: Mapped[List['ReadStatus']] = relationship(back_populates='user')
|
read_statuses: Mapped[List['ReadStatus']] = relationship(back_populates='user')
|
||||||
|
|
||||||
@@ -261,7 +261,7 @@ class Chat(BaseModel, DateModel):
|
|||||||
|
|
||||||
guid: Mapped[uuid.UUID] = mapped_column(String(36), nullable=True, default=lambda: str(uuid.uuid4()))
|
guid: Mapped[uuid.UUID] = mapped_column(String(36), nullable=True, default=lambda: str(uuid.uuid4()))
|
||||||
|
|
||||||
users: Mapped[List['User']] = relationship(secondary='chat_participant', back_populates='chats')
|
users: Mapped[List['User']] = relationship(secondary='system_chat_participant', back_populates='chats')
|
||||||
messages: Mapped[List['Message']] = relationship(back_populates='chat', cascade='all,delete')
|
messages: Mapped[List['Message']] = relationship(back_populates='chat', cascade='all,delete')
|
||||||
is_deleted: Mapped[bool] = mapped_column(Boolean, default=False)
|
is_deleted: Mapped[bool] = mapped_column(Boolean, default=False)
|
||||||
read_statuses: Mapped[List['ReadStatus']] = relationship(back_populates='chat', cascade='all,delete')
|
read_statuses: Mapped[List['ReadStatus']] = relationship(back_populates='chat', cascade='all,delete')
|
||||||
@@ -326,11 +326,12 @@ class PointsTransaction(BaseModel, DateModel):
|
|||||||
description: Mapped[str] = mapped_column(String(255), nullable=True, comment='交易描述')
|
description: Mapped[str] = mapped_column(String(255), nullable=True, comment='交易描述')
|
||||||
|
|
||||||
|
|
||||||
|
class ChatParticipant(BaseModel):
|
||||||
chat_participant = Table(
|
__tablename__ = 'system_chat_participant'
|
||||||
'chat_participant',
|
__table_args__ = (
|
||||||
BaseModel.metadata,
|
{'comment': '聊天参与者关联表'}
|
||||||
Column('user_id', Integer, ForeignKey('system_users.user_id'), primary_key=True),
|
|
||||||
Column('chat_id', Integer, ForeignKey('system_chats.id'), primary_key=True),
|
|
||||||
comment='聊天参与者关联表'
|
|
||||||
)
|
)
|
||||||
|
id: None = None
|
||||||
|
user_id: Mapped[int] = mapped_column(Integer, ForeignKey('system_users.user_id'), primary_key=True)
|
||||||
|
chat_id: Mapped[int] = mapped_column(Integer, ForeignKey('system_chats.id'), primary_key=True)
|
||||||
|
is_deleted: Mapped[bool] = mapped_column(Boolean, default=False)
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ from sqlalchemy.orm import selectinload
|
|||||||
from core.base import schema as base_schema
|
from core.base import schema as base_schema
|
||||||
from core.db.crud import CRUD
|
from core.db.crud import CRUD
|
||||||
from core.db.engine import get_async_session
|
from core.db.engine import get_async_session
|
||||||
from core.db.models import Message, Chat, User, ReadStatus, chat_participant
|
from core.db.models import Message, Chat, User, ReadStatus, ChatParticipant
|
||||||
from core.routers.auth.schema import LoginUser
|
from core.routers.auth.schema import LoginUser
|
||||||
from core.routers.auth.services import get_current_user_form_http, get_current_user_form_ws
|
from core.routers.auth.services import get_current_user_form_http, get_current_user_form_ws
|
||||||
from core.base.exceptions import WebsocketTooManyRequests
|
from core.base.exceptions import WebsocketTooManyRequests
|
||||||
@@ -47,10 +47,12 @@ async def get_all_chats(
|
|||||||
):
|
):
|
||||||
filter_conditions = (
|
filter_conditions = (
|
||||||
exists().where(
|
exists().where(
|
||||||
(chat_participant.c.chat_id == Chat.id) &
|
and_(
|
||||||
(chat_participant.c.user_id == login_user.user_id)
|
ChatParticipant.chat_id == Chat.id,
|
||||||
) &
|
ChatParticipant.user_id == login_user.user_id,
|
||||||
Chat.is_deleted.is_(False)
|
ChatParticipant.is_deleted == False
|
||||||
|
)
|
||||||
|
)
|
||||||
)
|
)
|
||||||
query = (
|
query = (
|
||||||
select(Chat)
|
select(Chat)
|
||||||
@@ -163,12 +165,12 @@ async def create_new_chat(
|
|||||||
.where(
|
.where(
|
||||||
and_(
|
and_(
|
||||||
exists().where(
|
exists().where(
|
||||||
(chat_participant.c.chat_id == Chat.id) &
|
(ChatParticipant.chat_id == Chat.id) &
|
||||||
(chat_participant.c.user_id == login_user.user_id)
|
(ChatParticipant.user_id == login_user.user_id)
|
||||||
),
|
),
|
||||||
exists().where(
|
exists().where(
|
||||||
(chat_participant.c.chat_id == Chat.id) &
|
(ChatParticipant.chat_id == Chat.id) &
|
||||||
(chat_participant.c.user_id == recipient_user.user_id)
|
(ChatParticipant.user_id == recipient_user.user_id)
|
||||||
),
|
),
|
||||||
Chat.is_deleted == False
|
Chat.is_deleted == False
|
||||||
)
|
)
|
||||||
@@ -209,23 +211,34 @@ async def delete_single_chat(
|
|||||||
db_session: AsyncSession = Depends(get_async_session),
|
db_session: AsyncSession = Depends(get_async_session),
|
||||||
):
|
):
|
||||||
subquery = (
|
subquery = (
|
||||||
select(chat_participant.c.chat_id)
|
select(ChatParticipant.chat_id)
|
||||||
.where(chat_participant.c.user_id == login_user.user_id)
|
.where(ChatParticipant.user_id == login_user.user_id)
|
||||||
).scalar_subquery()
|
).scalar_subquery()
|
||||||
|
|
||||||
query = (
|
query = (
|
||||||
update(Chat)
|
select(Chat)
|
||||||
.where(and_(
|
.where(and_(
|
||||||
Chat.id.in_(subquery),
|
Chat.id.in_(subquery),
|
||||||
Chat.guid == chat_guid
|
Chat.guid == chat_guid
|
||||||
))
|
))
|
||||||
.values(is_deleted=True)
|
|
||||||
)
|
)
|
||||||
result = await db_session.execute(query)
|
result = await db_session.execute(query)
|
||||||
await db_session.commit()
|
chat = result.scalar_one_or_none()
|
||||||
|
|
||||||
if result.rowcount == 0:
|
if chat is None:
|
||||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail='聊天不存在')
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail='聊天不存在')
|
||||||
|
else:
|
||||||
|
db_curd = CRUD(db_session, Chat)
|
||||||
|
await db_curd.update(
|
||||||
|
data={'is_deleted': True},
|
||||||
|
filters={'id': chat.id},
|
||||||
|
)
|
||||||
|
|
||||||
|
await db_curd.update(
|
||||||
|
model=ChatParticipant,
|
||||||
|
data={'is_deleted': True},
|
||||||
|
filters={'user_id': login_user.user_id, 'chat_id': chat.id},
|
||||||
|
)
|
||||||
|
|
||||||
return {'message': '聊天已删除'}
|
return {'message': '聊天已删除'}
|
||||||
|
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ class ChatUnreadOut(BaseModel):
|
|||||||
created_at: str
|
created_at: str
|
||||||
updated_at: str
|
updated_at: str
|
||||||
recipient_user: LiteUser
|
recipient_user: LiteUser
|
||||||
|
is_delete: bool = False
|
||||||
last_message: PreviewMessage | None
|
last_message: PreviewMessage | None
|
||||||
unread_messages_count: int
|
unread_messages_count: int
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ from sqlalchemy.ext.asyncio import AsyncSession
|
|||||||
from sqlalchemy.orm import aliased, selectinload
|
from sqlalchemy.orm import aliased, selectinload
|
||||||
|
|
||||||
from core.base.exceptions import WebsocketTooManyRequests
|
from core.base.exceptions import WebsocketTooManyRequests
|
||||||
from core.db.models import User, Chat, ReadStatus, Message, chat_participant
|
from core.db.models import User, Chat, ReadStatus, Message, ChatParticipant
|
||||||
from core.routers.auth.schema import LoginUser
|
from core.routers.auth.schema import LoginUser
|
||||||
from core.utils.helper import format_date
|
from core.utils.helper import format_date
|
||||||
|
|
||||||
@@ -38,12 +38,14 @@ async def get_user_active_direct_chats(
|
|||||||
direct_chats_dict[str(direct_chat.guid)] = direct_chat.id
|
direct_chats_dict[str(direct_chat.guid)] = direct_chat.id
|
||||||
return direct_chats_dict
|
return direct_chats_dict
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
async def get_unread_messages_per_chat(
|
async def get_unread_messages_per_chat(
|
||||||
db_session: AsyncSession, chats: list[Chat], login_user: LoginUser
|
db_session: AsyncSession, chats: list[Chat], login_user: LoginUser
|
||||||
) -> list[dict]:
|
) -> list[dict]:
|
||||||
"""获取聊天未读消息数"""
|
"""获取聊天未读消息数"""
|
||||||
unread_messages_info_per_chat = {chat.id: {'message_count': 0} for chat in chats}
|
unread_messages_info_per_chat: dict[int, dict] = {chat.id: {'message_count': 0} for chat in chats}
|
||||||
|
|
||||||
# 查询消息情况
|
# 查询消息情况
|
||||||
read_status_alias = aliased(ReadStatus)
|
read_status_alias = aliased(ReadStatus)
|
||||||
@@ -85,6 +87,7 @@ async def get_unread_messages_per_chat(
|
|||||||
updated_at=format_date(chat.updated_at),
|
updated_at=format_date(chat.updated_at),
|
||||||
recipient_user=users[0],
|
recipient_user=users[0],
|
||||||
last_message=messages_info.get('message'),
|
last_message=messages_info.get('message'),
|
||||||
|
is_delete=chat.is_deleted,
|
||||||
unread_messages_count=messages_info.get('message_count'),
|
unread_messages_count=messages_info.get('message_count'),
|
||||||
))
|
))
|
||||||
|
|
||||||
@@ -93,8 +96,8 @@ async def get_unread_messages_per_chat(
|
|||||||
|
|
||||||
async def get_chat(chat_guid, login_user: User, db_session: AsyncSession) -> Chat | None:
|
async def get_chat(chat_guid, login_user: User, db_session: AsyncSession) -> Chat | None:
|
||||||
subquery = (
|
subquery = (
|
||||||
select(chat_participant.c.chat_id)
|
select(ChatParticipant.chat_id)
|
||||||
.where(chat_participant.c.user_id == login_user.user_id)
|
.where(ChatParticipant.user_id == login_user.user_id)
|
||||||
).scalar_subquery()
|
).scalar_subquery()
|
||||||
|
|
||||||
query = (
|
query = (
|
||||||
|
|||||||
Reference in New Issue
Block a user