This commit is contained in:
27942
2026-01-28 16:00:56 +08:00
parent 03117098c7
commit 6262a67f46
116 changed files with 7821 additions and 5657 deletions

View File

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