haha
This commit is contained in:
@@ -10,7 +10,7 @@ from ninja_jwt.authentication import JWTAuth
|
||||
from ninja.pagination import paginate, PageNumberPagination
|
||||
|
||||
from apps.users.models import User
|
||||
from apps.products.models import Product, Website, Category
|
||||
from apps.products.models import Product, Website, Category, ComparisonTag, ComparisonTagItem, ProductPrice, ProductPriceHistory
|
||||
from apps.bounties.models import Bounty, BountyDispute, PaymentEvent
|
||||
|
||||
router = Router()
|
||||
@@ -40,6 +40,10 @@ class SimpleOut(Schema):
|
||||
name: str
|
||||
|
||||
|
||||
class MessageOut(Schema):
|
||||
message: str
|
||||
|
||||
|
||||
class BountyAdminOut(Schema):
|
||||
id: int
|
||||
title: str
|
||||
@@ -193,6 +197,77 @@ class ProductImagesIn(Schema):
|
||||
image: Optional[str] = None
|
||||
|
||||
|
||||
class ComparisonTagAdminOut(Schema):
|
||||
id: int
|
||||
name: str
|
||||
slug: str
|
||||
description: Optional[str] = None
|
||||
cover_image: Optional[str] = None
|
||||
icon: Optional[str] = None
|
||||
sort_order: int
|
||||
is_active: bool
|
||||
product_count: int = 0
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
|
||||
|
||||
class ComparisonTagIn(Schema):
|
||||
name: str
|
||||
slug: str
|
||||
description: Optional[str] = None
|
||||
cover_image: Optional[str] = None
|
||||
icon: Optional[str] = None
|
||||
sort_order: int = 0
|
||||
is_active: bool = True
|
||||
|
||||
|
||||
class ComparisonTagUpdate(Schema):
|
||||
name: Optional[str] = None
|
||||
slug: Optional[str] = None
|
||||
description: Optional[str] = None
|
||||
cover_image: Optional[str] = None
|
||||
icon: Optional[str] = None
|
||||
sort_order: Optional[int] = None
|
||||
is_active: Optional[bool] = None
|
||||
|
||||
|
||||
class ComparisonTagItemOut(Schema):
|
||||
id: int
|
||||
tag_id: int
|
||||
product_id: int
|
||||
product_name: str
|
||||
product_image: Optional[str] = None
|
||||
product_status: str
|
||||
sort_order: int
|
||||
is_pinned: bool
|
||||
created_at: datetime
|
||||
|
||||
|
||||
class ComparisonTagItemIn(Schema):
|
||||
product_ids: List[int]
|
||||
sort_order: int = 0
|
||||
is_pinned: bool = False
|
||||
|
||||
|
||||
class ComparisonTagItemUpdate(Schema):
|
||||
sort_order: Optional[int] = None
|
||||
is_pinned: Optional[bool] = None
|
||||
|
||||
|
||||
class BackfillHistoryIn(Schema):
|
||||
product_ids: Optional[List[int]] = None
|
||||
|
||||
|
||||
class BackfillHistoryOut(Schema):
|
||||
created: int
|
||||
skipped: int
|
||||
|
||||
|
||||
class RecordHistoryIn(Schema):
|
||||
product_ids: Optional[List[int]] = None
|
||||
force: bool = False
|
||||
|
||||
|
||||
@router.get("/products/pending/", response=List[ProductAdminOut], auth=JWTAuth())
|
||||
@paginate(PageNumberPagination, page_size=20)
|
||||
def list_pending_products(request):
|
||||
@@ -262,3 +337,235 @@ def update_product_images(request, product_id: int, data: ProductImagesIn):
|
||||
product.images = images
|
||||
product.save(update_fields=["image", "images", "updated_at"])
|
||||
return product
|
||||
|
||||
|
||||
# ==================== Comparison Tag Management ====================
|
||||
|
||||
@router.get("/compare-tags/", response=List[ComparisonTagAdminOut], auth=JWTAuth())
|
||||
def list_compare_tags(request):
|
||||
require_admin(request.auth)
|
||||
tags = ComparisonTag.objects.all().order_by("sort_order", "id")
|
||||
return [
|
||||
ComparisonTagAdminOut(
|
||||
id=tag.id,
|
||||
name=tag.name,
|
||||
slug=tag.slug,
|
||||
description=tag.description,
|
||||
cover_image=tag.cover_image,
|
||||
icon=tag.icon,
|
||||
sort_order=tag.sort_order,
|
||||
is_active=tag.is_active,
|
||||
product_count=tag.items.count(),
|
||||
created_at=tag.created_at,
|
||||
updated_at=tag.updated_at,
|
||||
)
|
||||
for tag in tags
|
||||
]
|
||||
|
||||
|
||||
@router.post("/compare-tags/", response=ComparisonTagAdminOut, auth=JWTAuth())
|
||||
def create_compare_tag(request, data: ComparisonTagIn):
|
||||
require_admin(request.auth)
|
||||
if ComparisonTag.objects.filter(slug=data.slug).exists():
|
||||
raise HttpError(400, "标签标识已存在")
|
||||
tag = ComparisonTag.objects.create(**data.dict())
|
||||
return ComparisonTagAdminOut(
|
||||
id=tag.id,
|
||||
name=tag.name,
|
||||
slug=tag.slug,
|
||||
description=tag.description,
|
||||
cover_image=tag.cover_image,
|
||||
icon=tag.icon,
|
||||
sort_order=tag.sort_order,
|
||||
is_active=tag.is_active,
|
||||
product_count=0,
|
||||
created_at=tag.created_at,
|
||||
updated_at=tag.updated_at,
|
||||
)
|
||||
|
||||
|
||||
@router.patch("/compare-tags/{tag_id}", response=ComparisonTagAdminOut, auth=JWTAuth())
|
||||
def update_compare_tag(request, tag_id: int, data: ComparisonTagUpdate):
|
||||
require_admin(request.auth)
|
||||
try:
|
||||
tag = ComparisonTag.objects.get(id=tag_id)
|
||||
except ComparisonTag.DoesNotExist:
|
||||
raise HttpError(404, "标签不存在")
|
||||
update_data = data.dict(exclude_unset=True)
|
||||
if "slug" in update_data:
|
||||
if ComparisonTag.objects.filter(slug=update_data["slug"]).exclude(id=tag_id).exists():
|
||||
raise HttpError(400, "标签标识已存在")
|
||||
for key, value in update_data.items():
|
||||
setattr(tag, key, value)
|
||||
tag.save()
|
||||
return ComparisonTagAdminOut(
|
||||
id=tag.id,
|
||||
name=tag.name,
|
||||
slug=tag.slug,
|
||||
description=tag.description,
|
||||
cover_image=tag.cover_image,
|
||||
icon=tag.icon,
|
||||
sort_order=tag.sort_order,
|
||||
is_active=tag.is_active,
|
||||
product_count=tag.items.count(),
|
||||
created_at=tag.created_at,
|
||||
updated_at=tag.updated_at,
|
||||
)
|
||||
|
||||
|
||||
@router.delete("/compare-tags/{tag_id}", response=MessageOut, auth=JWTAuth())
|
||||
def delete_compare_tag(request, tag_id: int):
|
||||
require_admin(request.auth)
|
||||
deleted, _ = ComparisonTag.objects.filter(id=tag_id).delete()
|
||||
if not deleted:
|
||||
raise HttpError(404, "标签不存在")
|
||||
return MessageOut(message="标签已删除")
|
||||
|
||||
|
||||
@router.get("/compare-tags/{tag_id}/items/", response=List[ComparisonTagItemOut], auth=JWTAuth())
|
||||
def list_compare_tag_items(request, tag_id: int):
|
||||
require_admin(request.auth)
|
||||
items = (
|
||||
ComparisonTagItem.objects.select_related("product")
|
||||
.filter(tag_id=tag_id)
|
||||
.order_by("-is_pinned", "sort_order", "id")
|
||||
)
|
||||
return [
|
||||
ComparisonTagItemOut(
|
||||
id=item.id,
|
||||
tag_id=item.tag_id,
|
||||
product_id=item.product_id,
|
||||
product_name=item.product.name,
|
||||
product_image=item.product.image,
|
||||
product_status=item.product.status,
|
||||
sort_order=item.sort_order,
|
||||
is_pinned=item.is_pinned,
|
||||
created_at=item.created_at,
|
||||
)
|
||||
for item in items
|
||||
]
|
||||
|
||||
|
||||
@router.post("/compare-tags/{tag_id}/items/", response=List[ComparisonTagItemOut], auth=JWTAuth())
|
||||
def add_compare_tag_items(request, tag_id: int, data: ComparisonTagItemIn):
|
||||
require_admin(request.auth)
|
||||
try:
|
||||
tag = ComparisonTag.objects.get(id=tag_id)
|
||||
except ComparisonTag.DoesNotExist:
|
||||
raise HttpError(404, "标签不存在")
|
||||
product_ids = list(dict.fromkeys(data.product_ids or []))
|
||||
if not product_ids:
|
||||
raise HttpError(400, "请选择商品")
|
||||
existing = set(
|
||||
ComparisonTagItem.objects.filter(tag=tag, product_id__in=product_ids).values_list("product_id", flat=True)
|
||||
)
|
||||
items = []
|
||||
for product_id in product_ids:
|
||||
if product_id in existing:
|
||||
continue
|
||||
try:
|
||||
product = Product.objects.get(id=product_id)
|
||||
except Product.DoesNotExist:
|
||||
continue
|
||||
items.append(
|
||||
ComparisonTagItem(
|
||||
tag=tag,
|
||||
product=product,
|
||||
sort_order=data.sort_order,
|
||||
is_pinned=data.is_pinned,
|
||||
)
|
||||
)
|
||||
if items:
|
||||
ComparisonTagItem.objects.bulk_create(items)
|
||||
|
||||
return list_compare_tag_items(request, tag_id)
|
||||
|
||||
|
||||
@router.patch("/compare-tags/{tag_id}/items/{item_id}", response=ComparisonTagItemOut, auth=JWTAuth())
|
||||
def update_compare_tag_item(request, tag_id: int, item_id: int, data: ComparisonTagItemUpdate):
|
||||
require_admin(request.auth)
|
||||
try:
|
||||
item = ComparisonTagItem.objects.select_related("product").get(id=item_id, tag_id=tag_id)
|
||||
except ComparisonTagItem.DoesNotExist:
|
||||
raise HttpError(404, "标签商品不存在")
|
||||
update_data = data.dict(exclude_unset=True)
|
||||
for key, value in update_data.items():
|
||||
setattr(item, key, value)
|
||||
item.save()
|
||||
return ComparisonTagItemOut(
|
||||
id=item.id,
|
||||
tag_id=item.tag_id,
|
||||
product_id=item.product_id,
|
||||
product_name=item.product.name,
|
||||
product_image=item.product.image,
|
||||
product_status=item.product.status,
|
||||
sort_order=item.sort_order,
|
||||
is_pinned=item.is_pinned,
|
||||
created_at=item.created_at,
|
||||
)
|
||||
|
||||
|
||||
@router.delete("/compare-tags/{tag_id}/items/{item_id}", response=MessageOut, auth=JWTAuth())
|
||||
def delete_compare_tag_item(request, tag_id: int, item_id: int):
|
||||
require_admin(request.auth)
|
||||
deleted, _ = ComparisonTagItem.objects.filter(id=item_id, tag_id=tag_id).delete()
|
||||
if not deleted:
|
||||
raise HttpError(404, "标签商品不存在")
|
||||
return MessageOut(message="标签商品已移除")
|
||||
|
||||
|
||||
def _record_price_history(product_ids: Optional[List[int]] = None, force: bool = False):
|
||||
queryset = ProductPrice.objects.select_related("product", "website").all()
|
||||
if product_ids:
|
||||
queryset = queryset.filter(product_id__in=product_ids)
|
||||
created = 0
|
||||
skipped = 0
|
||||
for price in queryset:
|
||||
latest = ProductPriceHistory.objects.filter(
|
||||
product_id=price.product_id,
|
||||
website_id=price.website_id,
|
||||
).order_by("-recorded_at").first()
|
||||
if force or (not latest or latest.price != price.price):
|
||||
ProductPriceHistory.objects.create(
|
||||
product_id=price.product_id,
|
||||
website_id=price.website_id,
|
||||
price=price.price,
|
||||
)
|
||||
created += 1
|
||||
else:
|
||||
skipped += 1
|
||||
return created, skipped
|
||||
|
||||
|
||||
@router.post("/price-history/backfill/", response=BackfillHistoryOut, auth=JWTAuth())
|
||||
def backfill_price_history(request, data: BackfillHistoryIn):
|
||||
require_admin(request.auth)
|
||||
created, skipped = _record_price_history(data.product_ids, force=False)
|
||||
return BackfillHistoryOut(created=created, skipped=skipped)
|
||||
|
||||
|
||||
@router.post("/compare-tags/{tag_id}/backfill-history/", response=BackfillHistoryOut, auth=JWTAuth())
|
||||
def backfill_tag_price_history(request, tag_id: int):
|
||||
require_admin(request.auth)
|
||||
product_ids = list(
|
||||
ComparisonTagItem.objects.filter(tag_id=tag_id).values_list("product_id", flat=True)
|
||||
)
|
||||
created, skipped = _record_price_history(product_ids, force=False)
|
||||
return BackfillHistoryOut(created=created, skipped=skipped)
|
||||
|
||||
|
||||
@router.post("/price-history/record/", response=BackfillHistoryOut, auth=JWTAuth())
|
||||
def record_price_history(request, data: RecordHistoryIn):
|
||||
require_admin(request.auth)
|
||||
created, skipped = _record_price_history(data.product_ids, force=data.force)
|
||||
return BackfillHistoryOut(created=created, skipped=skipped)
|
||||
|
||||
|
||||
@router.post("/compare-tags/{tag_id}/record-history/", response=BackfillHistoryOut, auth=JWTAuth())
|
||||
def record_tag_price_history(request, tag_id: int, data: RecordHistoryIn):
|
||||
require_admin(request.auth)
|
||||
product_ids = list(
|
||||
ComparisonTagItem.objects.filter(tag_id=tag_id).values_list("product_id", flat=True)
|
||||
)
|
||||
created, skipped = _record_price_history(product_ids, force=data.force)
|
||||
return BackfillHistoryOut(created=created, skipped=skipped)
|
||||
|
||||
Reference in New Issue
Block a user