jiaweiqi 3 yıl önce
ebeveyn
işleme
580be2ae5e

+ 12 - 0
apps/activity/filters.py

@@ -63,10 +63,22 @@ class OrderFilter(django_filters.FilterSet):
     activity = django_filters.CharFilter(field_name='activity')
     delete = django_filters.CharFilter(field_name='delete')
 
+    order_status = django_filters.CharFilter(method='find_by_order_status')
+
     class Meta:
         model = Order
         fields = '__all__'
 
+    def find_by_order_status(self, queryset, *args):
+        if args[1]:
+            if args[1] == '1': # 进行中
+                queryset = queryset.filter(status=Order.DEFAULT)
+            elif args[1] == '2': # 已完成
+                queryset = queryset.filter(status=Order.FINISH)
+            elif args[1] == '3': # 已失败
+                queryset = queryset.filter(status=Order.FAIL)
+        return queryset
+
 
 
 class CouponFilter(django_filters.FilterSet):

+ 272 - 14
apps/activity/models.py

@@ -8,8 +8,8 @@ from django.conf import settings
 
 from apps.exceptions import CustomError
 from apps.account.models import Branch
-from apps.customer.models import Customer
-from util.wechatpay import WechatPay, WeChatResponse
+from apps.customer.models import Customer, CustomerWechat
+from util.wechatpay import WechatPay, WeChatResponse, SplitAccountFuc
 
 
 class Activity(models.Model):
@@ -67,6 +67,248 @@ class Activity(models.Model):
         return instance
 
 
+class SplitAccount(models.Model):
+    WAIT = 1
+    PROCESSING = 2
+    FINISHED = 3
+    STATUS_CHOICES = (
+        (WAIT, u'待分账'),
+        (PROCESSING, u'处理中'),
+        (FINISHED, u'分账完成'),
+    )
+
+    branch = models.ForeignKey(Branch, verbose_name=u"门店", on_delete=models.PROTECT, editable=False)
+    no = models.CharField(max_length=64, verbose_name=u"单号")
+    create_time = models.DateTimeField(verbose_name=u"创建时间", default=timezone.now)
+    status = models.PositiveSmallIntegerField(choices=STATUS_CHOICES, verbose_name=u"分账状态", default=WAIT)
+    amount = models.BigIntegerField(verbose_name=u"分账金额", null=True)  # 单位分
+    order_id = models.CharField(max_length=100, verbose_name=u"微信分账单号", null=True, editable=False)  # 微信分账单号,微信支付系统返回的唯一标识
+
+    class Meta:
+        db_table = "split_account"
+        verbose_name = u"分账"
+        ordering = ('-id',)
+        index_together = (
+            'create_time',
+            'status',
+        )
+        unique_together = (
+            'no',
+        )
+        default_permissions = ()
+
+
+    def unfreezeAccount(self):
+        # 解冻剩余资金  如果由于客户或其他原因 无法完成分账  就手动解除资金冻结
+        pass
+
+    def handSplitAccont(self, spd, spr, pay):
+        '''手动申请分账(在自动申请分账失败后  手动申请分账)'''
+        if spd.status == SplitAccountDetail.SUCCESS:
+            raise CustomError(u'该分账已成功,禁止重复申请!')
+        appid = ''
+        mchid = ''
+        private_key = ''
+        cert_serial_no = ''
+        apiv3_key = ''
+        spc = SplitAccountFuc(appid, mchid, private_key, cert_serial_no, apiv3_key, cert_dir=None, proxy=None)
+        # 状态是"待分账" 的查询分账
+        if spd.status == SplitAccountDetail.PENDING:
+            self.splictAccountQuery(spc, pay.no, self.no, spd)
+
+            if spd.status == SplitAccountDetail.SUCCESS:
+                raise CustomError(u'该分账已成功!')
+
+            if spd.status == SplitAccountDetail.PENDING:
+                raise CustomError(u'该分账正在处理!')
+        # 添加分账接收人
+        self.splictAccountAddReceiver(spc, spr)
+        # 分账申请
+        receivers = [{'account': spd.account, 'amount': spd.amount, 'description': "支付推荐佣金"}]
+        success, data = spc.splitaccount_order(pay.no, self.no, receivers)
+        if not success:
+            raise CustomError(u'分账申请失败!')
+        # 分账请求成功  结果不一定成功
+        self.order_id = data['order_id']
+        if data['state'] == 'PROCESSING':
+            self.status = SplitAccount.PROCESSING
+        if data['state'] == 'FINISHED':
+            self.status = SplitAccount.FINISHED
+        self.save()
+        for item in data['receivers']:
+            # 现在只对一个推荐人分账
+            if item['account'] == spd.account:
+                if item['result'] == 'PENDING':
+                    spd.status = SplitAccountDetail.PENDING
+                elif item['result'] == 'SUCCESS':
+                    spd.status = SplitAccountDetail.SUCCESS
+                elif item['result'] == 'CLOSED':
+                    spd.status = SplitAccountDetail.CLOSED
+                    spd.fail_reason = item['fail_reason']
+                spd.create_time = item['create_time']
+                spd.finish_time = item['finish_time']
+                spd.detail_no = item['detail_id']
+                spd.save()
+
+    def splictAccountAddReceiver(self, split_account_func, spr):
+        '''添加分账接收人'''
+        if spr.status != SplitAccountReceiver.ADD:
+            add = split_account_func.splitaccount_addreceiver(spr.account)
+            if not add:
+                raise CustomError(u'添加分账接收方失败!')
+            spr.status = SplitAccountReceiver.ADD
+            spr.save()
+
+    def splictAccountQuery(self, split_account_func, transaction_id, out_order_no, detail):
+        '''查询分账结果'''
+        success, data = split_account_func.splitaccount_orderquery(transaction_id, out_order_no)
+        if not success:
+            raise CustomError(u'[%s]查询分账结果失败!' % out_order_no)
+        if data['state'] == 'PROCESSING':
+            self.status = SplitAccount.PROCESSING
+        if data['state'] == 'FINISHED':
+            self.status = SplitAccount.FINISHED
+        self.save()
+        for item in data['receivers']:
+            if detail and detail.account == item['account']:
+                if item['result'] == 'SUCCESS':
+                    detail.status = SplitAccountDetail.SUCCESS
+                elif item['result'] == 'CLOSED':
+                    detail.status = SplitAccountDetail.CLOSED
+                    detail.fail_reason = item['fail_reason']
+                detail.create_time = item['create_time']
+                detail.finish_time = item['finish_time']
+                detail.detail_no = item['detail_id']
+                detail.save()
+
+    def splitAccount(self, spd, spr, pay):
+        if spd.status != SplitAccountDetail.DEFAULT:
+            return
+        appid = ''
+        mchid = ''
+        private_key = ''
+        cert_serial_no = ''
+        apiv3_key = ''
+        spc = SplitAccountFuc(appid, mchid, private_key, cert_serial_no, apiv3_key, cert_dir=None, proxy=None)
+
+        if spr.status != SplitAccountReceiver.ADD:
+            add = spc.splitaccount_addreceiver(spr.account)
+            if not add:
+                # 添加分账接收方失败 不再继续
+                return
+            spr.status = SplitAccountReceiver.ADD
+            spr.save()
+
+        receivers = [{'account': spd.account, 'amount': spd.amount, 'description': "支付推荐佣金"}]
+        success, data = spc.splitaccount_order(pay.no, self.no, receivers)
+        if not success:
+            # 分账请求失败 不再继续
+            return
+         # 分账请求成功  结果不一定成功
+        self.order_id = data['order_id']
+        if data['state'] == 'PROCESSING':
+            self.status = SplitAccount.PROCESSING
+        if data['state'] == 'FINISHED':
+            self.status = SplitAccount.FINISHED
+        self.save()
+        for item in data['receivers']:
+            # 现在只对一个推荐人分账
+            if item['account'] == spd.account:
+                if item['result'] == 'PENDING':
+                    spd.status = SplitAccountDetail.PENDING
+                elif item['result'] == 'SUCCESS':
+                    spd.status = SplitAccountDetail.SUCCESS
+                elif item['result'] == 'CLOSED':
+                    spd.status = SplitAccountDetail.CLOSED
+                    spd.fail_reason = item['fail_reason']
+                spd.create_time = item['create_time']
+                spd.finish_time = item['finish_time']
+                spd.detail_no = item['detail_id']
+                spd.save()
+
+    @staticmethod
+    def _addnew(branch, order_id, rebate, customer):
+        if rebate <= 0:
+            return ''
+        customer_wechat = CustomerWechat.objects.filter(customer=customer).first()
+        if not customer_wechat:
+            return ''
+
+        no = "FZ" + timezone.now().strftime('%y%m%d%H%M%S') + str(order_id)
+        instance = SplitAccount.objects.create(
+            branch=branch,
+            no=no,
+            status=SplitAccount.WAIT,
+            amount=rebate
+        )
+
+        detail = SplitAccountDetail.objects.create(
+            main=instance,
+            customer=customer,
+            account=customer_wechat.openid,
+            amount=rebate
+        )
+
+        receiver = SplitAccountReceiver.objects.filter(account=detail.account).first()
+        if not receiver:
+            SplitAccountReceiver.objects.create(
+                account=detail.account
+            )
+        return instance, detail, receiver
+
+
+class SplitAccountDetail(models.Model):
+    DEFAULT = 0
+    PENDING = 1
+    SUCCESS = 2
+    CLOSED = 3
+    STATUS_CHOICES = (
+        (DEFAULT, u'未处理'),
+        (PENDING, u'待分账'),
+        (SUCCESS, u'分账成功'),
+        (CLOSED, u'已关闭'),
+    )
+
+    main = models.ForeignKey(SplitAccount, verbose_name=u'分账单', on_delete=models.PROTECT, editable=False)
+    customer = models.ForeignKey(Customer, verbose_name=u'客户', on_delete=models.PROTECT, editable=False)
+    account = models.CharField(max_length=100, verbose_name=u'接收账户') # 接受方的openid
+    amount = models.BigIntegerField(verbose_name=u'分账金额')  # 单位是分
+    status = models.PositiveSmallIntegerField(choices=STATUS_CHOICES, verbose_name=u"分账结果", default=DEFAULT)
+    fail_reason = models.CharField(max_length=1000, verbose_name=u"分账失败原因", null=True, blank=True)
+    create_time = models.DateTimeField(verbose_name=u"分账创建时间", null=True, blank=True)
+    finish_time = models.DateTimeField(verbose_name=u"分账完成时间", null=True, blank=True)
+    detail_no = models.CharField(verbose_name=u'分账明细单号', max_length=100, null=True, editable=False)
+
+    class Meta:
+        db_table = "split_account_detail"
+        verbose_name = u"分账详细"
+        ordering = ('-id',)
+        index_together = ('status',)
+        default_permissions = ()
+
+
+class SplitAccountReceiver(models.Model):
+    UN_ADD = 1
+    ADD = 2
+    DELETE = 3
+    STATUS_CHOICES = (
+        (UN_ADD, u'待添加'),
+        (ADD, u'已添加'),
+        (DELETE, u'已删除'),
+    )
+
+    account = models.CharField(max_length=100, verbose_name=u'接收账户') # 接受方的openid
+    status = models.PositiveSmallIntegerField(choices=STATUS_CHOICES, verbose_name=u"状态", default=UN_ADD)
+    create_time = models.DateTimeField(verbose_name=u"创建时间", default=timezone.now)
+
+    class Meta:
+        db_table = "split_account_receiver"
+        verbose_name = u"分账接收人"
+        ordering = ('-id',)
+        index_together = ('account', 'status', )
+        default_permissions = ()
+
+
 class Pay(models.Model):
 
     WAIT = 1
@@ -83,8 +325,9 @@ class Pay(models.Model):
     create_time = models.DateTimeField(verbose_name=u"创建时间", default=timezone.now)
     customer = models.ForeignKey(Customer, verbose_name=u'客户', on_delete=models.PROTECT)
     status = models.PositiveSmallIntegerField(choices=STATUS_CHOICES, verbose_name=u"支付状态")
-    precreate_amount = models.BigIntegerField(verbose_name=u"预支付金额")
-    amount = models.BigIntegerField(verbose_name=u"实际支付金额", null=True)
+    profit_sharing = models.CharField(max_length=10, verbose_name=u"是否分账", default="N")
+    precreate_amount = models.BigIntegerField(verbose_name=u"预支付金额") # 单位分
+    amount = models.BigIntegerField(verbose_name=u"实际支付金额", null=True) # 单位分
 
     class Meta:
         db_table = "pay"
@@ -124,12 +367,12 @@ class Pay(models.Model):
         return instance
 
     @staticmethod
-    def wechatPay(app, branch, customer, amount, openid):
-        instance = Pay._addnew(branch, customer, amount)
+    def wechatPay(app, branch, customer, amount, openid, profit_sharing):
+        instance = Pay._addnew(branch, customer, amount, profit_sharing)
         return instance, instance._wechatUnifiedOrder(app, openid)
 
     @staticmethod
-    def _addnew(branch, customer, amount):
+    def _addnew(branch, customer, amount, profit_sharing):
         if amount <= 0:
             raise CustomError(u'无效的付款金额!')
 
@@ -138,18 +381,26 @@ class Pay(models.Model):
             branch=branch,
             no=no,
             customer=customer,
-            type=type,
             status=Pay.WAIT,
-            precreate_amount=amount
+            precreate_amount=amount,
+            profit_sharing=profit_sharing
         )
         return instance
 
     def _wechatUnifiedOrder(self, app, openid):
         wechatpay = WechatPay(app.authorizer_appid, app.agent_num, app.agent_key)
-        wechatpay.unifiedOrder(self.no, self.precreate_amount, openid)
+        wechatpay.unifiedOrder(self.no, self.precreate_amount, openid, self.profit_sharing)
         data = wechatpay.getAppString()
         return data
 
+    def split_account(self):
+        order = Order.objects.filter(pay=self).first()
+        if order and self.status == Pay.CONFIRM and self.profit_sharing == "Y":
+            sp, spd, spr = SplitAccount._addnew(self.branch, self.id, self.rebate, self.recommend_member)
+            order.splitaccount = sp
+            order.save()
+            sp.splitAccount(spd, spr, self)
+
 
 class Order(models.Model):
     DEFAULT = 0
@@ -163,15 +414,16 @@ class Order(models.Model):
     branch = models.ForeignKey(Branch, verbose_name=u"门店", on_delete=models.PROTECT, editable=False)
     activity = models.ForeignKey(Activity, verbose_name=u"活动", on_delete=models.PROTECT)
     pay = models.ForeignKey(Pay, verbose_name='支付信息', on_delete=models.PROTECT, null=True)
+    splitaccount = models.ForeignKey(SplitAccount, verbose_name='分账信息', on_delete=models.PROTECT, null=True)
     member = models.ForeignKey(Customer, verbose_name=u"会员", on_delete=models.PROTECT, editable=False)
-    amount = models.FloatField(verbose_name=u"费用", default=0)
+    amount = models.BigIntegerField(verbose_name=u"费用", default=0)  # 单位分
     status = models.PositiveSmallIntegerField(choices=STATUS_CHOICES, verbose_name=u"状态", default=DEFAULT, editable=False)
     model = models.CharField(max_length=200, verbose_name=u"车型", null=True, blank=True)
     tel = models.CharField(max_length=100, verbose_name=u"电话", null=True, blank=True)
     number = models.CharField(max_length=50, verbose_name=u"车牌号", null=True, blank=True)
     name = models.CharField(max_length=50, verbose_name=u"姓名", null=True, blank=True)
     recommend_member = models.ForeignKey(Customer, verbose_name=u"推荐人", related_name='recommend_member_ref_customer', on_delete=models.PROTECT, null=True, blank=True, editable=False)
-    rebate = models.FloatField(verbose_name=u"返利", default=0)
+    rebate = models.BigIntegerField(verbose_name=u"返利", default=0)  # 单位分
     create_time = models.DateTimeField(verbose_name=u"添加时间", auto_now_add=True, editable=False)
     delete = models.BooleanField(verbose_name=u"删除", default=False)
 
@@ -191,16 +443,22 @@ class Order(models.Model):
             total_fee = checkRexponse.orderquery(pay_no)
             if int(total_fee) == int(self.amount):
                 wechatpay = WechatPay(app.authorizer_appid, app.agent_num, app.agent_key)
-                data = wechatpay.unifiedOrder(openid, pay_no, self.amount)
+                data = wechatpay.unifiedOrder(openid, pay_no, self.amount, self.pay.profit_sharing)
                 return data
             self.pay.payClosed()
 
-        pay, data = Pay.wechatPay(app, self.branch, self.member, self.amount, openid)
+        profit_sharing = "N"
+        if self.rebate > 0 and self.recommend_member:
+            profit_sharing = "Y"
+        pay, data = Pay.wechatPay(app, self.branch, self.member, self.amount, openid, profit_sharing)
         self.pay = pay
         self.save()
         return data
 
     def orderPayConfirm(self, amount):
+        # 在这进行分账  先去查分账接收人的状态  如果没有就创建一条记录 然后添加分账接收人 然后分账
+        # 如果有且状态是已添加  就直接分账
+        # 如果有且状态是待添加或已删除 就先添加分账接收人 然后分账
         self.status = Order.FINISH
         self.amount = amount
         self.save()

+ 2 - 0
apps/api/views.py

@@ -41,6 +41,8 @@ class WechatNotifyView(APIView):
                 pay = Pay.getByNo(no)
                 pay.payConfirm(amount)
                 BizLog.objects.addnew(pay.customer.user, BizLog.INSERT, u'微信支付成功,no=%s' % no, param)
+            # 微信支付成功之后分账
+            pay.split_account()
         except Exception as e:
             traceback.print_exc()
             return HttpResponse(WechatPayNotify.response_fail())

+ 1 - 1
apps/customer/coupon/serializers.py

@@ -10,4 +10,4 @@ class MemberCouponSerializer(serializers.ModelSerializer):
 
     class Meta:
         model = MemberCoupon
-        fields = ('activity_name', 'coupon_name', 'receive_date', 'end_date', )
+        fields = ('activity_name', 'coupon_name', 'receive_date', 'end_date', 'write_off_time', )

+ 22 - 8
apps/customer/order/serializers.py

@@ -2,6 +2,8 @@
 from apps.exceptions import CustomError
 from rest_framework import serializers
 from apps.activity.models import Order
+from apps.customer.models import Customer
+from util.format import Formater
 
 
 class ActivityOrderSerializer(serializers.ModelSerializer):
@@ -22,6 +24,7 @@ class ActivityOrderSerializer(serializers.ModelSerializer):
 
 
 class OrderSerializer(serializers.ModelSerializer):
+    activity_name = serializers.CharField(source='activity.title', read_only=True)
 
     class Meta:
         model = Order
@@ -29,19 +32,30 @@ class OrderSerializer(serializers.ModelSerializer):
 
     def validate(self, attrs):
         attrs['member'] = self.context['request'].customer
-        if 'activity' in attrs:
+        if 'activity' in attrs and attrs['activity']:
             attrs['activity'].checkStatus()
-            required_signs = attrs['activity'].required_signs.split(',')
+            required_signs = []
+            if attrs['activity'].required_signs:
+                required_signs = attrs['activity'].required_signs.split(',')
             for item in required_signs:
-                if (item == u'姓名') and ('name' not in attrs):
-                    raise CustomError(u'请输入姓名')
-                if (item == u'车牌号') and ('number' not in attrs):
-                    raise CustomError(u'请输入车牌号')
-                if (item == u'车型') and ('model' not in attrs):
-                    raise CustomError(u'请输入车型')
+                if item == u'姓名':
+                    if ('name' not in attrs) or (not attrs['name']):
+                        raise CustomError(u'请输入姓名')
+                if item == u'车牌号':
+                    if ('number' not in attrs) or (not attrs['number']):
+                        raise CustomError(u'请输入车牌号')
+                if item == u'车型':
+                    if ('model' not in attrs) or (not attrs['model']):
+                        raise CustomError(u'请输入车型')
+        if 'customer_id' in attrs and attrs['customer_id']:
+            recommend_member = Customer.objects.filter(id=attrs['customer_id']).first(0)
+            if recommend_member:
+                attrs['recommend_member'] = recommend_member
         return attrs
 
     def create(self, validated_data):
         validated_data['branch'] = validated_data['activity'].branch
+        validated_data['amount'] = Formater.formatPrice(validated_data['activity'].amount)
+        validated_data['rebate'] = Formater.formatPrice(validated_data['activity'].rebate)
         instance = super(OrderSerializer, self).create(validated_data)
         return instance

+ 27 - 1
apps/customer/order/views.py

@@ -43,6 +43,7 @@ class OrderViewSet(ModelViewSet):
 
                 if instance.amount == 0:
                     instance.status = Order.FINISH
+                    instance.save()
                     BizLog.objects.addnew(request.customer.user, BizLog.INSERT, u"添加商品订单,id=%d" % instance.id, validated_data)
                 else:
                     openid = request.POST.get('openid', None)
@@ -50,8 +51,12 @@ class OrderViewSet(ModelViewSet):
                         raise CustomError(u'未获取openid!')
                     appid = request.POST.get('appid', None)
                     app = WechatApplet.getByAppid(appid)
-                    pay, query_string = Pay.wechatPay(app, instance.branch, instance.member, instance.amount, openid)
+                    profit_sharing = "N"
+                    if instance.rebate > 0 and instance.recommend_member:
+                        profit_sharing = "Y"
+                    pay, query_string = Pay.wechatPay(app, instance.branch, instance.member, instance.amount, openid, profit_sharing)
                     instance.pay = pay
+                    instance.save()
                     BizLog.objects.addnew(request.customer.user, BizLog.INSERT, u'添加商品订单,id=%d' % instance.id, validated_data)
                     if query_string:
                         return response_ok(query_string)
@@ -85,6 +90,27 @@ class OrderViewSet(ModelViewSet):
             traceback.print_exc()
             return response_error(str(e))
 
+    @action(methods=['post'], detail=True)
+    def del_order(self, request, pk):
+
+        try:
+            order = Order.objects.filter(id=pk).first()
+            if not order:
+                raise CustomError(u'未找到相应的订单!')
+            if order.delete:
+                raise CustomError(u'该订单已删除!')
+
+            with transaction.atomic():
+                order.delete = True
+                order.save()
+                BizLog.objects.addnew(request.customer.user, BizLog.DELETE, u'删除订单,id=%d' % order.id, request.data)
+            return response_ok()
+        except CustomError as e:
+            return response_error(e.get_error_msg())
+        except Exception as e:
+            traceback.print_exc()
+            return response_error(str(e))
+
 
 class ActivityOrderListView(generics.ListAPIView):
     '''

+ 11 - 0
util/format.py

@@ -3,6 +3,17 @@
 import datetime
 from datetime import timedelta
 
+
+class Formater():
+    @staticmethod
+    def formatPrice(value):
+        return int(round(float(value or 0) * 100,0))
+
+    @staticmethod
+    def formatPriceShow(value):
+        return '%.2f' % (float(value or 0)/100.0)
+
+
 def strfdate(d):
     if d:
         return d.strftime('%Y-%m-%d')

+ 236 - 0
util/splitaccount_tool.py

@@ -0,0 +1,236 @@
+# coding=utf-8
+
+import os
+import uuid
+import requests
+import json
+import xmltodict
+import time
+from datetime import datetime
+from hashlib import md5
+from django.conf import settings
+
+from util.exceptions import CustomError
+
+from cryptography.exceptions import InvalidSignature, InvalidTag
+from cryptography.hazmat.backends import default_backend
+from cryptography.hazmat.primitives.asymmetric.padding import MGF1, OAEP, PKCS1v15
+from cryptography.hazmat.primitives.ciphers.aead import AESGCM
+from cryptography.hazmat.primitives.hashes import SHA1, SHA256
+from cryptography.hazmat.primitives.hmac import HMAC
+from cryptography.hazmat.primitives.serialization import load_pem_private_key
+from cryptography.x509 import load_pem_x509_certificate
+from base64 import b64decode, b64encode
+
+
+class SplitAccountTool(object):
+    '''直连服务商  分账  文档https://pay.weixin.qq.com/wiki/doc/apiv3/index.shtml'''
+    GET = 'GET'
+    POST = 'POST'
+
+    def __init__(self, appid, mchid, private_key, cert_serial_no, apiv3_key, cert_dir=None, proxy=None):
+        self._mchid = mchid  # 商户号
+        self._appid = appid
+        self._private_key = load_private_key(private_key)  # 商户证书私钥
+        self._cert_serial_no = cert_serial_no  # 商户证书序列号
+        self._apiv3_key = apiv3_key  # 商户APIv3密钥
+        self._cert_dir = cert_dir  # 平台证书存放目录
+        self._gate_way = 'https://api.mch.weixin.qq.com'
+        self._proxy = proxy
+        self._certificates = []
+        self._init_certificates()
+
+    def _init_certificates(self):
+        if self._cert_dir and os.path.exists(self._cert_dir):
+            for file_name in os.listdir(self._cert_dir):
+                if not file_name.lower().endswith('.pem'):
+                    continue
+                with open(self._cert_dir + file_name, encoding="utf-8") as f:
+                    certificate = load_certificate(f.read())
+                now = datetime.utcnow()
+                if certificate and now >= certificate.not_valid_before and now <= certificate.not_valid_after:
+                    self._certificates.append(certificate)
+        if not self._certificates:
+            self._update_certificates()
+        if not self._certificates:
+            raise CustomError('没有wechatpay平台证书,请仔细检查您的初始化参数!')
+
+    def _update_certificates(self):
+        path = '/v3/certificates'
+        self._certificates.clear()
+        code, message = self.request(path, skip_verify=True)
+        if code != 200:
+            return
+        data = json.loads(message).get('data')
+        for value in data:
+            serial_no = value.get('serial_no')
+            effective_time = value.get('effective_time')
+            expire_time = value.get('expire_time')
+            encrypt_certificate = value.get('encrypt_certificate')
+            algorithm = nonce = associated_data = ciphertext = None
+            if encrypt_certificate:
+                algorithm = encrypt_certificate.get('algorithm')
+                nonce = encrypt_certificate.get('nonce')
+                associated_data = encrypt_certificate.get('associated_data')
+                ciphertext = encrypt_certificate.get('ciphertext')
+            if not (serial_no and effective_time and expire_time and algorithm and nonce and associated_data and ciphertext):
+                continue
+            cert_str = aes_decrypt(
+                nonce=nonce,
+                ciphertext=ciphertext,
+                associated_datformat_private_keya=associated_data,
+                apiv3_key=self._apiv3_key)
+            certificate = load_certificate(cert_str)
+            if not certificate:
+                continue
+            now = datetime.utcnow()
+            if now < certificate.not_valid_before or now > certificate.not_valid_after:
+                continue
+            self._certificates.append(certificate)
+            if not self._cert_dir:
+                continue
+            if not os.path.exists(self._cert_dir):
+                os.makedirs(self._cert_dir)
+            if not os.path.exists(self._cert_dir + serial_no + '.pem'):
+                f = open(self._cert_dir + serial_no + '.pem', 'w')
+                f.write(cert_str)
+                f.close()
+
+    def _verify_signature(self, headers, body):
+        signature = headers.get('Wechatpay-Signature')
+        timestamp = headers.get('Wechatpay-Timestamp')
+        nonce = headers.get('Wechatpay-Nonce')
+        serial_no = headers.get('Wechatpay-Serial')
+        cert_found = False
+        for cert in self._certificates:
+            if int('0x' + serial_no, 16) == cert.serial_number:
+                cert_found = True
+                certificate = cert
+                break
+        if not cert_found:
+            self._update_certificates()
+            for cert in self._certificates:
+                if int('0x' + serial_no, 16) == cert.serial_number:
+                    cert_found = True
+                    certificate = cert
+                    break
+            if not cert_found:
+                return False
+        if not rsa_verify(timestamp, nonce, body, signature, certificate):
+            return False
+        return True
+
+    def rsa_sign(self, private_key, sign_str):
+        message = sign_str.encode('UTF-8')
+        signature = private_key.sign(data=message, padding=PKCS1v15(), algorithm=SHA256())
+        sign = b64encode(signature).decode('UTF-8').replace('\n', '')
+        return sign
+
+    def build_authorization(self, path, method, mchid, serial_no, private_key, data=None, nonce_str=None):
+        '''
+        一、构建签名串
+        签名串一共有五行,每一行为一个参数。行尾以 \n(换行符,ASCII编码值为0x0A)结束,包括最后一行。如果参数本身以\n结束,也需要附加一个\n。
+        第一步,获取HTTP请求的方法(GET, POST, PUT)等
+        第二步,获取请求的绝对URL,并去除域名部分得到参与签名的URL。如果请求中有查询参数,URL末尾应附加有'?'和对应的查询字符串。
+        第三步,获取发起请求时的系统当前时间戳,即格林威治时间1970年01月01日00时00分00秒(北京时间1970年01月01日08时00分00秒)起至现在的总秒数,作为请求时间戳。微信支付会拒绝处理很久之前发起的请求,请商户保持自身系统的时间准确。
+        第四步,生成一个请求随机串,可参见生成随机数算法。这里,我们使用命令行直接生成一个。
+        第五步,获取请求中的请求报文主体(request body)。
+            请求方法为GET时,报文主体为空。
+            当请求方法为POST或PUT时,请使用真实发送的JSON报文。
+            图片上传API,请使用meta对应的JSON报文。
+            对于下载证书的接口来说,请求报文主体是一个空串。
+        二、计算签名值
+        绝大多数编程语言提供的签名函数支持对签名数据进行签名。强烈建议商户调用该类函数,使用商户私钥对待签名串进行SHA256 with RSA签名,并对签名结果进行Base64编码得到签名值。
+        三、设置HTTP头
+         微信支付商户API V3要求通过HTTP Authorization头来传递签名  Authorization由认证类型和签名信息两部分组成
+        1.认证类型,目前为WECHATPAY2-SHA256-RSA2048
+        2.签名信息
+            发起请求的商户(包括直连商户、服务商或渠道商)的商户号 mchid
+            商户API证书序列号serial_no,用于声明所使用的证书
+            请求随机串nonce_str
+            时间戳timestamp
+            签名值signature
+        注:以上五项签名信息,无顺序要求。
+        '''
+        timeStamp = str(int(time.time()))
+        nonce_str = nonce_str or ''.join(str(uuid.uuid4()).split('-')).upper()
+        body = data if isinstance(data, str) else json.dumps(data) if data else ''
+        sign_str = '%s\n%s\n%s\n%s\n%s\n' % (method, path, timeStamp, nonce_str, body)
+        signature = self.rsa_sign(private_key=private_key, sign_str=sign_str)
+        authorization = 'WECHATPAY2-SHA256-RSA2048 mchid="%s",nonce_str="%s",signature="%s",timestamp="%s",serial_no="%s"' % (
+        mchid, nonce_str, signature, timeStamp, serial_no)
+        return authorization
+
+    def request(self, path, method=GET, data=None, skip_verify=False, sign_data=None, files=None, cipher_data=False, headers={}):
+        if files:
+            headers.update({'Content-Type': 'multipart/form-data'})
+        else:
+            headers.update({'Content-Type': 'application/json'})
+        headers.update({'Accept': 'application/json'})
+        authorization = self.build_authorization(path, method, self._mchid, self._cert_serial_no, self._private_key, data=sign_data if sign_data else data)
+        headers.update({'Authorization': authorization})
+        if method == SplitAccountTool.GET:
+            response = requests.get(url=self._gate_way + path, headers=headers, proxies=self._proxy)
+        elif method == SplitAccountTool.POST:
+            response = requests.post(url=self._gate_way + path, json=None if files else data, data=data if files else None, headers=headers, files=files, proxies=self._proxy)
+        else:
+            raise CustomError('请选择正确的请求方式!')
+
+        if response.status_code in range(200, 300) and not skip_verify:
+
+            if not self._verify_signature(response.headers, response.text):
+                raise CustomError('签名验证失败!')
+
+        return response.status_code, response.text if 'application/json' in response.headers.get('Content-Type') else response.content
+
+
+def format_private_key(private_key_str):
+    pem_start = '-----BEGIN PRIVATE KEY-----\n'
+    pem_end = '\n-----END PRIVATE KEY-----'
+    if not private_key_str.startswith(pem_start):
+        private_key_str = pem_start + private_key_str
+    if not private_key_str.endswith(pem_end):
+        private_key_str = private_key_str + pem_end
+    return private_key_str
+
+
+def load_private_key(private_key_str):
+    try:
+        return load_pem_private_key(data=format_private_key(private_key_str).encode('UTF-8'), password=None, backend=default_backend())
+    except:
+        raise CustomError('商户证书私钥加载失败!')
+
+
+def rsa_verify(timestamp, nonce, body, signature, certificate):
+    '''验证签名'''
+    sign_str = '%s\n%s\n%s\n' % (timestamp, nonce, body)
+    public_key = certificate.public_key()
+    message = sign_str.encode('UTF-8')
+    signature = b64decode(signature)
+    try:
+        public_key.verify(signature, message, PKCS1v15(), SHA256())
+    except InvalidSignature:
+        return False
+    return True
+
+
+def load_certificate(certificate_str):
+    try:
+        return load_pem_x509_certificate(data=certificate_str.encode('UTF-8'), backend=default_backend())
+    except:
+        return None
+
+
+def aes_decrypt(nonce, ciphertext, associated_data, apiv3_key):
+    '''回调信息解密'''
+    key_bytes = apiv3_key.encode('UTF-8')
+    nonce_bytes = nonce.encode('UTF-8')
+    associated_data_bytes = associated_data.encode('UTF-8')
+    data = b64decode(ciphertext)
+    aesgcm = AESGCM(key=key_bytes)
+    try:
+        result = aesgcm.decrypt(nonce=nonce_bytes, data=data, associated_data=associated_data_bytes).decode('UTF-8')
+    except InvalidTag:
+        result = None
+    return result
+

+ 0 - 99
util/wechatcashout.py

@@ -1,99 +0,0 @@
-# coding=utf-8
-
-import uuid
-import os
-import requests
-import json
-import xmltodict
-import time
-from hashlib import md5
-from django.conf import settings
-
-from util.exceptions import CustomError
-
-
-class WechatAgentPay():
-
-    def __init__(self, appid, mch_id, partner_trade_no, openid, amount, merchant_key, cert, key):
-        self.params = {
-            'mch_appid': appid,                       # 申请商户号的appid或商户号绑定的appid
-            'mchid': mch_id,                          # 微信支付分配的商户号
-            'nonce_str': generate_nonce_str(),        # 随机字符串,不长于32位
-            'partner_trade_no': partner_trade_no,     # 商户订单号,需保持唯一性
-            'openid': openid,                         # 商户appid下,某用户的openid
-            'check_name': 'NO_CHECK',                 # 校验用户姓名选项
-            'amount': amount,                         # 金额  企业付款金额 单位为分
-            'desc': u'佣金',                          # 企业付款备注
-        }
-        self.params['sign'] = generate_sign(self.params, merchant_key)
-        self.url = 'https://api.mch.weixin.qq.com/mmpaymkttransfers/promotion/transfers'
-        self.cert_file = cert
-        self.cert_key = key
-
-    def pay(self):
-        result = send_cert_request(self.url, self.params, self.cert_file, self.cert_key)
-        return result
-
-
-class PayQuery():
-
-    def __init__(self, partner_trade_no, mch_id, appid, merchant_key, cert, key):
-        self.url = 'https://api.mch.weixin.qq.com/mmpaymkttransfers/gettransferinfo'
-        self.cert_file = cert
-        self.cert_key = key
-        self.params = {
-            'nonce_str': generate_nonce_str(),
-            'partner_trade_no': partner_trade_no,
-            'mch_id': mch_id,
-            'appid': appid,
-        }
-        self.params['sign'] = generate_sign(self.params, merchant_key)
-
-    def query(self):
-        result = send_cert_request(self.url, self.params, self.cert_file, self.cert_key)
-        return result
-
-
-
-def send_cert_request(url, param, cert_file, cert_key):
-    '''
-    发送携带证书的xml请求
-    '''
-    xml = xmltodict.unparse({'root': param})
-    response = requests.post(url, data=xml.encode('utf-8'), headers={'Content-Type': 'text/xml'}, cert=(cert_file, cert_key), verify=False)
-    xmlmsg  = json.loads(json.dumps(xmltodict.parse(response.content)))['xml']
-    print(xmlmsg)
-    return xmlmsg
-
-def generate_nonce_str():
-    """
-    生成随机字符串
-    """
-    return str(uuid.uuid4()).replace('-', '')
-
-def generate_sign(params, merchant_key):
-    """
-    生成md5签名的参数
-    """
-    if 'sign' in params:
-        params.pop('sign')
-    src = '&'.join(['%s=%s' % (k, v) for k, v in sorted(params.items())]) + '&key=%s' % merchant_key
-    return md5(src.encode('utf-8')).hexdigest().upper()
-
-def validate_sign(resp_dict, merchant_key):
-    """
-    验证微信返回的签名
-    """
-    if 'sign' not in resp_dict:
-        return False
-    wx_sign = resp_dict['sign']
-    sign = generate_sign(resp_dict, merchant_key)
-    if sign == wx_sign:
-        return True
-    return False
-
-def generate_response_data(resp_dict):
-    """
-    字典转xml
-    """
-    return xmltodict.unparse({'xml': resp_dict}, pretty=True, full_document=False).encode('utf-8')

+ 195 - 3
util/wechatpay.py

@@ -7,8 +7,9 @@ import xmltodict
 import time
 from hashlib import md5
 from django.conf import settings
-
+from util.splitaccount_tool import SplitAccountTool
 from util.exceptions import CustomError
+from apps.foundation.models import BizLog
 
 # 微信支付sign_type
 WEIXIN_SIGN_TYPE = 'MD5'
@@ -24,6 +25,196 @@ WEIXIN_QUERY_ORDER_URL = 'https://api.mch.weixin.qq.com/pay/orderquery'
 WEIXIN_CALLBACK_API = 'https://lsr.zzly.vip/api/wechat_notify/'
 
 
+class SplitAccountFuc(object):
+    '''直连服务商  分账  文档https://pay.weixin.qq.com/wiki/doc/apiv3/index.shtml'''
+
+    def __init__(self,appid, mchid, private_key, cert_serial_no, apiv3_key, cert_dir=None, proxy=None):
+        self._appid = appid
+        self._mchid = mchid
+        self._core = SplitAccountTool(appid, mchid, private_key, cert_serial_no, apiv3_key, cert_dir=cert_dir, proxy=proxy)
+
+    def splitaccount_order(self, transaction_id, out_order_no, receivers):
+        '''
+        请求分账  微信订单支付成功后,服务商代特约商户发起分账请求,将结算后的钱分到分账接收方
+        注意:对同一笔订单最多能发起50次分账请求,每次请求最多分给50个接收方
+        此接口采用异步处理模式,即在接收到商户请求后,优先受理请求再异步处理,最终的分账结果可以通过查询分账接口获取
+        请求分账里边的openid  是需要先调用添加分账接收方接口添加分账关系
+        '''
+        path = "/v3/profitsharing/orders"
+        params = {
+            'appid': self._appid,
+            'transaction_id': transaction_id,    # 微信支付订单号
+            'out_order_no': out_order_no,      # 商户系统内部的分账单号,在商户系统内部唯一,同一分账单号多次请求等同一次。只能是数字、大小写字母_-|*@
+            'receivers': [],
+            'unfreeze_unsplit': True  # 是否解冻剩余未分金额   如果只分一次就填true
+        }
+
+        for item in receivers:
+            receiver_item = {
+                'type': 'PERSONAL_OPENID',
+                'account': item['account'],
+                'amount': int(round(item['amount'], 0)),   # 单位为分  只能为整数  不能超过原订单支付金额及最大分账比例金额
+                'description': item['description']
+            }
+
+            params['receivers'].append(receiver_item)
+
+        success = True
+        data = {}
+        try:
+            code, message = self._core.request(path, SplitAccountTool.POST, data=params)
+            result = json.loads(message)
+            if code == 200:
+                data['order_id'] = result.get('order_id')
+                data['state'] = result.get('state')
+                data['receivers'] = result.get('receivers')
+            else:
+                success = False
+                BizLog.objects.addnew('', BizLog.INSERT, u'[%s]分账失败!原因:' % out_order_no + result.get('code'))
+        except CustomError as e:
+            success = False
+            BizLog.objects.addnew('', BizLog.INSERT, u'[%s]分账失败!原因:' % out_order_no + e.get_error_msg())
+        except Exception as e:
+            success = False
+            BizLog.objects.addnew('', BizLog.INSERT, u'[%s]分账失败!原因:' % out_order_no + str(e))
+
+        return success, data
+
+    def splitaccount_addreceiver(self, account):
+        '''添加分账接收方'''
+        path = "/v3/profitsharing/receivers/add"
+        params = {
+            'appid': self._appid,
+            'type': "PERSONAL_OPENID",
+            'account': account,  # 接收人的openid
+            'relation_type': "USER",  # body子商户与接收方的关系
+        }
+
+        success = True
+        try:
+            code, message = self._core.request(path, SplitAccountTool.POST, data=params)
+            result = json.loads(message)
+            if code != 200:
+                success = False
+                BizLog.objects.addnew('', BizLog.INSERT, u'[%s]添加分账接收方失败!原因:' % account + result.get('code'))
+        except CustomError as e:
+            success = False
+            BizLog.objects.addnew('', BizLog.INSERT, u'[%s]添加分账接收方失败!原因:' % account + e.get_error_msg())
+        except Exception as e:
+            success = False
+            BizLog.objects.addnew('', BizLog.INSERT, u'[%s]添加分账接收方失败!原因:' % account + str(e))
+
+        return success
+
+    def splitaccount_deletereceiver(self, account):
+        '''删除分账接收方'''
+        path = "/v3/profitsharing/receivers/delete"
+        params = {
+            'appid': self._appid,
+            'type': "PERSONAL_OPENID",
+            'account': account,  # 接收人的openid
+        }
+
+        success = True
+        try:
+            code, message = self._core.request(path, SplitAccountTool.POST, data=params)
+            result = json.loads(message)
+            if code != 200:
+                success = False
+                BizLog.objects.addnew('', BizLog.INSERT, u'[%s]删除分账接收方失败!原因:' % account + result.get('code'))
+        except CustomError as e:
+            success = False
+            BizLog.objects.addnew('', BizLog.INSERT, u'[%s]删除分账接收方失败!原因:' % account + e.get_error_msg())
+        except Exception as e:
+            success = False
+            BizLog.objects.addnew('', BizLog.INSERT, u'[%s]删除分账接收方失败!原因:' % account + str(e))
+
+        return success
+
+    def splitaccount_orderquery(self, transaction_id, out_order_no):
+        '''
+        查询分账结果
+        transaction_id  微信支付订单号
+        out_order_no  商户分账单号
+        '''
+        success = True
+        data = {}
+        if transaction_id and out_order_no:
+            path = '/v3/profitsharing/orders/%s?transaction_id=%s' % (out_order_no, transaction_id)
+        else:
+            success = False
+            BizLog.objects.addnew('', BizLog.INSERT, u'[%s]查询分账结果失败!原因:参数错误!' % out_order_no)
+            return success, data
+
+        try:
+            code, message = self._core.request(path)
+            result = json.loads(message)
+            if code == 200:
+                data['order_id'] = result.get('order_id')
+                data['state'] = result.get('state')
+                data['receivers'] = result.get('receivers')
+            else:
+                success = False
+                BizLog.objects.addnew('', BizLog.INSERT, u'[%s]查询分账结果失败!原因:' % out_order_no + result.get('code'))
+        except CustomError as e:
+            success = False
+            BizLog.objects.addnew('', BizLog.INSERT, u'[%s]查询分账结果失败!原因:' % out_order_no + e.get_error_msg())
+        except Exception as e:
+            success = False
+            BizLog.objects.addnew('', BizLog.INSERT, u'[%s]查询分账结果失败!原因:' % out_order_no + str(e))
+        return success, data
+
+    def splitaccount_return(self):
+        '''请求分账回退'''
+        pass
+
+    def splitaccount_returnquery(self):
+        '''查询分账回退结果'''
+        pass
+
+    def splitaccount_unfreeze(self, transaction_id, out_order_no):
+        '''解冻剩余资金'''
+        path = "/v3/profitsharing/orders"
+        params = {
+            'transaction_id': transaction_id,  # 微信支付订单号
+            'out_order_no': out_order_no,  # 商户系统内部的分账单号,在商户系统内部唯一,同一分账单号多次请求等同一次。只能是数字、大小写字母_-|*@
+            'description': "解冻资金"
+        }
+
+        success = True
+        data = {}
+        try:
+            code, message = self._core.request(path, SplitAccountTool.POST, data=params)
+            result = json.loads(message)
+            if code == 200:
+                data['order_id'] = result.get('order_id')
+                data['state'] = result.get('state')
+                data['receivers'] = result.get('receivers')
+            else:
+                success = False
+                BizLog.objects.addnew('', BizLog.INSERT, u'[%s]解冻剩余资金失败!原因:'% out_order_no + result.get('code'))
+        except CustomError as e:
+            success = False
+            BizLog.objects.addnew('', BizLog.INSERT, u'[%s]解冻剩余资金失败!原因:'% out_order_no + e.get_error_msg())
+        except Exception as e:
+            success = False
+            BizLog.objects.addnew('', BizLog.INSERT, u'[%s]解冻剩余资金失败!原因:'% out_order_no + str(e))
+
+        return success, data
+
+    def splitaccount_amountquery(self):
+        '''查询剩余待分金额'''
+        pass
+
+    def splitaccount_configquery(self):
+        '''查询最大分账比例'''
+        pass
+
+    def splitaccount_bill(self):
+        '''申请分账账单'''
+        pass
+
+
 class WeChatResponse():
     def __init__(self,appid, agent_num, agent_key):
         self.params = {
@@ -89,9 +280,10 @@ class WechatPay():
         data.pop('appId')
         return data
 
-    def unifiedOrder(self,out_trade_no,total_fee, openid):
+    def unifiedOrder(self,out_trade_no,total_fee, openid, profit_sharing):
+        self.params['profit_sharing'] = profit_sharing  # 是否分账参数  Y  需要分账  N 不分账  字母大写默认不分账
         self.params['out_trade_no'] = out_trade_no
-        self.params['total_fee'] = int(round(total_fee / 100,0))
+        self.params['total_fee'] = int(round(total_fee, 0))
         self.params['openid'] = openid
         self.params['nonce_str'] = generate_nonce_str()
         self.params['sign'] = generate_sign(self.params, self.merchant_key)