This commit is contained in:
jeremygan2021
2026-02-11 03:00:38 +08:00
parent c3b4373c94
commit 96d5598fb5
57 changed files with 2239 additions and 577 deletions

View File

@@ -12,7 +12,7 @@ links = [
"admin:shop_distributor_changelist",
"admin:shop_esp32config_changelist",
"admin:shop_service_changelist",
"admin:shop_arservice_changelist",
"admin:shop_vbcourse_changelist",
"admin:shop_order_changelist",
"admin:shop_serviceorder_changelist",
"admin:shop_withdrawal_changelist",

View File

@@ -232,9 +232,9 @@ UNFOLD = {
"link": reverse_lazy("admin:shop_service_changelist"),
},
{
"title": "AR体验",
"icon": "view_in_ar",
"link": reverse_lazy("admin:shop_arservice_changelist"),
"title": "VB课程",
"icon": "school",
"link": reverse_lazy("admin:shop_vbcourse_changelist"),
},
],
},

Binary file not shown.

View File

@@ -4,7 +4,7 @@ from django.db.models import Sum
from django import forms
from unfold.admin import ModelAdmin, TabularInline
from unfold.decorators import display
from .models import ESP32Config, Order, Salesperson, WeChatPayConfig, Service, ARService, ProductFeature, CommissionLog, WeChatUser, Distributor, Withdrawal, ServiceOrder
from .models import ESP32Config, Order, Salesperson, WeChatPayConfig, Service, VBCourse, ProductFeature, CommissionLog, WeChatUser, Distributor, Withdrawal, ServiceOrder
import qrcode
from io import BytesIO
import base64
@@ -19,11 +19,11 @@ class ExternalUploadWidget(forms.URLInput):
super().__init__(*args, **kwargs)
self.upload_url = upload_url
self.attrs.update({
'class': 'upload-url-input',
'class': 'upload-url-input vTextField',
'data-upload-url': upload_url,
'data-accept': accept,
'readonly': 'readonly',
'placeholder': '上传文件后自动生成URL'
'placeholder': '上传文件后自动生成URL',
'style': 'width: 100%;'
})
class Media:
@@ -141,18 +141,26 @@ class ServiceOrderAdmin(ModelAdmin):
}),
)
@admin.register(ARService)
class ARServiceAdmin(ModelAdmin):
list_display = ('title', 'created_at')
search_fields = ('title', 'description')
@admin.register(VBCourse)
class VBCourseAdmin(ModelAdmin):
list_display = ('title', 'course_type', 'tag', 'instructor', 'lesson_count', 'duration', 'created_at')
search_fields = ('title', 'description', 'instructor', 'tag')
list_filter = ('course_type', 'instructor', 'tag')
fieldsets = (
('基本信息', {
'fields': ('title', 'description')
'fields': ('title', 'description', 'course_type', 'tag')
}),
('封面/长图', {
('课程详情', {
'fields': ('instructor', 'duration', 'lesson_count')
}),
('封面', {
'fields': ('cover_image', 'cover_image_url'),
'description': '图片上传和URL二选一优先使用URL'
}),
('详情页长图', {
'fields': ('detail_image', 'detail_image_url'),
'description': '图片上传和URL二选一优先使用URL'
}),
)
@admin.register(Salesperson)

View File

@@ -0,0 +1,35 @@
# Generated by Django 6.0.1 on 2026-02-10 18:48
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('shop', '0017_withdrawal'),
]
operations = [
migrations.CreateModel(
name='VBCourse',
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='课程简介')),
('course_type', models.CharField(choices=[('software', '软件课程'), ('hardware', '硬件课程')], default='software', max_length=20, verbose_name='课程类型')),
('duration', models.CharField(default='30分钟', help_text='例如: 30分钟', max_length=50, verbose_name='课程时长')),
('lesson_count', models.IntegerField(default=1, verbose_name='课时数量')),
('instructor', models.CharField(default='VB讲师', max_length=50, verbose_name='讲师')),
('cover_image', models.ImageField(blank=True, null=True, upload_to='courses/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': 'VB课程',
'verbose_name_plural': 'VB课程管理',
},
),
migrations.DeleteModel(
name='ARService',
),
]

View File

@@ -0,0 +1,28 @@
# Generated by Django 6.0.1 on 2026-02-10 18:55
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('shop', '0018_vbcourse_delete_arservice'),
]
operations = [
migrations.AddField(
model_name='vbcourse',
name='detail_image',
field=models.ImageField(blank=True, null=True, upload_to='courses/details/', verbose_name='详情页长图 (上传)'),
),
migrations.AddField(
model_name='vbcourse',
name='detail_image_url',
field=models.URLField(blank=True, help_text='如果填写了URL将优先使用URL', null=True, verbose_name='详情页长图 (URL)'),
),
migrations.AddField(
model_name='vbcourse',
name='tag',
field=models.CharField(blank=True, help_text='例如: 热门, 推荐, 进阶', max_length=20, verbose_name='标签'),
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 6.0.1 on 2026-02-10 18:58
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('shop', '0019_vbcourse_detail_image_vbcourse_detail_image_url_and_more'),
]
operations = [
migrations.AlterField(
model_name='vbcourse',
name='course_type',
field=models.CharField(choices=[('software', '软件课程'), ('hardware', '硬件课程'), ('incubation', '产品商业孵化')], default='software', max_length=20, verbose_name='课程类型'),
),
]

View File

@@ -312,19 +312,36 @@ class ServiceOrder(models.Model):
verbose_name_plural = "服务订单列表"
class ARService(models.Model):
class VBCourse(models.Model):
"""
AR体验服务模型
VB Coding 课程模型
"""
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)")
COURSE_TYPE_CHOICES = (
('software', '软件课程'),
('hardware', '硬件课程'),
('incubation', '产品商业孵化'),
)
title = models.CharField(max_length=100, verbose_name="课程名称")
description = models.TextField(verbose_name="课程简介")
course_type = models.CharField(max_length=20, choices=COURSE_TYPE_CHOICES, default='software', verbose_name="课程类型")
duration = models.CharField(max_length=50, verbose_name="课程时长", help_text="例如: 30分钟", default="30分钟")
lesson_count = models.IntegerField(default=1, verbose_name="课时数量")
instructor = models.CharField(max_length=50, verbose_name="讲师", default="VB讲师")
tag = models.CharField(max_length=20, blank=True, verbose_name="标签", help_text="例如: 热门, 推荐, 进阶")
cover_image = models.ImageField(upload_to='courses/covers/', blank=True, null=True, verbose_name="封面图 (上传)")
cover_image_url = models.URLField(blank=True, null=True, verbose_name="封面图 (URL)")
detail_image = models.ImageField(upload_to='courses/details/', blank=True, null=True, verbose_name="详情页长图 (上传)")
detail_image_url = models.URLField(blank=True, null=True, verbose_name="详情页长图 (URL)", help_text="如果填写了URL将优先使用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体验管理"
verbose_name = "VB课程"
verbose_name_plural = "VB课程管理"

View File

@@ -1,5 +1,5 @@
from rest_framework import serializers
from .models import ESP32Config, Order, Salesperson, Service, ARService, ProductFeature, ServiceOrder, WeChatUser, Distributor, Withdrawal
from .models import ESP32Config, Order, Salesperson, Service, VBCourse, ProductFeature, ServiceOrder, WeChatUser, Distributor, Withdrawal
class WeChatUserSerializer(serializers.ModelSerializer):
class Meta:
@@ -101,14 +101,16 @@ class ServiceOrderSerializer(serializers.ModelSerializer):
return super().create(validated_data)
class ARServiceSerializer(serializers.ModelSerializer):
class VBCourseSerializer(serializers.ModelSerializer):
"""
AR服务序列化器
VB课程序列化器
"""
display_cover_image = serializers.SerializerMethodField()
display_detail_image = serializers.SerializerMethodField()
course_type_display = serializers.CharField(source='get_course_type_display', read_only=True)
class Meta:
model = ARService
model = VBCourse
fields = '__all__'
def get_display_cover_image(self, obj):
@@ -118,6 +120,13 @@ class ARServiceSerializer(serializers.ModelSerializer):
return obj.cover_image.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 ESP32ConfigSerializer(serializers.ModelSerializer):
"""
ESP32配置序列化器

View File

@@ -2,7 +2,7 @@ from django.urls import path, include, re_path
from rest_framework.routers import DefaultRouter
from .views import (
ESP32ConfigViewSet, OrderViewSet, order_check_view,
ServiceViewSet, ARServiceViewSet, ServiceOrderViewSet,
ServiceViewSet, VBCourseViewSet, ServiceOrderViewSet,
payment_finish, pay, send_sms_code, wechat_login, update_user_info, DistributorViewSet
)
@@ -10,7 +10,7 @@ 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'courses', VBCourseViewSet)
router.register(r'service-orders', ServiceOrderViewSet)
router.register(r'distributor', DistributorViewSet, basename='distributor')

View File

@@ -5,8 +5,8 @@ from django.shortcuts import render
from django.views.decorators.csrf import csrf_exempt
from django.http import HttpResponse
from drf_spectacular.utils import extend_schema, extend_schema_view, OpenApiParameter, OpenApiExample
from .models import ESP32Config, Order, WeChatPayConfig, Service, ARService, ServiceOrder, Salesperson, CommissionLog, WeChatUser, Distributor, Withdrawal
from .serializers import ESP32ConfigSerializer, OrderSerializer, ServiceSerializer, ARServiceSerializer, ServiceOrderSerializer, WeChatUserSerializer, DistributorSerializer, WithdrawalSerializer
from .models import ESP32Config, Order, WeChatPayConfig, Service, VBCourse, ServiceOrder, Salesperson, CommissionLog, WeChatUser, Distributor, Withdrawal
from .serializers import ESP32ConfigSerializer, OrderSerializer, ServiceSerializer, VBCourseSerializer, ServiceOrderSerializer, WeChatUserSerializer, DistributorSerializer, WithdrawalSerializer
from django.core.signing import TimestampSigner, BadSignature, SignatureExpired
from django.contrib.auth.models import User
from wechatpayv3 import WeChatPay, WeChatPayType
@@ -508,15 +508,15 @@ def payment_finish(request):
return HttpResponse(str(e), status=500)
@extend_schema_view(
list=extend_schema(summary="获取AR服务列表", description="获取所有可用的AR服务"),
retrieve=extend_schema(summary="获取AR服务详情", description="获取指定AR服务的详细信息")
list=extend_schema(summary="获取VB课程列表", description="获取所有可用的VB课程"),
retrieve=extend_schema(summary="获取VB课程详情", description="获取指定VB课程的详细信息")
)
class ARServiceViewSet(viewsets.ReadOnlyModelViewSet):
class VBCourseViewSet(viewsets.ReadOnlyModelViewSet):
"""
AR服务列表和详情
VB课程列表和详情
"""
queryset = ARService.objects.all().order_by('-created_at')
serializer_class = ARServiceSerializer
queryset = VBCourse.objects.all().order_by('-created_at')
serializer_class = VBCourseSerializer
def order_check_view(request):
"""