2026-07-03 16:36:06 +08:00
|
|
|
|
from django import forms
|
|
|
|
|
|
from django.contrib import admin, messages
|
|
|
|
|
|
from django.contrib.admin.utils import quote, unquote
|
2026-07-02 09:46:32 +08:00
|
|
|
|
from django.contrib.auth.admin import UserAdmin as DjangoUserAdmin
|
2026-07-03 16:36:06 +08:00
|
|
|
|
from django.core.exceptions import PermissionDenied
|
|
|
|
|
|
from django.http import Http404, HttpResponseRedirect
|
|
|
|
|
|
from django.template.response import TemplateResponse
|
|
|
|
|
|
from django.urls import path, reverse
|
|
|
|
|
|
from django.utils.html import format_html
|
|
|
|
|
|
|
|
|
|
|
|
from apps.billing.services import WalletAdjustmentError, adjust_wallet_points
|
2026-07-02 09:07:15 +08:00
|
|
|
|
|
2026-07-02 15:06:00 +08:00
|
|
|
|
from .models import ApiKey, User, UserWallet
|
2026-07-02 09:46:32 +08:00
|
|
|
|
|
|
|
|
|
|
|
2026-07-03 16:36:06 +08:00
|
|
|
|
class WalletAdjustmentForm(forms.Form):
|
|
|
|
|
|
points_delta = forms.IntegerField(
|
|
|
|
|
|
label="点数变动",
|
|
|
|
|
|
help_text="正数加点,负数扣点;不可为 0。",
|
|
|
|
|
|
)
|
|
|
|
|
|
reason = forms.CharField(
|
|
|
|
|
|
label="调整原因",
|
|
|
|
|
|
widget=forms.Textarea(attrs={"rows": 4}),
|
|
|
|
|
|
help_text="必填,会写入点数流水用于审计。",
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
def clean_points_delta(self):
|
|
|
|
|
|
points_delta = self.cleaned_data["points_delta"]
|
|
|
|
|
|
if points_delta == 0:
|
|
|
|
|
|
raise forms.ValidationError("点数变动不可为 0。")
|
|
|
|
|
|
return points_delta
|
|
|
|
|
|
|
|
|
|
|
|
def clean_reason(self):
|
|
|
|
|
|
reason = self.cleaned_data["reason"].strip()
|
|
|
|
|
|
if not reason:
|
|
|
|
|
|
raise forms.ValidationError("必须填写调整原因。")
|
|
|
|
|
|
return reason
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-07-02 09:46:32 +08:00
|
|
|
|
@admin.register(User)
|
|
|
|
|
|
class UserAdmin(DjangoUserAdmin):
|
|
|
|
|
|
fieldsets = DjangoUserAdmin.fieldsets + (
|
2026-07-04 16:27:45 +08:00
|
|
|
|
("业务信息", {"fields": ("payment_user_id", "status", "created_at")}),
|
2026-07-02 09:46:32 +08:00
|
|
|
|
)
|
|
|
|
|
|
readonly_fields = ("created_at",)
|
|
|
|
|
|
list_display = (
|
|
|
|
|
|
"username",
|
|
|
|
|
|
"email",
|
|
|
|
|
|
"status",
|
2026-07-03 16:36:06 +08:00
|
|
|
|
"wallet_points_balance",
|
2026-07-02 09:46:32 +08:00
|
|
|
|
"is_staff",
|
|
|
|
|
|
"is_active",
|
|
|
|
|
|
"created_at",
|
|
|
|
|
|
)
|
|
|
|
|
|
list_filter = DjangoUserAdmin.list_filter + ("status",)
|
|
|
|
|
|
search_fields = ("username", "email", "payment_user_id")
|
2026-07-02 15:06:00 +08:00
|
|
|
|
|
2026-07-03 16:36:06 +08:00
|
|
|
|
def get_queryset(self, request):
|
|
|
|
|
|
return super().get_queryset(request).select_related("wallet")
|
|
|
|
|
|
|
2026-07-04 16:27:45 +08:00
|
|
|
|
@admin.display(description="点数余额")
|
2026-07-03 16:36:06 +08:00
|
|
|
|
def wallet_points_balance(self, obj):
|
|
|
|
|
|
wallet = getattr(obj, "wallet", None)
|
|
|
|
|
|
return wallet.points_balance if wallet else 0
|
|
|
|
|
|
|
2026-07-02 15:06:00 +08:00
|
|
|
|
|
|
|
|
|
|
@admin.register(UserWallet)
|
|
|
|
|
|
class UserWalletAdmin(admin.ModelAdmin):
|
2026-07-03 16:36:06 +08:00
|
|
|
|
list_display = ("user", "points_balance", "updated_at", "adjust_points_link")
|
|
|
|
|
|
search_fields = ("user__username", "user__email", "user__payment_user_id")
|
|
|
|
|
|
readonly_fields = (
|
|
|
|
|
|
"user",
|
|
|
|
|
|
"points_balance",
|
|
|
|
|
|
"created_at",
|
|
|
|
|
|
"updated_at",
|
|
|
|
|
|
"adjust_points_link",
|
|
|
|
|
|
)
|
|
|
|
|
|
fields = (
|
|
|
|
|
|
"user",
|
|
|
|
|
|
"points_balance",
|
|
|
|
|
|
"created_at",
|
|
|
|
|
|
"updated_at",
|
|
|
|
|
|
"adjust_points_link",
|
|
|
|
|
|
)
|
2026-07-02 15:06:00 +08:00
|
|
|
|
ordering = ("user__username",)
|
2026-07-03 16:36:06 +08:00
|
|
|
|
list_select_related = ("user",)
|
2026-07-02 15:06:00 +08:00
|
|
|
|
|
|
|
|
|
|
def has_add_permission(self, request):
|
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
|
|
def has_delete_permission(self, request, obj=None):
|
|
|
|
|
|
return False
|
|
|
|
|
|
|
2026-07-03 16:36:06 +08:00
|
|
|
|
def get_urls(self):
|
|
|
|
|
|
urls = super().get_urls()
|
|
|
|
|
|
custom_urls = [
|
|
|
|
|
|
path(
|
|
|
|
|
|
"<path:object_id>/adjust-points/",
|
|
|
|
|
|
self.admin_site.admin_view(self.adjust_points_view),
|
|
|
|
|
|
name="users_userwallet_adjust_points",
|
|
|
|
|
|
),
|
|
|
|
|
|
]
|
|
|
|
|
|
return custom_urls + urls
|
|
|
|
|
|
|
|
|
|
|
|
@admin.display(description="手工调点")
|
|
|
|
|
|
def adjust_points_link(self, obj):
|
|
|
|
|
|
if obj is None:
|
|
|
|
|
|
return ""
|
|
|
|
|
|
url = reverse(
|
|
|
|
|
|
"admin:users_userwallet_adjust_points",
|
|
|
|
|
|
args=(quote(obj.pk),),
|
|
|
|
|
|
current_app=self.admin_site.name,
|
|
|
|
|
|
)
|
|
|
|
|
|
return format_html('<a class="button" href="{}">调点</a>', url)
|
|
|
|
|
|
|
|
|
|
|
|
def adjust_points_view(self, request, object_id):
|
|
|
|
|
|
wallet = self.get_object(request, unquote(object_id))
|
|
|
|
|
|
if wallet is None:
|
|
|
|
|
|
raise Http404("UserWallet does not exist.")
|
|
|
|
|
|
if not self.has_change_permission(request, wallet):
|
|
|
|
|
|
raise PermissionDenied
|
|
|
|
|
|
|
|
|
|
|
|
if request.method == "POST":
|
|
|
|
|
|
form = WalletAdjustmentForm(request.POST)
|
|
|
|
|
|
if form.is_valid():
|
|
|
|
|
|
try:
|
|
|
|
|
|
result = adjust_wallet_points(
|
|
|
|
|
|
user=wallet.user,
|
|
|
|
|
|
points_delta=form.cleaned_data["points_delta"],
|
|
|
|
|
|
reason=form.cleaned_data["reason"],
|
|
|
|
|
|
actor=request.user,
|
|
|
|
|
|
)
|
|
|
|
|
|
except WalletAdjustmentError as exc:
|
|
|
|
|
|
form.add_error(None, str(exc))
|
|
|
|
|
|
else:
|
|
|
|
|
|
messages.success(
|
|
|
|
|
|
request,
|
|
|
|
|
|
(
|
|
|
|
|
|
f"已调整 {wallet.user} 点数 {result.points_delta:+d},"
|
|
|
|
|
|
f"当前余额 {result.balance_after}。"
|
|
|
|
|
|
),
|
|
|
|
|
|
)
|
|
|
|
|
|
change_url = reverse(
|
|
|
|
|
|
"admin:users_userwallet_change",
|
|
|
|
|
|
args=(quote(wallet.pk),),
|
|
|
|
|
|
current_app=self.admin_site.name,
|
|
|
|
|
|
)
|
|
|
|
|
|
return HttpResponseRedirect(change_url)
|
|
|
|
|
|
else:
|
|
|
|
|
|
form = WalletAdjustmentForm()
|
|
|
|
|
|
|
|
|
|
|
|
context = {
|
|
|
|
|
|
**self.admin_site.each_context(request),
|
|
|
|
|
|
"title": "手工调整点数",
|
|
|
|
|
|
"opts": self.model._meta,
|
|
|
|
|
|
"original": wallet,
|
|
|
|
|
|
"form": form,
|
|
|
|
|
|
"media": self.media + form.media,
|
|
|
|
|
|
"has_change_permission": self.has_change_permission(request, wallet),
|
|
|
|
|
|
}
|
|
|
|
|
|
return TemplateResponse(
|
|
|
|
|
|
request,
|
|
|
|
|
|
"admin/users/userwallet/adjust_points.html",
|
|
|
|
|
|
context,
|
|
|
|
|
|
)
|
|
|
|
|
|
|
2026-07-02 15:06:00 +08:00
|
|
|
|
|
|
|
|
|
|
@admin.register(ApiKey)
|
|
|
|
|
|
class ApiKeyAdmin(admin.ModelAdmin):
|
|
|
|
|
|
list_display = ("key_prefix", "user", "name", "status", "last_used_at", "created_at")
|
|
|
|
|
|
list_filter = ("status", "created_at")
|
|
|
|
|
|
search_fields = ("key_prefix", "user__username", "user__email", "name")
|
2026-07-03 16:36:06 +08:00
|
|
|
|
list_select_related = ("user",)
|
2026-07-02 15:06:00 +08:00
|
|
|
|
readonly_fields = (
|
|
|
|
|
|
"user",
|
|
|
|
|
|
"key_prefix",
|
|
|
|
|
|
"key_hash_masked",
|
|
|
|
|
|
"last_used_at",
|
|
|
|
|
|
"created_at",
|
|
|
|
|
|
"updated_at",
|
|
|
|
|
|
)
|
|
|
|
|
|
fields = (
|
|
|
|
|
|
"user",
|
|
|
|
|
|
"name",
|
|
|
|
|
|
"status",
|
|
|
|
|
|
"key_prefix",
|
|
|
|
|
|
"key_hash_masked",
|
|
|
|
|
|
"last_used_at",
|
|
|
|
|
|
"created_at",
|
|
|
|
|
|
"updated_at",
|
|
|
|
|
|
)
|
|
|
|
|
|
ordering = ("-created_at", "-id")
|
|
|
|
|
|
|
2026-07-04 16:27:45 +08:00
|
|
|
|
@admin.display(description="Key 哈希")
|
2026-07-02 15:06:00 +08:00
|
|
|
|
def key_hash_masked(self, obj):
|
|
|
|
|
|
if not obj.key_hash:
|
|
|
|
|
|
return ""
|
|
|
|
|
|
return f"{obj.key_hash[:8]}...{obj.key_hash[-6:]}"
|
|
|
|
|
|
|
|
|
|
|
|
def has_add_permission(self, request):
|
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
|
|
def has_delete_permission(self, request, obj=None):
|
|
|
|
|
|
return False
|