更新admin 和 swagger

This commit is contained in:
jeremygan2021
2026-02-02 14:32:24 +08:00
parent 6af90017d5
commit c93bf9ef11
26 changed files with 407 additions and 53 deletions

View File

@@ -32,6 +32,7 @@ ALLOWED_HOSTS = ["*"]
# Application definition
INSTALLED_APPS = [
'unfold', # django-unfold必须在admin之前
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
@@ -40,6 +41,7 @@ INSTALLED_APPS = [
'django.contrib.staticfiles',
'rest_framework',
'corsheaders',
'drf_spectacular', # Swagger文档生成
'shop',
]
@@ -126,3 +128,54 @@ USE_TZ = True
# https://docs.djangoproject.com/en/6.0/howto/static-files/
STATIC_URL = 'static/'
STATIC_ROOT = BASE_DIR / 'staticfiles'
# 静态文件配置
STATICFILES_DIRS = [
BASE_DIR / 'static',
]
# Django REST Framework配置
REST_FRAMEWORK = {
'DEFAULT_SCHEMA_CLASS': 'drf_spectacular.openapi.AutoSchema',
'DEFAULT_AUTHENTICATION_CLASSES': [],
'DEFAULT_PERMISSION_CLASSES': [],
}
# drf-spectacular配置
SPECTACULAR_SETTINGS = {
'TITLE': '科技公司产品购买API',
'DESCRIPTION': '科技公司产品购买官网的API文档',
'VERSION': '1.0.0',
'SERVE_INCLUDE_SCHEMA': True,
'SERVE_PERMISSIONS': ['rest_framework.permissions.AllowAny'],
'COMPONENT_SPLIT_REQUEST': True,
'SCHEMA_PATH_PREFIX': r'/api/v[0-9]',
'SWAGGER_UI_SETTINGS': {
'deepLinking': True,
'persistAuthorization': True,
'displayOperationId': True,
},
}
# django-unfold配置
UNFOLD = {
"SITE_TITLE": "科技公司产品管理",
"SITE_HEADER": "科技公司产品购买系统",
"SITE_URL": "/",
"COLORS": {
"primary": {
"50": "rgb(240 249 255)",
"100": "rgb(224 242 254)",
"200": "rgb(186 230 253)",
"300": "rgb(125 211 252)",
"400": "rgb(56 189 248)",
"500": "rgb(14 165 233)",
"600": "rgb(2 132 199)",
"700": "rgb(3 105 161)",
"800": "rgb(7 89 133)",
"900": "rgb(12 74 110)",
"950": "rgb(8 47 73)",
},
},
}

View File

@@ -1,7 +1,19 @@
from django.contrib import admin
from django.urls import path, include
from django.conf import settings
from django.conf.urls.static import static
from drf_spectacular.views import SpectacularAPIView, SpectacularSwaggerView, SpectacularRedocView
urlpatterns = [
path('admin/', admin.site.urls),
path('api/', include('shop.urls')),
# Swagger文档路由
path('api/schema/', SpectacularAPIView.as_view(), name='schema'),
path('api/docs/', SpectacularSwaggerView.as_view(url_name='schema'), name='swagger-ui'),
path('api/redoc/', SpectacularRedocView.as_view(url_name='schema'), name='redoc'),
]
# 静态文件配置(开发环境)
if settings.DEBUG:
urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)

View File

@@ -1,8 +1,18 @@
asgiref==3.11.0
attrs==25.4.0
Django==6.0.1
django-cors-headers==4.9.0
django-unfold==0.77.1
djangorestframework==3.16.1
drf-spectacular==0.29.0
inflection==0.5.1
jsonschema==4.26.0
jsonschema-specifications==2025.9.1
pillow==12.1.0
psycopg2-binary==2.9.11
PyYAML==6.0.3
qrcode==8.2
referencing==0.37.0
rpds-py==0.30.0
sqlparse==0.5.5
uritemplate==4.2.0

View File

@@ -1,6 +1,8 @@
from django.contrib import admin
from django.utils.html import format_html
from django.db.models import Sum
from unfold.admin import ModelAdmin, TabularInline
from unfold.decorators import display
from .models import ESP32Config, Order, Salesperson, WeChatPayConfig, Service, ARService, ProductFeature
import qrcode
from io import BytesIO
@@ -11,13 +13,13 @@ admin.site.site_header = "量迹AI硬件销售管理后台"
admin.site.site_title = "量迹AI后台"
admin.site.index_title = "欢迎使用量迹AI管理系统"
class ProductFeatureInline(admin.TabularInline):
class ProductFeatureInline(TabularInline):
model = ProductFeature
extra = 1
fields = ('title', 'description', 'icon_name', 'icon_image', 'icon_url', 'order')
@admin.register(WeChatPayConfig)
class WeChatPayConfigAdmin(admin.ModelAdmin):
class WeChatPayConfigAdmin(ModelAdmin):
list_display = ('app_id', 'mch_id', 'is_active', 'notify_url')
list_filter = ('is_active',)
search_fields = ('app_id', 'mch_id')
@@ -34,7 +36,7 @@ class WeChatPayConfigAdmin(admin.ModelAdmin):
)
@admin.register(ESP32Config)
class ESP32ConfigAdmin(admin.ModelAdmin):
class ESP32ConfigAdmin(ModelAdmin):
list_display = ('name', 'chip_type', 'price', 'has_camera', 'has_microphone')
list_filter = ('chip_type', 'has_camera')
search_fields = ('name', 'description')
@@ -53,13 +55,16 @@ class ESP32ConfigAdmin(admin.ModelAdmin):
)
@admin.register(Service)
class ServiceAdmin(admin.ModelAdmin):
class ServiceAdmin(ModelAdmin):
list_display = ('title', 'created_at')
search_fields = ('title', 'description')
fieldsets = (
('基本信息', {
'fields': ('title', 'description', 'color')
}),
('价格与交付', {
'fields': ('price', 'unit', 'delivery_time', 'delivery_content')
}),
('图标', {
'fields': ('icon', 'icon_url'),
'description': '图标上传和URL二选一优先使用URL'
@@ -74,7 +79,7 @@ class ServiceAdmin(admin.ModelAdmin):
)
@admin.register(ARService)
class ARServiceAdmin(admin.ModelAdmin):
class ARServiceAdmin(ModelAdmin):
list_display = ('title', 'created_at')
search_fields = ('title', 'description')
fieldsets = (
@@ -88,7 +93,7 @@ class ARServiceAdmin(admin.ModelAdmin):
)
@admin.register(Salesperson)
class SalespersonAdmin(admin.ModelAdmin):
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')
@@ -100,12 +105,11 @@ class SalespersonAdmin(admin.ModelAdmin):
)
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}"
total_sales.short_description = "累计销售额 (已支付)"
total_sales.admin_order_field = '_total_sales'
def total_sales_display(self, obj):
return self.total_sales(obj)
@@ -116,15 +120,16 @@ class SalespersonAdmin(admin.ModelAdmin):
base_url = "http://localhost:5173"
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">打开推广链接</a>', url)
view_promotion_url.short_description = "推广链接"
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 "请先保存以生成二维码"
@@ -144,9 +149,7 @@ class SalespersonAdmin(admin.ModelAdmin):
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" />', img_str)
promotion_qr_code.short_description = "推广二维码"
return format_html('<img src="data:image/png;base64,{}" width="200" height="200" class="qr-code" />', img_str)
fieldsets = (
('基本信息', {
@@ -161,7 +164,7 @@ class SalespersonAdmin(admin.ModelAdmin):
)
@admin.register(Order)
class OrderAdmin(admin.ModelAdmin):
class OrderAdmin(ModelAdmin):
list_display = ('id', 'customer_name', 'config', 'total_price', 'status', 'salesperson', 'created_at')
list_filter = ('status', 'salesperson', 'created_at')
search_fields = ('id', 'customer_name', 'phone_number', 'wechat_trade_no')

View File

@@ -0,0 +1,55 @@
# Generated by Django 6.0.1 on 2026-02-02 06:16
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('shop', '0007_productfeature'),
]
operations = [
migrations.AddField(
model_name='service',
name='delivery_content',
field=models.TextField(blank=True, help_text='描述将交付给客户的具体成果', verbose_name='交付内容'),
),
migrations.AddField(
model_name='service',
name='delivery_time',
field=models.CharField(blank=True, help_text='例如3-5个工作日', max_length=50, verbose_name='预计交付周期'),
),
migrations.AddField(
model_name='service',
name='price',
field=models.DecimalField(decimal_places=2, default=0, max_digits=10, verbose_name='起步价格'),
),
migrations.AddField(
model_name='service',
name='unit',
field=models.CharField(default='', help_text='例如:次、小时、月、个', max_length=20, verbose_name='计费单位'),
),
migrations.CreateModel(
name='ServiceOrder',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('customer_name', models.CharField(max_length=100, verbose_name='客户姓名')),
('company_name', models.CharField(blank=True, max_length=100, verbose_name='公司名称')),
('phone_number', models.CharField(max_length=20, verbose_name='联系电话')),
('email', models.EmailField(blank=True, max_length=254, verbose_name='电子邮箱')),
('requirements', models.TextField(blank=True, verbose_name='具体需求描述')),
('total_price', models.DecimalField(decimal_places=2, default=0, max_digits=10, verbose_name='预估总价')),
('status', models.CharField(choices=[('pending', '待沟通/待支付'), ('processing', '服务进行中'), ('completed', '已完成'), ('cancelled', '已取消')], default='pending', max_length=20, verbose_name='订单状态')),
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
('updated_at', models.DateTimeField(auto_now=True, verbose_name='更新时间')),
('salesperson', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='shop.salesperson', verbose_name='所属销售员')),
('service', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='shop.service', verbose_name='所选服务')),
],
options={
'verbose_name': '服务订单',
'verbose_name_plural': '服务订单列表',
},
),
]

View File

@@ -137,6 +137,10 @@ class Service(models.Model):
icon_url = models.URLField(blank=True, null=True, verbose_name="图标 (URL)")
description = models.TextField(verbose_name="简介")
features = models.TextField(verbose_name="特性列表", help_text="每行一个特性")
price = models.DecimalField(max_digits=10, decimal_places=2, default=0, verbose_name="起步价格")
unit = models.CharField(max_length=20, default="", verbose_name="计费单位", help_text="例如:次、小时、月、个")
delivery_time = models.CharField(max_length=50, blank=True, verbose_name="预计交付周期", help_text="例如3-5个工作日")
delivery_content = models.TextField(blank=True, verbose_name="交付内容", help_text="描述将交付给客户的具体成果")
color = models.CharField(max_length=20, default="#00f0ff", verbose_name="主题色")
detail_image = models.ImageField(upload_to='services/details/', blank=True, null=True, verbose_name="详情页长图 (上传)")
detail_image_url = models.URLField(blank=True, null=True, verbose_name="详情页长图 (URL)")
@@ -150,6 +154,39 @@ class Service(models.Model):
verbose_name_plural = "AI服务管理"
class ServiceOrder(models.Model):
"""
AI服务订单模型
"""
STATUS_CHOICES = (
('pending', '待沟通/待支付'),
('processing', '服务进行中'),
('completed', '已完成'),
('cancelled', '已取消'),
)
service = models.ForeignKey(Service, on_delete=models.CASCADE, verbose_name="所选服务")
customer_name = models.CharField(max_length=100, verbose_name="客户姓名")
company_name = models.CharField(max_length=100, blank=True, verbose_name="公司名称")
phone_number = models.CharField(max_length=20, verbose_name="联系电话")
email = models.EmailField(blank=True, verbose_name="电子邮箱")
requirements = models.TextField(verbose_name="具体需求描述", blank=True)
total_price = models.DecimalField(max_digits=10, decimal_places=2, verbose_name="预估总价", default=0)
status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='pending', verbose_name="订单状态")
salesperson = models.ForeignKey(Salesperson, on_delete=models.SET_NULL, null=True, blank=True, verbose_name="所属销售员")
created_at = models.DateTimeField(auto_now_add=True, verbose_name="创建时间")
updated_at = models.DateTimeField(auto_now=True, verbose_name="更新时间")
def __str__(self):
return f"{self.customer_name} - {self.service.title}"
class Meta:
verbose_name = "服务订单"
verbose_name_plural = "服务订单列表"
class ARService(models.Model):
"""
AR体验服务模型

View File

@@ -1,5 +1,5 @@
from rest_framework import serializers
from .models import ESP32Config, Order, Salesperson, Service, ARService, ProductFeature
from .models import ESP32Config, Order, Salesperson, Service, ARService, ProductFeature, ServiceOrder
class ProductFeatureSerializer(serializers.ModelSerializer):
"""
@@ -49,6 +49,38 @@ class ServiceSerializer(serializers.ModelSerializer):
return obj.detail_image.url
return None
class ServiceOrderSerializer(serializers.ModelSerializer):
"""
AI服务订单序列化器
"""
service_name = serializers.CharField(source='service.title', read_only=True)
# 接收前端传来的 ref_code
ref_code = serializers.CharField(write_only=True, required=False, allow_blank=True)
class Meta:
model = ServiceOrder
fields = ['id', 'service', 'service_name', 'customer_name', 'company_name',
'phone_number', 'email', 'requirements', 'total_price', 'status', 'created_at', 'ref_code']
read_only_fields = ['total_price', 'status', 'created_at']
def create(self, validated_data):
ref_code = validated_data.pop('ref_code', None)
service = validated_data.get('service')
# 默认设置预估总价为服务起步价
if service:
validated_data['total_price'] = service.price
# 尝试关联销售员
if ref_code:
try:
salesperson = Salesperson.objects.get(code=ref_code)
validated_data['salesperson'] = salesperson
except Salesperson.DoesNotExist:
pass
return super().create(validated_data)
class ARServiceSerializer(serializers.ModelSerializer):
"""
AR服务序列化器

View File

@@ -1,12 +1,13 @@
from django.urls import path, include
from rest_framework.routers import DefaultRouter
from .views import ESP32ConfigViewSet, OrderViewSet, order_check_view, ServiceViewSet, ARServiceViewSet
from .views import ESP32ConfigViewSet, OrderViewSet, order_check_view, ServiceViewSet, ARServiceViewSet, ServiceOrderViewSet
router = DefaultRouter()
router.register(r'configs', ESP32ConfigViewSet)
router.register(r'orders', OrderViewSet)
router.register(r'services', ServiceViewSet)
router.register(r'ar', ARServiceViewSet)
router.register(r'service-orders', ServiceOrderViewSet)
urlpatterns = [
path('', include(router.urls)),

View File

@@ -2,8 +2,8 @@ from rest_framework import viewsets, status
from rest_framework.decorators import action
from rest_framework.response import Response
from django.shortcuts import render
from .models import ESP32Config, Order, WeChatPayConfig, Service, ARService
from .serializers import ESP32ConfigSerializer, OrderSerializer, ServiceSerializer, ARServiceSerializer
from .models import ESP32Config, Order, WeChatPayConfig, Service, ARService, ServiceOrder
from .serializers import ESP32ConfigSerializer, OrderSerializer, ServiceSerializer, ARServiceSerializer, ServiceOrderSerializer
class ARServiceViewSet(viewsets.ReadOnlyModelViewSet):
"""
@@ -28,6 +28,13 @@ class ServiceViewSet(viewsets.ReadOnlyModelViewSet):
queryset = Service.objects.all().order_by('-created_at')
serializer_class = ServiceSerializer
class ServiceOrderViewSet(viewsets.ModelViewSet):
"""
AI服务订单管理
"""
queryset = ServiceOrder.objects.all()
serializer_class = ServiceOrderSerializer
class ESP32ConfigViewSet(viewsets.ReadOnlyModelViewSet):
"""
提供ESP32配置选项的列表和详情