diff --git a/alembic/versions/c64a944a6139_add_order_record_table.py b/alembic/versions/c64a944a6139_add_order_record_table.py new file mode 100644 index 0000000..ab9dc43 --- /dev/null +++ b/alembic/versions/c64a944a6139_add_order_record_table.py @@ -0,0 +1,49 @@ +"""add order_record table + +Revision ID: c64a944a6139 +Revises: 9c559acc164a +Create Date: 2026-05-26 21:09:40.047619 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = 'c64a944a6139' +down_revision: Union[str, Sequence[str], None] = '9c559acc164a' +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('order_record', + sa.Column('id', sa.Integer(), autoincrement=True, nullable=False), + sa.Column('user_id', sa.Integer(), nullable=False), + sa.Column('platform', sa.String(length=30), nullable=False), + sa.Column('store_name', sa.String(length=128), nullable=False), + sa.Column('dishes', sa.Text(), nullable=False), + sa.Column('order_date', sa.String(length=10), nullable=False), + sa.Column('price', sa.String(length=20), nullable=False), + sa.Column('original_price', sa.String(length=20), nullable=True), + sa.Column('savings', sa.String(length=20), nullable=True), + sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=False), + sa.ForeignKeyConstraint(['user_id'], ['user.id'], ), + sa.PrimaryKeyConstraint('id') + ) + with op.batch_alter_table('order_record', schema=None) as batch_op: + batch_op.create_index(batch_op.f('ix_order_record_user_id'), ['user_id'], unique=False) + + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table('order_record', schema=None) as batch_op: + batch_op.drop_index(batch_op.f('ix_order_record_user_id')) + + op.drop_table('order_record') + # ### end Alembic commands ### diff --git a/app/api/v1/user.py b/app/api/v1/user.py new file mode 100644 index 0000000..c092420 --- /dev/null +++ b/app/api/v1/user.py @@ -0,0 +1,38 @@ +from __future__ import annotations + +import json +import logging + +from fastapi import APIRouter, Query + +from app.api.deps import CurrentUser, DbSession +from app.crud import order_record as crud_order +from app.schemas.order_record import OrderListResponse, OrderRecordOut + +logger = logging.getLogger("shagua.api.user") + +router = APIRouter(prefix="/api/v1/user", tags=["user"]) + + +@router.get("/records", response_model=OrderListResponse, summary="分页查询当前用户的订单记录") +def list_records( + user: CurrentUser, + db: DbSession, + page: int = Query(1, ge=1), + size: int = Query(20, ge=1, le=100), +) -> OrderListResponse: + rows, has_next = crud_order.get_records_page(db, user_id=user.id, page=page, size=size) + records = [ + OrderRecordOut( + id=r.id, + platform=r.platform, + store_name=r.store_name, + dishes=json.loads(r.dishes) if r.dishes else [], + order_date=r.order_date, + price=r.price, + original_price=r.original_price, + savings=r.savings, + ) + for r in rows + ] + return OrderListResponse(records=records, has_next=has_next, page=page) diff --git a/app/crud/order_record.py b/app/crud/order_record.py new file mode 100644 index 0000000..6d0ce6a --- /dev/null +++ b/app/crud/order_record.py @@ -0,0 +1,26 @@ +from __future__ import annotations + +from sqlalchemy import func, select +from sqlalchemy.orm import Session + +from app.models.order_record import OrderRecord + + +def get_records_page( + db: Session, + *, + user_id: int, + page: int = 1, + size: int = 20, +) -> tuple[list[OrderRecord], bool]: + offset = (page - 1) * size + stmt = ( + select(OrderRecord) + .where(OrderRecord.user_id == user_id) + .order_by(OrderRecord.order_date.desc(), OrderRecord.id.desc()) + .offset(offset) + .limit(size + 1) + ) + rows = list(db.execute(stmt).scalars().all()) + has_next = len(rows) > size + return rows[:size], has_next diff --git a/app/main.py b/app/main.py index c4fe096..e5cea44 100644 --- a/app/main.py +++ b/app/main.py @@ -13,6 +13,7 @@ from fastapi.middleware.cors import CORSMiddleware from app.api.v1.auth import router as auth_router from app.api.v1.meituan import router as meituan_router +from app.api.v1.user import router as user_router from app.core.config import settings from app.core.logging import setup_logging @@ -59,3 +60,4 @@ def health() -> dict[str, str]: app.include_router(auth_router) app.include_router(meituan_router) +app.include_router(user_router) diff --git a/app/models/__init__.py b/app/models/__init__.py index d288759..77dce8e 100644 --- a/app/models/__init__.py +++ b/app/models/__init__.py @@ -1,2 +1,3 @@ """所有 ORM model 必须在这里 import 一次,Alembic / metadata 才能扫到。""" from app.models.user import User # noqa: F401 +from app.models.order_record import OrderRecord # noqa: F401 diff --git a/app/models/order_record.py b/app/models/order_record.py new file mode 100644 index 0000000..6dcce31 --- /dev/null +++ b/app/models/order_record.py @@ -0,0 +1,32 @@ +from __future__ import annotations + +from datetime import datetime, timezone + +from sqlalchemy import DateTime, ForeignKey, Integer, Numeric, String, Text, func +from sqlalchemy.orm import Mapped, mapped_column + +from app.db.base import Base + + +def _utcnow() -> datetime: + return datetime.now(timezone.utc) + + +class OrderRecord(Base): + __tablename__ = "order_record" + + id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) + user_id: Mapped[int] = mapped_column(Integer, ForeignKey("user.id"), nullable=False, index=True) + + platform: Mapped[str] = mapped_column(String(30), nullable=False) + store_name: Mapped[str] = mapped_column(String(128), nullable=False) + dishes: Mapped[str] = mapped_column(Text, nullable=False, default="[]") + + order_date: Mapped[str] = mapped_column(String(10), nullable=False) + price: Mapped[str] = mapped_column(String(20), nullable=False) + original_price: Mapped[str | None] = mapped_column(String(20), nullable=True) + savings: Mapped[str | None] = mapped_column(String(20), nullable=True) + + created_at: Mapped[datetime] = mapped_column( + DateTime(timezone=True), server_default=func.now(), nullable=False + ) diff --git a/app/schemas/order_record.py b/app/schemas/order_record.py new file mode 100644 index 0000000..7ce8948 --- /dev/null +++ b/app/schemas/order_record.py @@ -0,0 +1,22 @@ +from __future__ import annotations + +from pydantic import BaseModel, ConfigDict + + +class OrderRecordOut(BaseModel): + model_config = ConfigDict(from_attributes=True) + + id: int + platform: str + store_name: str + dishes: list[str] + order_date: str + price: str + original_price: str | None = None + savings: str | None = None + + +class OrderListResponse(BaseModel): + records: list[OrderRecordOut] + has_next: bool = False + page: int = 1