This commit is contained in:
jeremygan2021
2026-02-11 04:06:51 +08:00
parent 96d5598fb5
commit 1100143a6e
36 changed files with 1223 additions and 401 deletions

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, VBCourse, ProductFeature, CommissionLog, WeChatUser, Distributor, Withdrawal, ServiceOrder
from .models import ESP32Config, Order, Salesperson, WeChatPayConfig, Service, VCCourse, ProductFeature, CommissionLog, WeChatUser, Distributor, Withdrawal, ServiceOrder, CourseEnrollment
import qrcode
from io import BytesIO
import base64
@@ -83,7 +83,7 @@ class ESP32ConfigAdmin(ModelAdmin):
inlines = [ProductFeatureInline]
fieldsets = (
('基本信息', {
'fields': ('name', 'price', 'stock', 'description')
'fields': ('name', 'price', 'stock', 'commission_rate', 'description')
}),
('硬件参数', {
'fields': ('chip_type', 'flash_size', 'ram_size', 'has_camera', 'has_microphone')
@@ -141,17 +141,21 @@ class ServiceOrderAdmin(ModelAdmin):
}),
)
@admin.register(VBCourse)
class VBCourseAdmin(ModelAdmin):
list_display = ('title', 'course_type', 'tag', 'instructor', 'lesson_count', 'duration', 'created_at')
@admin.register(VCCourse)
class VCCourseAdmin(ModelAdmin):
list_display = ('title', 'course_type', 'price', 'tag', 'instructor', 'lesson_count', 'duration', 'created_at')
search_fields = ('title', 'description', 'instructor', 'tag')
list_filter = ('course_type', 'instructor', 'tag')
fieldsets = (
('基本信息', {
'fields': ('title', 'description', 'course_type', 'tag')
'fields': ('title', 'description', 'course_type', 'tag', 'price')
}),
('讲师信息', {
'fields': ('instructor', 'instructor_title', 'instructor_desc', 'instructor_avatar', 'instructor_avatar_url'),
'description': '讲师头像上传和URL二选一优先使用URL'
}),
('课程详情', {
'fields': ('instructor', 'duration', 'lesson_count')
'fields': ('duration', 'lesson_count', 'content')
}),
('封面', {
'fields': ('cover_image', 'cover_image_url'),
@@ -163,6 +167,24 @@ class VBCourseAdmin(ModelAdmin):
}),
)
@admin.register(CourseEnrollment)
class CourseEnrollmentAdmin(ModelAdmin):
list_display = ('customer_name', 'course', 'phone_number', 'status', 'created_at')
list_filter = ('status', 'course', 'created_at')
search_fields = ('customer_name', 'phone_number', 'wechat_id')
fieldsets = (
('报名信息', {
'fields': ('course', 'status', 'created_at')
}),
('客户资料', {
'fields': ('customer_name', 'phone_number', 'wechat_id', 'email', 'message')
}),
('销售归属', {
'fields': ('salesperson', 'distributor')
}),
)
@admin.register(Salesperson)
class SalespersonAdmin(ModelAdmin):
list_display = ('name', 'code', 'total_sales', 'view_promotion_url')
@@ -240,14 +262,14 @@ class SalespersonAdmin(ModelAdmin):
@admin.register(CommissionLog)
class CommissionLogAdmin(ModelAdmin):
list_display = ('id', 'salesperson', 'amount', 'level', 'status', 'created_at')
list_filter = ('status', 'level', 'salesperson', 'created_at')
search_fields = ('salesperson__name', 'order__id')
list_display = ('id', 'salesperson', 'distributor', 'amount', 'level', 'status', 'created_at')
list_filter = ('status', 'level', 'salesperson', 'distributor', 'created_at')
search_fields = ('salesperson__name', 'distributor__user__nickname', 'order__id')
readonly_fields = ('amount', 'level', 'created_at')
fieldsets = (
('基本信息', {
'fields': ('salesperson', 'order', 'amount', 'level')
'fields': ('salesperson', 'distributor', 'order', 'amount', 'level')
}),
('状态管理', {
'fields': ('status', 'created_at')
@@ -256,23 +278,31 @@ class CommissionLogAdmin(ModelAdmin):
@admin.register(Order)
class OrderAdmin(ModelAdmin):
list_display = ('id', 'customer_name', 'config', 'total_price', 'status', 'courier_name', 'tracking_number', 'salesperson', 'created_at')
list_filter = ('status', 'salesperson', 'created_at')
list_display = ('id', 'customer_name', 'get_item_name', 'total_price', 'status', 'salesperson', 'distributor', 'created_at')
list_filter = ('status', 'salesperson', 'distributor', 'created_at')
search_fields = ('id', 'customer_name', 'phone_number', 'wechat_trade_no')
readonly_fields = ('total_price', 'created_at', 'wechat_trade_no')
def get_item_name(self, obj):
if obj.config:
return f"[硬件] {obj.config.name}"
if obj.course:
return f"[课程] {obj.course.title}"
return "未知商品"
get_item_name.short_description = "购买商品"
fieldsets = (
('订单信息', {
'fields': ('config', 'quantity', 'total_price', 'status', 'created_at')
'fields': ('config', 'course', 'quantity', 'total_price', 'status', 'created_at')
}),
('客户信息', {
'fields': ('customer_name', 'phone_number', 'shipping_address')
'fields': ('customer_name', 'phone_number', 'shipping_address', 'wechat_user')
}),
('物流信息', {
'fields': ('courier_name', 'tracking_number')
}),
('销售归属', {
'fields': ('salesperson',)
'fields': ('salesperson', 'distributor')
}),
('支付信息', {
'fields': ('wechat_trade_no',)

View File

@@ -0,0 +1,29 @@
# Generated by Django 6.0.1 on 2026-02-10 19:08
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('shop', '0020_alter_vbcourse_course_type'),
]
operations = [
migrations.AddField(
model_name='commissionlog',
name='distributor',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='commissions', to='shop.distributor', verbose_name='获佣分销员'),
),
migrations.AddField(
model_name='order',
name='distributor',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='orders', to='shop.distributor', verbose_name='所属分销员'),
),
migrations.AlterField(
model_name='commissionlog',
name='salesperson',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='commissions', to='shop.salesperson', verbose_name='获佣销售员'),
),
]

View File

@@ -0,0 +1,45 @@
# Generated by Django 6.0.1 on 2026-02-10 19:20
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('shop', '0021_commissionlog_distributor_order_distributor_and_more'),
]
operations = [
migrations.AddField(
model_name='vbcourse',
name='content',
field=models.TextField(blank=True, help_text='支持Markdown或HTML', verbose_name='详细内容'),
),
migrations.AddField(
model_name='vbcourse',
name='price',
field=models.DecimalField(decimal_places=2, default=0, help_text='0表示免费', max_digits=10, verbose_name='价格'),
),
migrations.CreateModel(
name='CourseEnrollment',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('customer_name', models.CharField(max_length=100, verbose_name='姓名')),
('phone_number', models.CharField(max_length=20, verbose_name='联系电话')),
('email', models.EmailField(blank=True, max_length=254, verbose_name='电子邮箱')),
('wechat_id', models.CharField(blank=True, max_length=50, verbose_name='微信号')),
('message', models.TextField(blank=True, verbose_name='留言/备注')),
('status', models.CharField(choices=[('pending', '待联系'), ('contacted', '已联系'), ('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='更新时间')),
('course', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='enrollments', to='shop.vbcourse', verbose_name='咨询课程')),
('distributor', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='shop.distributor', verbose_name='所属分销员')),
('salesperson', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='shop.salesperson', verbose_name='所属销售员')),
],
options={
'verbose_name': '课程报名',
'verbose_name_plural': '课程报名管理',
},
),
]

View File

@@ -0,0 +1,24 @@
# Generated by Django 6.0.1 on 2026-02-10 19:23
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('shop', '0022_vbcourse_content_vbcourse_price_courseenrollment'),
]
operations = [
migrations.AddField(
model_name='order',
name='course',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='orders', to='shop.vbcourse', verbose_name='所选课程'),
),
migrations.AlterField(
model_name='order',
name='config',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='shop.esp32config', verbose_name='所选配置'),
),
]

View File

@@ -0,0 +1,33 @@
# Generated by Django 6.0.1 on 2026-02-10 19:28
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('shop', '0023_order_course_alter_order_config'),
]
operations = [
migrations.AddField(
model_name='vbcourse',
name='instructor_avatar',
field=models.ImageField(blank=True, null=True, upload_to='instructors/avatars/', verbose_name='讲师头像 (上传)'),
),
migrations.AddField(
model_name='vbcourse',
name='instructor_avatar_url',
field=models.URLField(blank=True, null=True, verbose_name='讲师头像 (URL)'),
),
migrations.AddField(
model_name='vbcourse',
name='instructor_desc',
field=models.TextField(blank=True, default='拥有多年开发经验,擅长...', verbose_name='讲师简介'),
),
migrations.AddField(
model_name='vbcourse',
name='instructor_title',
field=models.CharField(default='资深讲师', max_length=50, verbose_name='讲师头衔'),
),
]

View File

@@ -0,0 +1,55 @@
# Generated by Django 6.0.1 on 2026-02-10 19:44
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('shop', '0024_vbcourse_instructor_avatar_and_more'),
]
operations = [
migrations.CreateModel(
name='VCCourse',
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', '硬件课程'), ('incubation', '产品商业孵化')], 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='VC讲师', max_length=50, verbose_name='讲师')),
('instructor_title', models.CharField(default='资深讲师', max_length=50, verbose_name='讲师头衔')),
('instructor_avatar', models.ImageField(blank=True, null=True, upload_to='instructors/avatars/', verbose_name='讲师头像 (上传)')),
('instructor_avatar_url', models.URLField(blank=True, null=True, verbose_name='讲师头像 (URL)')),
('instructor_desc', models.TextField(blank=True, default='拥有多年开发经验,擅长...', verbose_name='讲师简介')),
('tag', models.CharField(blank=True, help_text='例如: 热门, 推荐, 进阶', max_length=20, verbose_name='标签')),
('price', models.DecimalField(decimal_places=2, default=0, help_text='0表示免费', max_digits=10, verbose_name='价格')),
('content', models.TextField(blank=True, help_text='支持Markdown或HTML', 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)')),
('detail_image', models.ImageField(blank=True, null=True, upload_to='courses/details/', verbose_name='详情页长图 (上传)')),
('detail_image_url', models.URLField(blank=True, help_text='如果填写了URL将优先使用URL', null=True, verbose_name='详情页长图 (URL)')),
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
],
options={
'verbose_name': 'VC课程',
'verbose_name_plural': 'VC课程管理',
},
),
migrations.AlterField(
model_name='courseenrollment',
name='course',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='enrollments', to='shop.vccourse', verbose_name='咨询课程'),
),
migrations.AlterField(
model_name='order',
name='course',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='orders', to='shop.vccourse', verbose_name='所选课程'),
),
migrations.DeleteModel(
name='VBCourse',
),
]

View File

@@ -165,7 +165,8 @@ class CommissionLog(models.Model):
)
order = models.ForeignKey('Order', on_delete=models.CASCADE, verbose_name="关联订单", related_name='commissions')
salesperson = models.ForeignKey(Salesperson, on_delete=models.CASCADE, verbose_name="获佣销售员", related_name='commissions')
salesperson = models.ForeignKey(Salesperson, on_delete=models.CASCADE, verbose_name="获佣销售员", related_name='commissions', null=True, blank=True)
distributor = models.ForeignKey(Distributor, on_delete=models.CASCADE, verbose_name="获佣分销员", related_name='commissions', null=True, blank=True)
amount = models.DecimalField(max_digits=10, decimal_places=2, verbose_name="佣金金额")
level = models.IntegerField(default=1, verbose_name="分销层级", help_text="1: 直接销售, 2: 二级分销")
status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='pending', verbose_name="状态")
@@ -219,13 +220,15 @@ class Order(models.Model):
('cancelled', '已取消'),
)
config = models.ForeignKey(ESP32Config, on_delete=models.CASCADE, verbose_name="所选配置")
config = models.ForeignKey(ESP32Config, on_delete=models.CASCADE, verbose_name="所选配置", null=True, blank=True)
course = models.ForeignKey('VCCourse', on_delete=models.SET_NULL, null=True, blank=True, verbose_name="所选课程", related_name='orders')
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')
distributor = models.ForeignKey(Distributor, on_delete=models.SET_NULL, null=True, blank=True, verbose_name="所属分销员", related_name='orders')
# 关联微信用户
wechat_user = models.ForeignKey(WeChatUser, on_delete=models.SET_NULL, null=True, blank=True, verbose_name="下单微信用户", related_name='orders')
@@ -312,9 +315,9 @@ class ServiceOrder(models.Model):
verbose_name_plural = "服务订单列表"
class VBCourse(models.Model):
class VCCourse(models.Model):
"""
VB Coding 课程模型
VC (VB Coding) 课程模型
"""
COURSE_TYPE_CHOICES = (
('software', '软件课程'),
@@ -327,10 +330,17 @@ class VBCourse(models.Model):
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讲师")
instructor = models.CharField(max_length=50, verbose_name="讲师", default="VC讲师")
instructor_title = models.CharField(max_length=50, verbose_name="讲师头衔", default="资深讲师")
instructor_avatar = models.ImageField(upload_to='instructors/avatars/', blank=True, null=True, verbose_name="讲师头像 (上传)")
instructor_avatar_url = models.URLField(blank=True, null=True, verbose_name="讲师头像 (URL)")
instructor_desc = models.TextField(blank=True, verbose_name="讲师简介", default="拥有多年开发经验,擅长...")
tag = models.CharField(max_length=20, blank=True, verbose_name="标签", help_text="例如: 热门, 推荐, 进阶")
price = models.DecimalField(max_digits=10, decimal_places=2, default=0, verbose_name="价格", help_text="0表示免费")
content = models.TextField(blank=True, verbose_name="详细内容", help_text="支持Markdown或HTML")
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)")
@@ -343,5 +353,40 @@ class VBCourse(models.Model):
return self.title
class Meta:
verbose_name = "VB课程"
verbose_name_plural = "VB课程管理"
verbose_name = "VC课程"
verbose_name_plural = "VC课程管理"
class CourseEnrollment(models.Model):
"""
课程报名/咨询记录
"""
STATUS_CHOICES = (
('pending', '待联系'),
('contacted', '已联系'),
('completed', '已完成'),
('cancelled', '已取消'),
)
course = models.ForeignKey(VCCourse, on_delete=models.CASCADE, verbose_name="咨询课程", related_name='enrollments')
customer_name = models.CharField(max_length=100, verbose_name="姓名")
phone_number = models.CharField(max_length=20, verbose_name="联系电话")
email = models.EmailField(blank=True, verbose_name="电子邮箱")
wechat_id = models.CharField(max_length=50, blank=True, verbose_name="微信号")
message = models.TextField(blank=True, 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="所属销售员")
distributor = models.ForeignKey(Distributor, 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.course.title}"
class Meta:
verbose_name = "课程报名"
verbose_name_plural = "课程报名管理"

View File

@@ -1,5 +1,23 @@
from rest_framework import serializers
from .models import ESP32Config, Order, Salesperson, Service, VBCourse, ProductFeature, ServiceOrder, WeChatUser, Distributor, Withdrawal
from .models import ESP32Config, Order, Salesperson, Service, VCCourse, ProductFeature, ServiceOrder, WeChatUser, Distributor, Withdrawal, CommissionLog, CourseEnrollment
class CommissionLogSerializer(serializers.ModelSerializer):
"""
佣金记录序列化器
"""
order_info = serializers.SerializerMethodField()
class Meta:
model = CommissionLog
fields = ['id', 'amount', 'level', 'status', 'created_at', 'order_info']
read_only_fields = ['id', 'amount', 'level', 'status', 'created_at', 'order_info']
def get_order_info(self, obj):
return {
'order_id': obj.order.id,
'total_price': obj.order.total_price,
'customer_name': obj.order.customer_name
}
class WeChatUserSerializer(serializers.ModelSerializer):
class Meta:
@@ -69,6 +87,37 @@ class ServiceSerializer(serializers.ModelSerializer):
return obj.detail_image.url
return None
class CourseEnrollmentSerializer(serializers.ModelSerializer):
"""
课程报名序列化器
"""
course_title = serializers.CharField(source='course.title', read_only=True)
ref_code = serializers.CharField(write_only=True, required=False, allow_blank=True)
class Meta:
model = CourseEnrollment
fields = ['id', 'course', 'course_title', 'customer_name', 'phone_number', 'email', 'wechat_id', 'message', 'status', 'created_at', 'ref_code']
read_only_fields = ['status', 'created_at']
def create(self, validated_data):
ref_code = validated_data.pop('ref_code', None)
# 尝试关联销售员或分销员
if ref_code:
try:
salesperson = Salesperson.objects.get(code=ref_code)
validated_data['salesperson'] = salesperson
except Salesperson.DoesNotExist:
pass
try:
distributor = Distributor.objects.get(invite_code=ref_code)
validated_data['distributor'] = distributor
except Distributor.DoesNotExist:
pass
return super().create(validated_data)
class ServiceOrderSerializer(serializers.ModelSerializer):
"""
AI服务订单序列化器
@@ -101,16 +150,16 @@ class ServiceOrderSerializer(serializers.ModelSerializer):
return super().create(validated_data)
class VBCourseSerializer(serializers.ModelSerializer):
class VCCourseSerializer(serializers.ModelSerializer):
"""
VB课程序列化器
VC课程序列化器
"""
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 = VBCourse
model = VCCourse
fields = '__all__'
def get_display_cover_image(self, obj):
@@ -151,6 +200,7 @@ class OrderSerializer(serializers.ModelSerializer):
订单序列化器
"""
config_name = serializers.CharField(source='config.name', read_only=True)
course_title = serializers.CharField(source='course.title', read_only=True)
config_image = serializers.SerializerMethodField()
salesperson_name = serializers.CharField(source='salesperson.name', read_only=True)
salesperson_code = serializers.CharField(source='salesperson.code', read_only=True)
@@ -159,41 +209,76 @@ class OrderSerializer(serializers.ModelSerializer):
class Meta:
model = Order
fields = ['id', 'config', 'config_name', 'config_image', 'quantity', 'total_price', 'status', 'created_at', 'updated_at', 'wechat_trade_no',
fields = ['id', 'config', 'config_name', 'config_image', 'course', 'course_title', 'quantity', 'total_price', 'status', 'created_at', 'updated_at', 'wechat_trade_no',
'customer_name', 'phone_number', 'shipping_address', 'ref_code', 'salesperson_name', 'salesperson_code', 'courier_name', 'tracking_number']
read_only_fields = ['total_price', 'status', 'wechat_trade_no', 'created_at', 'updated_at']
extra_kwargs = {
'customer_name': {'required': True},
'phone_number': {'required': True},
'shipping_address': {'required': True},
}
def validate(self, data):
# 如果是部分更新 (PATCH),可能不需要校验所有字段,但这里主要用于创建
if self.instance:
return data
config = data.get('config')
course = data.get('course')
if not config and not course:
raise serializers.ValidationError("必须选择一种商品(硬件配置或课程)")
if config and course:
raise serializers.ValidationError("一次只能购买一种类型的商品")
if config and not data.get('shipping_address'):
raise serializers.ValidationError({"shipping_address": "购买硬件产品需要填写收货地址"})
return data
def get_config_image(self, obj):
if obj.config.static_image_url:
return obj.config.static_image_url
if obj.config.detail_image_url:
return obj.config.detail_image_url
if obj.config.detail_image:
return obj.config.detail_image.url
if obj.config:
if obj.config.static_image_url:
return obj.config.static_image_url
if obj.config.detail_image_url:
return obj.config.detail_image_url
if obj.config.detail_image:
return obj.config.detail_image.url
elif obj.course:
if obj.course.cover_image_url:
return obj.course.cover_image_url
if obj.course.cover_image:
return obj.course.cover_image.url
return None
def create(self, validated_data):
"""
重写创建方法,自动计算总价并关联销售员
重写创建方法,自动计算总价并关联销售员/分销员
"""
config = validated_data.get('config')
course = validated_data.get('course')
quantity = validated_data.get('quantity', 1)
ref_code = validated_data.pop('ref_code', None)
validated_data['total_price'] = config.price * quantity
# 尝试关联销售员
if config:
validated_data['total_price'] = config.price * quantity
elif course:
validated_data['total_price'] = course.price * quantity
# 尝试关联销售员或分销员
if ref_code:
# 1. 尝试查找旧版销售员
try:
salesperson = Salesperson.objects.get(code=ref_code)
validated_data['salesperson'] = salesperson
except Salesperson.DoesNotExist:
# 如果找不到对应的销售员,忽略该推广码,仍继续创建订单(算作自然流量)
pass
# 2. 尝试查找新版分销员
try:
distributor = Distributor.objects.get(invite_code=ref_code)
validated_data['distributor'] = distributor
except Distributor.DoesNotExist:
pass
return super().create(validated_data)

View File

@@ -2,15 +2,17 @@ from django.urls import path, include, re_path
from rest_framework.routers import DefaultRouter
from .views import (
ESP32ConfigViewSet, OrderViewSet, order_check_view,
ServiceViewSet, VBCourseViewSet, ServiceOrderViewSet,
payment_finish, pay, send_sms_code, wechat_login, update_user_info, DistributorViewSet
ServiceViewSet, VCCourseViewSet, ServiceOrderViewSet,
payment_finish, pay, send_sms_code, wechat_login, update_user_info, DistributorViewSet,
CourseEnrollmentViewSet
)
router = DefaultRouter()
router.register(r'configs', ESP32ConfigViewSet)
router.register(r'orders', OrderViewSet)
router.register(r'services', ServiceViewSet)
router.register(r'courses', VBCourseViewSet)
router.register(r'courses', VCCourseViewSet)
router.register(r'course-enrollments', CourseEnrollmentViewSet)
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, VBCourse, ServiceOrder, Salesperson, CommissionLog, WeChatUser, Distributor, Withdrawal
from .serializers import ESP32ConfigSerializer, OrderSerializer, ServiceSerializer, VBCourseSerializer, ServiceOrderSerializer, WeChatUserSerializer, DistributorSerializer, WithdrawalSerializer
from .models import ESP32Config, Order, WeChatPayConfig, Service, VCCourse, ServiceOrder, Salesperson, CommissionLog, WeChatUser, Distributor, Withdrawal, CourseEnrollment
from .serializers import ESP32ConfigSerializer, OrderSerializer, ServiceSerializer, VCCourseSerializer, ServiceOrderSerializer, WeChatUserSerializer, DistributorSerializer, WithdrawalSerializer, CommissionLogSerializer, CourseEnrollmentSerializer
from django.core.signing import TimestampSigner, BadSignature, SignatureExpired
from django.contrib.auth.models import User
from wechatpayv3 import WeChatPay, WeChatPayType
@@ -215,6 +215,7 @@ def pay(request):
# 1. 获取并验证请求参数
good_id = request.data.get('goodid')
order_type = request.data.get('type', 'config') # 默认为 config
quantity = int(request.data.get('quantity', 1))
customer_name = request.data.get('customer_name')
phone_number = request.data.get('phone_number')
@@ -237,15 +238,23 @@ def pay(request):
return Response({'error': f'支付配置错误: {error_msg}'}, status=status.HTTP_400_BAD_REQUEST)
# 3. 查找商品和销售员,创建订单
try:
product = ESP32Config.objects.get(id=good_id)
except ESP32Config.DoesNotExist:
print(f"商品不存在: {good_id}")
return Response({'error': f'找不到 ID 为 {good_id} 的商品'}, status=status.HTTP_404_NOT_FOUND)
product = None
if order_type == 'course':
try:
product = VBCourse.objects.get(id=good_id)
except VBCourse.DoesNotExist:
print(f"课程不存在: {good_id}")
return Response({'error': f'找不到 ID 为 {good_id} 的课程'}, status=status.HTTP_404_NOT_FOUND)
else:
try:
product = ESP32Config.objects.get(id=good_id)
except ESP32Config.DoesNotExist:
print(f"商品不存在: {good_id}")
return Response({'error': f'找不到 ID 为 {good_id} 的商品'}, status=status.HTTP_404_NOT_FOUND)
# 检查库存
if product.stock < quantity:
return Response({'error': f'库存不足,仅剩 {product.stock}'}, status=status.HTTP_400_BAD_REQUEST)
# 检查库存 (仅针对硬件)
if product.stock < quantity:
return Response({'error': f'库存不足,仅剩 {product.stock}'}, status=status.HTTP_400_BAD_REQUEST)
salesperson = None
if ref_code:
@@ -255,24 +264,34 @@ def pay(request):
total_price = product.price * quantity
amount_in_cents = int(total_price * 100)
order = Order.objects.create(
config=product,
quantity=quantity,
total_price=total_price,
customer_name=customer_name,
phone_number=phone_number,
shipping_address=shipping_address,
salesperson=salesperson,
status='pending'
)
order_kwargs = {
'quantity': quantity,
'total_price': total_price,
'customer_name': customer_name,
'phone_number': phone_number,
'shipping_address': shipping_address,
'salesperson': salesperson,
'status': 'pending'
}
if order_type == 'course':
order_kwargs['course'] = product
else:
order_kwargs['config'] = product
# 扣减库存
product.stock -= quantity
product.save()
order = Order.objects.create(**order_kwargs)
# 扣减库存 (仅针对硬件)
if order_type != 'course':
product.stock -= quantity
product.save()
# 4. 调用微信支付接口
out_trade_no = f"PAY{order.id}T{int(time.time())}"
description = f"购买 {product.name} x {quantity}"
if order_type == 'course':
description = f"报名 {product.title}"
else:
description = f"购买 {product.name} x {quantity}"
# 保存商户订单号到数据库,方便后续查询
order.out_trade_no = out_trade_no
@@ -459,13 +478,19 @@ def payment_finish(request):
order.save()
print(f"订单 {order.id} 状态已更新")
# 计算佣金
# 计算佣金 (旧版销售员系统)
try:
salesperson = order.salesperson
if salesperson:
# 1. 计算直接佣金 (一级)
# 优先级: 产品独立分润比例 > 销售员个人分润比例
rate_1 = order.config.commission_rate if order.config.commission_rate > 0 else salesperson.commission_rate
rate_1 = 0
if order.config:
rate_1 = order.config.commission_rate if order.config.commission_rate > 0 else salesperson.commission_rate
elif order.course:
# 课程暂时使用销售员默认比例
rate_1 = salesperson.commission_rate
amount_1 = order.total_price * rate_1
if amount_1 > 0:
@@ -476,7 +501,7 @@ def payment_finish(request):
level=1,
status='pending'
)
print(f"生成一级佣金: {salesperson.name} - {amount_1}")
print(f"生成一级佣金(Salesperson): {salesperson.name} - {amount_1}")
# 2. 计算上级佣金 (二级)
parent = salesperson.parent
@@ -492,9 +517,61 @@ def payment_finish(request):
level=2,
status='pending'
)
print(f"生成二级佣金: {parent.name} - {amount_2}")
print(f"生成二级佣金(Salesperson): {parent.name} - {amount_2}")
# 计算佣金 (新版分销员系统)
distributor = order.distributor
if distributor:
# 1. 计算直接佣金 (一级)
# 优先级: 产品独立分润比例 > 分销员个人分润比例
rate_1 = 0
if order.config:
rate_1 = order.config.commission_rate if order.config.commission_rate > 0 else distributor.commission_rate
elif order.course:
# 课程暂时使用分销员默认比例
rate_1 = distributor.commission_rate
amount_1 = order.total_price * rate_1
if amount_1 > 0:
CommissionLog.objects.create(
order=order,
distributor=distributor,
amount=amount_1,
level=1,
status='settled' # 简化流程,直接结算到余额
)
# 更新余额
distributor.total_earnings += amount_1
distributor.withdrawable_balance += amount_1
distributor.save()
print(f"生成一级佣金(Distributor): {distributor.user.nickname} - {amount_1}")
# 2. 计算上级佣金 (二级)
parent = distributor.parent
if parent:
# 二级固定比例 2% (0.02)
rate_2 = 0.02
amount_2 = order.total_price * models.DecimalField(max_digits=5, decimal_places=4).to_python(rate_2)
if amount_2 > 0:
CommissionLog.objects.create(
order=order,
distributor=parent,
amount=amount_2,
level=2,
status='settled'
)
# 更新余额
parent.total_earnings += amount_2
parent.withdrawable_balance += amount_2
parent.save()
print(f"生成二级佣金(Distributor): {parent.user.nickname} - {amount_2}")
except Exception as e:
print(f"佣金计算失败: {str(e)}")
import traceback
traceback.print_exc()
except Exception as e:
print(f"订单更新失败: {str(e)}")
@@ -508,15 +585,22 @@ def payment_finish(request):
return HttpResponse(str(e), status=500)
@extend_schema_view(
list=extend_schema(summary="获取VB课程列表", description="获取所有可用的VB课程"),
retrieve=extend_schema(summary="获取VB课程详情", description="获取指定VB课程的详细信息")
list=extend_schema(summary="获取VC课程列表", description="获取所有可用的VC课程"),
retrieve=extend_schema(summary="获取VC课程详情", description="获取指定VC课程的详细信息")
)
class VBCourseViewSet(viewsets.ReadOnlyModelViewSet):
class VCCourseViewSet(viewsets.ReadOnlyModelViewSet):
"""
VB课程列表和详情
VC课程列表和详情
"""
queryset = VBCourse.objects.all().order_by('-created_at')
serializer_class = VBCourseSerializer
queryset = VCCourse.objects.all().order_by('-created_at')
serializer_class = VCCourseSerializer
class CourseEnrollmentViewSet(viewsets.ModelViewSet):
"""
课程报名管理
"""
queryset = CourseEnrollment.objects.all().order_by('-created_at')
serializer_class = CourseEnrollmentSerializer
def order_check_view(request):
"""
@@ -989,3 +1073,64 @@ class DistributorViewSet(viewsets.GenericViewSet):
return Response({'message': 'Withdrawal request submitted'})
@action(detail=False, methods=['get'])
def earnings(self, request):
"""查看个人分销金额及明细"""
user = get_current_wechat_user(request)
if not user or not hasattr(user, 'distributor'):
return Response({'error': 'Unauthorized'}, status=401)
distributor = user.distributor
logs = CommissionLog.objects.filter(distributor=distributor).order_by('-created_at')
page = self.paginate_queryset(logs)
if page is not None:
serializer = CommissionLogSerializer(page, many=True)
return self.get_paginated_response(serializer.data)
serializer = CommissionLogSerializer(logs, many=True)
return Response(serializer.data)
@action(detail=False, methods=['get'])
def team(self, request):
"""查看团队(二级分销情况)"""
user = get_current_wechat_user(request)
if not user or not hasattr(user, 'distributor'):
return Response({'error': 'Unauthorized'}, status=401)
distributor = user.distributor
# 直推下级
children = Distributor.objects.filter(parent=distributor)
children_data = DistributorSerializer(children, many=True).data
# 二级分销收益统计
second_level_earnings = CommissionLog.objects.filter(distributor=distributor, level=2).aggregate(total=models.Sum('amount'))['total'] or 0
return Response({
'children_count': children.count(),
'children': children_data,
'second_level_earnings': second_level_earnings
})
@action(detail=False, methods=['get'])
def orders(self, request):
"""查看分销订单"""
user = get_current_wechat_user(request)
if not user or not hasattr(user, 'distributor'):
return Response({'error': 'Unauthorized'}, status=401)
distributor = user.distributor
# 查找我赚了钱的订单
commission_logs = CommissionLog.objects.filter(distributor=distributor).select_related('order')
order_ids = commission_logs.values_list('order_id', flat=True)
orders = Order.objects.filter(id__in=order_ids).order_by('-created_at')
page = self.paginate_queryset(orders)
if page is not None:
serializer = OrderSerializer(page, many=True)
return self.get_paginated_response(serializer.data)
serializer = OrderSerializer(orders, many=True)
return Response(serializer.data)