Files
ai_web/backend/apps/admin/api.py
2026-01-29 13:18:59 +08:00

265 lines
7.7 KiB
Python

"""
Admin API routes for managing core data.
"""
from typing import List, Optional
from datetime import datetime
from django.utils import timezone
from ninja import Router, Schema
from ninja.errors import HttpError
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.bounties.models import Bounty, BountyDispute, PaymentEvent
router = Router()
def require_admin(user):
if not user or user.role != 'admin' or not user.is_active:
raise HttpError(403, "仅管理员可访问")
class UserOut(Schema):
id: int
open_id: str
name: Optional[str] = None
role: str
is_active: bool
created_at: datetime
class UserUpdateIn(Schema):
role: Optional[str] = None
is_active: Optional[bool] = None
class SimpleOut(Schema):
id: int
name: str
class BountyAdminOut(Schema):
id: int
title: str
status: str
reward: str
publisher_id: int
acceptor_id: Optional[int] = None
is_escrowed: bool
is_paid: bool
created_at: datetime
class Config:
from_attributes = True
@staticmethod
def resolve_reward(obj):
return str(obj.reward)
class PaymentEventOut(Schema):
id: int
event_id: str
event_type: str
bounty_id: Optional[int] = None
success: bool
processed_at: datetime
class Config:
from_attributes = True
class DisputeOut(Schema):
id: int
bounty_id: int
initiator_id: int
status: str
created_at: datetime
class Config:
from_attributes = True
@router.get("/users/", response=List[UserOut], auth=JWTAuth())
@paginate(PageNumberPagination, page_size=20)
def list_users(request):
require_admin(request.auth)
return User.objects.all().order_by('-created_at')
@router.patch("/users/{user_id}", response=UserOut, auth=JWTAuth())
def update_user(request, user_id: int, data: UserUpdateIn):
require_admin(request.auth)
try:
user = User.objects.get(id=user_id)
except User.DoesNotExist:
raise HttpError(404, "用户不存在")
update_data = data.dict(exclude_unset=True)
for key, value in update_data.items():
setattr(user, key, value)
user.save()
return user
@router.get("/categories/", response=List[SimpleOut], auth=JWTAuth())
@paginate(PageNumberPagination, page_size=50)
def list_categories(request):
require_admin(request.auth)
return Category.objects.values('id', 'name').order_by('name')
@router.get("/websites/", response=List[SimpleOut], auth=JWTAuth())
@paginate(PageNumberPagination, page_size=50)
def list_websites(request):
require_admin(request.auth)
return Website.objects.values('id', 'name').order_by('name')
@router.get("/products/", response=List[SimpleOut], auth=JWTAuth())
@paginate(PageNumberPagination, page_size=50)
def list_products(request):
require_admin(request.auth)
return Product.objects.values('id', 'name').order_by('-created_at')
@router.get("/bounties/", response=List[BountyAdminOut], auth=JWTAuth())
@paginate(PageNumberPagination, page_size=20)
def list_bounties(request, status: Optional[str] = None):
require_admin(request.auth)
queryset = Bounty.objects.select_related("publisher", "acceptor").all().order_by('-created_at')
if status:
queryset = queryset.filter(status=status)
return queryset
@router.get("/disputes/", response=List[DisputeOut], auth=JWTAuth())
@paginate(PageNumberPagination, page_size=20)
def list_disputes(request, status: Optional[str] = None):
require_admin(request.auth)
disputes = BountyDispute.objects.all().order_by('-created_at')
if status:
disputes = disputes.filter(status=status)
return disputes
@router.get("/payments/", response=List[PaymentEventOut], auth=JWTAuth())
@paginate(PageNumberPagination, page_size=20)
def list_payment_events(request):
require_admin(request.auth)
return PaymentEvent.objects.all().order_by('-processed_at')
# ==================== Product Review ====================
class ProductAdminOut(Schema):
"""Product admin output schema with all fields."""
id: int
name: str
description: Optional[str] = None
image: Optional[str] = None
images: List[str] = []
category_id: int
category_name: Optional[str] = None
status: str
submitted_by_id: Optional[int] = None
submitted_by_name: Optional[str] = None
reject_reason: Optional[str] = None
reviewed_at: Optional[datetime] = None
created_at: datetime
updated_at: datetime
@staticmethod
def resolve_category_name(obj):
return obj.category.name if obj.category else None
@staticmethod
def resolve_submitted_by_name(obj):
if obj.submitted_by:
return obj.submitted_by.name or obj.submitted_by.open_id
return None
class ProductReviewIn(Schema):
"""Product review input schema."""
approved: bool
reject_reason: Optional[str] = None
class ProductImagesIn(Schema):
"""Product images update input schema."""
images: List[str] = []
image: Optional[str] = None
@router.get("/products/pending/", response=List[ProductAdminOut], auth=JWTAuth())
@paginate(PageNumberPagination, page_size=20)
def list_pending_products(request):
"""List all pending products for review."""
require_admin(request.auth)
return Product.objects.select_related("category", "submitted_by").filter(
status='pending'
).order_by('-created_at')
@router.get("/products/all/", response=List[ProductAdminOut], auth=JWTAuth())
@paginate(PageNumberPagination, page_size=20)
def list_all_products(request, status: Optional[str] = None):
"""List all products with optional status filter."""
require_admin(request.auth)
queryset = Product.objects.select_related("category", "submitted_by").order_by('-created_at')
if status:
queryset = queryset.filter(status=status)
return queryset
@router.post("/products/{product_id}/review/", response=ProductAdminOut, auth=JWTAuth())
def review_product(request, product_id: int, data: ProductReviewIn):
"""Approve or reject a product."""
require_admin(request.auth)
try:
product = Product.objects.select_related("category", "submitted_by").get(id=product_id)
except Product.DoesNotExist:
raise HttpError(404, "商品不存在")
if product.status != 'pending':
raise HttpError(400, "只能审核待审核状态的商品")
if data.approved:
product.status = 'approved'
product.reject_reason = None
else:
if not data.reject_reason:
raise HttpError(400, "拒绝时需要提供原因")
product.status = 'rejected'
product.reject_reason = data.reject_reason
product.reviewed_at = timezone.now()
product.save()
return product
@router.put("/products/{product_id}/images/", response=ProductAdminOut, auth=JWTAuth())
def update_product_images(request, product_id: int, data: ProductImagesIn):
require_admin(request.auth)
try:
product = Product.objects.select_related("category", "submitted_by").get(id=product_id)
except Product.DoesNotExist:
raise HttpError(404, "商品不存在")
max_images = 6
images = [url.strip() for url in (data.images or []) if url and url.strip()]
image = (data.image or "").strip() or (images[0] if images else None)
if image and image not in images:
images.insert(0, image)
if len(images) > max_images:
raise HttpError(400, f"最多上传{max_images}张图片")
product.image = image or None
product.images = images
product.save(update_fields=["image", "images", "updated_at"])
return product