mopybird před 1 rokem
rodič
revize
9141de55f4

+ 5 - 1
apps/web/common/__init__.py

@@ -1,2 +1,6 @@
 # -*- coding: utf-8 -*-
-#!/usr/bin/env python
+# !/usr/bin/env python
+
+from apps.web.utils import concat_server_end_url
+
+ALIPAY_NOTIFY_URL = concat_server_end_url(uri = '/common/ali/notify')

+ 11 - 0
apps/web/common/transaction/__init__.py

@@ -183,6 +183,10 @@ class OrderMainType(StrEnum):
     REFUND = 'R'  # 退款单
     WITHDRAW = 'W'  # 提现单
 
+    MERCHANT_WITHDRAW = 'M'  # 商户手工提现单
+
+    OTHER = 'O'
+
 
 '''
   UserPaySubType - 用户支付订单子类型
@@ -190,6 +194,7 @@ class OrderMainType(StrEnum):
   子类型在UserPaySubType和DealerPaySubType中是唯一的, 不能重复
 '''
 
+
 class RefundSubType(IterConstant):
     REFUND = 'R'
     REVOKE = 'V'
@@ -230,3 +235,9 @@ class UserConsumeSubType(StrEnum):
     CARD = 'C'
     COIN = 'I'
     VIRTUAL_CARD = 'V'  # 虚拟卡消费
+
+
+class OtherOrderSubType(IterConstant):
+    JD_WITHDRAW = 'J'
+    ALI_USER_AGREEMENT_SIG = 'A'
+    ALI_FUND_BOOK_USER_ID = 'B'

+ 0 - 3
apps/web/common/transaction/withdraw.py

@@ -401,9 +401,6 @@ class WithdrawService(object):
             if self.payee.abnormal:
                 raise ServiceException({'result': 0, 'description': u'该帐号资金异常,请联系客服处理', 'payload': {}})
 
-            self.payee.check_withdraw_min_fee(
-                income_type = self.income_type, amount = self.amount, pay_type = self.pay_type)
-
             self.payee.can_withdraw_today(self.amount)
 
             #: 大额提现单预警

+ 33 - 2
apps/web/common/views.py

@@ -26,6 +26,7 @@ from apps import serviceCache
 from apps.web import district
 from apps.web.ad.models import Advertisement, AdStatistics
 from apps.web.agent.models import MoniApp
+from apps.web.common import ALIPAY_NOTIFY_URL
 from apps.web.common.models import AddressType, WithdrawRecord, WithdrawBanks, WithdrawDistrict, \
     WithdrawBranchBanks, WithdrawBankCard
 from apps.web.common.models import FrontendLog, FAQ
@@ -36,7 +37,7 @@ from apps.web.constant import Const, AppPlatformType, AdSpace, USER_RECHARGE_TYP
 from apps.web.core import ROLE
 from apps.web.core.exceptions import InvalidFileSize, InvalidFileName
 from apps.web.core.file import AliOssFileUploader, WechatSubscriptionAccountVerifyFileUploader
-from apps.web.core.models import WechatPayApp, AliApp
+from apps.web.core.models import WechatPayApp, AliApp, AliUserAgreementSign, AliFundAccountBookApp
 
 from apps.web.core.utils import DefaultJsonErrorResponse, JsonOkResponse, JsonErrorResponse
 from apps.web.dealer.models import Dealer
@@ -530,13 +531,43 @@ def aliNotify(request):
                 '<?xml version="1.0" encoding="GBK"?><alipay><response><success>true</success></response><app_cert_sn>{}</app_cert_sn><sign>{}</sign><sign_type>RSA2</sign_type></alipay>'.format(
                     gateway.client.app_cert_sn, sign))
 
-    if payload['msg_method'] == 'alipay.fund.trans.order.changed':
+    if 'msg_method' in payload and payload['msg_method'] == 'alipay.fund.trans.order.changed':
         payload['biz_content'] = json.loads(payload['biz_content'])
         order_no = payload['biz_content']['out_biz_no']
         if WithdrawRecord.is_my(order_no):
             return AliPayWithdrawNotifier(record_cls = WithdrawRecord).do(payload)
         else:
             logger.warn('not support this order. orderNo = {}'.format(order_no))
+
+        return HttpResponse('success')
+
+    elif 'notify_type' in payload and payload['notify_type'] == 'dut_user_sign':
+        external_agreement_no = payload['external_agreement_no']
+
+        sign_model = AliUserAgreementSign.objects(externalAgreementNo = external_agreement_no).first()
+        if sign_model:
+            sign_model.agreementNo = payload['agreement_no']
+            sign_model.signTime = datetime.datetime.strptime(payload['sign_time'], "%Y-%m-%d %H:%M:%S")
+            sign_model.save()
+
+            if payload['status'] == 'NORMAL' and sign_model.attachParas['action'] == 'make_fund_account_book':
+                agreementNo = sign_model.agreementNo
+                certNo = sign_model.attachParas['certNo']
+
+                if not AliFundAccountBookApp.objects(agreementNo = agreementNo, certNo = certNo).first():
+                    bookModel = AliFundAccountBookApp.apply(sign_model)
+
+                    create_rv = PlatformPayGatewayUtil.get_ali_isv_pay_gateway().create_fund_accountbook(bookModel)
+
+                    bookModel.appid = create_rv['account_book_id']
+
+                    card_info = create_rv['ext_card_info']
+                    card_info.pop('status')
+                    bookModel.cardInfo = card_info
+                    bookModel.save()
+
+        return HttpResponse('success')
+
     else:
         logger.warn('not support this method = {}'.format(payload['msg_method']))
         return HttpResponse('success')

+ 4 - 1
apps/web/core/__init__.py

@@ -20,6 +20,7 @@ logger = logging.getLogger(__name__)
 class PayAppType(IterConstant):
     # 资金池模式只使用ALIPAY,WECHAT,JD_AGGR三种支付方式
     ALIPAY = 'alipay'
+    ALIPAY_FUND_BOOK = 'alipay_fund_book'
     WECHAT = 'wechat'
 
     # 各经销商可使用的支付方式(包括JD_AGGR以及ALIPAY+WECHAT)
@@ -45,7 +46,9 @@ PAY_APP_MAP = {
     PayAppType.PLATFORM_PROMOTION: 'apps.web.core.models.PlatformPromotionApp',
     PayAppType.PLATFORM_WALLET: 'app.web.core.models.PlatformWalletApp',
     PayAppType.PLATFORM_RECONCILE: 'app.web.core.models.PlatformReconcileApp',
-    PayAppType.MANUAL: 'apps.web.core.models.ManualPayApp'
+    PayAppType.MANUAL: 'apps.web.core.models.ManualPayApp',
+    PayAppType.ALIPAY_FUND_BOOK: 'apps.web.core.models.AliFundAccountBookApp'
+
 }
 
 APP_KEY_DELIMITER = '-'

+ 181 - 44
apps/web/core/models.py

@@ -3,6 +3,7 @@
 
 import base64
 import datetime
+import hashlib
 import logging
 import os
 from importlib import import_module
@@ -10,17 +11,20 @@ from importlib import import_module
 import simplejson as json
 from django.conf import settings
 from django.core.cache import cache
+from django.utils.functional import cached_property
 from django.utils.module_loading import import_string
 from mongoengine import StringField, DateTimeField, EmbeddedDocument, DictField, BooleanField, DynamicField, ListField, \
-    IntField, LazyReferenceField, ObjectIdField, DynamicDocument, EmbeddedDocumentField
+    IntField, LazyReferenceField, ObjectIdField, DynamicDocument, EmbeddedDocumentField, GenericLazyReferenceField
 from typing import List, cast, TYPE_CHECKING
 
 from apilib.monetary import RMB
+from apilib.systypes import IterConstant
 from apilib.utils import flatten
 from apilib.utils_json import json_loads, json_dumps
 from apilib.utils_string import encrypt_display
 from apilib.utils_sys import ThreadLock
 from apps import serviceCache
+from apps.web.common.transaction import OrderNoMaker, OrderMainType, OtherOrderSubType
 from apps.web.constant import Const, PARTITION_ROLE
 from apps.web.core import PayAppType, APP_KEY_DELIMITER, ROLE
 from apps.web.core.db import Searchable
@@ -1512,6 +1516,131 @@ class AliApp(PayAppBase):
 
         return getattr(self, '__client__')
 
+    @property
+    def withdraw_payer_info(self):
+        return None
+
+    def withdraw_product_code(self, bank):
+        if bank:
+            return 'TRANS_BANKCARD_NO_PWD'
+        else:
+            return 'TRANS_ACCOUNT_NO_PWD'
+
+    def withdraw_biz_scene(self, bank):
+        return 'DIRECT_TRANSFER'
+
+
+class AliFundAccountBookApp(PayAppBase):
+    """
+    阿里资金转账APP
+    """
+
+    appid = StringField(verbose_name = u'资金记账本id', required = True, null = False)
+    agreementNo = StringField(verbose_name = u'协议编号', required = True, null = False)
+
+    certNo = StringField(verbose_name = u'企业营业执照认证号')
+
+    merchantUserId = StringField(verbose_name = u'外部商户系统会员的唯一标识;创建记账本的幂等字段')
+    merchantUserType = StringField(verbose_name = u'外部商户用户类型,固定值')
+    sceneCode = StringField(verbose_name = u'资金记账本的业务场景')
+
+    cardInfo = DictField(verbose_name = u'虚拟银行卡信息')
+
+    search_fields = ('appid', 'name', 'remarks')
+
+    meta = {
+        'indexes': [
+            {
+                'fields': ['appid'], 'unique': True
+            },
+        ],
+        'collection': 'ali_fund_account_book',
+        'db_alias': 'default'
+    }
+
+    def __repr__(self):
+        return '<AliFundAccountBookApp appid={}>'.format(self.appid)
+
+    @classmethod
+    def make_user_id(cls, external_agreement_no, cert_no):
+        return '{}{}'.format(external_agreement_no, cert_no)
+
+    @classmethod
+    def apply(cls, signModel):
+        # type: (AliUserAgreementSign)->AliFundAccountBookApp
+
+        return cls(
+            agentNo = signModel.cooperAppId,
+            merchantUserId = cls.make_user_id(signModel.externalAgreementNo, signModel.attachParas['certNo']),
+            merchantUserType = 'BUSINESS_ORGANIZATION',
+            sceneCode = 'SATF_FUND_BOOK',
+            agreementNo = signModel.agreementNo,
+            certNo = signModel.attachParas['certNo']
+        )
+
+    @property
+    def __valid_check__(self):
+        if not self.appid or not self.agreementNo:
+            return False
+        else:
+            return True
+
+    @cached_property
+    def ISVApp(self):
+        return AliApp.objects(appid = self.agentNo).first()
+
+    @property
+    def support_withdraw(self):
+        return self.ISVApp.supportWithdraw
+
+    @property
+    def support_withdraw_bank(self):
+        return self.ISVApp.supportWithdrawBank
+
+    @property
+    def withdraw_payer_info(self):
+        return {
+            'identity_type': 'ACCOUNT_BOOK_ID',
+            'identity': self.appid,
+            'ext_info': json.dumps({
+                'agreement_no': self.agreementNo
+            })
+        }
+
+    def withdraw_product_code(self, bank):
+        return 'SINGLE_TRANSFER_NO_PWD'
+
+    def withdraw_biz_scene(self, bank):
+        return 'ENTRUST_TRANSFER'
+
+    def new_withdraw_gateway(self, gateway_version = None):
+        from apps.web.core.payment.ali import AliPayWithdrawGateway
+        return AliPayWithdrawGateway(self)
+
+    @property
+    def pay_app_type(self):
+        return PayAppType.ALIPAY_FUND_BOOK
+
+    @property
+    def __gateway_key__(self):
+        _ = [
+            self.occupantId,
+            self.appid,
+            self.occupant.role
+        ]
+        return APP_KEY_DELIMITER.join(_)
+
+    @property
+    def client(self):
+        if hasattr(self, '__client__'):
+            return getattr(self, '__client__')
+
+        _client = self.ISVApp.client
+        setattr(self, '__client__', _client)
+
+        return _client
+
+
 
 class PlatformAppBase(PayAppBase):
     """
@@ -1778,9 +1907,13 @@ class WithdrawEntity(EmbeddedDocument):
     wechatWithdrawApp = LazyReferenceField(document_type = WechatPayApp, default = None)
     alipayWithdrawApp = LazyReferenceField(document_type = AliApp, default = None)
 
+    aliWithdrawApp = GenericLazyReferenceField(choices = [AliApp, AliFundAccountBookApp], default = None)
+
     @property
     def alipay_app(self):
-        if self.alipayWithdrawApp:
+        if self.aliWithdrawApp:
+            return self.aliWithdrawApp.fetch()
+        elif self.alipayWithdrawApp:
             return self.alipayWithdrawApp.fetch()
         else:
             return None
@@ -2257,51 +2390,55 @@ class DriverEventer(DynamicDocument):
         return eval('eventer_module.builder(device_adapter)')
 
 
-class CustomerPayRelation(Searchable):
+class AliUserAgreementSign(Searchable):
     """
-    app 和使用者的关联表
-    为 多对多的关系
+    阿里用户签约
     """
-    ownerId = StringField(verbose_name=u"持有者ID")
-    ownerRole = StringField(verbose_name=u"持有者角色")
 
-    appId = StringField(verbose_name=u"app的ID")
-    appType = StringField(verbose_name=u"支付APP的种类")
+    class SignStatus(IterConstant):
+        INIT = 'INIT'
+        TEMP = 'TEMP'
+        NORMAL = 'NORMAL'
+        STOP = 'STOP'
+
+    externalAgreementNo = StringField(verbose_name = u"商户传入的协议编号")
+    agreementNo = StringField(verbose_name = u"签约号")
+
+    personalProductCode = StringField(verbose_name = u'个人产品码')
+    signScene = StringField(verbose_name = u'场景码')
+    accessParams = DictField(verbose_name = u'端类型')
+    productCode = StringField(verbose_name = u"商家签约的产品码")
 
-    active = BooleanField(verbose_name=u"是否处于使用状态", default=False)
-    isDelete = BooleanField(verbose_name=u"是否已经删除", default=False)
+    thirdPartyType = StringField(verbose_name = u"商家类型")
+
+    externalLogonId = StringField(verbose_name = u"用户在商户网站的登录账号,用于在签约页面展示,如果为空,则不展示")
+
+    dateTimeAdded = DateTimeField(verbose_name = u"创建时间")
+    signTime = DateTimeField(verbose_name = u"创建时间")
+
+    attachParas = DictField(verbose_name = u'签约完成后操作所需要的参数')
+
+    cooperAppId = StringField(verbose_name = u'合作单位APPID')
+
+    meta = {"collection": "ali_user_agreement_sign", "db_alias": "default"}
 
     @classmethod
-    def add_relation(cls, customer, app):
-        rel = cls.objects.filter(ownerId=str(customer.id), appId=str(app.id))
-        if not rel:
-            rel = cls()
-            rel.ownerId = str(customer.id)
-            rel.ownerRole = str(customer.role)
-            rel.appId = str(app.id)
-            rel.appType = str(app.__class__.__name__)
-            return rel.save()
-        else:
-            return rel
-
-    def active_app(self):
-        """
-        激活app 使之处于可用状态
-        """
-        self.active = True
-        return self.save()
-
-    def deactive_app(self):
-        """
-        关闭app 使之处于不可用状态
-        """
-        self.active = False
-        return self.save()
-
-    def delete_relation(self):
-        """
-        删除app
-        """
-        app = self.deactive_app()
-        app.isDelete = True
-        return app.save()
+    def issue(cls, cooperAppId, external_agreement_no, external_logon_id, attach):
+        return cls(
+            cooperAppId = cooperAppId,
+            externalAgreementNo = external_agreement_no,
+            personalProductCode = 'FUND_SAFT_SIGN_WITHHOLDING_P',
+            signScene = 'INDUSTRY|SATF_ACC',
+            accessParams = {"channel": "QRCODE"},
+            productCode = 'FUND_SAFT_SIGN_WITHHOLDING',
+            thirdPartyType = 'PARTNER',
+            externalLogonId = external_logon_id,
+            attachParas = attach).save()
+
+    @classmethod
+    def makeNo(cls, external_logon_id):
+        identifier = hashlib.md5(external_logon_id.encode()).hexdigest().upper()
+        return OrderNoMaker.make_order_no_32(
+            identifier, OrderMainType.OTHER, OtherOrderSubType.ALI_USER_AGREEMENT_SIG)
+
+

+ 94 - 7
apps/web/core/payment/ali.py

@@ -1,12 +1,13 @@
 # -*- coding: utf-8 -*-
 # !/usr/bin/env python
+
 import datetime
 import logging
 
 import arrow
 import simplejson as json
 from django.conf import settings
-from typing import TYPE_CHECKING
+from typing import TYPE_CHECKING, Optional
 
 from apilib.monetary import RMB
 from apilib.monetary import quantize
@@ -23,7 +24,7 @@ from library.alipay import AliPayServiceException
 logger = logging.getLogger(__name__)
 
 if TYPE_CHECKING:
-    from apps.web.core.models import AliApp
+    from apps.web.core.models import AliApp, AliUserAgreementSign, AliFundAccountBookApp
 
 
 class AlipayWithdrawQueryResult(dict):
@@ -208,7 +209,7 @@ class AliPayGateway(PaymentGateway, AlipayMixin):
 
 class AliPayWithdrawGateway(WithdrawGateway, AlipayMixin):
     def __init__(self, app):
-        # type: (AliApp)->None
+        # type: (Optional[AliApp,AliFundAccountBookApp])->None
         super(AliPayWithdrawGateway, self).__init__(app)
         self.__gateway_type__ = AppPlatformType.WITHDRAW
 
@@ -225,12 +226,17 @@ class AliPayWithdrawGateway(WithdrawGateway, AlipayMixin):
         :return:
         """
 
+        app = self.app  # type: Optional[AliApp, AliFundAccountBookApp]
+
         payee_info = {
             'identity_type': 'BANKCARD_ACCOUNT',
             'identity': bank_card.accountCode,
             'name': cn(bank_card.accountName)
         }
 
+        payer_info, withdraw_product_code, withdraw_biz_scene = \
+            app.withdraw_payer_info, app.withdraw_product_code(True), app.withdraw_biz_scene(True)
+
         if bank_card.accountType == WithdrawBankCard.AccountType.PUBLIC:
             payee_info['bankcard_ext_info'] = {
                 'inst_name': cn(bank_card.bankName),
@@ -253,8 +259,13 @@ class AliPayWithdrawGateway(WithdrawGateway, AlipayMixin):
             }
 
         result = self.client.api_alipay_fund_trans_uni_transfer(
-            out_biz_no = order_no, trans_amount = str(total_amount),
-            payee_info = payee_info, order_title = order_title)
+            out_biz_no = order_no,
+            trans_amount = str(total_amount),
+            payee_info = payee_info,
+            order_title = order_title,
+            product_code = withdraw_product_code,
+            biz_scene = withdraw_biz_scene,
+            payer_info = payer_info)
 
         if result['code'] == u'10000':
             return result
@@ -286,15 +297,25 @@ class AliPayWithdrawGateway(WithdrawGateway, AlipayMixin):
         :return:
         """
 
+        app = self.app  # type: Optional[AliApp, AliFundAccountBookApp]
+
         payee_info = {
             'identity': payOpenId,
             'identity_type': 'ALIPAY_LOGON_ID',
             'name': cn(real_user_name)
         }
 
+        payer_info, withdraw_product_code, withdraw_biz_scene = \
+            app.withdraw_payer_info, app.withdraw_product_code(False), app.withdraw_biz_scene(False)
+
         result = self.client.api_alipay_fund_trans_uni_transfer(
-            out_biz_no = order_no, trans_amount = str(amount),
-            payee_info = payee_info, order_title = subject, product_code = 'TRANS_ACCOUNT_NO_PWD')
+            out_biz_no = order_no,
+            trans_amount = str(amount),
+            payee_info = payee_info,
+            order_title = subject,
+            product_code = withdraw_product_code,
+            biz_scene = withdraw_biz_scene,
+            payer_info = payer_info)
 
         if result['code'] == u'10000':
             return result
@@ -309,3 +330,69 @@ class AliPayWithdrawGateway(WithdrawGateway, AlipayMixin):
         :return:
         """
         return self.get_transfer_result_via_bank(order_no = order_no)
+
+
+class AliPayISVGateway(WithdrawGateway, AlipayMixin):
+    def __init__(self, app):
+        # type: (AliApp)->None
+
+        super(AliPayISVGateway, self).__init__(app)
+        self.__gateway_type__ = AppPlatformType.ALIPAY
+
+    def create_transfer_agreement_sign(self, agreement_sign_model, notify_url = None):
+        # type:(AliUserAgreementSign, str)->str
+
+        result = self.client.api_alipay_user_agreement_page_sign(
+            personal_product_code = agreement_sign_model.personalProductCode,
+            sign_scene = agreement_sign_model.signScene,
+            access_params = agreement_sign_model.accessParams,
+            product_code = agreement_sign_model.productCode,
+            external_agreement_no = agreement_sign_model.externalAgreementNo,
+            third_party_type = agreement_sign_model.thirdPartyType,
+            external_logon_id = agreement_sign_model.externalLogonId,
+            notify_url = notify_url)
+        return result
+
+    def query_transfer_agreement(self, agreement_sign_model):
+        # type:(AliUserAgreementSign)->dict
+
+        return self.client.api_alipay_user_agreement_query(
+            personal_product_code = agreement_sign_model.personalProductCode,
+            sign_scene = agreement_sign_model.signScene,
+            product_code = agreement_sign_model.productCode,
+            external_agreement_no = agreement_sign_model.externalAgreementNo)
+
+    def create_fund_accountbook(self, apply_info):
+        # type:(AliFundAccountBookApp)->dict
+
+        return self.client.api_alipay_fund_accountbook_create(
+            merchant_user_id = apply_info.merchantUserId,
+            merchant_user_type = apply_info.merchantUserType,
+            scene_code = apply_info.sceneCode,
+            ext_info = json.dumps({
+                'agreement_no': apply_info.agreementNo,
+                'cert_no': apply_info.certNo
+            }))
+
+    def query_fund_accountbook(self, book_app):
+        # type:(AliFundAccountBookApp)->dict
+
+        return self.client.api_alipay_fund_accountbook_query(
+            account_book_id = book_app.appid,
+            scene_code = book_app.sceneCode,
+            ext_info = json.dumps({
+                'agreement_no': book_app.agreementNo
+            }))
+
+    def create_fund_trans_page_pay_url(self, book_app, out_biz_no, trans_amount, remark = u'转账', order_title = u'代发专项充值'):
+        # type:(AliFundAccountBookApp, str, RMB, str, str)->dict
+
+        time_expire = (datetime.datetime.now() + datetime.timedelta(hours = 1)).strftime("%Y-%m-%d %H:%M")
+        return self.client.api_alipay_fund_trans_page_pay(
+            agreement_no = book_app.agreementNo,
+            account_book_id = book_app.appid,
+            trans_amount = str(trans_amount),
+            out_biz_no = out_biz_no,
+            time_expire = time_expire,
+            remark = remark,
+            order_title = order_title)

+ 8 - 1
apps/web/helpers.py

@@ -20,7 +20,8 @@ from apps.web.core.auth.ali import AlipayAuthBridge
 from apps.web.core.auth.wechat import WechatAuthBridge
 from apps.web.core.bridge.wechat import WechatClientProxy
 from apps.web.core.datastructures import BaseVisitor
-from apps.web.core.models import SystemSettings, ManualPayApp
+from apps.web.core.models import SystemSettings, ManualPayApp, AliApp
+from apps.web.core.payment.ali import AliPayISVGateway
 from apps.web.models import MongoTempTable
 from apps.web.utils import is_user, is_dealer, is_agent, is_anonymous
 
@@ -335,6 +336,12 @@ def get_manual_pay_gateway(dealer):
     my_app.occupant = dealer
     return my_app.new_gateway(AppPlatformType.PLATFORM)
 
+class PlatformPayGatewayUtil(object):
+    @classmethod
+    def get_ali_isv_pay_gateway(cls):
+        app = AliApp.objects(appid = settings.ALI_ISV_APP_ID).first()
+        return AliPayISVGateway(app)
+
 #############
 ### Misc ####
 #############

+ 2 - 0
configs/base.py

@@ -700,3 +700,5 @@ ALIPAY_IMP_CPA_RUHUI_V3 = '773604407794823173'
 ALIPAY_IMP_CPA_LAXIN_V3 = '773837769738319876'
 
 CELERY_PUBLISH_METHOD = os.environ.get('CELERY_PUBLISH_METHOD', 'gevent')
+
+ALI_ISV_APP_ID = '2021001191645419'