mi
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.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,64 @@
|
||||
# Generated by Django 6.0.1 on 2026-02-10 16:35
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('shop', '0015_esp32config_commission_rate_and_more'),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='WeChatUser',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('openid', models.CharField(max_length=64, unique=True, verbose_name='OpenID')),
|
||||
('unionid', models.CharField(blank=True, db_index=True, max_length=64, null=True, verbose_name='UnionID')),
|
||||
('session_key', models.CharField(blank=True, max_length=64, verbose_name='SessionKey')),
|
||||
('nickname', models.CharField(blank=True, max_length=64, verbose_name='昵称')),
|
||||
('avatar_url', models.URLField(blank=True, verbose_name='头像URL')),
|
||||
('gender', models.IntegerField(default=0, help_text='0:未知, 1:男, 2:女', verbose_name='性别')),
|
||||
('country', models.CharField(blank=True, max_length=64, verbose_name='国家')),
|
||||
('province', models.CharField(blank=True, max_length=64, verbose_name='省份')),
|
||||
('city', models.CharField(blank=True, max_length=64, verbose_name='城市')),
|
||||
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
|
||||
('updated_at', models.DateTimeField(auto_now=True, verbose_name='更新时间')),
|
||||
('user', models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='wechat_profile', to=settings.AUTH_USER_MODEL, verbose_name='关联系统用户')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': '微信用户',
|
||||
'verbose_name_plural': '微信用户管理',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Distributor',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('level', models.IntegerField(default=1, verbose_name='分销等级')),
|
||||
('commission_rate', models.DecimalField(decimal_places=4, default=0.1, help_text='例如 0.10 表示 10%', max_digits=5, verbose_name='分佣比例')),
|
||||
('total_earnings', models.DecimalField(decimal_places=2, default=0.0, max_digits=12, verbose_name='累计收益')),
|
||||
('withdrawable_balance', models.DecimalField(decimal_places=2, default=0.0, max_digits=12, verbose_name='可提现余额')),
|
||||
('status', models.CharField(choices=[('pending', '审核中'), ('active', '正常'), ('disabled', '已禁用')], default='pending', max_length=20, verbose_name='状态')),
|
||||
('invite_code', models.CharField(blank=True, max_length=20, unique=True, verbose_name='邀请码')),
|
||||
('qr_code_url', models.URLField(blank=True, verbose_name='推广二维码URL')),
|
||||
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='申请时间')),
|
||||
('updated_at', models.DateTimeField(auto_now=True, verbose_name='更新时间')),
|
||||
('parent', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='children', to='shop.distributor', verbose_name='上级分销员')),
|
||||
('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='distributor', to='shop.wechatuser', verbose_name='关联微信用户')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': '分销员',
|
||||
'verbose_name_plural': '分销员管理',
|
||||
},
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='order',
|
||||
name='wechat_user',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='orders', to='shop.wechatuser', verbose_name='下单微信用户'),
|
||||
),
|
||||
]
|
||||
30
backend/shop/migrations/0017_withdrawal.py
Normal file
30
backend/shop/migrations/0017_withdrawal.py
Normal file
@@ -0,0 +1,30 @@
|
||||
# Generated by Django 6.0.1 on 2026-02-10 16:35
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('shop', '0016_wechatuser_distributor_order_wechat_user'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Withdrawal',
|
||||
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='提现金额')),
|
||||
('status', models.CharField(choices=[('pending', '审核中'), ('approved', '已打款'), ('rejected', '已拒绝')], default='pending', max_length=20, verbose_name='状态')),
|
||||
('remark', models.TextField(blank=True, verbose_name='备注')),
|
||||
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='申请时间')),
|
||||
('updated_at', models.DateTimeField(auto_now=True, verbose_name='更新时间')),
|
||||
('distributor', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='withdrawals', to='shop.distributor', verbose_name='分销员')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': '提现记录',
|
||||
'verbose_name_plural': '提现管理',
|
||||
},
|
||||
),
|
||||
]
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -3,6 +3,86 @@ from django.utils.html import format_html
|
||||
import qrcode
|
||||
from io import BytesIO
|
||||
import base64
|
||||
from django.contrib.auth.models import User
|
||||
|
||||
class WeChatUser(models.Model):
|
||||
"""
|
||||
微信小程序用户模型
|
||||
"""
|
||||
user = models.OneToOneField(User, on_delete=models.CASCADE, null=True, blank=True, related_name='wechat_profile', verbose_name="关联系统用户")
|
||||
openid = models.CharField(max_length=64, unique=True, verbose_name="OpenID")
|
||||
unionid = models.CharField(max_length=64, blank=True, null=True, verbose_name="UnionID", db_index=True)
|
||||
session_key = models.CharField(max_length=64, verbose_name="SessionKey", blank=True)
|
||||
nickname = models.CharField(max_length=64, verbose_name="昵称", blank=True)
|
||||
avatar_url = models.URLField(verbose_name="头像URL", blank=True)
|
||||
gender = models.IntegerField(default=0, verbose_name="性别", help_text="0:未知, 1:男, 2:女")
|
||||
country = models.CharField(max_length=64, verbose_name="国家", blank=True)
|
||||
province = models.CharField(max_length=64, verbose_name="省份", blank=True)
|
||||
city = models.CharField(max_length=64, verbose_name="城市", blank=True)
|
||||
created_at = models.DateTimeField(auto_now_add=True, verbose_name="创建时间")
|
||||
updated_at = models.DateTimeField(auto_now=True, verbose_name="更新时间")
|
||||
|
||||
def __str__(self):
|
||||
return self.nickname or self.openid
|
||||
|
||||
class Meta:
|
||||
verbose_name = "微信用户"
|
||||
verbose_name_plural = "微信用户管理"
|
||||
|
||||
|
||||
class Distributor(models.Model):
|
||||
"""
|
||||
分销员模型 (替代原 Salesperson 或与其并存,此处为新系统)
|
||||
"""
|
||||
STATUS_CHOICES = (
|
||||
('pending', '审核中'),
|
||||
('active', '正常'),
|
||||
('disabled', '已禁用'),
|
||||
)
|
||||
|
||||
user = models.OneToOneField(WeChatUser, on_delete=models.CASCADE, related_name='distributor', verbose_name="关联微信用户")
|
||||
parent = models.ForeignKey('self', on_delete=models.SET_NULL, null=True, blank=True, related_name='children', verbose_name="上级分销员")
|
||||
level = models.IntegerField(default=1, verbose_name="分销等级")
|
||||
commission_rate = models.DecimalField(max_digits=5, decimal_places=4, default=0.10, verbose_name="分佣比例", help_text="例如 0.10 表示 10%")
|
||||
total_earnings = models.DecimalField(max_digits=12, decimal_places=2, default=0.00, verbose_name="累计收益")
|
||||
withdrawable_balance = models.DecimalField(max_digits=12, decimal_places=2, default=0.00, verbose_name="可提现余额")
|
||||
status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='pending', verbose_name="状态")
|
||||
invite_code = models.CharField(max_length=20, unique=True, blank=True, verbose_name="邀请码")
|
||||
qr_code_url = models.URLField(blank=True, verbose_name="推广二维码URL")
|
||||
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.user.nickname} - {self.get_status_display()}"
|
||||
|
||||
class Meta:
|
||||
verbose_name = "分销员"
|
||||
verbose_name_plural = "分销员管理"
|
||||
|
||||
|
||||
class Withdrawal(models.Model):
|
||||
"""
|
||||
提现记录
|
||||
"""
|
||||
STATUS_CHOICES = (
|
||||
('pending', '审核中'),
|
||||
('approved', '已打款'),
|
||||
('rejected', '已拒绝'),
|
||||
)
|
||||
|
||||
distributor = models.ForeignKey(Distributor, on_delete=models.CASCADE, related_name='withdrawals', verbose_name="分销员")
|
||||
amount = models.DecimalField(max_digits=10, decimal_places=2, verbose_name="提现金额")
|
||||
status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='pending', verbose_name="状态")
|
||||
remark = models.TextField(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.distributor.user.nickname} - ¥{self.amount}"
|
||||
|
||||
class Meta:
|
||||
verbose_name = "提现记录"
|
||||
verbose_name_plural = "提现管理"
|
||||
|
||||
class ESP32Config(models.Model):
|
||||
"""
|
||||
@@ -146,6 +226,9 @@ class Order(models.Model):
|
||||
|
||||
# 销售归属
|
||||
salesperson = models.ForeignKey(Salesperson, 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')
|
||||
|
||||
# 用户信息
|
||||
customer_name = models.CharField(max_length=100, verbose_name="收货人姓名", default="")
|
||||
|
||||
@@ -1,5 +1,25 @@
|
||||
from rest_framework import serializers
|
||||
from .models import ESP32Config, Order, Salesperson, Service, ARService, ProductFeature, ServiceOrder
|
||||
from .models import ESP32Config, Order, Salesperson, Service, ARService, ProductFeature, ServiceOrder, WeChatUser, Distributor, Withdrawal
|
||||
|
||||
class WeChatUserSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = WeChatUser
|
||||
fields = ['id', 'nickname', 'avatar_url', 'gender', 'country', 'province', 'city']
|
||||
read_only_fields = ['id']
|
||||
|
||||
class DistributorSerializer(serializers.ModelSerializer):
|
||||
user_info = WeChatUserSerializer(source='user', read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = Distributor
|
||||
fields = ['id', 'user_info', 'level', 'commission_rate', 'total_earnings', 'withdrawable_balance', 'status', 'invite_code', 'qr_code_url']
|
||||
read_only_fields = ['level', 'commission_rate', 'total_earnings', 'withdrawable_balance', 'status', 'invite_code', 'qr_code_url']
|
||||
|
||||
class WithdrawalSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = Withdrawal
|
||||
fields = ['id', 'amount', 'status', 'remark', 'created_at']
|
||||
read_only_fields = ['status', 'created_at', 'remark']
|
||||
|
||||
class ProductFeatureSerializer(serializers.ModelSerializer):
|
||||
"""
|
||||
|
||||
@@ -3,7 +3,7 @@ from rest_framework.routers import DefaultRouter
|
||||
from .views import (
|
||||
ESP32ConfigViewSet, OrderViewSet, order_check_view,
|
||||
ServiceViewSet, ARServiceViewSet, ServiceOrderViewSet,
|
||||
payment_finish, pay, send_sms_code
|
||||
payment_finish, pay, send_sms_code, wechat_login, update_user_info, DistributorViewSet
|
||||
)
|
||||
|
||||
router = DefaultRouter()
|
||||
@@ -12,11 +12,14 @@ router.register(r'orders', OrderViewSet)
|
||||
router.register(r'services', ServiceViewSet)
|
||||
router.register(r'ar', ARServiceViewSet)
|
||||
router.register(r'service-orders', ServiceOrderViewSet)
|
||||
router.register(r'distributor', DistributorViewSet, basename='distributor')
|
||||
|
||||
urlpatterns = [
|
||||
re_path(r'^finish/?$', payment_finish, name='payment-finish'),
|
||||
re_path(r'^pay/?$', pay, name='wechat-pay-v3'),
|
||||
path('auth/send-sms/', send_sms_code, name='send-sms'),
|
||||
path('wechat/login/', wechat_login, name='wechat-login'),
|
||||
path('wechat/update/', update_user_info, name='wechat-update'),
|
||||
path('page/check-order/', order_check_view, name='check-order-page'),
|
||||
path('', include(router.urls)),
|
||||
]
|
||||
|
||||
@@ -5,8 +5,10 @@ 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
|
||||
from .serializers import ESP32ConfigSerializer, OrderSerializer, ServiceSerializer, ARServiceSerializer, ServiceOrderSerializer
|
||||
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 django.core.signing import TimestampSigner, BadSignature, SignatureExpired
|
||||
from django.contrib.auth.models import User
|
||||
from wechatpayv3 import WeChatPay, WeChatPayType
|
||||
from wechatpayv3.core import Core
|
||||
import xml.etree.ElementTree as ET
|
||||
@@ -560,6 +562,96 @@ class OrderViewSet(viewsets.ModelViewSet):
|
||||
queryset = Order.objects.all()
|
||||
serializer_class = OrderSerializer
|
||||
|
||||
def get_queryset(self):
|
||||
"""
|
||||
如果用户已通过微信登录,只返回自己的订单
|
||||
否则(如管理员)返回所有订单
|
||||
"""
|
||||
queryset = super().get_queryset()
|
||||
user = get_current_wechat_user(self.request)
|
||||
if user:
|
||||
return queryset.filter(wechat_user=user).order_by('-created_at')
|
||||
return queryset.order_by('-created_at')
|
||||
|
||||
@action(detail=True, methods=['post'])
|
||||
def prepay_miniprogram(self, request, pk=None):
|
||||
"""
|
||||
小程序支付下单 (返回 wx.requestPayment 所需参数)
|
||||
"""
|
||||
order = self.get_object()
|
||||
if order.status == 'paid':
|
||||
return Response({'error': '订单已支付'}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
user = get_current_wechat_user(request)
|
||||
if not user:
|
||||
return Response({'error': 'Unauthorized'}, status=401)
|
||||
|
||||
# 绑定用户
|
||||
if not order.wechat_user:
|
||||
order.wechat_user = user
|
||||
order.save()
|
||||
|
||||
wechat_config = WeChatPayConfig.objects.filter(is_active=True).first()
|
||||
if not wechat_config:
|
||||
return Response({'error': '支付系统维护中'}, status=status.HTTP_503_SERVICE_UNAVAILABLE)
|
||||
|
||||
# 初始化支付客户端
|
||||
wxpay, error_msg = get_wechat_pay_client()
|
||||
if not wxpay:
|
||||
return Response({'error': error_msg}, status=500)
|
||||
|
||||
amount_in_cents = int(order.total_price * 100)
|
||||
out_trade_no = f"PAY{order.id}T{int(time.time())}"
|
||||
order.out_trade_no = out_trade_no
|
||||
order.save()
|
||||
|
||||
try:
|
||||
# 统一下单 (JSAPI)
|
||||
code, message = wxpay.pay(
|
||||
description=f"购买 {order.config.name} x {order.quantity}",
|
||||
out_trade_no=out_trade_no,
|
||||
amount={'total': amount_in_cents, 'currency': 'CNY'},
|
||||
payer={'openid': user.openid}, # 小程序支付必须传 openid
|
||||
notify_url=wechat_config.notify_url
|
||||
)
|
||||
|
||||
result = json.loads(message)
|
||||
if code in range(200, 300):
|
||||
prepay_id = result.get('prepay_id')
|
||||
|
||||
# 生成小程序调起支付所需的参数
|
||||
timestamp = str(int(time.time()))
|
||||
nonce_str = str(uuid.uuid4()).replace('-', '')
|
||||
package = f"prepay_id={prepay_id}"
|
||||
|
||||
# 再次签名 (小程序端需要的签名)
|
||||
# 注意:WeChatPayV3 SDK 可能没有直接提供生成小程序签名的 helper,需手动计算
|
||||
# 签名串格式:appId\ntimeStamp\nnonceStr\npackage\n
|
||||
message_build = f"{wechat_config.app_id}\n{timestamp}\n{nonce_str}\n{package}\n"
|
||||
|
||||
# 使用商户私钥签名
|
||||
# 这里的私钥加载逻辑需复用 get_wechat_pay_client 中的逻辑,或者直接从 wxpay 实例获取 (如果它暴露了)
|
||||
# 简单起见,我们重新加载私钥
|
||||
private_key_str = wxpay._private_key # 假设 SDK 内部存储了 private_key (通常是 obj)
|
||||
# 由于 SDK 内部处理复杂,我们尝试用 cryptography 库签名
|
||||
|
||||
# 实际上 wechatpayv3 库提供了 sign 方法
|
||||
signature = wxpay.sign(message_build)
|
||||
|
||||
return Response({
|
||||
'timeStamp': timestamp,
|
||||
'nonceStr': nonce_str,
|
||||
'package': package,
|
||||
'signType': 'RSA',
|
||||
'paySign': signature,
|
||||
'out_trade_no': out_trade_no
|
||||
})
|
||||
else:
|
||||
return Response({'error': '微信下单失败', 'detail': result}, status=400)
|
||||
|
||||
except Exception as e:
|
||||
return Response({'error': str(e)}, status=500)
|
||||
|
||||
@action(detail=False, methods=['get'])
|
||||
def lookup(self, request):
|
||||
"""
|
||||
@@ -720,3 +812,180 @@ class OrderViewSet(viewsets.ModelViewSet):
|
||||
order.wechat_trade_no = f"WX_{str(uuid.uuid4())[:18]}"
|
||||
order.save()
|
||||
return Response({'status': 'success', 'message': '支付成功'})
|
||||
|
||||
def get_current_wechat_user(request):
|
||||
"""
|
||||
根据 Authorization 头获取当前微信用户
|
||||
"""
|
||||
auth_header = request.headers.get('Authorization')
|
||||
if not auth_header or not auth_header.startswith('Bearer '):
|
||||
return None
|
||||
token = auth_header.split(' ')[1]
|
||||
signer = TimestampSigner()
|
||||
try:
|
||||
# 签名包含 openid
|
||||
openid = signer.unsign(token, max_age=86400 * 30) # 30天有效
|
||||
return WeChatUser.objects.filter(openid=openid).first()
|
||||
except (BadSignature, SignatureExpired):
|
||||
return None
|
||||
|
||||
@extend_schema(
|
||||
summary="微信小程序登录",
|
||||
request={
|
||||
'application/json': {
|
||||
'properties': {'code': {'type': 'string', 'description': 'wx.login获取的code'}},
|
||||
'required': ['code']
|
||||
}
|
||||
},
|
||||
responses={200: {'properties': {'token': {'type': 'string'}, 'openid': {'type': 'string'}}}}
|
||||
)
|
||||
@api_view(['POST'])
|
||||
def wechat_login(request):
|
||||
code = request.data.get('code')
|
||||
if not code:
|
||||
return Response({'error': 'Code is required'}, status=400)
|
||||
|
||||
config = WeChatPayConfig.objects.filter(is_active=True).first()
|
||||
if not config or not config.app_id or not config.app_secret:
|
||||
return Response({'error': 'WeChat config missing'}, status=500)
|
||||
|
||||
# 换取 OpenID
|
||||
url = f"https://api.weixin.qq.com/sns/jscode2session?appid={config.app_id}&secret={config.app_secret}&js_code={code}&grant_type=authorization_code"
|
||||
try:
|
||||
res = requests.get(url, timeout=10)
|
||||
data = res.json()
|
||||
except Exception as e:
|
||||
return Response({'error': str(e)}, status=500)
|
||||
|
||||
if 'errcode' in data and data['errcode'] != 0:
|
||||
return Response({'error': data.get('errmsg')}, status=400)
|
||||
|
||||
openid = data.get('openid')
|
||||
session_key = data.get('session_key')
|
||||
unionid = data.get('unionid')
|
||||
|
||||
# 创建或更新用户
|
||||
user, created = WeChatUser.objects.update_or_create(
|
||||
openid=openid,
|
||||
defaults={
|
||||
'session_key': session_key,
|
||||
'unionid': unionid
|
||||
}
|
||||
)
|
||||
|
||||
# 生成 Token
|
||||
signer = TimestampSigner()
|
||||
token = signer.sign(openid)
|
||||
|
||||
return Response({
|
||||
'token': token,
|
||||
'openid': openid,
|
||||
'is_new': created,
|
||||
'nickname': user.nickname
|
||||
})
|
||||
|
||||
@extend_schema(
|
||||
summary="更新微信用户信息",
|
||||
request=WeChatUserSerializer,
|
||||
)
|
||||
@api_view(['POST'])
|
||||
def update_user_info(request):
|
||||
user = get_current_wechat_user(request)
|
||||
if not user:
|
||||
return Response({'error': 'Unauthorized'}, status=401)
|
||||
|
||||
serializer = WeChatUserSerializer(user, data=request.data, partial=True)
|
||||
if serializer.is_valid():
|
||||
serializer.save()
|
||||
return Response(serializer.data)
|
||||
return Response(serializer.errors, status=400)
|
||||
|
||||
|
||||
class DistributorViewSet(viewsets.GenericViewSet):
|
||||
"""
|
||||
分销员接口
|
||||
"""
|
||||
queryset = Distributor.objects.all()
|
||||
serializer_class = DistributorSerializer
|
||||
|
||||
@action(detail=False, methods=['post'])
|
||||
def register(self, request):
|
||||
user = get_current_wechat_user(request)
|
||||
if not user:
|
||||
return Response({'error': 'Unauthorized'}, status=401)
|
||||
|
||||
if hasattr(user, 'distributor'):
|
||||
return Response({'error': 'Already registered'}, status=400)
|
||||
|
||||
# 检查是否有关联上级 (通过 invite_code)
|
||||
parent = None
|
||||
invite_code = request.data.get('invite_code')
|
||||
if invite_code:
|
||||
parent = Distributor.objects.filter(invite_code=invite_code).first()
|
||||
|
||||
# 生成自己的邀请码
|
||||
my_invite_code = str(uuid.uuid4())[:8]
|
||||
|
||||
distributor = Distributor.objects.create(
|
||||
user=user,
|
||||
parent=parent,
|
||||
invite_code=my_invite_code,
|
||||
status='pending' # 需要审核
|
||||
)
|
||||
|
||||
return Response(DistributorSerializer(distributor).data)
|
||||
|
||||
@action(detail=False, methods=['get'])
|
||||
def info(self, request):
|
||||
user = get_current_wechat_user(request)
|
||||
if not user:
|
||||
return Response({'error': 'Unauthorized'}, status=401)
|
||||
|
||||
if not hasattr(user, 'distributor'):
|
||||
return Response({'error': 'Not a distributor'}, status=404)
|
||||
|
||||
return Response(DistributorSerializer(user.distributor).data)
|
||||
|
||||
@action(detail=False, methods=['post'])
|
||||
def invite(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
|
||||
if distributor.qr_code_url:
|
||||
return Response({'qr_code_url': distributor.qr_code_url})
|
||||
|
||||
# 调用微信接口生成小程序码 (wxacode.getUnlimited)
|
||||
# 这里简化处理,返回模拟URL或需要实现具体逻辑
|
||||
# 实际逻辑需要获取 AccessToken 然后调用 API
|
||||
return Response({'qr_code_url': 'https://placeholder.com/qrcode.png'})
|
||||
|
||||
@action(detail=False, methods=['post'])
|
||||
def withdraw(self, request):
|
||||
user = get_current_wechat_user(request)
|
||||
if not user or not hasattr(user, 'distributor'):
|
||||
return Response({'error': 'Unauthorized'}, status=401)
|
||||
|
||||
amount = float(request.data.get('amount', 0))
|
||||
if amount <= 0:
|
||||
return Response({'error': 'Invalid amount'}, status=400)
|
||||
|
||||
distributor = user.distributor
|
||||
if distributor.withdrawable_balance < amount:
|
||||
return Response({'error': 'Insufficient balance'}, status=400)
|
||||
|
||||
# 创建提现记录
|
||||
Withdrawal.objects.create(
|
||||
distributor=distributor,
|
||||
amount=amount,
|
||||
status='pending'
|
||||
)
|
||||
|
||||
# 扣减余额
|
||||
distributor.withdrawable_balance -= models.DecimalField(max_digits=12, decimal_places=2).to_python(amount)
|
||||
distributor.save()
|
||||
|
||||
return Response({'message': 'Withdrawal request submitted'})
|
||||
|
||||
|
||||
Reference in New Issue
Block a user