Files
ai_web/backend/apps/admin/api.py

265 lines
7.7 KiB
Python
Raw Normal View History

2026-01-27 18:15:43 +08:00
"""
Admin API routes for managing core data.
"""
from typing import List, Optional
2026-01-28 16:00:56 +08:00
from datetime import datetime
from django.utils import timezone
2026-01-27 18:15:43 +08:00
from ninja import Router, Schema
from ninja.errors import HttpError
from ninja_jwt.authentication import JWTAuth
2026-01-28 16:00:56 +08:00
from ninja.pagination import paginate, PageNumberPagination
2026-01-27 18:15:43 +08:00
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):
2026-01-28 16:00:56 +08:00
if not user or user.role != 'admin' or not user.is_active:
2026-01-27 18:15:43 +08:00
raise HttpError(403, "仅管理员可访问")
class UserOut(Schema):
id: int
open_id: str
name: Optional[str] = None
role: str
is_active: bool
2026-01-28 16:00:56 +08:00
created_at: datetime
2026-01-27 18:15:43 +08:00
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
2026-01-28 16:00:56 +08:00
created_at: datetime
class Config:
from_attributes = True
@staticmethod
def resolve_reward(obj):
return str(obj.reward)
2026-01-27 18:15:43 +08:00
class PaymentEventOut(Schema):
id: int
event_id: str
event_type: str
bounty_id: Optional[int] = None
success: bool
2026-01-28 16:00:56 +08:00
processed_at: datetime
class Config:
from_attributes = True
2026-01-27 18:15:43 +08:00
class DisputeOut(Schema):
id: int
bounty_id: int
initiator_id: int
status: str
2026-01-28 16:00:56 +08:00
created_at: datetime
class Config:
from_attributes = True
2026-01-27 18:15:43 +08:00
@router.get("/users/", response=List[UserOut], auth=JWTAuth())
2026-01-28 16:00:56 +08:00
@paginate(PageNumberPagination, page_size=20)
2026-01-27 18:15:43 +08:00
def list_users(request):
require_admin(request.auth)
2026-01-28 16:00:56 +08:00
return User.objects.all().order_by('-created_at')
2026-01-27 18:15:43 +08:00
@router.patch("/users/{user_id}", response=UserOut, auth=JWTAuth())
def update_user(request, user_id: int, data: UserUpdateIn):
require_admin(request.auth)
2026-01-28 16:00:56 +08:00
try:
user = User.objects.get(id=user_id)
except User.DoesNotExist:
raise HttpError(404, "用户不存在")
2026-01-27 18:15:43 +08:00
update_data = data.dict(exclude_unset=True)
for key, value in update_data.items():
setattr(user, key, value)
user.save()
2026-01-28 16:00:56 +08:00
return user
2026-01-27 18:15:43 +08:00
@router.get("/categories/", response=List[SimpleOut], auth=JWTAuth())
2026-01-28 16:00:56 +08:00
@paginate(PageNumberPagination, page_size=50)
2026-01-27 18:15:43 +08:00
def list_categories(request):
require_admin(request.auth)
2026-01-28 16:00:56 +08:00
return Category.objects.values('id', 'name').order_by('name')
2026-01-27 18:15:43 +08:00
@router.get("/websites/", response=List[SimpleOut], auth=JWTAuth())
2026-01-28 16:00:56 +08:00
@paginate(PageNumberPagination, page_size=50)
2026-01-27 18:15:43 +08:00
def list_websites(request):
require_admin(request.auth)
2026-01-28 16:00:56 +08:00
return Website.objects.values('id', 'name').order_by('name')
2026-01-27 18:15:43 +08:00
@router.get("/products/", response=List[SimpleOut], auth=JWTAuth())
2026-01-28 16:00:56 +08:00
@paginate(PageNumberPagination, page_size=50)
2026-01-27 18:15:43 +08:00
def list_products(request):
require_admin(request.auth)
2026-01-28 16:00:56 +08:00
return Product.objects.values('id', 'name').order_by('-created_at')
2026-01-27 18:15:43 +08:00
@router.get("/bounties/", response=List[BountyAdminOut], auth=JWTAuth())
2026-01-28 16:00:56 +08:00
@paginate(PageNumberPagination, page_size=20)
2026-01-27 18:15:43 +08:00
def list_bounties(request, status: Optional[str] = None):
require_admin(request.auth)
2026-01-28 16:00:56 +08:00
queryset = Bounty.objects.select_related("publisher", "acceptor").all().order_by('-created_at')
2026-01-27 18:15:43 +08:00
if status:
queryset = queryset.filter(status=status)
2026-01-28 16:00:56 +08:00
return queryset
2026-01-27 18:15:43 +08:00
@router.get("/disputes/", response=List[DisputeOut], auth=JWTAuth())
2026-01-28 16:00:56 +08:00
@paginate(PageNumberPagination, page_size=20)
2026-01-27 18:15:43 +08:00
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)
2026-01-28 16:00:56 +08:00
return disputes
2026-01-27 18:15:43 +08:00
@router.get("/payments/", response=List[PaymentEventOut], auth=JWTAuth())
2026-01-28 16:00:56 +08:00
@paginate(PageNumberPagination, page_size=20)
2026-01-27 18:15:43 +08:00
def list_payment_events(request):
require_admin(request.auth)
2026-01-28 16:00:56 +08:00
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
2026-01-29 13:18:59 +08:00
images: List[str] = []
2026-01-28 16:00:56 +08:00
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
2026-01-29 13:18:59 +08:00
class ProductImagesIn(Schema):
"""Product images update input schema."""
images: List[str] = []
image: Optional[str] = None
2026-01-28 16:00:56 +08:00
@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
2026-01-29 13:18:59 +08:00
@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