fix: 3D Show
This commit is contained in:
0
backend/shop/__init__.py
Normal file
0
backend/shop/__init__.py
Normal file
186
backend/shop/admin.py
Normal file
186
backend/shop/admin.py
Normal file
@@ -0,0 +1,186 @@
|
||||
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
|
||||
import base64
|
||||
|
||||
# 自定义后台标题
|
||||
admin.site.site_header = "量迹AI硬件销售管理后台"
|
||||
admin.site.site_title = "量迹AI后台"
|
||||
admin.site.index_title = "欢迎使用量迹AI管理系统"
|
||||
|
||||
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')
|
||||
list_filter = ('is_active',)
|
||||
search_fields = ('app_id', 'mch_id')
|
||||
fieldsets = (
|
||||
('基本配置', {
|
||||
'fields': ('app_id', 'mch_id', 'is_active')
|
||||
}),
|
||||
('安全配置', {
|
||||
'fields': ('api_key', 'app_secret')
|
||||
}),
|
||||
('回调配置', {
|
||||
'fields': ('notify_url',)
|
||||
}),
|
||||
)
|
||||
|
||||
@admin.register(ESP32Config)
|
||||
class ESP32ConfigAdmin(ModelAdmin):
|
||||
list_display = ('name', 'chip_type', 'price', 'has_camera', 'has_microphone')
|
||||
list_filter = ('chip_type', 'has_camera')
|
||||
search_fields = ('name', 'description')
|
||||
inlines = [ProductFeatureInline]
|
||||
fieldsets = (
|
||||
('基本信息', {
|
||||
'fields': ('name', 'price', 'description')
|
||||
}),
|
||||
('硬件参数', {
|
||||
'fields': ('chip_type', 'flash_size', 'ram_size', 'has_camera', 'has_microphone')
|
||||
}),
|
||||
('详情页图片', {
|
||||
'fields': ('detail_image', 'detail_image_url'),
|
||||
'description': '图片上传和URL二选一,优先使用URL'
|
||||
}),
|
||||
)
|
||||
|
||||
@admin.register(Service)
|
||||
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'
|
||||
}),
|
||||
('详情页图片', {
|
||||
'fields': ('detail_image', 'detail_image_url'),
|
||||
'description': '图片上传和URL二选一,优先使用URL'
|
||||
}),
|
||||
('详细内容', {
|
||||
'fields': ('features',)
|
||||
}),
|
||||
)
|
||||
|
||||
@admin.register(ARService)
|
||||
class ARServiceAdmin(ModelAdmin):
|
||||
list_display = ('title', 'created_at')
|
||||
search_fields = ('title', 'description')
|
||||
fieldsets = (
|
||||
('基本信息', {
|
||||
'fields': ('title', 'description')
|
||||
}),
|
||||
('封面/长图', {
|
||||
'fields': ('cover_image', 'cover_image_url'),
|
||||
'description': '图片上传和URL二选一,优先使用URL'
|
||||
}),
|
||||
)
|
||||
|
||||
@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):
|
||||
# 假设前端部署在 localhost:5173,生产环境需配置
|
||||
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" 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',)
|
||||
}),
|
||||
)
|
||||
|
||||
@admin.register(Order)
|
||||
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')
|
||||
readonly_fields = ('total_price', 'created_at', 'wechat_trade_no')
|
||||
|
||||
fieldsets = (
|
||||
('订单信息', {
|
||||
'fields': ('config', 'quantity', 'total_price', 'status', 'created_at')
|
||||
}),
|
||||
('客户信息', {
|
||||
'fields': ('customer_name', 'phone_number', 'shipping_address')
|
||||
}),
|
||||
('销售归属', {
|
||||
'fields': ('salesperson',)
|
||||
}),
|
||||
('支付信息', {
|
||||
'fields': ('wechat_trade_no',)
|
||||
}),
|
||||
)
|
||||
5
backend/shop/apps.py
Normal file
5
backend/shop/apps.py
Normal file
@@ -0,0 +1,5 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class ShopConfig(AppConfig):
|
||||
name = 'shop'
|
||||
50
backend/shop/migrations/0001_initial.py
Normal file
50
backend/shop/migrations/0001_initial.py
Normal file
@@ -0,0 +1,50 @@
|
||||
# Generated by Django 6.0.1 on 2026-02-02 04:06
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='ESP32Config',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=100, verbose_name='配置名称')),
|
||||
('chip_type', models.CharField(help_text='例如: ESP32-S3, ESP32-C3', max_length=50, verbose_name='芯片型号')),
|
||||
('flash_size', models.IntegerField(default=4, verbose_name='Flash大小(MB)')),
|
||||
('ram_size', models.IntegerField(default=2, verbose_name='PSRAM大小(MB)')),
|
||||
('has_camera', models.BooleanField(default=False, verbose_name='是否包含摄像头')),
|
||||
('has_microphone', models.BooleanField(default=False, verbose_name='是否包含麦克风')),
|
||||
('price', models.DecimalField(decimal_places=2, max_digits=10, verbose_name='价格')),
|
||||
('description', models.TextField(blank=True, verbose_name='描述')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': '硬件配置',
|
||||
'verbose_name_plural': '硬件配置列表',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Order',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('quantity', models.IntegerField(default=1, verbose_name='数量')),
|
||||
('total_price', models.DecimalField(decimal_places=2, max_digits=10, verbose_name='总价')),
|
||||
('status', models.CharField(choices=[('pending', '待支付'), ('paid', '已支付'), ('shipped', '已发货'), ('cancelled', '已取消')], default='pending', max_length=20, verbose_name='订单状态')),
|
||||
('wechat_trade_no', models.CharField(blank=True, max_length=100, null=True, verbose_name='微信支付单号')),
|
||||
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
|
||||
('updated_at', models.DateTimeField(auto_now=True, verbose_name='更新时间')),
|
||||
('config', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='shop.esp32config', verbose_name='所选配置')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': '订单',
|
||||
'verbose_name_plural': '订单列表',
|
||||
},
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,28 @@
|
||||
# Generated by Django 6.0.1 on 2026-02-02 04:10
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('shop', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='order',
|
||||
name='customer_name',
|
||||
field=models.CharField(default='', max_length=100, verbose_name='收货人姓名'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='order',
|
||||
name='phone_number',
|
||||
field=models.CharField(default='', max_length=20, verbose_name='联系电话'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='order',
|
||||
name='shipping_address',
|
||||
field=models.TextField(default='', verbose_name='发货地址'),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,36 @@
|
||||
# Generated by Django 6.0.1 on 2026-02-02 04:14
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('shop', '0002_order_customer_name_order_phone_number_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Salesperson',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=50, verbose_name='销售员姓名')),
|
||||
('code', models.CharField(help_text='唯一的推广标识码,如: zhangsan01', max_length=20, unique=True, verbose_name='推广码')),
|
||||
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': '销售员',
|
||||
'verbose_name_plural': '销售员管理',
|
||||
},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='esp32config',
|
||||
options={'verbose_name': '硬件配置 (小智参数)', 'verbose_name_plural': '硬件配置 (小智参数)'},
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='order',
|
||||
name='salesperson',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='orders', to='shop.salesperson', verbose_name='所属销售员'),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,44 @@
|
||||
# Generated by Django 5.2.10 on 2026-02-02 04:56
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('shop', '0003_salesperson_alter_esp32config_options_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='WeChatPayConfig',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('app_id', models.CharField(max_length=50, verbose_name='AppID')),
|
||||
('mch_id', models.CharField(max_length=50, verbose_name='商户号(MchID)')),
|
||||
('api_key', models.CharField(max_length=100, verbose_name='API密钥(Key)')),
|
||||
('app_secret', models.CharField(blank=True, max_length=100, null=True, verbose_name='AppSecret')),
|
||||
('notify_url', models.URLField(verbose_name='回调通知地址')),
|
||||
('is_active', models.BooleanField(default=True, verbose_name='是否启用')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': '微信支付配置',
|
||||
'verbose_name_plural': '微信支付配置',
|
||||
},
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='esp32config',
|
||||
name='id',
|
||||
field=models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='order',
|
||||
name='id',
|
||||
field=models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='salesperson',
|
||||
name='id',
|
||||
field=models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,50 @@
|
||||
# Generated by Django 6.0.1 on 2026-02-02 05:30
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('shop', '0004_wechatpayconfig_alter_esp32config_id_alter_order_id_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Service',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('title', models.CharField(max_length=100, verbose_name='服务名称')),
|
||||
('icon', models.ImageField(upload_to='services/icons/', verbose_name='图标')),
|
||||
('description', models.TextField(verbose_name='简介')),
|
||||
('features', models.TextField(help_text='每行一个特性', verbose_name='特性列表')),
|
||||
('color', models.CharField(default='#00f0ff', max_length=20, verbose_name='主题色')),
|
||||
('detail_image', models.ImageField(blank=True, null=True, upload_to='services/details/', verbose_name='详情页长图')),
|
||||
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'AI服务',
|
||||
'verbose_name_plural': 'AI服务管理',
|
||||
},
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='esp32config',
|
||||
name='id',
|
||||
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='order',
|
||||
name='id',
|
||||
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='salesperson',
|
||||
name='id',
|
||||
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='wechatpayconfig',
|
||||
name='id',
|
||||
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,58 @@
|
||||
# Generated by Django 6.0.1 on 2026-02-02 05:40
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('shop', '0005_service_alter_esp32config_id_alter_order_id_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='ARService',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('title', models.CharField(max_length=100, verbose_name='体验名称')),
|
||||
('description', models.TextField(verbose_name='简介')),
|
||||
('cover_image', models.ImageField(blank=True, null=True, upload_to='ar/covers/', verbose_name='封面/长图 (上传)')),
|
||||
('cover_image_url', models.URLField(blank=True, null=True, verbose_name='封面/长图 (URL)')),
|
||||
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'AR体验',
|
||||
'verbose_name_plural': 'AR体验管理',
|
||||
},
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='esp32config',
|
||||
name='detail_image',
|
||||
field=models.ImageField(blank=True, null=True, upload_to='products/details/', verbose_name='详情页长图 (上传)'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='esp32config',
|
||||
name='detail_image_url',
|
||||
field=models.URLField(blank=True, help_text='如果填写了URL,将优先使用URL', null=True, verbose_name='详情页长图 (URL)'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='service',
|
||||
name='detail_image_url',
|
||||
field=models.URLField(blank=True, null=True, verbose_name='详情页长图 (URL)'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='service',
|
||||
name='icon_url',
|
||||
field=models.URLField(blank=True, null=True, verbose_name='图标 (URL)'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='service',
|
||||
name='detail_image',
|
||||
field=models.ImageField(blank=True, null=True, upload_to='services/details/', verbose_name='详情页长图 (上传)'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='service',
|
||||
name='icon',
|
||||
field=models.ImageField(blank=True, null=True, upload_to='services/icons/', verbose_name='图标 (上传)'),
|
||||
),
|
||||
]
|
||||
32
backend/shop/migrations/0007_productfeature.py
Normal file
32
backend/shop/migrations/0007_productfeature.py
Normal file
@@ -0,0 +1,32 @@
|
||||
# Generated by Django 6.0.1 on 2026-02-02 06:04
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('shop', '0006_arservice_esp32config_detail_image_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='ProductFeature',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('title', models.CharField(max_length=50, verbose_name='特性标题')),
|
||||
('description', models.TextField(verbose_name='特性描述')),
|
||||
('icon_name', models.CharField(blank=True, help_text='例如: SafetyCertificate, Eye, Thunderbolt', max_length=50, null=True, verbose_name='Antd图标名称')),
|
||||
('icon_image', models.ImageField(blank=True, null=True, upload_to='products/features/', verbose_name='特性图标 (上传)')),
|
||||
('icon_url', models.URLField(blank=True, null=True, verbose_name='特性图标 (URL)')),
|
||||
('order', models.IntegerField(default=0, help_text='数字越小越靠前', verbose_name='排序权重')),
|
||||
('product', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='features', to='shop.esp32config', verbose_name='所属产品')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': '产品特性',
|
||||
'verbose_name_plural': '产品特性',
|
||||
'ordering': ['order'],
|
||||
},
|
||||
),
|
||||
]
|
||||
@@ -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': '服务订单列表',
|
||||
},
|
||||
),
|
||||
]
|
||||
0
backend/shop/migrations/__init__.py
Normal file
0
backend/shop/migrations/__init__.py
Normal file
205
backend/shop/models.py
Normal file
205
backend/shop/models.py
Normal file
@@ -0,0 +1,205 @@
|
||||
from django.db import models
|
||||
from django.utils.html import format_html
|
||||
import qrcode
|
||||
from io import BytesIO
|
||||
import base64
|
||||
|
||||
class ESP32Config(models.Model):
|
||||
"""
|
||||
ESP32 硬件配置选项模型
|
||||
用于定义可售卖的硬件参数
|
||||
"""
|
||||
name = models.CharField(max_length=100, verbose_name="配置名称")
|
||||
chip_type = models.CharField(max_length=50, verbose_name="芯片型号", help_text="例如: ESP32-S3, ESP32-C3")
|
||||
flash_size = models.IntegerField(verbose_name="Flash大小(MB)", default=4)
|
||||
ram_size = models.IntegerField(verbose_name="PSRAM大小(MB)", default=2)
|
||||
has_camera = models.BooleanField(default=False, verbose_name="是否包含摄像头")
|
||||
has_microphone = models.BooleanField(default=False, verbose_name="是否包含麦克风")
|
||||
price = models.DecimalField(max_digits=10, decimal_places=2, verbose_name="价格")
|
||||
description = models.TextField(verbose_name="描述", blank=True)
|
||||
detail_image = models.ImageField(upload_to='products/details/', blank=True, null=True, verbose_name="详情页长图 (上传)")
|
||||
detail_image_url = models.URLField(blank=True, null=True, verbose_name="详情页长图 (URL)", help_text="如果填写了URL,将优先使用URL")
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.name} - ¥{self.price}"
|
||||
|
||||
class Meta:
|
||||
verbose_name = "硬件配置 (小智参数)"
|
||||
verbose_name_plural = "硬件配置 (小智参数)"
|
||||
|
||||
|
||||
class ProductFeature(models.Model):
|
||||
"""
|
||||
产品特性模型 (关联到具体硬件配置)
|
||||
"""
|
||||
product = models.ForeignKey(ESP32Config, on_delete=models.CASCADE, related_name='features', verbose_name="所属产品")
|
||||
title = models.CharField(max_length=50, verbose_name="特性标题")
|
||||
description = models.TextField(verbose_name="特性描述")
|
||||
icon_name = models.CharField(max_length=50, blank=True, null=True, verbose_name="Antd图标名称", help_text="例如: SafetyCertificate, Eye, Thunderbolt")
|
||||
icon_image = models.ImageField(upload_to='products/features/', blank=True, null=True, verbose_name="特性图标 (上传)")
|
||||
icon_url = models.URLField(blank=True, null=True, verbose_name="特性图标 (URL)")
|
||||
order = models.IntegerField(default=0, verbose_name="排序权重", help_text="数字越小越靠前")
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.product.name} - {self.title}"
|
||||
|
||||
class Meta:
|
||||
verbose_name = "产品特性"
|
||||
verbose_name_plural = "产品特性"
|
||||
ordering = ['order']
|
||||
|
||||
|
||||
class Salesperson(models.Model):
|
||||
"""
|
||||
销售人员模型
|
||||
"""
|
||||
name = models.CharField(max_length=50, verbose_name="销售员姓名")
|
||||
code = models.CharField(max_length=20, unique=True, verbose_name="推广码", help_text="唯一的推广标识码,如: zhangsan01")
|
||||
created_at = models.DateTimeField(auto_now_add=True, verbose_name="创建时间")
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.name} ({self.code})"
|
||||
|
||||
class Meta:
|
||||
verbose_name = "销售员"
|
||||
verbose_name_plural = "销售员管理"
|
||||
|
||||
|
||||
class WeChatPayConfig(models.Model):
|
||||
"""
|
||||
微信支付配置模型
|
||||
"""
|
||||
app_id = models.CharField(max_length=50, verbose_name="AppID")
|
||||
mch_id = models.CharField(max_length=50, verbose_name="商户号(MchID)")
|
||||
api_key = models.CharField(max_length=100, verbose_name="API密钥(Key)")
|
||||
app_secret = models.CharField(max_length=100, verbose_name="AppSecret", blank=True, null=True)
|
||||
notify_url = models.URLField(verbose_name="回调通知地址")
|
||||
is_active = models.BooleanField(default=True, verbose_name="是否启用")
|
||||
|
||||
class Meta:
|
||||
verbose_name = "微信支付配置"
|
||||
verbose_name_plural = "微信支付配置"
|
||||
|
||||
def __str__(self):
|
||||
return f"微信支付配置 ({'启用' if self.is_active else '禁用'})"
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
# 确保只有一个启用的配置
|
||||
if self.is_active:
|
||||
WeChatPayConfig.objects.filter(is_active=True).exclude(id=self.id).update(is_active=False)
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
|
||||
class Order(models.Model):
|
||||
"""
|
||||
订单模型
|
||||
记录用户的购买请求和支付状态
|
||||
"""
|
||||
STATUS_CHOICES = (
|
||||
('pending', '待支付'),
|
||||
('paid', '已支付'),
|
||||
('shipped', '已发货'),
|
||||
('cancelled', '已取消'),
|
||||
)
|
||||
|
||||
config = models.ForeignKey(ESP32Config, on_delete=models.CASCADE, verbose_name="所选配置")
|
||||
quantity = models.IntegerField(default=1, verbose_name="数量")
|
||||
total_price = models.DecimalField(max_digits=10, decimal_places=2, verbose_name="总价")
|
||||
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="所属销售员", related_name='orders')
|
||||
|
||||
# 用户信息
|
||||
customer_name = models.CharField(max_length=100, verbose_name="收货人姓名", default="")
|
||||
phone_number = models.CharField(max_length=20, verbose_name="联系电话", default="")
|
||||
shipping_address = models.TextField(verbose_name="发货地址", default="")
|
||||
|
||||
# 微信支付相关字段
|
||||
wechat_trade_no = models.CharField(max_length=100, blank=True, null=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"Order #{self.id} - {self.customer_name} - {self.status}"
|
||||
|
||||
class Meta:
|
||||
verbose_name = "订单"
|
||||
verbose_name_plural = "订单列表"
|
||||
|
||||
|
||||
class Service(models.Model):
|
||||
"""
|
||||
AI服务项目模型
|
||||
"""
|
||||
title = models.CharField(max_length=100, verbose_name="服务名称")
|
||||
icon = models.ImageField(upload_to='services/icons/', blank=True, null=True, verbose_name="图标 (上传)")
|
||||
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)")
|
||||
created_at = models.DateTimeField(auto_now_add=True, verbose_name="创建时间")
|
||||
|
||||
def __str__(self):
|
||||
return self.title
|
||||
|
||||
class Meta:
|
||||
verbose_name = "AI服务"
|
||||
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体验服务模型
|
||||
"""
|
||||
title = models.CharField(max_length=100, verbose_name="体验名称")
|
||||
description = models.TextField(verbose_name="简介")
|
||||
cover_image = models.ImageField(upload_to='ar/covers/', blank=True, null=True, verbose_name="封面/长图 (上传)")
|
||||
cover_image_url = models.URLField(blank=True, null=True, verbose_name="封面/长图 (URL)")
|
||||
created_at = models.DateTimeField(auto_now_add=True, verbose_name="创建时间")
|
||||
|
||||
def __str__(self):
|
||||
return self.title
|
||||
|
||||
class Meta:
|
||||
verbose_name = "AR体验"
|
||||
verbose_name_plural = "AR体验管理"
|
||||
158
backend/shop/serializers.py
Normal file
158
backend/shop/serializers.py
Normal file
@@ -0,0 +1,158 @@
|
||||
from rest_framework import serializers
|
||||
from .models import ESP32Config, Order, Salesperson, Service, ARService, ProductFeature, ServiceOrder
|
||||
|
||||
class ProductFeatureSerializer(serializers.ModelSerializer):
|
||||
"""
|
||||
产品特性序列化器
|
||||
"""
|
||||
display_icon = serializers.SerializerMethodField()
|
||||
|
||||
class Meta:
|
||||
model = ProductFeature
|
||||
fields = ['title', 'description', 'icon_name', 'display_icon', 'order']
|
||||
|
||||
def get_display_icon(self, obj):
|
||||
if obj.icon_url:
|
||||
return obj.icon_url
|
||||
if obj.icon_image:
|
||||
return obj.icon_image.url
|
||||
return None
|
||||
|
||||
class ServiceSerializer(serializers.ModelSerializer):
|
||||
"""
|
||||
AI服务序列化器
|
||||
"""
|
||||
features_list = serializers.SerializerMethodField()
|
||||
display_icon = serializers.SerializerMethodField()
|
||||
display_detail_image = serializers.SerializerMethodField()
|
||||
|
||||
class Meta:
|
||||
model = Service
|
||||
fields = '__all__'
|
||||
|
||||
def get_features_list(self, obj):
|
||||
if obj.features:
|
||||
return [line.strip() for line in obj.features.split('\n') if line.strip()]
|
||||
return []
|
||||
|
||||
def get_display_icon(self, obj):
|
||||
if obj.icon_url:
|
||||
return obj.icon_url
|
||||
if obj.icon:
|
||||
return obj.icon.url
|
||||
return None
|
||||
|
||||
def get_display_detail_image(self, obj):
|
||||
if obj.detail_image_url:
|
||||
return obj.detail_image_url
|
||||
if obj.detail_image:
|
||||
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服务序列化器
|
||||
"""
|
||||
display_cover_image = serializers.SerializerMethodField()
|
||||
|
||||
class Meta:
|
||||
model = ARService
|
||||
fields = '__all__'
|
||||
|
||||
def get_display_cover_image(self, obj):
|
||||
if obj.cover_image_url:
|
||||
return obj.cover_image_url
|
||||
if obj.cover_image:
|
||||
return obj.cover_image.url
|
||||
return None
|
||||
|
||||
class ESP32ConfigSerializer(serializers.ModelSerializer):
|
||||
"""
|
||||
ESP32配置序列化器
|
||||
"""
|
||||
display_detail_image = serializers.SerializerMethodField()
|
||||
features = ProductFeatureSerializer(many=True, read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = ESP32Config
|
||||
fields = '__all__'
|
||||
|
||||
def get_display_detail_image(self, obj):
|
||||
if obj.detail_image_url:
|
||||
return obj.detail_image_url
|
||||
if obj.detail_image:
|
||||
return obj.detail_image.url
|
||||
return None
|
||||
|
||||
|
||||
class OrderSerializer(serializers.ModelSerializer):
|
||||
"""
|
||||
订单序列化器
|
||||
"""
|
||||
config_name = serializers.CharField(source='config.name', read_only=True)
|
||||
# 接收前端传来的 ref_code,用于查找 Salesperson
|
||||
ref_code = serializers.CharField(write_only=True, required=False, allow_blank=True)
|
||||
|
||||
class Meta:
|
||||
model = Order
|
||||
fields = ['id', 'config', 'config_name', 'quantity', 'total_price', 'status', 'created_at', 'wechat_trade_no',
|
||||
'customer_name', 'phone_number', 'shipping_address', 'ref_code']
|
||||
read_only_fields = ['total_price', 'status', 'wechat_trade_no', 'created_at']
|
||||
extra_kwargs = {
|
||||
'customer_name': {'required': True},
|
||||
'phone_number': {'required': True},
|
||||
'shipping_address': {'required': True},
|
||||
}
|
||||
|
||||
def create(self, validated_data):
|
||||
"""
|
||||
重写创建方法,自动计算总价并关联销售员
|
||||
"""
|
||||
config = validated_data.get('config')
|
||||
quantity = validated_data.get('quantity', 1)
|
||||
ref_code = validated_data.pop('ref_code', None)
|
||||
|
||||
validated_data['total_price'] = config.price * quantity
|
||||
|
||||
# 尝试关联销售员
|
||||
if ref_code:
|
||||
try:
|
||||
salesperson = Salesperson.objects.get(code=ref_code)
|
||||
validated_data['salesperson'] = salesperson
|
||||
except Salesperson.DoesNotExist:
|
||||
# 如果找不到对应的销售员,忽略该推广码,仍继续创建订单(算作自然流量)
|
||||
pass
|
||||
|
||||
return super().create(validated_data)
|
||||
151
backend/shop/templates/shop/order_check.html
Normal file
151
backend/shop/templates/shop/order_check.html
Normal file
@@ -0,0 +1,151 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>订单查询 - 量迹AI硬件</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
||||
max-width: 600px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
.container {
|
||||
background-color: white;
|
||||
padding: 30px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
||||
}
|
||||
h1 {
|
||||
text-align: center;
|
||||
color: #333;
|
||||
}
|
||||
.form-group {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
label {
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
font-weight: bold;
|
||||
}
|
||||
input {
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
box-sizing: border-box;
|
||||
font-size: 16px;
|
||||
}
|
||||
button {
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
background-color: #007bff;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
font-size: 16px;
|
||||
cursor: pointer;
|
||||
}
|
||||
button:hover {
|
||||
background-color: #0056b3;
|
||||
}
|
||||
#result {
|
||||
margin-top: 30px;
|
||||
}
|
||||
.order-card {
|
||||
border: 1px solid #eee;
|
||||
padding: 15px;
|
||||
border-radius: 4px;
|
||||
margin-bottom: 15px;
|
||||
background-color: #fff;
|
||||
}
|
||||
.order-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
border-bottom: 1px solid #eee;
|
||||
padding-bottom: 10px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.status {
|
||||
font-weight: bold;
|
||||
}
|
||||
.status-paid { color: green; }
|
||||
.status-pending { color: orange; }
|
||||
.status-shipped { color: blue; }
|
||||
.error { color: red; text-align: center; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>订单状态查询</h1>
|
||||
<div class="form-group">
|
||||
<label for="phone">请输入手机号码查询:</label>
|
||||
<input type="tel" id="phone" placeholder="请输入下单时填写的手机号" required>
|
||||
</div>
|
||||
<button onclick="searchOrders()">查询订单</button>
|
||||
|
||||
<div id="result"></div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
async function searchOrders() {
|
||||
const phone = document.getElementById('phone').value;
|
||||
const resultDiv = document.getElementById('result');
|
||||
|
||||
if (!phone) {
|
||||
alert('请输入手机号码');
|
||||
return;
|
||||
}
|
||||
|
||||
resultDiv.innerHTML = '<p style="text-align:center">查询中...</p>';
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/orders/lookup/?phone=${phone}`);
|
||||
const data = await response.json();
|
||||
|
||||
if (response.ok) {
|
||||
if (data.length === 0) {
|
||||
resultDiv.innerHTML = '<p class="error">未找到相关订单</p>';
|
||||
return;
|
||||
}
|
||||
|
||||
let html = '';
|
||||
data.forEach(order => {
|
||||
const statusMap = {
|
||||
'pending': '待支付',
|
||||
'paid': '已支付',
|
||||
'shipped': '已发货',
|
||||
'cancelled': '已取消'
|
||||
};
|
||||
const statusText = statusMap[order.status] || order.status;
|
||||
const statusClass = `status-${order.status}`;
|
||||
|
||||
html += `
|
||||
<div class="order-card">
|
||||
<div class="order-header">
|
||||
<span>订单号: ${order.id}</span>
|
||||
<span class="status ${statusClass}">${statusText}</span>
|
||||
</div>
|
||||
<div>
|
||||
<p><strong>商品:</strong> ${order.config_name || '未命名配置'}</p>
|
||||
<p><strong>数量:</strong> ${order.quantity}</p>
|
||||
<p><strong>总价:</strong> ¥${order.total_price}</p>
|
||||
<p><strong>下单时间:</strong> ${new Date(order.created_at).toLocaleString()}</p>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
resultDiv.innerHTML = html;
|
||||
} else {
|
||||
resultDiv.innerHTML = `<p class="error">${data.error || '查询失败'}</p>`;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error:', error);
|
||||
resultDiv.innerHTML = '<p class="error">网络错误,请稍后重试</p>';
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
3
backend/shop/tests.py
Normal file
3
backend/shop/tests.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
||||
15
backend/shop/urls.py
Normal file
15
backend/shop/urls.py
Normal file
@@ -0,0 +1,15 @@
|
||||
from django.urls import path, include
|
||||
from rest_framework.routers import DefaultRouter
|
||||
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)),
|
||||
path('page/check-order/', order_check_view, name='check-order-page'),
|
||||
]
|
||||
145
backend/shop/views.py
Normal file
145
backend/shop/views.py
Normal file
@@ -0,0 +1,145 @@
|
||||
from rest_framework import viewsets, status
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.response import Response
|
||||
from django.shortcuts import render
|
||||
from drf_spectacular.utils import extend_schema, extend_schema_view, OpenApiParameter, OpenApiExample
|
||||
from .models import ESP32Config, Order, WeChatPayConfig, Service, ARService, ServiceOrder
|
||||
from .serializers import ESP32ConfigSerializer, OrderSerializer, ServiceSerializer, ARServiceSerializer, ServiceOrderSerializer
|
||||
|
||||
@extend_schema_view(
|
||||
list=extend_schema(summary="获取AR服务列表", description="获取所有可用的AR服务"),
|
||||
retrieve=extend_schema(summary="获取AR服务详情", description="获取指定AR服务的详细信息")
|
||||
)
|
||||
class ARServiceViewSet(viewsets.ReadOnlyModelViewSet):
|
||||
"""
|
||||
AR服务列表和详情
|
||||
"""
|
||||
queryset = ARService.objects.all().order_by('-created_at')
|
||||
serializer_class = ARServiceSerializer
|
||||
import uuid
|
||||
import time
|
||||
import hashlib
|
||||
|
||||
def order_check_view(request):
|
||||
"""
|
||||
订单查询页面视图
|
||||
"""
|
||||
return render(request, 'shop/order_check.html')
|
||||
|
||||
@extend_schema_view(
|
||||
list=extend_schema(summary="获取AI服务列表", description="获取所有可用的AI服务"),
|
||||
retrieve=extend_schema(summary="获取AI服务详情", description="获取指定AI服务的详细信息")
|
||||
)
|
||||
class ServiceViewSet(viewsets.ReadOnlyModelViewSet):
|
||||
"""
|
||||
AI服务列表和详情
|
||||
"""
|
||||
queryset = Service.objects.all().order_by('-created_at')
|
||||
serializer_class = ServiceSerializer
|
||||
|
||||
class ServiceOrderViewSet(viewsets.ModelViewSet):
|
||||
"""
|
||||
AI服务订单管理
|
||||
"""
|
||||
queryset = ServiceOrder.objects.all()
|
||||
serializer_class = ServiceOrderSerializer
|
||||
|
||||
@extend_schema_view(
|
||||
list=extend_schema(summary="获取ESP32配置列表", description="获取所有可用的ESP32硬件配置选项"),
|
||||
retrieve=extend_schema(summary="获取ESP32配置详情", description="获取指定ESP32配置的详细信息")
|
||||
)
|
||||
class ESP32ConfigViewSet(viewsets.ReadOnlyModelViewSet):
|
||||
"""
|
||||
提供ESP32配置选项的列表和详情
|
||||
"""
|
||||
queryset = ESP32Config.objects.all()
|
||||
serializer_class = ESP32ConfigSerializer
|
||||
|
||||
|
||||
class OrderViewSet(viewsets.ModelViewSet):
|
||||
"""
|
||||
订单管理视图集
|
||||
支持创建订单和查询订单状态
|
||||
"""
|
||||
queryset = Order.objects.all()
|
||||
serializer_class = OrderSerializer
|
||||
|
||||
@action(detail=False, methods=['get'])
|
||||
def lookup(self, request):
|
||||
"""
|
||||
根据电话号码查询订单状态
|
||||
URL: /api/orders/lookup/?phone=13800138000
|
||||
"""
|
||||
phone = request.query_params.get('phone')
|
||||
if not phone:
|
||||
return Response({'error': '请提供电话号码'}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
# 简单校验
|
||||
orders = Order.objects.filter(phone_number=phone).order_by('-created_at')
|
||||
serializer = self.get_serializer(orders, many=True)
|
||||
return Response(serializer.data)
|
||||
|
||||
@action(detail=True, methods=['post'])
|
||||
def initiate_payment(self, request, pk=None):
|
||||
"""
|
||||
发起支付请求
|
||||
获取微信支付配置并生成签名
|
||||
"""
|
||||
order = self.get_object()
|
||||
|
||||
if order.status == 'paid':
|
||||
return Response({'error': '订单已支付'}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
# 获取微信支付配置
|
||||
wechat_config = WeChatPayConfig.objects.filter(is_active=True).first()
|
||||
if not wechat_config:
|
||||
# 如果没有配置,为了演示方便,回退到模拟数据,或者报错
|
||||
# 这里我们报错提示需要在后台配置
|
||||
return Response({'error': '支付系统维护中 (未配置支付参数)'}, status=status.HTTP_503_SERVICE_UNAVAILABLE)
|
||||
|
||||
# 构造支付参数
|
||||
# 注意:实际生产环境必须在此处调用微信【统一下单】接口获取真实的 prepay_id
|
||||
# 这里为了演示完整流程,我们使用配置中的参数生成合法的签名结构,但 prepay_id 是模拟的
|
||||
|
||||
app_id = wechat_config.app_id
|
||||
timestamp = str(int(time.time()))
|
||||
nonce_str = str(uuid.uuid4()).replace('-', '')
|
||||
|
||||
# 模拟的 prepay_id
|
||||
prepay_id = f"wx{str(uuid.uuid4()).replace('-', '')}"
|
||||
package = f"prepay_id={prepay_id}"
|
||||
sign_type = 'MD5'
|
||||
|
||||
# 生成签名 (WeChat Pay V2 MD5 Signature)
|
||||
# 签名步骤:
|
||||
# 1. 设所有发送或者接收到的数据为集合M,将集合M内非空参数值的参数按照参数名ASCII码从小到大排序(字典序)
|
||||
# 2. 使用URL键值对的格式(即key1=value1&key2=value2…)拼接成字符串stringA
|
||||
# 3. 在stringA最后拼接上key得到stringSignTemp字符串,并对stringSignTemp进行MD5运算,再将得到的字符串所有字符转换为大写
|
||||
|
||||
stringA = f"appId={app_id}&nonceStr={nonce_str}&package={package}&signType={sign_type}&timeStamp={timestamp}"
|
||||
string_sign_temp = f"{stringA}&key={wechat_config.api_key}"
|
||||
pay_sign = hashlib.md5(string_sign_temp.encode('utf-8')).hexdigest().upper()
|
||||
|
||||
payment_params = {
|
||||
'appId': app_id,
|
||||
'timeStamp': timestamp,
|
||||
'nonceStr': nonce_str,
|
||||
'package': package,
|
||||
'signType': sign_type,
|
||||
'paySign': pay_sign,
|
||||
'orderId': order.id,
|
||||
'amount': str(order.total_price)
|
||||
}
|
||||
|
||||
return Response(payment_params)
|
||||
|
||||
@action(detail=True, methods=['post'])
|
||||
def confirm_payment(self, request, pk=None):
|
||||
"""
|
||||
模拟支付成功回调/确认
|
||||
"""
|
||||
order = self.get_object()
|
||||
order.status = 'paid'
|
||||
order.wechat_trade_no = f"WX_{str(uuid.uuid4())[:18]}"
|
||||
order.save()
|
||||
return Response({'status': 'success', 'message': '支付成功'})
|
||||
Reference in New Issue
Block a user