All checks were successful
Deploy to Server / deploy (push) Successful in 46s
405 lines
15 KiB
Python
405 lines
15 KiB
Python
from django.contrib import admin
|
||
from django.utils.html import format_html
|
||
from django.db.models import Sum
|
||
from django import forms
|
||
from unfold.admin import ModelAdmin, TabularInline
|
||
from unfold.decorators import display
|
||
from adminsortable2.admin import SortableAdminMixin
|
||
from .models import ESP32Config, Order, Salesperson, WeChatPayConfig, Service, VCCourse, ProductFeature, CommissionLog, WeChatUser, Distributor, Withdrawal, ServiceOrder, CourseEnrollment
|
||
import qrcode
|
||
from io import BytesIO
|
||
import base64
|
||
|
||
# 自定义后台标题
|
||
admin.site.site_header = "量迹AI科技硬件/服务商场后台"
|
||
admin.site.site_title = "量迹AI后台"
|
||
admin.site.index_title = "欢迎使用量迹AI管理系统"
|
||
|
||
class ExternalUploadWidget(forms.URLInput):
|
||
def __init__(self, upload_url, accept='*', *args, **kwargs):
|
||
super().__init__(*args, **kwargs)
|
||
self.upload_url = upload_url
|
||
self.attrs.update({
|
||
'class': 'upload-url-input vTextField',
|
||
'data-upload-url': upload_url,
|
||
'data-accept': accept,
|
||
'placeholder': '上传文件后自动生成URL',
|
||
'style': 'width: 100%;'
|
||
})
|
||
|
||
class Media:
|
||
js = ('shop/js/admin_upload.js',)
|
||
css = {
|
||
'all': ('shop/css/admin_upload.css',)
|
||
}
|
||
|
||
class ESP32ConfigAdminForm(forms.ModelForm):
|
||
class Meta:
|
||
model = ESP32Config
|
||
fields = '__all__'
|
||
widgets = {
|
||
'static_image_url': ExternalUploadWidget(
|
||
upload_url='https://data.tangledup-ai.com/upload?folder=market_page%2Fhardware_xiaozhi%2Fproduct_static_image',
|
||
accept='image/*'
|
||
),
|
||
'model_3d_url': ExternalUploadWidget(
|
||
upload_url='https://data.tangledup-ai.com/upload?folder=market_page%2Fhardware_xiaozhi%2Fproduct_3D_image',
|
||
accept='.zip'
|
||
),
|
||
}
|
||
|
||
class ProductFeatureInline(TabularInline):
|
||
model = ProductFeature
|
||
extra = 1
|
||
fields = ('title', 'description', 'icon_name', 'icon_image', 'icon_url', 'order')
|
||
|
||
@admin.register(WeChatPayConfig)
|
||
class WeChatPayConfigAdmin(ModelAdmin):
|
||
list_display = ('app_id', 'mch_id', 'is_active', 'notify_url', 'updated_at_display')
|
||
list_filter = ('is_active',)
|
||
search_fields = ('app_id', 'mch_id')
|
||
|
||
def updated_at_display(self, obj):
|
||
# 假设模型没有 updated_at,如果有可以显示,这里仅作占位或移除
|
||
return "N/A"
|
||
updated_at_display.short_description = "更新时间"
|
||
|
||
fieldsets = (
|
||
('核心配置 (登录与支付)', {
|
||
'fields': ('app_id', 'app_secret', 'mch_id', 'is_active'),
|
||
'description': 'AppID 和 AppSecret 是小程序登录和支付的基础凭证。请确保 AppID 与小程序后台一致 (项目中优先使用 wxdf2ca73e6c0929f0)。'
|
||
}),
|
||
('微信支付 V3 安全配置 (推荐)', {
|
||
'fields': ('apiv3_key', 'mch_cert_serial_no', 'mch_private_key'),
|
||
'description': '使用 Native 支付必须配置这些项。私钥可以粘贴在这里,或者放在 backend/certs/apiclient_key.pem 文件中。'
|
||
}),
|
||
('微信支付 V2 安全配置 (旧版)', {
|
||
'fields': ('api_key',),
|
||
'classes': ('collapse',),
|
||
'description': '仅旧版支付接口需要 API Key (V2)。'
|
||
}),
|
||
('回调配置', {
|
||
'fields': ('notify_url',)
|
||
}),
|
||
)
|
||
|
||
@admin.register(ESP32Config)
|
||
class ESP32ConfigAdmin(SortableAdminMixin, ModelAdmin):
|
||
form = ESP32ConfigAdminForm
|
||
list_display = ('name', 'chip_type', 'price', 'stock', 'has_camera', 'has_microphone', 'order')
|
||
list_filter = ('chip_type', 'has_camera')
|
||
search_fields = ('name', 'description')
|
||
inlines = [ProductFeatureInline]
|
||
fieldsets = (
|
||
('基本信息', {
|
||
'fields': ('name', 'price', 'stock', 'commission_rate', 'description')
|
||
}),
|
||
('硬件参数', {
|
||
'fields': ('chip_type', 'flash_size', 'ram_size', 'has_camera', 'has_microphone')
|
||
}),
|
||
('详情页图片', {
|
||
'fields': ('detail_image', 'detail_image_url'),
|
||
'description': '图片上传和URL二选一,优先使用URL'
|
||
}),
|
||
('多媒体资源', {
|
||
'fields': ('static_image_url', 'model_3d_url'),
|
||
'description': '产品静态图和3D模型的外部链接'
|
||
}),
|
||
)
|
||
|
||
@admin.register(Service)
|
||
class ServiceAdmin(SortableAdminMixin, ModelAdmin):
|
||
list_display = ('title', 'created_at', 'order')
|
||
search_fields = ('title', 'description')
|
||
fieldsets = (
|
||
('基本信息', {
|
||
'fields': ('title', 'description', 'color')
|
||
}),
|
||
('价格与交付', {
|
||
'fields': ('price', 'unit', 'delivery_time', 'delivery_content')
|
||
}),
|
||
('图标', {
|
||
'fields': ('icon', 'icon_url'),
|
||
'description': '图标上传和URL二选一,优先使用URL'
|
||
}),
|
||
('详情页图片', {
|
||
'fields': ('detail_image', 'detail_image_url'),
|
||
'description': '图片上传和URL二选一,优先使用URL'
|
||
}),
|
||
('详细内容', {
|
||
'fields': ('features',)
|
||
}),
|
||
)
|
||
|
||
@admin.register(ServiceOrder)
|
||
class ServiceOrderAdmin(ModelAdmin):
|
||
list_display = ('id', 'customer_name', 'service', 'total_price', 'status', 'salesperson', 'created_at')
|
||
list_filter = ('status', 'service', 'salesperson', 'created_at')
|
||
search_fields = ('id', 'customer_name', 'phone_number', 'email')
|
||
readonly_fields = ('total_price', 'created_at', 'updated_at')
|
||
|
||
fieldsets = (
|
||
('订单信息', {
|
||
'fields': ('service', 'status', 'total_price', 'created_at')
|
||
}),
|
||
('客户信息', {
|
||
'fields': ('customer_name', 'company_name', 'phone_number', 'email', 'requirements')
|
||
}),
|
||
('销售归属', {
|
||
'fields': ('salesperson',)
|
||
}),
|
||
)
|
||
|
||
@admin.register(VCCourse)
|
||
class VCCourseAdmin(SortableAdminMixin, ModelAdmin):
|
||
list_display = ('title', 'course_type', 'price', 'tag', 'instructor', 'lesson_count', 'duration', 'created_at', 'order')
|
||
search_fields = ('title', 'description', 'instructor', 'tag')
|
||
list_filter = ('course_type', 'instructor', 'tag')
|
||
fieldsets = (
|
||
('基本信息', {
|
||
'fields': ('title', 'description', 'course_type', 'tag', 'price')
|
||
}),
|
||
('讲师信息', {
|
||
'fields': ('instructor', 'instructor_title', 'instructor_desc', 'instructor_avatar', 'instructor_avatar_url'),
|
||
'description': '讲师头像上传和URL二选一,优先使用URL'
|
||
}),
|
||
('课程详情', {
|
||
'fields': ('duration', 'lesson_count', 'content')
|
||
}),
|
||
('封面', {
|
||
'fields': ('cover_image', 'cover_image_url'),
|
||
'description': '图片上传和URL二选一,优先使用URL'
|
||
}),
|
||
('详情页长图', {
|
||
'fields': ('detail_image', 'detail_image_url'),
|
||
'description': '图片上传和URL二选一,优先使用URL'
|
||
}),
|
||
)
|
||
|
||
@admin.register(CourseEnrollment)
|
||
class CourseEnrollmentAdmin(ModelAdmin):
|
||
list_display = ('customer_name', 'course', 'phone_number', 'status', 'created_at')
|
||
list_filter = ('status', 'course', 'created_at')
|
||
search_fields = ('customer_name', 'phone_number', 'wechat_id')
|
||
|
||
fieldsets = (
|
||
('报名信息', {
|
||
'fields': ('course', 'status', 'created_at')
|
||
}),
|
||
('客户资料', {
|
||
'fields': ('customer_name', 'phone_number', 'wechat_id', 'email', 'message')
|
||
}),
|
||
('销售归属', {
|
||
'fields': ('salesperson', 'distributor')
|
||
}),
|
||
)
|
||
|
||
@admin.register(Salesperson)
|
||
class SalespersonAdmin(ModelAdmin):
|
||
list_display = ('name', 'code', 'total_sales', 'view_promotion_url')
|
||
search_fields = ('name', 'code')
|
||
readonly_fields = ('promotion_qr_code', 'promotion_url_display', 'total_sales_display')
|
||
|
||
def get_queryset(self, request):
|
||
queryset = super().get_queryset(request)
|
||
queryset = queryset.annotate(
|
||
_total_sales=Sum('orders__total_price', default=0)
|
||
)
|
||
return queryset
|
||
|
||
@display(description="累计销售额 (已支付)", ordering='_total_sales')
|
||
def total_sales(self, obj):
|
||
# 仅计算已支付的订单
|
||
paid_sales = obj.orders.filter(status='paid').aggregate(total=Sum('total_price'))['total']
|
||
return f"¥{paid_sales or 0:.2f}"
|
||
|
||
def total_sales_display(self, obj):
|
||
return self.total_sales(obj)
|
||
total_sales_display.short_description = "累计销售额 (已支付)"
|
||
|
||
def promotion_url(self, obj):
|
||
# 生产环境配置
|
||
base_url = "https://market.quant-speed.com"
|
||
return f"{base_url}/?ref={obj.code}"
|
||
|
||
@display(description="推广链接")
|
||
def view_promotion_url(self, obj):
|
||
url = self.promotion_url(obj)
|
||
return format_html('<a href="{}" target="_blank" class="button">打开推广链接</a>', url)
|
||
|
||
def promotion_url_display(self, obj):
|
||
return self.promotion_url(obj)
|
||
promotion_url_display.short_description = "完整推广链接"
|
||
|
||
@display(description="推广二维码")
|
||
def promotion_qr_code(self, obj):
|
||
if not obj.code:
|
||
return "请先保存以生成二维码"
|
||
|
||
url = self.promotion_url(obj)
|
||
qr = qrcode.QRCode(
|
||
version=1,
|
||
error_correction=qrcode.constants.ERROR_CORRECT_L,
|
||
box_size=10,
|
||
border=4,
|
||
)
|
||
qr.add_data(url)
|
||
qr.make(fit=True)
|
||
|
||
img = qr.make_image(fill_color="black", back_color="white")
|
||
buffer = BytesIO()
|
||
img.save(buffer, format="PNG")
|
||
img_str = base64.b64encode(buffer.getvalue()).decode()
|
||
|
||
return format_html('<img src="data:image/png;base64,{}" width="200" height="200" class="qr-code" />', img_str)
|
||
|
||
fieldsets = (
|
||
('基本信息', {
|
||
'fields': ('name', 'code')
|
||
}),
|
||
('推广工具', {
|
||
'fields': ('promotion_url_display', 'promotion_qr_code')
|
||
}),
|
||
('业绩统计', {
|
||
'fields': ('total_sales_display',)
|
||
}),
|
||
('分销设置', {
|
||
'fields': ('parent', 'commission_rate', 'second_level_rate'),
|
||
'description': '设置上级分销员及各级分润比例'
|
||
}),
|
||
)
|
||
|
||
@admin.register(CommissionLog)
|
||
class CommissionLogAdmin(ModelAdmin):
|
||
list_display = ('id', 'salesperson', 'distributor', 'amount', 'level', 'status', 'created_at')
|
||
list_filter = ('status', 'level', 'salesperson', 'distributor', 'created_at')
|
||
search_fields = ('salesperson__name', 'distributor__user__nickname', 'order__id')
|
||
readonly_fields = ('amount', 'level', 'created_at')
|
||
|
||
fieldsets = (
|
||
('基本信息', {
|
||
'fields': ('salesperson', 'distributor', 'order', 'amount', 'level')
|
||
}),
|
||
('状态管理', {
|
||
'fields': ('status', 'created_at')
|
||
}),
|
||
)
|
||
|
||
@admin.register(Order)
|
||
class OrderAdmin(ModelAdmin):
|
||
list_display = ('id', 'customer_name', 'get_item_name', 'total_price', 'status', 'salesperson', 'distributor', 'created_at')
|
||
list_filter = ('status', 'salesperson', 'distributor', 'created_at')
|
||
search_fields = ('id', 'customer_name', 'phone_number', 'wechat_trade_no')
|
||
readonly_fields = ('total_price', 'created_at', 'wechat_trade_no')
|
||
|
||
def get_item_name(self, obj):
|
||
if obj.config:
|
||
return f"[硬件] {obj.config.name}"
|
||
if obj.course:
|
||
return f"[课程] {obj.course.title}"
|
||
return "未知商品"
|
||
get_item_name.short_description = "购买商品"
|
||
|
||
fieldsets = (
|
||
('订单信息', {
|
||
'fields': ('config', 'course', 'quantity', 'total_price', 'status', 'created_at')
|
||
}),
|
||
('客户信息', {
|
||
'fields': ('customer_name', 'phone_number', 'shipping_address', 'wechat_user')
|
||
}),
|
||
('物流信息', {
|
||
'fields': ('courier_name', 'tracking_number')
|
||
}),
|
||
('销售归属', {
|
||
'fields': ('salesperson', 'distributor')
|
||
}),
|
||
('支付信息', {
|
||
'fields': ('wechat_trade_no',)
|
||
}),
|
||
)
|
||
|
||
@admin.register(WeChatUser)
|
||
class WeChatUserAdmin(ModelAdmin):
|
||
list_display = ('nickname', 'phone_number', 'is_star', 'title', 'avatar_display', 'gender_display', 'province', 'city', 'created_at')
|
||
search_fields = ('nickname', 'openid', 'phone_number')
|
||
list_filter = ('is_star', 'gender', 'province', 'city', 'created_at')
|
||
readonly_fields = ('openid', 'unionid', 'session_key', 'created_at', 'updated_at')
|
||
|
||
def avatar_display(self, obj):
|
||
if obj.avatar_url:
|
||
return format_html('<img src="{}" width="50" height="50" style="border-radius: 50%;" />', obj.avatar_url)
|
||
return "暂无"
|
||
avatar_display.short_description = "头像"
|
||
|
||
def gender_display(self, obj):
|
||
choices = {0: '未知', 1: '男', 2: '女'}
|
||
return choices.get(obj.gender, '未知')
|
||
gender_display.short_description = "性别"
|
||
|
||
fieldsets = (
|
||
('基本信息', {
|
||
'fields': ('user', 'nickname', 'phone_number', 'avatar_url', 'gender')
|
||
}),
|
||
('专家认证', {
|
||
'fields': ('is_star', 'title'),
|
||
'description': '标记为明星技术用户/专家,将在社区中展示'
|
||
}),
|
||
('位置信息', {
|
||
'fields': ('country', 'province', 'city')
|
||
}),
|
||
('认证信息', {
|
||
'fields': ('openid', 'unionid', 'session_key'),
|
||
'classes': ('collapse',)
|
||
}),
|
||
('时间信息', {
|
||
'fields': ('created_at', 'updated_at')
|
||
}),
|
||
)
|
||
|
||
@admin.register(Distributor)
|
||
class DistributorAdmin(ModelAdmin):
|
||
list_display = ('get_nickname', 'level', 'status', 'total_earnings', 'withdrawable_balance', 'invite_code', 'created_at')
|
||
search_fields = ('user__nickname', 'invite_code')
|
||
list_filter = ('status', 'level', 'created_at')
|
||
readonly_fields = ('total_earnings', 'withdrawable_balance', 'qr_code_url', 'created_at', 'updated_at')
|
||
autocomplete_fields = ['user', 'parent']
|
||
|
||
def get_nickname(self, obj):
|
||
return obj.user.nickname
|
||
get_nickname.short_description = "微信昵称"
|
||
get_nickname.admin_order_field = 'user__nickname'
|
||
|
||
fieldsets = (
|
||
('分销员信息', {
|
||
'fields': ('user', 'parent', 'level', 'status')
|
||
}),
|
||
('收益概览', {
|
||
'fields': ('commission_rate', 'total_earnings', 'withdrawable_balance')
|
||
}),
|
||
('推广信息', {
|
||
'fields': ('invite_code', 'qr_code_url')
|
||
}),
|
||
('时间信息', {
|
||
'fields': ('created_at', 'updated_at')
|
||
}),
|
||
)
|
||
|
||
@admin.register(Withdrawal)
|
||
class WithdrawalAdmin(ModelAdmin):
|
||
list_display = ('get_distributor', 'amount', 'status', 'created_at')
|
||
list_filter = ('status', 'created_at')
|
||
search_fields = ('distributor__user__nickname',)
|
||
|
||
def get_distributor(self, obj):
|
||
return obj.distributor.user.nickname
|
||
get_distributor.short_description = "分销员"
|
||
|
||
fieldsets = (
|
||
('提现详情', {
|
||
'fields': ('distributor', 'amount', 'status', 'remark')
|
||
}),
|
||
('时间信息', {
|
||
'fields': ('created_at', 'updated_at')
|
||
}),
|
||
)
|