fix: 3D Show

This commit is contained in:
xiaoma
2026-02-02 19:10:34 +08:00
commit b8024da3dc
61 changed files with 4123 additions and 0 deletions

0
backend/shop/__init__.py Normal file
View File

186
backend/shop/admin.py Normal file
View 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
View File

@@ -0,0 +1,5 @@
from django.apps import AppConfig
class ShopConfig(AppConfig):
name = 'shop'

View 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': '订单列表',
},
),
]

View File

@@ -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='发货地址'),
),
]

View File

@@ -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='所属销售员'),
),
]

View File

@@ -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'),
),
]

View File

@@ -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'),
),
]

View File

@@ -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='图标 (上传)'),
),
]

View 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'],
},
),
]

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

205
backend/shop/models.py Normal file
View 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
View 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)

View 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
View File

@@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

15
backend/shop/urls.py Normal file
View 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
View 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': '支付成功'})