""" 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