一、对于将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/
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目录中。文件下载