odoo与支付宝对接

odoo支持多种支付方式,国内来说最常见的莫非支付宝和微信支付了,今天我们就来手动实现一个支付宝对接模块。

沙箱环境

由于支付宝对接需要各种申请权限,好在官方提供了一个沙箱环境,我们就在沙箱环境下进行我们的开发。首先,我们用支付宝去申请一个沙箱账号:

AppID是支付宝应用必须的一个应用ID,申请应用时会自动分配一个。

然后我们需要设置应用的密钥,支付宝支持的加密方式有RSA和RSA2两种,推荐的长度是2048,我们可以使用官方的密钥生成工具来帮助我们生成两种密钥。

因为我们是Python应用,所以选择PKCS1格式。

将生成完的密钥上传到支付宝后台,密钥这步就算完成了。

另外,我们可以再下载一个安卓版的测试钱包,使用官方提供的沙箱账号登录测试。

Python SDK

支付宝的验签和请求流程可谓相当麻烦,我们这里使用自己编写的SDK包来帮助我们简化我们的开发任务。(没有使用官方SDK的原因,一方面是因为过于臃肿,另一方面官方的SDK代码一种浓浓的java味道)

Payment Alipay

各种支付方式在odoo中都是一个payment.acquirer的对象,因此我们需要继承这个对象。

provider用来指明支付提供商,这里当然就是支付宝了,因为我们是继承,所以需要再父类的基础上添加一个provider,命名为alipay。

剩下的字段就是支付宝集成需要的字段,主要包括:

  • seller_id:卖家ID 用来验证收款方ID
  • alipay_appid: 前面提到的appid
  • alipay_secret: 前面提到的密钥
  • alipay_public_key: 支付宝公钥,区别于应用公钥。应用于支付宝异步消息验签。
  • alipay_sign_type: 验签方式,RSA和RSA2
1
2
3
4
5
6
7
8
9
10
class AcquirerAlipay(models.Model):
_inherit = 'payment.acquirer'

provider = fields.Selection(selection_add=[('alipay', "AliPay")])
seller_id = fields.Char("Alipay Seller Id", required=True)
alipay_appid = fields.Char("Alipay AppId", required=True)
alipay_secret = fields.Binary("Merchant Private Key")
alipay_public_key = fields.Binary("Alipay Public Key")
alipay_sign_type = fields.Selection(
selection=[('rsa', 'RSA'), ('rsa2', 'RSA2')], string="Sign Type")

跳转支付宝付款

在Shop中有一步是选择支付方式,然后跳转支付提供方的页面进行付款。这一步在payment中是通过_get_form_action_url方法来实现的,父类会根据不同支付方式的名称不同,调用不同的子类方法,例如我们这里的支付名称是alipay,因此我们的方法名称就要命名为:alipay_get_form_action_url。

1
2
3
@api.multi
def alipay_get_form_action_url(self):
return "/payment_alipay/jump"

我们这里只跳转到了一个中间URL,是因为有些参数只有controller中才能获取。

1
2
3
4
5
6
7
8
@http.route('/payment_alipay/jump', auth='public')
def index(self, **kw):
"""跳转至支付宝付款页面"""
kw["csrf_token"] = request.csrf_token()
kw["notify_url"] = self._notify_url
alipay = request.env["payment.acquirer"].sudo().search(
[('provider', '=', 'alipay')], limit=1)
return redirect_with_hash(alipay._get_alipay_url(kw))

我们在web页面中购买了某项产品之后,然后点击支付按跳转到支付宝页面进行支付,支付完成后,我们需要告诉支付宝回跳到我们的网站。这个参数是通过return_url来实现的。

我们下单支付,并会跳页面,使用的是支付宝的统一收单下单并支付页面接口接口。在我们的sdk中对应的接口是trade_page_pay。

因此我们可以看到_get_alipay_url方法调用的就是trade_page_pay:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@api.model
def _get_alipay_url(self, params=None):
"""Alipay URL"""
base_url = self.env['ir.config_parameter'].sudo(
).get_param('web.base.url')
# 额外的参数
passback_params = quote_plus("&".join(
f"{k}={v}" for k, v in params.items() if v)) if params else None
alipay = self._get_alipay()
alipay.return_url = f'{base_url}{params["return_url"]}'
alipay.notify_url = f'{base_url}{params["notify_url"]}'

return alipay.pay.trade_page_pay(params["reference"], params["amount"],
params["reference"], product_code="FAST_INSTANT_TRADE_PAY",
passback_params=passback_params)

支付结果验证

支付完成后,支付宝会通过同步和异步两种方式来告诉我们支付结果。同步就是通过回跳页面中的参数,异步是在将支付结果Post到我们调用接口时传入的notify_url参数的URL。

为了保证支付结果,我们要对这两种支付结果都进行验证。

odoo中的支付过程是payment.transaction对象,我们根据支付宝返回的结果来控制payment.transaction的状态。

同步验证

同步验证就是把支付宝回传的订单号,去支付宝服务器进行查询验证,确保支付成功,如果没有支付成功,则挂起这次支付。如果异步结果在前,那么直接返回异步的结果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
@api.multi
def _alipay_form_validate(self, data):
"""验证支付"""
if self.state == 'done':
_logger.info(f"支付已经验证:{data['out_trade_no']}")
return True
result = {
"acquirer_reference": data["trade_no"]
}
# 根据支付宝同步返回的信息,去支付宝服务器查询
payment = self.env["payment.acquirer"].sudo().search(
[('provider', '=', 'alipay')], limit=1)
alipay = payment._get_alipay()
res = alipay.pay.trade_query(out_trade_no=data["out_trade_no"])
# 校验结果
if res["code"] == "10000" and res["trade_status"] in ("TRADE_SUCCESS", "TRADE_FINISHED"):
_logger.info(f"支付单:{data['out_trade_no']} 已成功付款")
self._set_transaction_done()
if res["code"] == "10000" and res["trade_status"] == "WAIT_BUYER_PAY":
_logger.info(f"支付单:{data['out_trade_no']} 正等待付款...")
self._set_transaction_pending()
if res["code"] == "10000" and res["trade_status"] == "TRADE_CLOSED":
_logger.info(f"支付单:{data['out_trade_no']} 已关闭或已退款.")
self._set_transaction_cancel()
return self.write(result)

异步验证

异步验证,需要验证支付宝推送的消息是否跟自身的匹配,校验订单信息等,如果都一致,则通过。否则挂起本次支付。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
def _verify_pay(self, data):
"""
验证支付宝返回的信息
"""
alipay = self._get_alipay()
# 验证是否符合验签逻辑
if not alipay.comm.validate_sign(data):
_logger.warn(f"支付宝推送支付结果验签失败:{data}")
return False
# 校验收款方
if self.alipay_appid != data["app_id"]:
_logger.warn(f"支付宝推送AppID校验失败:{data['app_id']}")
return False
if self.seller_id != data["seller_id"]:
_logger.warn(f"支付宝推送卖家ID校验失败:{data['seller_id']}")
return False
# 校验支付信息
transaction = self.env["payment.transaction"].sudo().search(
[('reference', '=', data["out_trade_no"])], limit=1)
if float(transaction.amount) != float(data["total_amount"]):
_logger.warn(
f"支付宝推送金额{float(transaction.amount)}与系统订单不符:{float(data['total_amount'])}")
return False
# 将支付结果设置完成
if transaction.state != "done" and data["trade_status"] == "TRADE_SUCCESS":
transaction.acquirer_reference = data["trade_no"]
transaction._set_transaction_done()
return True

odoo 中文翻译的一个坑

当我接入完成,付款完成之后,页面在处理支付宝返回的信息时报了一个错误:

1
not all arguments converted during string formatting

经过排查,原因出在中文翻译文件中…

有一句原文是:

1
The transaction %s with %s for %s has been confirmed. The related payment is posted: %s

居然给翻译成了:

1
%s的%s交易%s已确认。等待获取发送付款状态......

很明显,最后一个占位符被这条翻译者给吃了…

完整示例

这里是一个完整的示例:

  1. 设置支付参数:

  1. 选择商品

  1. 确认订单

  1. 填写收货信息

  1. 选择支付方式

  1. 支付宝支付

  1. 支付完成

你的支持我的动力