更新admin 和 swagger
This commit is contained in:
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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)",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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')
|
||||
|
||||
@@ -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': '服务订单列表',
|
||||
},
|
||||
),
|
||||
]
|
||||
@@ -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体验服务模型
|
||||
|
||||
@@ -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服务序列化器
|
||||
|
||||
@@ -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)),
|
||||
|
||||
@@ -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配置选项的列表和详情
|
||||
|
||||
Reference in New Issue
Block a user