From 709e0a26680372c44fd768c117c7e362b6fed749 Mon Sep 17 00:00:00 2001 From: AjunPlus <403621656@qq.com> Date: Mon, 23 Feb 2026 23:21:01 +0800 Subject: [PATCH 1/5] feat:refine work~~~ --- api/deps.py | 23 ++++++++++++++++++++++- api/routes/users.py | 23 +++++++++++++++++------ crud.py | 42 ++++++++++++++++++++++++++++++++++++++---- models.py | 4 ++-- 4 files changed, 79 insertions(+), 13 deletions(-) diff --git a/api/deps.py b/api/deps.py index ce32bfb..02833d7 100644 --- a/api/deps.py +++ b/api/deps.py @@ -1,9 +1,10 @@ from sqlmodel import Session -from typing import Annotated +from typing import Annotated, Any from fastapi import Depends, HTTPException from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer from collections.abc import Generator +from models import User from core.db import engine from core.security import local_token @@ -28,3 +29,23 @@ def require_fixed_token( TokenDeps = Annotated[HTTPAuthorizationCredentials, Depends(require_fixed_token)] SessionDeps = Annotated[Session, Depends(get_db)] + + +def parse_filters(params: dict[str, Any]) -> dict[str, tuple[str, Any]]: + filters = {} + for key, value in params.items(): + if value is None or value == "": + continue + + for operator in ["_ne", "_lte", "_lt", "_gte", "_gt", "_like"]: + if key.endswith(operator): + field_name = key[: -len(operator)] + filters[field_name] = (operator[1:], value) + break + + else: + if key not in ["_start", "_end", "page", "_per_page", "_sort", "_order", "id"]: + if hasattr(User, key): + filters[key] = ("eq", value) + + return filters diff --git a/api/routes/users.py b/api/routes/users.py index d553b2c..672f149 100644 --- a/api/routes/users.py +++ b/api/routes/users.py @@ -1,25 +1,36 @@ import uuid import crud -from crud import UserNotFound, UserAlreadyExists -from fastapi import APIRouter, HTTPException +from fastapi import APIRouter, HTTPException, Response, Query +from typing import Literal, Annotated from api.deps import SessionDeps +from crud import UserNotFound, UserAlreadyExists from models import ( UserPublic, UserCreate, UserRegister, UserUpdate, - UsersPublic, - Message + Message, ) router = APIRouter(prefix="/users", tags=["users"]) -@router.get("/", response_model=UsersPublic) -def read_users(*, session: SessionDeps, skip: int = 0, limit: int = 100) -> UsersPublic: +@router.get("/", response_model=list[UserPublic]) +def read_users( + *, + response: Response, + session: SessionDeps, + _start: Annotated[int, Query(ge=0)] = 0, + _end: Annotated[int, Query(ge=1)] = 100, + page: Annotated[int, Query(ge=1)] = 1, + _per_page: Annotated[int, Query(ge=1, le=100)] = 100, + _sort: Annotated[str | None, Query()] = None, + _order: Literal["asc", "desc"] = "asc", +) -> list[UserPublic]: + users = crud.get_users(session=session, skip=skip, limit=limit) return users diff --git a/crud.py b/crud.py index be8d9ea..57b5f9b 100644 --- a/crud.py +++ b/crud.py @@ -2,6 +2,7 @@ from pydantic import EmailStr from sqlmodel import Session, select, func +from typing import Any, Literal from models import UserCreate, User, UserUpdate, Users from core.security import get_password_hash @@ -33,12 +34,45 @@ def get_users( session: Session, skip: int=0, limit: int=100, + sort_field: str | None =None, + sort_order: Literal["asc", "desc"]="asc", + filters: dict[str, tuple[str, Any]] | None=None, ) -> Users: - statement_count = select(func.count()).select_from(User) - count = session.exec(statement_count).one() - statement = select(User).offset(skip).limit(limit) + statement = select(User) + if filters: + for field_name, (operator, value) in filters.items(): + if not hasattr(User, field_name): + continue + column = getattr(User, field_name) + + if operator == "eq": + statement = statement.where(column == value) + elif operator == "ne": + statement = statement.where(column != value) + elif operator == "lt": + statement = statement.where(column < value) + elif operator == "lte": + statement = statement.where(column <= value) + elif operator == "gt": + statement = statement.where(column > value) + elif operator == "gte": + statement = statement.where(column >= value) + elif operator == "like": + statement = statement.where(column.ilike(f"%{value}%")) + + count_statement = select(func.count()).select_from(statement) + count = session.exec(count_statement).one() + + if sort_field and hasattr(User, sort_field): + column = getattr(User, sort_field) + if sort_order == "asc": + statement = statement.order_by(column.asc()) + else: + statement = statement.order_by(column.desc()) + + statement = statement.offset(skip).limit(limit) users = session.exec(statement).all() - return Users(data=users, count=count) + return Users(data=users, total=count) def delete_user(*, user_id: uuid.UUID, session: Session) -> None: diff --git a/models.py b/models.py index aa43762..fcc88ab 100644 --- a/models.py +++ b/models.py @@ -18,7 +18,7 @@ class User(UserBase, table=True): class Users(SQLModel): data: list[User] - count: int + total: int class UserCreate(UserBase): @@ -52,7 +52,7 @@ class UserPublic(UserBase): class UsersPublic(SQLModel): data: list[UserPublic] - count: int + total: int class Message(SQLModel): From baf3b27427619fdb6fa21ba380c6010e27eea735 Mon Sep 17 00:00:00 2001 From: AjunPlus <403621656@qq.com> Date: Tue, 24 Feb 2026 23:55:55 +0800 Subject: [PATCH 2/5] =?UTF-8?q?feat:=E6=B7=BB=E5=8A=A0=E5=88=86=E9=A1=B5?= =?UTF-8?q?=E6=9F=A5=E8=AF=A2=EF=BC=8C=E4=BF=AE=E6=94=B9=E5=93=8D=E5=BA=94?= =?UTF-8?q?=E6=A0=BC=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- api/routes/users.py | 78 +++++++++++++++++++++++++++++++-------------- 1 file changed, 54 insertions(+), 24 deletions(-) diff --git a/api/routes/users.py b/api/routes/users.py index 672f149..8e41a25 100644 --- a/api/routes/users.py +++ b/api/routes/users.py @@ -1,16 +1,19 @@ import uuid import crud -from fastapi import APIRouter, HTTPException, Response, Query +from fastapi import APIRouter, HTTPException, Query, Request from typing import Literal, Annotated -from api.deps import SessionDeps +from api.deps import SessionDeps, parse_filters from crud import UserNotFound, UserAlreadyExists from models import ( UserPublic, UserCreate, + UserCreateResponse, UserRegister, UserUpdate, + UserUpdateResponse, + UsersPublic, Message, ) @@ -18,25 +21,52 @@ router = APIRouter(prefix="/users", tags=["users"]) -@router.get("/", response_model=list[UserPublic]) +@router.get("/", response_model=UsersPublic) def read_users( *, - response: Response, session: SessionDeps, - _start: Annotated[int, Query(ge=0)] = 0, - _end: Annotated[int, Query(ge=1)] = 100, - page: Annotated[int, Query(ge=1)] = 1, - _per_page: Annotated[int, Query(ge=1, le=100)] = 100, + request: Request, + _start: Annotated[int | None , Query(ge=0)] = None, + _end: Annotated[int | None, Query(ge=1)] = None, + page: Annotated[int | None, Query(ge=1)] = None, + _per_page: Annotated[int | None, Query(ge=1, le=100)] = None, _sort: Annotated[str | None, Query()] = None, - _order: Literal["asc", "desc"] = "asc", -) -> list[UserPublic]: - - users = crud.get_users(session=session, skip=skip, limit=limit) - return users - - -@router.post("/", response_model=UserPublic) -def create_user(*, user_in: UserCreate, session: SessionDeps) -> UserPublic: + _order: Annotated[Literal["asc", "desc"], Query()] = "asc", + id: Annotated[str | None, Query()] = None, +) -> UsersPublic: + all_params = dict(request.query_params) + + for key in ["_start", "_end", "page", "_per_page", "_sort", "_order", "id"]: + all_params.pop(key, None) + + if id: + user_ids = [uuid.UUID(uid.strip()) for uid in id.split(",")] + users = crud.get_user_by_ids(session=session, user_ids=user_ids) + return UsersPublic(data=users, total=len(users)) + + if _start is not None and _end is not None: + skip = _start + limit = _end - _start + elif page is not None and _per_page is not None: + skip = (page - 1) * _per_page + limit = _per_page + + filters = parse_filters(all_params) + + users = crud.get_users( + session=session, + skip=skip, + limit=limit, + sort_field=_sort, + sort_order=_order, + filters=filters, + ) + + return UsersPublic(data=users.data, total=users.total) + + +@router.post("/", response_model=UserCreateResponse) +def create_user(*, user_in: UserCreate, session: SessionDeps) -> UserCreateResponse: user = crud.get_user_by_email(email=user_in.email, session=session) if user: raise HTTPException(status_code=400, detail="Email already registered") @@ -44,8 +74,8 @@ def create_user(*, user_in: UserCreate, session: SessionDeps) -> UserPublic: return user -@router.post("/signup", response_model=UserPublic) -def register_user(*, session: SessionDeps, user_in: UserRegister) -> UserPublic: +@router.post("/signup", response_model=UserCreateResponse) +def register_user(*, session: SessionDeps, user_in: UserRegister) -> UserCreateResponse: user = crud.get_user_by_email(email=user_in.email, session=session) if user: raise HTTPException(status_code=400, detail="Email already registered") @@ -60,21 +90,21 @@ def read_user_by_id(*, session: SessionDeps, user_id: uuid.UUID) -> UserPublic | return user -@router.patch("/{user_id}",response_model=UserPublic) -def update_user(*, session: SessionDeps, user_id: uuid.UUID, user_in: UserUpdate) -> UserPublic: +@router.patch("/{user_id}",response_model=UserUpdateResponse) +def update_user(*, session: SessionDeps, user_id: uuid.UUID, user_in: UserUpdate) -> UserUpdateResponse: try: user = crud.update_user(session=session, user_id=user_id, user_in=user_in) + return user except UserNotFound: raise HTTPException(status_code=404, detail="User not found") except UserAlreadyExists: raise HTTPException(status_code=409, detail="User with this email already exists") - return user @router.delete("/{user_id}", response_model=Message) def delete_user(*, session: SessionDeps, user_id: uuid.UUID) -> Message: try: - crud.delete_user(session=session, user_id=user_id) + result = crud.delete_user(session=session, user_id=user_id) + return result except UserNotFound: raise HTTPException(status_code=404, detail="User not found") - return Message(message="User has been deleted!") From 84f5fbf88e0d07ddfc77de9c2ab13ff349f4996a Mon Sep 17 00:00:00 2001 From: AjunPlus <403621656@qq.com> Date: Tue, 24 Feb 2026 23:56:51 +0800 Subject: [PATCH 3/5] =?UTF-8?q?feat:=E4=BF=AE=E6=94=B9=E7=94=A8=E6=88=B7?= =?UTF-8?q?=E8=A7=A3=E6=9E=90=E5=87=BD=E6=95=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crud.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/crud.py b/crud.py index 57b5f9b..50d8b83 100644 --- a/crud.py +++ b/crud.py @@ -4,7 +4,7 @@ from sqlmodel import Session, select, func from typing import Any, Literal -from models import UserCreate, User, UserUpdate, Users +from models import UserCreate, User, UserUpdate, Users, Message from core.security import get_password_hash @@ -23,6 +23,12 @@ def get_user_by_id(*, user_id: uuid.UUID, session: Session) -> User | None: return db_user +def get_user_by_ids(*, session: Session, user_ids: list[uuid.UUID]) -> list[User]: + statement = select(User).where(User.id.in_(user_ids)) + users = session.exec(statement).all() + return users + + def get_user_by_email(*, email: EmailStr, session: Session) -> User | None: statement = select(User).where(User.email == email) db_user = session.exec(statement).first() @@ -75,12 +81,13 @@ def get_users( return Users(data=users, total=count) -def delete_user(*, user_id: uuid.UUID, session: Session) -> None: +def delete_user(*, user_id: uuid.UUID, session: Session) -> Message: user_db = session.get(User, user_id) if not user_db: raise UserNotFound(user_id) session.delete(user_db) session.commit() + return Message(id=user_id) def create_user(*, user_create: UserCreate, session: Session) -> User: From 7f664a7705930e8172f7563367395d57ab725b21 Mon Sep 17 00:00:00 2001 From: AjunPlus <403621656@qq.com> Date: Tue, 24 Feb 2026 23:57:25 +0800 Subject: [PATCH 4/5] =?UTF-8?q?feat:=E6=A8=A1=E5=9E=8B=E6=B7=BB=E5=8A=A0?= =?UTF-8?q?=E5=88=9B=E5=BB=BA=E6=97=B6=E9=97=B4=EF=BC=8C=E4=BF=AE=E6=94=B9?= =?UTF-8?q?=E6=97=B6=E9=97=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- models.py | 35 +++++++++++++++++++++++++++++++++-- 1 file changed, 33 insertions(+), 2 deletions(-) diff --git a/models.py b/models.py index fcc88ab..1599cdd 100644 --- a/models.py +++ b/models.py @@ -1,6 +1,8 @@ import uuid -from sqlmodel import SQLModel, Field +from datetime import datetime, timezone +from sqlmodel import SQLModel, Field, Column +from sqlalchemy import DateTime, func from pydantic import EmailStr @@ -15,6 +17,25 @@ class User(UserBase, table=True): id: uuid.UUID = Field(default_factory=uuid.uuid4, primary_key=True) hashed_password: str + created_at: datetime = Field( + default_factory=lambda: datetime.now(timezone.utc), + sa_column=Column( + DateTime(timezone=True), + nullable=False, + server_default=func.now(), + ), + ) + + updated_at: datetime = Field( + default_factory=lambda: datetime.now(timezone.utc), + sa_column=Column( + DateTime(timezone=True), + nullable=False, + server_default=func.now(), + onupdate=func.now(), + ), + ) + class Users(SQLModel): data: list[User] @@ -50,13 +71,23 @@ class UserPublic(UserBase): id: uuid.UUID +class UserCreateResponse(UserBase): + id: uuid.UUID + created_at: datetime = Field(serialization_alias="createdAt") + + +class UserUpdateResponse(UserBase): + id: uuid.UUID + updated_at: datetime = Field(serialization_alias="updatedAt") + + class UsersPublic(SQLModel): data: list[UserPublic] total: int class Message(SQLModel): - message: str + id: uuid.UUID class BearerToken(SQLModel): From 4a60c89a57d7ea24b75da5472f6ddbb668ab3b05 Mon Sep 17 00:00:00 2001 From: AjunPlus <403621656@qq.com> Date: Tue, 24 Feb 2026 23:57:42 +0800 Subject: [PATCH 5/5] =?UTF-8?q?feat:=E6=A8=A1=E5=9E=8B=E6=B7=BB=E5=8A=A0?= =?UTF-8?q?=E5=88=9B=E5=BB=BA=E6=97=B6=E9=97=B4=EF=BC=8C=E4=BF=AE=E6=94=B9?= =?UTF-8?q?=E6=97=B6=E9=97=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ..._add_created_at_and_updated_at_to_users.py | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 alembic/versions/744577be90bf_add_created_at_and_updated_at_to_users.py diff --git a/alembic/versions/744577be90bf_add_created_at_and_updated_at_to_users.py b/alembic/versions/744577be90bf_add_created_at_and_updated_at_to_users.py new file mode 100644 index 0000000..36cf237 --- /dev/null +++ b/alembic/versions/744577be90bf_add_created_at_and_updated_at_to_users.py @@ -0,0 +1,34 @@ +"""add created_at and updated_at to users + +Revision ID: 744577be90bf +Revises: dd5df88ee3df +Create Date: 2026-02-24 22:16:44.362026 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = '744577be90bf' +down_revision: Union[str, Sequence[str], None] = 'dd5df88ee3df' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + """Upgrade schema.""" + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('user', sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False)) + op.add_column('user', sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False)) + # ### end Alembic commands ### + + +def downgrade() -> None: + """Downgrade schema.""" + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column('user', 'updated_at') + op.drop_column('user', 'created_at') + # ### end Alembic commands ###