From cc097a587f0231a77429065b39631aa46de7840b Mon Sep 17 00:00:00 2001 From: xingc Date: Thu, 15 May 2025 18:24:05 +0800 Subject: [PATCH] =?UTF-8?q?feat(chat):=20=E8=81=8A=E5=A4=A9=E5=8D=95?= =?UTF-8?q?=E5=90=91=E5=88=A0=E9=99=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- alembic/env.py | 26 +++++++- .../versions/2d63d72e671a_trim_card_table.py | 2 +- ...03470b2c09_del_product_action_free_gift.py | 16 ++--- ...9466c397aa2_add_chat_participant_to_orm.py | 62 +++++++++++++++++++ core/db/models.py | 27 ++++---- core/routers/chat/routes.py | 45 +++++++++----- core/routers/chat/schema.py | 1 + core/routers/chat/services.py | 11 ++-- 8 files changed, 147 insertions(+), 43 deletions(-) create mode 100644 alembic/versions/e9466c397aa2_add_chat_participant_to_orm.py diff --git a/alembic/env.py b/alembic/env.py index c3423b1..9384a3d 100644 --- a/alembic/env.py +++ b/alembic/env.py @@ -27,6 +27,30 @@ from core.db.models import BaseModel # 假设你的 SQLAlchemy 模型在 models 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): context.configure( connection=connection, @@ -62,4 +86,4 @@ else: asyncio.set_event_loop(loop) # 运行迁移 - loop.run_until_complete(run_migrations_online()) \ No newline at end of file + loop.run_until_complete(run_migrations_online()) diff --git a/alembic/versions/2d63d72e671a_trim_card_table.py b/alembic/versions/2d63d72e671a_trim_card_table.py index b9f8866..10eebca 100644 --- a/alembic/versions/2d63d72e671a_trim_card_table.py +++ b/alembic/versions/2d63d72e671a_trim_card_table.py @@ -24,4 +24,4 @@ def upgrade() -> None: def downgrade() -> None: - pass + op.drop_column("system_cards", 'diy_source') diff --git a/alembic/versions/4a03470b2c09_del_product_action_free_gift.py b/alembic/versions/4a03470b2c09_del_product_action_free_gift.py index a1d0b22..707bf9f 100644 --- a/alembic/versions/4a03470b2c09_del_product_action_free_gift.py +++ b/alembic/versions/4a03470b2c09_del_product_action_free_gift.py @@ -21,18 +21,18 @@ depends_on: Union[str, Sequence[str], None] = None def upgrade() -> None: # ### commands auto generated by Alembic - please adjust! ### op.alter_column('system_products', 'action', - existing_type=mysql.ENUM('VIP_RENEWAL', 'RECHARGE_POINTS', 'FREE_GIFT'), - comment='付款后的动作,会员续期:vip_renewal 积分:recharge_points', - existing_comment='付款后的动作,会员续期:vip_renewal 积分:recharge_points 赠送产品:free_gift', - existing_nullable=False) + existing_type=mysql.ENUM('VIP_RENEWAL', 'RECHARGE_POINTS', 'FREE_GIFT'), + comment='付款后的动作,会员续期:vip_renewal 积分:recharge_points', + existing_comment='付款后的动作,会员续期:vip_renewal 积分:recharge_points 赠送产品:free_gift', + existing_nullable=False) # ### end Alembic commands ### def downgrade() -> None: # ### commands auto generated by Alembic - please adjust! ### op.alter_column('system_products', 'action', - existing_type=mysql.ENUM('VIP_RENEWAL', 'RECHARGE_POINTS', 'FREE_GIFT'), - comment='付款后的动作,会员续期:vip_renewal 积分:recharge_points 赠送产品:free_gift', - existing_comment='付款后的动作,会员续期:vip_renewal 积分:recharge_points', - existing_nullable=False) + existing_type=mysql.ENUM('VIP_RENEWAL', 'RECHARGE_POINTS'), + comment='付款后的动作,会员续期:vip_renewal 积分:recharge_points', + existing_comment='付款后的动作,会员续期:vip_renewal 积分:recharge_points', + existing_nullable=False) # ### end Alembic commands ### diff --git a/alembic/versions/e9466c397aa2_add_chat_participant_to_orm.py b/alembic/versions/e9466c397aa2_add_chat_participant_to_orm.py new file mode 100644 index 0000000..c1cdf10 --- /dev/null +++ b/alembic/versions/e9466c397aa2_add_chat_participant_to_orm.py @@ -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 ### diff --git a/core/db/models.py b/core/db/models.py index 290db60..6d136db 100644 --- a/core/db/models.py +++ b/core/db/models.py @@ -97,7 +97,7 @@ class User(BaseModel, DateModel): foreign_keys='UserFollows.followed_id', 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') 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())) - 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') is_deleted: Mapped[bool] = mapped_column(Boolean, default=False) read_statuses: Mapped[List['ReadStatus']] = relationship(back_populates='chat', cascade='all,delete') @@ -320,17 +320,18 @@ class PointsTransaction(BaseModel, DateModel): user_id: Mapped[int] = mapped_column(Integer, comment='用户id') transaction_type: Mapped[int] = mapped_column(Integer, comment='交易类型:1-充值 2-消费 3-赠送 4-过期 5-系统调整') product_id: Mapped[int] = mapped_column(Integer, nullable=True, comment='关联产品id') - points : Mapped[int] = mapped_column(Integer, comment='积分数量(正数表示增加,负数表示减少)') - balance : Mapped[int] = mapped_column(Integer, comment='交易后余额') - status : Mapped[int] = mapped_column(Integer, default=1, comment='状态:0-无效 1-有效 2-已撤销') + points: Mapped[int] = mapped_column(Integer, comment='积分数量(正数表示增加,负数表示减少)') + balance: Mapped[int] = mapped_column(Integer, comment='交易后余额') + status: Mapped[int] = mapped_column(Integer, default=1, comment='状态:0-无效 1-有效 2-已撤销') description: Mapped[str] = mapped_column(String(255), nullable=True, comment='交易描述') - -chat_participant = Table( - 'chat_participant', - BaseModel.metadata, - Column('user_id', Integer, ForeignKey('system_users.user_id'), primary_key=True), - Column('chat_id', Integer, ForeignKey('system_chats.id'), primary_key=True), - comment='聊天参与者关联表' -) +class ChatParticipant(BaseModel): + __tablename__ = 'system_chat_participant' + __table_args__ = ( + {'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) diff --git a/core/routers/chat/routes.py b/core/routers/chat/routes.py index aa72fbe..eba7e0d 100644 --- a/core/routers/chat/routes.py +++ b/core/routers/chat/routes.py @@ -17,7 +17,7 @@ from sqlalchemy.orm import selectinload from core.base import schema as base_schema from core.db.crud import CRUD 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.services import get_current_user_form_http, get_current_user_form_ws from core.base.exceptions import WebsocketTooManyRequests @@ -46,11 +46,13 @@ async def get_all_chats( page_size: int = Query(default=15, gt=0, le=200), ): filter_conditions = ( - exists().where( - (chat_participant.c.chat_id == Chat.id) & - (chat_participant.c.user_id == login_user.user_id) - ) & - Chat.is_deleted.is_(False) + exists().where( + and_( + ChatParticipant.chat_id == Chat.id, + ChatParticipant.user_id == login_user.user_id, + ChatParticipant.is_deleted == False + ) + ) ) query = ( select(Chat) @@ -163,12 +165,12 @@ async def create_new_chat( .where( and_( exists().where( - (chat_participant.c.chat_id == Chat.id) & - (chat_participant.c.user_id == login_user.user_id) + (ChatParticipant.chat_id == Chat.id) & + (ChatParticipant.user_id == login_user.user_id) ), exists().where( - (chat_participant.c.chat_id == Chat.id) & - (chat_participant.c.user_id == recipient_user.user_id) + (ChatParticipant.chat_id == Chat.id) & + (ChatParticipant.user_id == recipient_user.user_id) ), Chat.is_deleted == False ) @@ -209,23 +211,34 @@ async def delete_single_chat( db_session: AsyncSession = Depends(get_async_session), ): subquery = ( - select(chat_participant.c.chat_id) - .where(chat_participant.c.user_id == login_user.user_id) + select(ChatParticipant.chat_id) + .where(ChatParticipant.user_id == login_user.user_id) ).scalar_subquery() query = ( - update(Chat) + select(Chat) .where(and_( Chat.id.in_(subquery), Chat.guid == chat_guid )) - .values(is_deleted=True) ) 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='聊天不存在') + 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': '聊天已删除'} diff --git a/core/routers/chat/schema.py b/core/routers/chat/schema.py index 9f9f3f8..307ab0a 100644 --- a/core/routers/chat/schema.py +++ b/core/routers/chat/schema.py @@ -36,6 +36,7 @@ class ChatUnreadOut(BaseModel): created_at: str updated_at: str recipient_user: LiteUser + is_delete: bool = False last_message: PreviewMessage | None unread_messages_count: int diff --git a/core/routers/chat/services.py b/core/routers/chat/services.py index 2927f6b..0ab43e6 100644 --- a/core/routers/chat/services.py +++ b/core/routers/chat/services.py @@ -11,7 +11,7 @@ from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.orm import aliased, selectinload 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.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 return direct_chats_dict + return None + async def get_unread_messages_per_chat( db_session: AsyncSession, chats: list[Chat], login_user: LoginUser ) -> 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) @@ -85,6 +87,7 @@ async def get_unread_messages_per_chat( updated_at=format_date(chat.updated_at), recipient_user=users[0], last_message=messages_info.get('message'), + is_delete=chat.is_deleted, 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: subquery = ( - select(chat_participant.c.chat_id) - .where(chat_participant.c.user_id == login_user.user_id) + select(ChatParticipant.chat_id) + .where(ChatParticipant.user_id == login_user.user_id) ).scalar_subquery() query = (