haha
This commit is contained in:
@@ -2,9 +2,12 @@
|
||||
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
|
||||
@@ -14,7 +17,7 @@ router = Router()
|
||||
|
||||
|
||||
def require_admin(user):
|
||||
if not user or user.role != 'admin':
|
||||
if not user or user.role != 'admin' or not user.is_active:
|
||||
raise HttpError(403, "仅管理员可访问")
|
||||
|
||||
|
||||
@@ -22,10 +25,9 @@ class UserOut(Schema):
|
||||
id: int
|
||||
open_id: str
|
||||
name: Optional[str] = None
|
||||
email: Optional[str] = None
|
||||
role: str
|
||||
is_active: bool
|
||||
created_at: str
|
||||
created_at: datetime
|
||||
|
||||
|
||||
class UserUpdateIn(Schema):
|
||||
@@ -47,7 +49,14 @@ class BountyAdminOut(Schema):
|
||||
acceptor_id: Optional[int] = None
|
||||
is_escrowed: bool
|
||||
is_paid: bool
|
||||
created_at: str
|
||||
created_at: datetime
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
@staticmethod
|
||||
def resolve_reward(obj):
|
||||
return str(obj.reward)
|
||||
|
||||
|
||||
class PaymentEventOut(Schema):
|
||||
@@ -56,7 +65,10 @@ class PaymentEventOut(Schema):
|
||||
event_type: str
|
||||
bounty_id: Optional[int] = None
|
||||
success: bool
|
||||
processed_at: str
|
||||
processed_at: datetime
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
class DisputeOut(Schema):
|
||||
@@ -64,116 +76,160 @@ class DisputeOut(Schema):
|
||||
bounty_id: int
|
||||
initiator_id: int
|
||||
status: str
|
||||
created_at: 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)
|
||||
users = User.objects.all().order_by('-created_at')
|
||||
return [
|
||||
UserOut(
|
||||
id=u.id,
|
||||
open_id=u.open_id,
|
||||
name=u.name,
|
||||
email=u.email,
|
||||
role=u.role,
|
||||
is_active=u.is_active,
|
||||
created_at=u.created_at.isoformat(),
|
||||
)
|
||||
for u in users
|
||||
]
|
||||
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)
|
||||
user = User.objects.get(id=user_id)
|
||||
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 UserOut(
|
||||
id=user.id,
|
||||
open_id=user.open_id,
|
||||
name=user.name,
|
||||
email=user.email,
|
||||
role=user.role,
|
||||
is_active=user.is_active,
|
||||
created_at=user.created_at.isoformat(),
|
||||
)
|
||||
return user
|
||||
|
||||
|
||||
@router.get("/categories/", response=List[SimpleOut], auth=JWTAuth())
|
||||
@paginate(PageNumberPagination, page_size=50)
|
||||
def list_categories(request):
|
||||
require_admin(request.auth)
|
||||
return [SimpleOut(id=c.id, name=c.name) for c in Category.objects.all()]
|
||||
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 [SimpleOut(id=w.id, name=w.name) for w in Website.objects.all()]
|
||||
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 [SimpleOut(id=p.id, name=p.name) for p in Product.objects.all()]
|
||||
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.all().order_by('-created_at')
|
||||
queryset = Bounty.objects.select_related("publisher", "acceptor").all().order_by('-created_at')
|
||||
if status:
|
||||
queryset = queryset.filter(status=status)
|
||||
return [
|
||||
BountyAdminOut(
|
||||
id=b.id,
|
||||
title=b.title,
|
||||
status=b.status,
|
||||
reward=str(b.reward),
|
||||
publisher_id=b.publisher_id,
|
||||
acceptor_id=b.acceptor_id,
|
||||
is_escrowed=b.is_escrowed,
|
||||
is_paid=b.is_paid,
|
||||
created_at=b.created_at.isoformat(),
|
||||
)
|
||||
for b in queryset
|
||||
]
|
||||
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 [
|
||||
DisputeOut(
|
||||
id=d.id,
|
||||
bounty_id=d.bounty_id,
|
||||
initiator_id=d.initiator_id,
|
||||
status=d.status,
|
||||
created_at=d.created_at.isoformat(),
|
||||
)
|
||||
for d in disputes
|
||||
]
|
||||
return disputes
|
||||
|
||||
|
||||
@router.get("/payments/", response=List[PaymentEventOut], auth=JWTAuth())
|
||||
@paginate(PageNumberPagination, page_size=20)
|
||||
def list_payment_events(request):
|
||||
require_admin(request.auth)
|
||||
events = PaymentEvent.objects.all().order_by('-processed_at')
|
||||
return [
|
||||
PaymentEventOut(
|
||||
id=e.id,
|
||||
event_id=e.event_id,
|
||||
event_type=e.event_type,
|
||||
bounty_id=e.bounty_id,
|
||||
success=e.success,
|
||||
processed_at=e.processed_at.isoformat(),
|
||||
)
|
||||
for e in events
|
||||
]
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user