diff --git a/backend/db.sqlite3 b/backend/db.sqlite3 index 5e31f0f..9de3983 100644 Binary files a/backend/db.sqlite3 and b/backend/db.sqlite3 differ diff --git a/backend/shop/__pycache__/admin.cpython-312.pyc b/backend/shop/__pycache__/admin.cpython-312.pyc index a844a80..c1409fc 100644 Binary files a/backend/shop/__pycache__/admin.cpython-312.pyc and b/backend/shop/__pycache__/admin.cpython-312.pyc differ diff --git a/backend/shop/__pycache__/models.cpython-312.pyc b/backend/shop/__pycache__/models.cpython-312.pyc index 76b3ce2..2bc2a56 100644 Binary files a/backend/shop/__pycache__/models.cpython-312.pyc and b/backend/shop/__pycache__/models.cpython-312.pyc differ diff --git a/backend/shop/__pycache__/urls.cpython-312.pyc b/backend/shop/__pycache__/urls.cpython-312.pyc index e427f95..fe344bf 100644 Binary files a/backend/shop/__pycache__/urls.cpython-312.pyc and b/backend/shop/__pycache__/urls.cpython-312.pyc differ diff --git a/backend/shop/__pycache__/views.cpython-312.pyc b/backend/shop/__pycache__/views.cpython-312.pyc index 45a7d66..ec8d7e8 100644 Binary files a/backend/shop/__pycache__/views.cpython-312.pyc and b/backend/shop/__pycache__/views.cpython-312.pyc differ diff --git a/backend/shop/admin.py b/backend/shop/admin.py index 4c3eed3..4125d20 100644 --- a/backend/shop/admin.py +++ b/backend/shop/admin.py @@ -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 +from .models import ESP32Config, Order, Salesperson, WeChatPayConfig, Service, ARService, ProductFeature, CommissionLog import qrcode from io import BytesIO import base64 @@ -160,8 +160,8 @@ class SalespersonAdmin(ModelAdmin): total_sales_display.short_description = "累计销售额 (已支付)" def promotion_url(self, obj): - # 假设前端部署在 localhost:15173,生产环境需配置 - base_url = "http://localhost:15173" + # 生产环境配置 + base_url = "https://market.quant-speed.com" return f"{base_url}/?ref={obj.code}" @display(description="推广链接") @@ -205,6 +205,26 @@ class SalespersonAdmin(ModelAdmin): ('业绩统计', { 'fields': ('total_sales_display',) }), + ('分销设置', { + 'fields': ('parent', 'commission_rate', 'second_level_rate'), + 'description': '设置上级分销员及各级分润比例' + }), + ) + +@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') + readonly_fields = ('amount', 'level', 'created_at') + + fieldsets = ( + ('基本信息', { + 'fields': ('salesperson', 'order', 'amount', 'level') + }), + ('状态管理', { + 'fields': ('status', 'created_at') + }), ) @admin.register(Order) diff --git a/backend/shop/migrations/0015_esp32config_commission_rate_and_more.py b/backend/shop/migrations/0015_esp32config_commission_rate_and_more.py new file mode 100644 index 0000000..222cc00 --- /dev/null +++ b/backend/shop/migrations/0015_esp32config_commission_rate_and_more.py @@ -0,0 +1,50 @@ +# Generated by Django 6.0.1 on 2026-02-10 15:48 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('shop', '0014_esp32config_stock_order_courier_name_and_more'), + ] + + operations = [ + migrations.AddField( + model_name='esp32config', + name='commission_rate', + field=models.DecimalField(decimal_places=4, default=0.0, help_text='例如 0.10 表示 10%,优先级高于销售员默认比例', max_digits=5, verbose_name='产品分润比例'), + ), + migrations.AddField( + model_name='salesperson', + name='commission_rate', + field=models.DecimalField(decimal_places=4, default=0.1, help_text='例如 0.10 表示 10%', max_digits=5, verbose_name='默认分润比例'), + ), + migrations.AddField( + model_name='salesperson', + name='parent', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='children', to='shop.salesperson', verbose_name='上级分销员'), + ), + migrations.AddField( + model_name='salesperson', + name='second_level_rate', + field=models.DecimalField(decimal_places=4, default=0.02, help_text='作为上级时可获得的分润比例,例如 0.02 表示 2%', max_digits=5, verbose_name='二级分销比例'), + ), + migrations.CreateModel( + name='CommissionLog', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('amount', models.DecimalField(decimal_places=2, max_digits=10, verbose_name='佣金金额')), + ('level', models.IntegerField(default=1, help_text='1: 直接销售, 2: 二级分销', verbose_name='分销层级')), + ('status', models.CharField(choices=[('pending', '待结算'), ('settled', '已结算'), ('cancelled', '已取消')], default='pending', max_length=20, verbose_name='状态')), + ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')), + ('order', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='commissions', to='shop.order', verbose_name='关联订单')), + ('salesperson', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='commissions', to='shop.salesperson', verbose_name='获佣销售员')), + ], + options={ + 'verbose_name': '佣金记录', + 'verbose_name_plural': '佣金结算', + }, + ), + ] diff --git a/backend/shop/models.py b/backend/shop/models.py index 5fec7ab..22790b2 100644 --- a/backend/shop/models.py +++ b/backend/shop/models.py @@ -17,6 +17,7 @@ class ESP32Config(models.Model): has_microphone = models.BooleanField(default=False, verbose_name="是否包含麦克风") stock = models.IntegerField(default=0, verbose_name="库存数量") price = models.DecimalField(max_digits=10, decimal_places=2, verbose_name="价格") + commission_rate = models.DecimalField(max_digits=5, decimal_places=4, default=0.00, verbose_name="产品分润比例", help_text="例如 0.10 表示 10%,优先级高于销售员默认比例") 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") @@ -58,6 +59,11 @@ 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") + parent = models.ForeignKey('self', on_delete=models.SET_NULL, null=True, blank=True, related_name='children', verbose_name="上级分销员") + + commission_rate = models.DecimalField(max_digits=5, decimal_places=4, default=0.10, verbose_name="默认分润比例", help_text="例如 0.10 表示 10%") + second_level_rate = models.DecimalField(max_digits=5, decimal_places=4, default=0.02, verbose_name="二级分销比例", help_text="作为上级时可获得的分润比例,例如 0.02 表示 2%") + created_at = models.DateTimeField(auto_now_add=True, verbose_name="创建时间") def __str__(self): @@ -68,6 +74,31 @@ class Salesperson(models.Model): verbose_name_plural = "销售员管理" +class CommissionLog(models.Model): + """ + 佣金结算记录 + """ + STATUS_CHOICES = ( + ('pending', '待结算'), + ('settled', '已结算'), + ('cancelled', '已取消'), + ) + + 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') + 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="状态") + created_at = models.DateTimeField(auto_now_add=True, verbose_name="创建时间") + + class Meta: + verbose_name = "佣金记录" + verbose_name_plural = "佣金结算" + + def __str__(self): + return f"{self.salesperson.name} - ¥{self.amount} ({self.get_status_display()})" + + class WeChatPayConfig(models.Model): """ 微信支付配置模型 diff --git a/backend/shop/serializers.py b/backend/shop/serializers.py index e7b5635..826f483 100644 --- a/backend/shop/serializers.py +++ b/backend/shop/serializers.py @@ -122,20 +122,32 @@ class OrderSerializer(serializers.ModelSerializer): 订单序列化器 """ config_name = serializers.CharField(source='config.name', 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) # 接收前端传来的 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'] + fields = ['id', 'config', 'config_name', 'config_image', '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 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 + return None + def create(self, validated_data): """ 重写创建方法,自动计算总价并关联销售员 diff --git a/backend/shop/views.py b/backend/shop/views.py index b91fd58..3826db8 100644 --- a/backend/shop/views.py +++ b/backend/shop/views.py @@ -5,7 +5,7 @@ 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 +from .models import ESP32Config, Order, WeChatPayConfig, Service, ARService, ServiceOrder, Salesperson, CommissionLog from .serializers import ESP32ConfigSerializer, OrderSerializer, ServiceSerializer, ARServiceSerializer, ServiceOrderSerializer from wechatpayv3 import WeChatPay, WeChatPayType from wechatpayv3.core import Core @@ -20,7 +20,9 @@ from cryptography.hazmat.primitives.ciphers.aead import AESGCM from django.conf import settings import requests import random +import threading from django.core.cache import cache +from time import sleep # 猴子补丁:绕过微信支付响应签名验证 # 原因是:在开发环境或证书未能正确下载时,SDK 会因为无法验证微信返回的签名而抛出异常。 @@ -141,39 +143,30 @@ def send_sms_code(request): cache_key = f"sms_code_{phone}" cache.set(cache_key, code, timeout=300) - # 调用外部短信API - try: - api_url = "https://data.tangledup-ai.com/api/send-sms" - payload = { - "phone_number": phone, - "code": code, - "template_code": "SMS_493295002", - "sign_name": "叠加态科技云南" - } - headers = { - "Content-Type": "application/json", - "accept": "application/json" - } - - response = requests.post(api_url, json=payload, headers=headers, timeout=15) - - if response.status_code == 200: - print(f"短信发送成功: {phone} -> {code}") - return Response({'message': '验证码已发送'}) - else: - print(f"短信发送失败: {response.text}") - return Response({'error': '短信发送失败,请稍后重试'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + # 异步发送短信 + def _send_async(): + try: + api_url = "https://data.tangledup-ai.com/api/send-sms" + payload = { + "phone_number": phone, + "code": code, + "template_code": "SMS_493295002", + "sign_name": "叠加态科技云南" + } + headers = { + "Content-Type": "application/json", + "accept": "application/json" + } + requests.post(api_url, json=payload, headers=headers, timeout=15) + print(f"短信异步发送请求已发出: {phone} -> {code}") + except Exception as e: + print(f"异步发送短信异常: {str(e)}") - except requests.exceptions.Timeout: - print(f"短信发送超时: {phone}") - # 超时并不一定代表失败,可能是对方响应慢。但为了安全起见,提示用户稍后重试或检查手机。 - # 考虑到用户反馈短信实际已收到,这里返回一个较为温和的错误或成功提示(视业务逻辑而定)。 - # 这里我们选择返回一个特定的错误,前端可以据此提示用户。 - return Response({'message': '短信请求已发送,请留意查收(如未收到请重试)'}, status=status.HTTP_200_OK) - - except Exception as e: - print(f"发送短信异常: {str(e)}") - return Response({'error': '短信服务异常'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + + threading.Thread(target=_send_async).start() + sleep(2) + # 立即返回成功,无需等待外部API响应 + return Response({'message': '验证码已发送'}) @extend_schema( summary="微信支付 V3 Native 下单", @@ -463,6 +456,44 @@ def payment_finish(request): order.wechat_trade_no = transaction_id 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 + amount_1 = order.total_price * rate_1 + + if amount_1 > 0: + CommissionLog.objects.create( + order=order, + salesperson=salesperson, + amount=amount_1, + level=1, + status='pending' + ) + print(f"生成一级佣金: {salesperson.name} - {amount_1}") + + # 2. 计算上级佣金 (二级) + parent = salesperson.parent + if parent: + rate_2 = parent.second_level_rate + amount_2 = order.total_price * rate_2 + + if amount_2 > 0: + CommissionLog.objects.create( + order=order, + salesperson=parent, + amount=amount_2, + level=2, + status='pending' + ) + print(f"生成二级佣金: {parent.name} - {amount_2}") + except Exception as e: + print(f"佣金计算失败: {str(e)}") + except Exception as e: print(f"订单更新失败: {str(e)}")