first commit
This commit is contained in:
0
backend/shop/__init__.py
Normal file
0
backend/shop/__init__.py
Normal file
BIN
backend/shop/__pycache__/__init__.cpython-312.pyc
Normal file
BIN
backend/shop/__pycache__/__init__.cpython-312.pyc
Normal file
Binary file not shown.
BIN
backend/shop/__pycache__/__init__.cpython-313.pyc
Normal file
BIN
backend/shop/__pycache__/__init__.cpython-313.pyc
Normal file
Binary file not shown.
BIN
backend/shop/__pycache__/admin.cpython-312.pyc
Normal file
BIN
backend/shop/__pycache__/admin.cpython-312.pyc
Normal file
Binary file not shown.
BIN
backend/shop/__pycache__/admin.cpython-313.pyc
Normal file
BIN
backend/shop/__pycache__/admin.cpython-313.pyc
Normal file
Binary file not shown.
BIN
backend/shop/__pycache__/apps.cpython-312.pyc
Normal file
BIN
backend/shop/__pycache__/apps.cpython-312.pyc
Normal file
Binary file not shown.
BIN
backend/shop/__pycache__/apps.cpython-313.pyc
Normal file
BIN
backend/shop/__pycache__/apps.cpython-313.pyc
Normal file
Binary file not shown.
BIN
backend/shop/__pycache__/models.cpython-312.pyc
Normal file
BIN
backend/shop/__pycache__/models.cpython-312.pyc
Normal file
Binary file not shown.
BIN
backend/shop/__pycache__/models.cpython-313.pyc
Normal file
BIN
backend/shop/__pycache__/models.cpython-313.pyc
Normal file
Binary file not shown.
BIN
backend/shop/__pycache__/serializers.cpython-312.pyc
Normal file
BIN
backend/shop/__pycache__/serializers.cpython-312.pyc
Normal file
Binary file not shown.
BIN
backend/shop/__pycache__/serializers.cpython-313.pyc
Normal file
BIN
backend/shop/__pycache__/serializers.cpython-313.pyc
Normal file
Binary file not shown.
BIN
backend/shop/__pycache__/urls.cpython-312.pyc
Normal file
BIN
backend/shop/__pycache__/urls.cpython-312.pyc
Normal file
Binary file not shown.
BIN
backend/shop/__pycache__/urls.cpython-313.pyc
Normal file
BIN
backend/shop/__pycache__/urls.cpython-313.pyc
Normal file
Binary file not shown.
BIN
backend/shop/__pycache__/views.cpython-312.pyc
Normal file
BIN
backend/shop/__pycache__/views.cpython-312.pyc
Normal file
Binary file not shown.
BIN
backend/shop/__pycache__/views.cpython-313.pyc
Normal file
BIN
backend/shop/__pycache__/views.cpython-313.pyc
Normal file
Binary file not shown.
177
backend/shop/admin.py
Normal file
177
backend/shop/admin.py
Normal file
@@ -0,0 +1,177 @@
|
||||
from django.contrib import admin
|
||||
from django.utils.html import format_html
|
||||
from django.db.models import Sum
|
||||
from .models import ESP32Config, Order, Salesperson, WeChatPayConfig, Service, ARService
|
||||
import qrcode
|
||||
from io import BytesIO
|
||||
import base64
|
||||
|
||||
# 自定义后台标题
|
||||
admin.site.site_header = "量迹AI硬件销售管理后台"
|
||||
admin.site.site_title = "量迹AI后台"
|
||||
admin.site.index_title = "欢迎使用量迹AI管理系统"
|
||||
|
||||
@admin.register(WeChatPayConfig)
|
||||
class WeChatPayConfigAdmin(admin.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(admin.ModelAdmin):
|
||||
list_display = ('name', 'chip_type', 'price', 'has_camera', 'has_microphone')
|
||||
list_filter = ('chip_type', 'has_camera')
|
||||
search_fields = ('name', 'description')
|
||||
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(admin.ModelAdmin):
|
||||
list_display = ('title', 'created_at')
|
||||
search_fields = ('title', 'description')
|
||||
fieldsets = (
|
||||
('基本信息', {
|
||||
'fields': ('title', 'description', 'color')
|
||||
}),
|
||||
('图标', {
|
||||
'fields': ('icon', 'icon_url'),
|
||||
'description': '图标上传和URL二选一,优先使用URL'
|
||||
}),
|
||||
('详情页图片', {
|
||||
'fields': ('detail_image', 'detail_image_url'),
|
||||
'description': '图片上传和URL二选一,优先使用URL'
|
||||
}),
|
||||
('详细内容', {
|
||||
'fields': ('features',)
|
||||
}),
|
||||
)
|
||||
|
||||
@admin.register(ARService)
|
||||
class ARServiceAdmin(admin.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(admin.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
|
||||
|
||||
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)
|
||||
total_sales_display.short_description = "累计销售额 (已支付)"
|
||||
|
||||
def promotion_url(self, obj):
|
||||
# 假设前端部署在 localhost:5173,生产环境需配置
|
||||
base_url = "http://localhost:5173"
|
||||
return f"{base_url}/?ref={obj.code}"
|
||||
|
||||
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 = "推广链接"
|
||||
|
||||
def promotion_url_display(self, obj):
|
||||
return self.promotion_url(obj)
|
||||
promotion_url_display.short_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" />', img_str)
|
||||
|
||||
promotion_qr_code.short_description = "推广二维码"
|
||||
|
||||
fieldsets = (
|
||||
('基本信息', {
|
||||
'fields': ('name', 'code')
|
||||
}),
|
||||
('推广工具', {
|
||||
'fields': ('promotion_url_display', 'promotion_qr_code')
|
||||
}),
|
||||
('业绩统计', {
|
||||
'fields': ('total_sales_display',)
|
||||
}),
|
||||
)
|
||||
|
||||
@admin.register(Order)
|
||||
class OrderAdmin(admin.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='图标 (上传)'),
|
||||
),
|
||||
]
|
||||
0
backend/shop/migrations/__init__.py
Normal file
0
backend/shop/migrations/__init__.py
Normal file
BIN
backend/shop/migrations/__pycache__/0001_initial.cpython-312.pyc
Normal file
BIN
backend/shop/migrations/__pycache__/0001_initial.cpython-312.pyc
Normal file
Binary file not shown.
BIN
backend/shop/migrations/__pycache__/0001_initial.cpython-313.pyc
Normal file
BIN
backend/shop/migrations/__pycache__/0001_initial.cpython-313.pyc
Normal file
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.
Binary file not shown.
BIN
backend/shop/migrations/__pycache__/__init__.cpython-312.pyc
Normal file
BIN
backend/shop/migrations/__pycache__/__init__.cpython-312.pyc
Normal file
Binary file not shown.
BIN
backend/shop/migrations/__pycache__/__init__.cpython-313.pyc
Normal file
BIN
backend/shop/migrations/__pycache__/__init__.cpython-313.pyc
Normal file
Binary file not shown.
147
backend/shop/models.py
Normal file
147
backend/shop/models.py
Normal file
@@ -0,0 +1,147 @@
|
||||
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 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="每行一个特性")
|
||||
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 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体验管理"
|
||||
108
backend/shop/serializers.py
Normal file
108
backend/shop/serializers.py
Normal file
@@ -0,0 +1,108 @@
|
||||
from rest_framework import serializers
|
||||
from .models import ESP32Config, Order, Salesperson, Service, ARService
|
||||
|
||||
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 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()
|
||||
|
||||
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.
|
||||
14
backend/shop/urls.py
Normal file
14
backend/shop/urls.py
Normal file
@@ -0,0 +1,14 @@
|
||||
from django.urls import path, include
|
||||
from rest_framework.routers import DefaultRouter
|
||||
from .views import ESP32ConfigViewSet, OrderViewSet, order_check_view, ServiceViewSet, ARServiceViewSet
|
||||
|
||||
router = DefaultRouter()
|
||||
router.register(r'configs', ESP32ConfigViewSet)
|
||||
router.register(r'orders', OrderViewSet)
|
||||
router.register(r'services', ServiceViewSet)
|
||||
router.register(r'ar', ARServiceViewSet)
|
||||
|
||||
urlpatterns = [
|
||||
path('', include(router.urls)),
|
||||
path('page/check-order/', order_check_view, name='check-order-page'),
|
||||
]
|
||||
125
backend/shop/views.py
Normal file
125
backend/shop/views.py
Normal file
@@ -0,0 +1,125 @@
|
||||
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
|
||||
|
||||
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')
|
||||
|
||||
class ServiceViewSet(viewsets.ReadOnlyModelViewSet):
|
||||
"""
|
||||
AI服务列表和详情
|
||||
"""
|
||||
queryset = Service.objects.all().order_by('-created_at')
|
||||
serializer_class = ServiceSerializer
|
||||
|
||||
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