183 lines
5.9 KiB
Python
183 lines
5.9 KiB
Python
import sqlite3
|
|
from typing import List
|
|
|
|
from fastapi import APIRouter, HTTPException, status
|
|
|
|
from app.database import Database
|
|
from app.schemas import Ad, AdCreate, AdUpdate
|
|
from app.temple_scope import TempleId, ensure_temple_exists
|
|
|
|
|
|
router = APIRouter(prefix="/ads", tags=["ads"])
|
|
|
|
AD_COLUMNS = """
|
|
ads.id, ads.temple_id, ads.slot_id, ad_slots.name AS slot_name, ad_slots.code AS slot_code,
|
|
ads.title, ads.image_url, ads.target_type, ads.target_url, ads.product_id,
|
|
products.name AS product_name, ads.sort_order, ads.is_active
|
|
"""
|
|
|
|
|
|
def row_to_ad(row: sqlite3.Row) -> Ad:
|
|
return Ad(
|
|
id=row["id"],
|
|
temple_id=row["temple_id"],
|
|
slot_id=row["slot_id"],
|
|
slot_name=row["slot_name"],
|
|
slot_code=row["slot_code"],
|
|
title=row["title"],
|
|
image_url=row["image_url"],
|
|
target_type=row["target_type"],
|
|
target_url=row["target_url"],
|
|
product_id=row["product_id"],
|
|
product_name=row["product_name"],
|
|
sort_order=row["sort_order"],
|
|
is_active=bool(row["is_active"]),
|
|
)
|
|
|
|
|
|
def ensure_slot_in_temple(db: Database, slot_id: int, temple_id: int) -> None:
|
|
row = db.execute(
|
|
"SELECT id FROM ad_slots WHERE id = ? AND temple_id = ?",
|
|
(slot_id, temple_id),
|
|
).fetchone()
|
|
if row is None:
|
|
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="请选择当前寺院下的广告位")
|
|
|
|
|
|
def ensure_product_in_temple(db: Database, product_id: int | None, temple_id: int) -> None:
|
|
if product_id is None:
|
|
return
|
|
row = db.execute(
|
|
"SELECT id FROM products WHERE id = ? AND temple_id = ?",
|
|
(product_id, temple_id),
|
|
).fetchone()
|
|
if row is None:
|
|
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="请选择当前寺院下的商品")
|
|
|
|
|
|
def normalize_ad_payload(
|
|
payload: AdCreate | AdUpdate,
|
|
temple_id: int,
|
|
db: Database,
|
|
fallback: Ad | None = None,
|
|
) -> tuple[int, str, str, str, str | None, int | None, int, int]:
|
|
data = payload.model_dump(exclude_unset=fallback is not None)
|
|
|
|
def get_value(name: str, default):
|
|
if name in data:
|
|
return data[name]
|
|
if fallback is not None:
|
|
return getattr(fallback, name)
|
|
return default
|
|
|
|
slot_id = get_value("slot_id", None)
|
|
if slot_id is None:
|
|
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="请选择广告位")
|
|
ensure_slot_in_temple(db, slot_id, temple_id)
|
|
|
|
target_type = get_value("target_type", "link")
|
|
target_url = get_value("target_url", None)
|
|
product_id = get_value("product_id", None)
|
|
|
|
if target_type == "link":
|
|
product_id = None
|
|
if not target_url:
|
|
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="链接类型需要填写跳转地址")
|
|
else:
|
|
target_url = None
|
|
ensure_product_in_temple(db, product_id, temple_id)
|
|
if product_id is None:
|
|
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="应用类型需要选择商品")
|
|
|
|
return (
|
|
slot_id,
|
|
get_value("title", ""),
|
|
get_value("image_url", ""),
|
|
target_type,
|
|
target_url,
|
|
product_id,
|
|
get_value("sort_order", 0),
|
|
int(get_value("is_active", True)),
|
|
)
|
|
|
|
|
|
@router.get("", response_model=List[Ad])
|
|
def list_ads(db: Database, temple_id: TempleId) -> List[Ad]:
|
|
ensure_temple_exists(db, temple_id)
|
|
rows = db.execute(
|
|
f"""
|
|
SELECT {AD_COLUMNS}
|
|
FROM ads
|
|
JOIN ad_slots ON ad_slots.id = ads.slot_id
|
|
LEFT JOIN products ON products.id = ads.product_id
|
|
WHERE ads.temple_id = ?
|
|
ORDER BY ad_slots.sort_order, ads.sort_order, ads.id
|
|
""",
|
|
(temple_id,),
|
|
).fetchall()
|
|
return [row_to_ad(row) for row in rows]
|
|
|
|
|
|
@router.post("", response_model=Ad, status_code=status.HTTP_201_CREATED)
|
|
def create_ad(payload: AdCreate, db: Database, temple_id: TempleId) -> Ad:
|
|
ensure_temple_exists(db, temple_id)
|
|
cursor = db.execute(
|
|
"""
|
|
INSERT INTO ads
|
|
(temple_id, slot_id, title, image_url, target_type, target_url, product_id, sort_order, is_active)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
""",
|
|
(temple_id, *normalize_ad_payload(payload, temple_id, db)),
|
|
)
|
|
db.commit()
|
|
return get_ad(cursor.lastrowid, db, temple_id)
|
|
|
|
|
|
@router.get("/{ad_id}", response_model=Ad)
|
|
def get_ad(ad_id: int, db: Database, temple_id: TempleId) -> Ad:
|
|
ensure_temple_exists(db, temple_id)
|
|
row = db.execute(
|
|
f"""
|
|
SELECT {AD_COLUMNS}
|
|
FROM ads
|
|
JOIN ad_slots ON ad_slots.id = ads.slot_id
|
|
LEFT JOIN products ON products.id = ads.product_id
|
|
WHERE ads.id = ? AND ads.temple_id = ?
|
|
""",
|
|
(ad_id, temple_id),
|
|
).fetchone()
|
|
if row is None:
|
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Ad not found")
|
|
return row_to_ad(row)
|
|
|
|
|
|
@router.patch("/{ad_id}", response_model=Ad)
|
|
def update_ad(ad_id: int, payload: AdUpdate, db: Database, temple_id: TempleId) -> Ad:
|
|
ad = get_ad(ad_id, db, temple_id)
|
|
if not payload.model_dump(exclude_unset=True):
|
|
return ad
|
|
|
|
db.execute(
|
|
"""
|
|
UPDATE ads
|
|
SET slot_id = ?, title = ?, image_url = ?, target_type = ?, target_url = ?,
|
|
product_id = ?, sort_order = ?, is_active = ?
|
|
WHERE id = ? AND temple_id = ?
|
|
""",
|
|
(*normalize_ad_payload(payload, temple_id, db, ad), ad_id, temple_id),
|
|
)
|
|
db.commit()
|
|
return get_ad(ad_id, db, temple_id)
|
|
|
|
|
|
@router.delete("/{ad_id}", status_code=status.HTTP_204_NO_CONTENT)
|
|
def delete_ad(ad_id: int, db: Database, temple_id: TempleId) -> None:
|
|
ensure_temple_exists(db, temple_id)
|
|
cursor = db.execute(
|
|
"DELETE FROM ads WHERE id = ? AND temple_id = ?",
|
|
(ad_id, temple_id),
|
|
)
|
|
db.commit()
|
|
if cursor.rowcount == 0:
|
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Ad not found")
|