mi
This commit is contained in:
@@ -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