From 0cf3145e86e2c276da7ca0249288d1ec11ee2cc3 Mon Sep 17 00:00:00 2001 From: xingc Date: Thu, 16 Oct 2025 15:28:07 +0800 Subject: [PATCH] =?UTF-8?q?feat(auth=E3=80=81user):=E6=96=B0=E5=A2=9E?= =?UTF-8?q?=E7=94=A8=E6=88=B7=E7=9A=84=E7=B2=89=E4=B8=9D=E6=95=B0=E3=80=81?= =?UTF-8?q?=E5=85=B3=E6=B3=A8=E6=95=B0=E3=80=81=E7=AE=80=E4=BB=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- core/db/models.py | 8 +++-- core/routers/auth/routes.py | 9 +++++- core/routers/user/actions.py | 62 ++++++++++++++++++++++++++++++++++++ core/routers/user/routes.py | 10 ++++-- core/routers/user/schema.py | 21 ++++++++---- 5 files changed, 99 insertions(+), 11 deletions(-) create mode 100644 core/routers/user/actions.py diff --git a/core/db/models.py b/core/db/models.py index 54079e3..5b23b56 100644 --- a/core/db/models.py +++ b/core/db/models.py @@ -11,9 +11,9 @@ from typing import List import ujson from sqlalchemy import ( - Column, Integer, String, Text, Date, + Integer, String, Text, Date, ForeignKey, DECIMAL, CheckConstraint, - UniqueConstraint, Index, Sequence, JSON, Enum, Boolean, UUID, Table + UniqueConstraint, Index, Sequence, JSON, Enum, Boolean ) from sqlalchemy.orm import ( DeclarativeBase, Mapped, mapped_column, relationship @@ -80,6 +80,10 @@ class User(BaseModel, DateModel): phone: Mapped[str] = mapped_column(String(11), nullable=True, comment='手机号码') vip_expire_date: Mapped[date] = mapped_column(Date, default=set_default_vip_expire_date, comment='会员到期时间') points: Mapped[int] = mapped_column(Integer, default=0, comment='会员积分') + description: Mapped[str] = mapped_column(String(200), default='', comment='用户简介') + fans_count: Mapped[int] = mapped_column(Integer, default=0, comment='粉丝数') + followers_count: Mapped[int] = mapped_column(Integer, default=0, comment='关注数') + last_active_at: Mapped[datetime] = mapped_column(nullable=True, comment='最后活跃时间') collections: Mapped[list['UserCollection']] = relationship( back_populates='user', diff --git a/core/routers/auth/routes.py b/core/routers/auth/routes.py index 4efdd5d..7335757 100644 --- a/core/routers/auth/routes.py +++ b/core/routers/auth/routes.py @@ -5,6 +5,7 @@ # @Author : xingc # @Desc : import logging +from datetime import datetime from typing import Annotated import ujson @@ -21,7 +22,7 @@ from core.db.crud import CRUD from core.db.engine import get_async_session, get_cache from core.db.models import User from core.utils.helper import generate_keyname -from core.wechat.api import wechat_api, decrypt_sensitive_data +from core.wechat.api import wechat_api from . import schema from .services import create_access_token @@ -96,10 +97,16 @@ async def wechat_login( 'wechat_union_id': union_id, 'phone': payload.phone, 'avatar_url': payload.avatar_url, + 'last_active_at': datetime.now(), }, filters={'wechat_openid': openid} ) await redis_conn.delete(keyname) + else: + await db_crud.update( + data={'last_active_at': datetime.now()}, + filters={'wechat_openid': openid} + ) result = create_access_token({ 'sub': str(user.user_id) diff --git a/core/routers/user/actions.py b/core/routers/user/actions.py new file mode 100644 index 0000000..7acb5f0 --- /dev/null +++ b/core/routers/user/actions.py @@ -0,0 +1,62 @@ +# -*- coding: utf-8 -*- +# @File: actions.py +# @Date: 2025/10/15 18:58 +# @Software: PyCharm +# @Author: xingc +# @Desc: +import logging + +from sqlalchemy.ext.asyncio import AsyncSession + +from core.db.crud import CRUD +from core.db.engine import async_db_session +from core.db.models import User, UserFollows + +logger = logging.getLogger(__name__) + +async def update_user_follower_total( + follower_id: int, + followed_id: int, + db_session: AsyncSession +): + """ + 更新用户计数 + :param follower_id: 关注者id + :param followed_id: 被关注者id + :param db_session: + :return: + """ + db_crud = CRUD(db_session, UserFollows) + + # 更新关注数 + followers_count = await db_crud.total( + filters={ + 'follower_id': follower_id, + 'followed_id': followed_id, + } + ) + await db_crud.update( + model=User, + data={ + 'followers_count': followers_count, + }, + filters={ + 'user_id': follower_id, + } + ) + + # 更新粉丝数 + fans_count = await db_crud.total( + filters={ + 'followed_id': followed_id, + } + ) + await db_crud.update( + model=User, + data={ + 'fans_count': fans_count, + }, + filters={ + 'user_id': followed_id, + } + ) diff --git a/core/routers/user/routes.py b/core/routers/user/routes.py index d28ab70..ae3a396 100644 --- a/core/routers/user/routes.py +++ b/core/routers/user/routes.py @@ -18,6 +18,7 @@ from core.routers.auth.schema import LoginUser from core.routers.auth.services import get_current_user_form_http from . import schema +from .actions import update_user_follower_total user_router = APIRouter(prefix='/user', tags=['用户模块']) @@ -158,10 +159,15 @@ async def follow_user( } if action == 'follower': await db_crud.create(data=record, filters=record) - - if action == 'unfollower': + elif action == 'unfollower': await db_crud.delete(filters=record) + await update_user_follower_total( + follower_id=login_user.user_id, + followed_id=follower_user.user_id, + db_session=db_session + ) + return {'message': 'ok'} diff --git a/core/routers/user/schema.py b/core/routers/user/schema.py index 35b20e7..a04dc1c 100644 --- a/core/routers/user/schema.py +++ b/core/routers/user/schema.py @@ -7,11 +7,12 @@ from datetime import date from typing import Optional -from fastapi.openapi.models import Schema -from pydantic import BaseModel, constr +from pydantic import constr + +from core.base.schema import BaseMixin -class User(BaseModel): +class User(BaseMixin): nickname: str user_id: int gender: Optional[int] = None @@ -22,9 +23,13 @@ class User(BaseModel): vip_expire_date: Optional[date] = None points: Optional[int] = 0 displayed_card_total: Optional[int] = 0 + description: Optional[str | None] = None + last_active_at: Optional[str | None] = None + fans_count: Optional[int] = 0 + followers_count: Optional[int] = 0 -class LiteUser(BaseModel): +class LiteUser(BaseMixin): nickname: str user_id: int gender: Optional[int] = None @@ -35,16 +40,20 @@ class LiteUser(BaseModel): class VisitUser(LiteUser): is_follow: Optional[bool] = False displayed_card_total: Optional[int] = 0 + description: Optional[str | None] = None + last_active_at: Optional[str | None] = None + fans_count: Optional[int] = 0 -class UserFollows(BaseModel): +class UserFollows(BaseMixin): nickname: str user_id: int gender: Optional[int] = None avatar_url: Optional[int] = None -class UpdateUserIn(BaseModel): +class UpdateUserIn(BaseMixin): nickname: Optional[constr(max_length=20)] = None gender: Optional[int] = None avatar_url: Optional[constr(pattern='^http[a-zA-Z0-9:/\.]+')] = None + description: Optional[constr(max_length=200) | None] = None