from __future__ import annotations from datetime import datetime from typing import List, Optional from fastapi import APIRouter, HTTPException from fastapi.responses import Response from pydantic import BaseModel, Field from app.db import get_pool router = APIRouter(tags=["discounts"]) async def _bump_domain_version(conn, domain: str) -> None: """Upsert domain_version: insert with version=1 for new domains, increment for existing.""" await conn.execute( """ INSERT INTO domain_version (domain, version, updated_at) VALUES ($1, 1, NOW()) ON CONFLICT (domain) DO UPDATE SET version = domain_version.version + 1, updated_at = NOW() """, domain, ) class DiscountIn(BaseModel): domain: str discount: float = Field(..., gt=0, le=1, description="折扣系数,如 0.8 表示八折") note: Optional[str] = None class DiscountOut(BaseModel): id: int domain: str discount: float note: Optional[str] created_at: datetime updated_at: datetime @router.get("/discounts", response_model=List[DiscountOut]) async def list_discounts() -> List[DiscountOut]: pool = get_pool() rows = await pool.fetch("SELECT * FROM discounts ORDER BY updated_at DESC") return [DiscountOut(**dict(r)) for r in rows] @router.post("/discounts", response_model=DiscountOut, status_code=201) async def create_discount(body: DiscountIn) -> DiscountOut: pool = get_pool() async with pool.acquire() as conn: async with conn.transaction(): row = await conn.fetchrow( """ INSERT INTO discounts (domain, discount, note) VALUES ($1, $2, $3) ON CONFLICT (domain) DO UPDATE SET discount = EXCLUDED.discount, note = EXCLUDED.note, updated_at = NOW() RETURNING * """, body.domain, body.discount, body.note, ) await _bump_domain_version(conn, body.domain) return DiscountOut(**dict(row)) @router.put("/discounts/{discount_id}", response_model=DiscountOut) async def update_discount(discount_id: int, body: DiscountIn) -> DiscountOut: pool = get_pool() async with pool.acquire() as conn: async with conn.transaction(): row = await conn.fetchrow( """ UPDATE discounts SET domain=$1, discount=$2, note=$3, updated_at=NOW() WHERE id=$4 RETURNING * """, body.domain, body.discount, body.note, discount_id, ) if not row: raise HTTPException(status_code=404, detail="不存在") await _bump_domain_version(conn, body.domain) return DiscountOut(**dict(row)) @router.delete("/discounts/{discount_id}", status_code=204, response_model=None) async def delete_discount(discount_id: int) -> Response: pool = get_pool() async with pool.acquire() as conn: async with conn.transaction(): # 先查出 domain,再删除,再 bump 版本 existing = await conn.fetchrow("SELECT domain FROM discounts WHERE id=$1", discount_id) if not existing: raise HTTPException(status_code=404, detail="不存在") result = await conn.execute("DELETE FROM discounts WHERE id=$1", discount_id) if result == "DELETE 0": raise HTTPException(status_code=404, detail="不存在") await conn.execute("DELETE FROM domain_version WHERE domain=$1", existing["domain"]) return Response(status_code=204)