如何在Ecommerce中加入支付宝模块

一、对于将Ecommerce加入支付宝模块,可以参考其内部PayPal模块的实现流程。PayPal的核心代码有三部分,描述如下(本工作在ginkgo.2版本上测试):
    1、通过JS和Python脚本生成支付链接,并跳转到PayPal网站。
    2、通过Python脚本和PayPal的SDK完成用户支付完成后的转跳和交易的处理。
    为了便于代码的跟踪和调试,将ecommerce和lms支付过程中的页面转跳和操作逻辑进行记录,以便于后期扩展支付功能。为了使得该记录在后续浏览时具有通用性和易读性,将lms的站点链接统一记录为”http://lms/“,将ecommerce的站点记录为”http://ecommerce/“。

二、显示购买按钮的核心代码(第一步)
       在LMS模块中,通过注册课程系统转跳到Ecommerce模块,此时的URL为“http://ecommerce/basket/”,这个URL对应的视图函数为:
#ecommerce/extensions/basket/views.py
class BasketSummaryView(BasketView):
    def get(self, request, *args, **kwargs):
        basket = request.basket
        ….
    def get_context_data(self, **kwargs):       #这是一个很重要的函数,用于在模板中加入其它变量
       context = super(BasketSummaryView, self).get_context_data(**kwargs)
       formset = context.get(‘formset’, [])
       lines = context.get(‘line_list’, [])
       ….
       payment_processors = site_configuration.get_payment_processors()  #用于获取所支持的支付平台。
       …..
      上述这个视图引入了另外一个模块的函数,同时get_context_data是一个很重要的函数,这个函数在模板中加入了更多的变量。这个视图函数的父类为BasketView,这个类在django-oscar中有定义,参考如下代码:
#src/oscar/apps/basket/views.py
class BasketView(ModelFormSetView):
    model = get_model(‘basket’, ‘Line’)
    basket_model = get_model(‘basket’, ‘Basket’)
    …..
    template_name = ‘basket/basket.html’  #此处为该视图的模板,这个模板在django-oscar中有定义,但被ecommerce中的模板给覆盖了
       该视图实际使用的模板为ecommerce/templates/oscar/basket/basket.html  (这一步为猜测),这个模板为框架,引入了同级目录中partials目录中的多模板文件,其中涉及到支付方式的文件为“partials/hosted_checkout_basket.html”。该文件中与支付按钮相关的内容如下:
#ecommerce/templates/oscar/basket/partials/hosted_checkout_basket.html
            <div class=”pull-right payment-buttons” data-basket-id=”{{ basket.id }}”>
                {% if free_basket %}       #如果该次支付不需要付费(使用了优惠券并且100%折扣)
                    <a href=”{% url ‘checkout:free-checkout’ %}”
                       data-track-type=”click”
                       data-track-event=”edx.bi.ecommerce.basket.free_checkout”
                       data-track-category=”checkout”
                       class=”btn btn-success checkout-button”>
                        {% trans “Place Order” %}
                    </a>
                {% else %}                #如果本次支付需要付费,则显示所有的付费按钮,paypal,cybersource等。
                    {% for processor in payment_processors %}            #payment_processors为付费的平台名称
                        <button data-track-type=”click”
                                data-track-event=”edx.bi.ecommerce.basket.payment_selected”
                                data-track-category=”checkout”
                                data-processor-name=”{{ processor.NAME|lower }}”
                                data-track-checkout-type=”hosted”
                                class=”btn payment-button”
                                id=”{{ processor.NAME|lower }}”>
                            {% if processor.NAME == ‘cybersource’ %}
                                {% trans “Checkout” %}
                            {% elif processor.NAME == ‘paypal’ %}
                                {# Translators: Do NOT translate the name PayPal. #}
                                {% trans “Checkout with PayPal” %}
                                                    {% elif processor.NAME == ‘alipay’ %}                #新加的代码,配合后续的代码修改才能在支付页面上显示支付宝
                                                           {% trans “Checkout with Alipay” %}
                            {% endif %}
                        </button>
                    {% endfor %}
                {% endif %}
            </div>
       从上述模板文件中可以看出,“payment_processors”是一个可枚举类型的数据,其包含了所对接的支付平台,而这个变量的内容来自于BasketSummaryView类中的get_context_data函数,在这个函数内将payment_processor进行了赋值。
#ecommerce/core/models.py
class SiteConfiguration(models.Model):
    def get_payment_processors(self):
        all_processors = self._all_payment_processors()
        all_processor_names = {processor.NAME for processor in all_processors}
        missing_processor_configurations = self.payment_processors_set – all_processor_names
        if missing_processor_configurations:
            processor_config_repr = “, “.join(missing_processor_configurations)
            log.warning(
                ‘Unknown payment processors [%s] are configured for site %s’, processor_config_repr, self.site.id
            )
        return [
            processor for processor in all_processors
            if processor.NAME in self.payment_processors_set and processor.is_enabled()   #确保is_enable的返回值
        ]
    def _all_payment_processors(self):
        “”” Returns all processor classes declared in settings. “””
        all_processors = [get_processor_class(path) for path in settings.PAYMENT_PROCESSORS]
        return all_processors
      对上述代码进行分析payment_processors_set变量来源于类的payment_processors,而payment_processors类型为CharField,该数值存放在数据库中,形式为’cybersource,paypal’(以逗号分开)。另外一个变量”all_processors”来源于ecommerce的配置文件,配置部分为settings.PAYMENT_PROCESSORS。因此要让页面显示出某个支付模板,要同时在这两个地方进行修改。分别为:
#/edx/etc/ecommerce.yml
PAYMENT_PROCESSOR_CONFIG:
    edx:
        paypal:
            cancel_url: http://127.0.0.1:8000/commerce/checkout/cancel/
            client_id: AYskdUzpJGCDSJx8xPhFJq4We0FPINEjOTToNH0klY1BdDKj5B-k9CkEfvgUrBknbFxKriYc0DYjsqOJ
            client_secret: EBDM5GgT2RVC8MNacNWFluqj3sx4PdL37qj-A86gypVKQEFDltVkuhTLGfiXHlc5gKn4pnwTcKyWp5sJ
            error_url: http://127.0.0.1:8000/commerce/checkout/error/
            mode: sandbox
            receipt_url: http://127.0.0.1:8000/commerce/checkout/receipt/
   alipay:
            app_id: 2016081900287513
            app_private_key_path: /edx/app/ecommerce/cert/app_private_key.pem
            alipay_public_key_path: /edx/app/ecommerce/cert/alipay_public_key.pem
            mode: sandbox
            app_notify_url: http://ecommerce/checkout/alipay/
            cancel_url: http://127.0.0.1:8000/commerce/checkout/cancel/
            error_url: http://127.0.0.1:8000/commerce/checkout/error/
            receipt_url: http://127.0.0.1:8000/commerce/checkout/receipt/
进入admin管理界面,并修改默认的站点配置。修改Payment processors(付款处理器)字段,加入alipay(以逗号分隔)
#ecommerce/settings/_oscar.py
PAYMENT_PROCESSORS = (
    ‘ecommerce.extensions.payment.processors.cybersource.Cybersource’,
    ‘ecommerce.extensions.payment.processors.paypal.Paypal’,
    ‘ecommerce.extensions.payment.processors.alipay.Alipay’, #加入这行,同时加入程序文件
)
..
PAYMENT_PROCESSOR_CONFIG = {
    ‘edx’: {
        ‘paypal’: {
            # ‘mode’ can be either ‘sandbox’ or ‘live’
            ‘mode’: None,
            ‘client_id’: None,
            ‘client_secret’: None,
            ‘receipt_path’: PAYMENT_PROCESSOR_RECEIPT_PATH,
            ‘cancel_checkout_path’: PAYMENT_PROCESSOR_CANCEL_PATH,
            ‘error_path’: PAYMENT_PROCESSOR_ERROR_PATH,
        },
        ‘alipay’: {
            # ‘mode’ can be either ‘sandbox’ or ‘live’
            ‘app_id’: None,
            ‘app_private_key_path’: None,
            ‘alipay_public_key_path’: None,
            ‘mode’: None,
            ‘client_id’: None,
            ‘client_secret’: None,
            ‘receipt_path’: PAYMENT_PROCESSOR_RECEIPT_PATH,
            ‘cancel_checkout_path’: PAYMENT_PROCESSOR_CANCEL_PATH,
            ‘error_path’: PAYMENT_PROCESSOR_ERROR_PATH,
        },
    },
      上述修改完成后,加入支付宝模块的代码,目录在”ecommerce/extensions/payment/processors/”中,该目录同时也有paypal和cybersource的支付平台对接的代码,为了减少代码量,直接复制一份,操作如下:
#cp paypal.py alipay.py     #拷贝完成后注意文件的权限
将文件内的类Paypal修改为AliPay,同时将NAME变量修改为’alipay’,修改 后的结果如下:
class Alipay(BasePaymentProcessor):     #后续功能的修改也是在本文件完成的
    NAME = ‘alipay’
    DEFAULT_PROFILE_NAME = ‘default’
    def __init__(self, site):
         ….
      修改后依然不显示,根据错误提示,发现processor.is_enabled()的返回值影响了显示,找到相关代码如下:
#ecommerce/extensions/payment/processors/__init__.py
class BasePaymentProcessor(object):
    def is_enabled(cls):
        return waffle.switch_is_active(settings.PAYMENT_PROCESSOR_SWITCH_PREFIX + cls.NAME)
      根据代码,是waffle的开关设置问题,这个开关在admin管理面板可以设置。配置方法如下,配置后重启ecommerce即可显示:
#支付开关的页面
      添加一个开关,仿照paypal和cybersource的样式,开关名字为payment_processor_active_alipay,同时勾选active状态。如果要禁用其他支付模块,可在此处把开关取消active状态。由于系统默认开启了paypal,而后续又要添加alipay,可以在此时通过开关禁用掉paypay。
三、支付时的核心代码(第二步)
      当用户点击支付时(Checkout With Alipay),JS代码会向“http://ecommerce/api/v2/checkout/”发送POST数据,内容如下:
{“basket_id”:19,”payment_processor”:”alipay”}
      这个请求的处理函数为:
#ecommerce/extensions/api/v2/views/checkout.py
class CheckoutView(APIView):
    “””
    Freezes a basket, and returns the information necessary to start the payment process.
    “””
    permission_classes = (IsAuthenticated,)
    def post(self, request):
        basket_id = request.data[‘basket_id’]
        payment_processor_name = request.data[‘payment_processor’]
        logger.info(
            ‘Checkout view called for basket [%s].’,
            basket_id
        )
        ……
        parameters = payment_processor.get_transaction_parameters(basket, request=request)      #该处为核心代码
        payment_page_url = parameters.pop(‘payment_page_url’)
        data = {
            ‘payment_form_data’: parameters,
            ‘payment_page_url’: payment_page_url,
            ‘payment_processor’: payment_processor.NAME,
        }
        serializer = CheckoutSerializer(data)
        return Response(serializer.data)
          上述代码的核心代码为 payment_processor.get_transaction_parameters(basket, request=request),而该代码实际为”ecommerce/extensions/payment/processors/alipay.py”中的代码。由于后续的代码修改要用到支付宝的第三方SDK,因此进入ecommerce的virtualenv环境,安装该软件包,过程如下:
cd /edx/app/ecommerce                 #进入软件目录
sudo -H -u ecommerce bash             #更改用户
source venvs/ecommerce/bin/active     #加载python虚拟环境和环境变量
source ecommerce_env
pip install python-alipay-sdk                         #安装第三方支付模块
     当前的代码为,该代码即可完成从ecommerce到支付宝平台的跳转
#ecommerce/ecommerce/extensions/payment/processors/alipay.py
from __future__ import unicode_literals
from __future__ import absolute_import  #work wich conflicts alipay sdk name and class name
….
from alipay import AliPay as AliPaySdk
import json
logger = logging.getLogger(__name__)
class Alipay(BasePaymentProcessor):
    NAME = ‘alipay’
    DEFAULT_PROFILE_NAME = ‘default’
    def __init__(self, site):
        super(Alipay, self).__init__(site)
        mode = self.configuration[‘mode’].decode(‘utf-8’)
        if mode ==’sandbox’:
        self.alipay_gateway = ‘https://openapi.alipaydev.com/gateway.do
        else:
        self.alipay_gateway = ‘https://openapi.alipay.com/gateway.do
        self.Debug = False if self.configuration[‘mode’] is ‘sandbox’ else True
        self.app_private_key_string = open(self.configuration[‘app_private_key_path’]).read()
        self.alipay_public_key_string = open(self.configuration[‘alipay_public_key_path’]).read()
        self.notify_url = urljoin(get_ecommerce_url(), reverse(‘alipay:notify’))
        self.return_url = urljoin(get_ecommerce_url(), reverse(‘alipay:return’))
        self.alipay = AliPaySdk(appid=self.configuration[‘app_id’] ,app_notify_url = self.notify_url ,app_private_key_string = self.app_private_key_string ,alipay_public_key_string = self.alipay_public_key_string ,sign_type = “RSA2” ,debug = self.Debug )
        # Number of times payment execution is retried after failure.
        #self.retry_attempts = PaypalProcessorConfiguration.get_solo().retry_attempts
    def get_transaction_parameters(self, basket, request=None, use_client_side_checkout=False, **kwargs):
        “””
        Create a new PayPal payment.
        Arguments:
            basket (Basket): The basket of products being purchased.
            request (Request, optional): A Request object which is used to construct PayPal’s `return_url`.
            use_client_side_checkout (bool, optional): This value is not used.
            **kwargs: Additional parameters; not used by this method.
        Returns:
            dict: PayPal-specific parameters required to complete a transaction. Must contain a URL
                to which users can be directed in order to approve a newly created payment.
        Raises:
            GatewayError: Indicates a general error or unexpected behavior on the part of PayPal which prevented
                a payment from being created.
        “””
    total_amount = unicode(basket.total_incl_tax)
    out_trade_no = basket.order_number #or basket.id ?
    subject = ‘Course Buy’
    alipay_buy_link = self.alipay_gateway+”?”+self.alipay.api_alipay_trade_page_pay(out_trade_no = out_trade_no, total_amount = total_amount, subject = subject, return_url = self.return_url ,notify_url = self.notify_url)
        parameters = {
            ‘payment_page_url’: alipay_buy_link,
        }
        return parameters
以支付完成后的转跳为例子(paypal),其核心代码如下:
#ecommerce/extensions/payment/views/paypal.py
def get(self, request):
        “””Handle an incoming user returned to us by PayPal after approving payment.”””
        payment_id = request.GET.get(‘paymentId’)
        payer_id = request.GET.get(‘PayerID’)
        logger.info(u”Payment [%s] approved by payer [%s]”, payment_id, payer_id)
        paypal_response = request.GET.dict()
        basket = self._get_basket(payment_id)
        if not basket:
            return redirect(self.payment_processor.error_url)
        receipt_url = get_receipt_page_url(
            order_number=basket.order_number,
            site_configuration=basket.site.siteconfiguration
        )
        …..
        #实际的支付代码
      PayPal和AliPay的交易流程有一定的不同,即AliPay有异步通知的步骤,因此当AliPay交易完成并转跳会原先的网址时,交易已经成功了。而PayPal则时在页面同步转跳后由原先网址的代码检查并确定支付的成功。
      因此对于上述代码中”receipt_url”这个地址对于支付宝的异步通知没有,这个地址只在同步转跳中发挥作用,即让支付宝在同步状态阶段来到这个页面。而生成这个页面需要的参数为”order_number”即订单编号。如下为支付宝在“异步通知”和“同步转跳”时接收到的数据:
#异步通知数据
{u’version’: u’1.0′, u’app_id’: u’2016081900287513′, u’sign’: u’bGVJbMok3RSalj+ayK0GTbqups9T0uv54tvk1x+xu7hGhzoFUqm8nvxuGpyrVMRjyv0U72K58g81b7iVKAHBpIG4Lzpaw7Rg5f4qet0E7rhnEoEj6xUKTTGGt2FkCbq97K2noprvMyxNE7RkkyMx1XieXdFhrBr0tjMAVPPlsdqZRrZ0olN+S8a419/Qrm6TCbSXtbHuNxsGRrTHH2AXtp5PQhC6uHUqlXqyRqRpGNSGphhcYfBNhl/Y65WudFJjhHO1rzGGYQ3MAKOgNGv/8QXAk4XGgDl6dFskvRXpF40K/c2JPZRsfXAu2lMnpLtONptwS5L9XJcU2FyWU9x4Vw==’, u’buyer_pay_amount’: u’0.01′, u’point_amount’: u’0.00′, u’subject’: u’\u6d4b\u8bd5\u8ba2\u5355′, u’charset’: u’utf-8′, u’gmt_create’: u’2018-08-18 00:05:36′, u’out_trade_no’: u’20161155′, u’invoice_amount’: u’0.01′, u’sign_type’: u’RSA2′, u’auth_app_id’: u’2016081900287513′, u’fund_bill_list’: u'[{“amount”:”0.01″,”fundChannel”:”ALIPAYACCOUNT”}]’, u’receipt_amount’: u’0.01′, u’trade_status’: u’TRADE_SUCCESS’, u’gmt_payment’: u’2018-08-18 00:05:46′, u’trade_no’: u’2018081821001004870200545301′, u’seller_id’: u’2088102172081509′, u’total_amount’: u’0.01′, u’notify_time’: u’2018-08-18 19:59:49′, u’notify_id’: u’eb07415964fae02dd2620c7cee2767cmpq’, u’notify_type’: u’trade_status_sync’, u’buyer_id’: u’2088102175004879′}
#同步跳转数据
{u’trade_no’: u’2018081821001004870200545301′, u’seller_id’: u’2088102172081509′, u’total_amount’: u’0.01′, u’timestamp’: u’2018-08-18 00:06:11′, u’charset’: u’utf-8′, u’app_id’: u’2016081900287513′, u’sign’: u’r7S79IgDAvO4600A8x8BZJ8tKPLG/asELyoTOmhLmHsIBZLgbaTdxt13+NbSvbi+GclrSBpJQZ9ypAN5J5UzmlvklUU4+E3oHHfG+l2A6874NtYeGUzQFgD3GVq7eDuwxixjvJWHwMTL+8/jykvfASuq+aZZZJ5FWsdpqAE5AJOU3YuCRef1Ht/rjQtmw+/dSRB9HFKj+jPJfkpzAOFWdyljfSN7NP7bAuJ4KInRZ32rBqBLSTR1jPYiUWWxELNbjWhr+pMOCDw9HDaYc9oPXvf2efDFbGU6AOh+8wyAHbmypTrEM3TrKezP6JJkP+i0mf5JfaUslzyNV125DM67nQ==’, u’out_trade_no’: u’20161155′, u’version’: u’1.0′, u’sign_type’: u’RSA2′, u’auth_app_id’: u’2016081900287513′, u’method’: u’alipay.trade.page.pay.return’}
       在这两个数据中,字段“out_trade_no”即可以用于保存”order_number”的数据,即在提交订单时,将order_number的数值赋值给out_trade_no”。
         为方便修改,将Ecommerce于支付宝对接的代码一并存放,核心代码涉及到了三个文件,分别是urls.py,processors/alipay.py,views/alipay.py三个文件,三个文件都在ecommerce/extensions/payment目录中。文件下载

发表评论

电子邮件地址不会被公开。 必填项已用*标注