This commit is contained in:
jeremygan2021
2026-02-11 00:19:33 +08:00
parent 0b3b81915b
commit 5232ab9960
10 changed files with 183 additions and 39 deletions

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
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)

View File

@@ -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': '佣金结算',
},
),
]

View File

@@ -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):
"""
微信支付配置模型

View File

@@ -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):
"""
重写创建方法,自动计算总价并关联销售员

View File

@@ -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)}")