236 lines
6.6 KiB
Python
236 lines
6.6 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
|
|
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
|
|
|
|
|
|
@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
|