sale
This commit is contained in:
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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)
|
||||
|
||||
@@ -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': '佣金结算',
|
||||
},
|
||||
),
|
||||
]
|
||||
@@ -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):
|
||||
"""
|
||||
微信支付配置模型
|
||||
|
||||
@@ -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):
|
||||
"""
|
||||
重写创建方法,自动计算总价并关联销售员
|
||||
|
||||
@@ -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)}")
|
||||
|
||||
|
||||
Reference in New Issue
Block a user