PAYATHON 2026

如何将 Odoo 与 Mobitel 和 Orange Money 支付网关集成?

支付阿杰

结论

Odoo 通常没有适用于 Mobitel 或 Orange Money 的通用支付接口,也不存在一套覆盖所有国家和运营商的公开 REST API。要完成集成,需要先向当地的 Mobitel、Orange Money 运营主体或其授权支付聚合商申请商户接入资料,再按照实际 API 开发 Odoo 支付模块。

有几个问题需要先弄清楚:

  • “Mobitel”可能指运营商支付、mCash,也可能指当地合作银行提供的账单支付服务。这三类服务使用的接口并不相同。
  • Orange Money 的 API、认证方式、币种和回调规则通常因国家或地区而异。
  • 部分接口不会公开展示。商户完成签约、KYC 和技术审核后,运营商才会提供文档和 Sandbox 凭据。
  • 即使找到了第三方 Odoo module,也需要确认它是否适用于你的国家、Odoo 版本和商户协议。

所以,第一步不是寻找某个固定的 REST URL,而是确认具体国家、业务模式和签约主体。

先确认支付业务模式

向运营商申请接口前,应先明确需要接入哪些能力:

  1. Merchant Payment

    客户通过手机钱包向商户付款。Odoo 创建支付订单并接收处理结果。

  2. Bill Payment / Biller Integration

    客户输入账单号,网关查询应付金额,完成缴费后通知 Odoo 销账。

  3. Payment Request / Push Payment

    Odoo 根据客户手机号发起扣款请求,由客户在手机上确认。

  4. Refund

    Odoo 对已经成功的交易发起退款。

  5. Payment Status Query

    如果异步通知丢失,Odoo 可以主动查询交易状态。

问题中提到的是账单支付。如果客户需要在钱包端输入账单号并查询欠款,通常要接入 Biller API,而不是普通的收款 API。运营商还可能要求商户单独签署账单机构接入协议。

向 Mobitel 和 Orange Money 索取的资料

联系当地企业业务或开发者支持团队时,至少应索取以下资料:

  • Merchant/Biller onboarding 流程
  • Sandbox 和 Production 环境地址
  • OAuth、API Key、证书或其他认证方式的说明
  • 创建支付请求的接口
  • 账单查询接口
  • 支付状态查询接口
  • 取消和退款接口
  • Webhook/Callback 数据格式
  • 签名生成和验签规则
  • IP 白名单及 TLS 证书要求
  • 错误码、超时和重试规则
  • 测试手机号、测试钱包和测试用例
  • 上线验收流程
  • 对账文件或交易查询接口

接入 Orange Money 时,需要先确定具体国家,例如塞内加尔、科特迪瓦或喀麦隆。不同市场可能由不同实体运营,不能假定它们使用相同的域名、请求字段或认证流程。

Odoo 侧的实现方式

取得接口资料后,可以开发一个自定义支付提供商模块,例如:

payment_mobile_money/
├── __init__.py
├── __manifest__.py
├── models/
│   ├── __init__.py
│   ├── payment_provider.py
│   └── payment_transaction.py
├── controllers/
│   ├── __init__.py
│   └── main.py
├── data/
│   └── payment_method_data.xml
└── views/
    └── payment_provider_views.xml

具体需要继承哪些模型、调用哪些方法,取决于 Odoo 版本。旧版 Odoo 通常使用 payment.acquirer,较新的版本则使用 payment.provider。开发前应查看目标版本自带的支付模块,例如 Stripe、PayPal 或其他支持异步通知的 provider,并按照同一版本的结构实现。

配置模型可以用来保存商户凭据和环境信息:

from odoo import fields, models


class PaymentProvider(models.Model):
    _inherit = "payment.provider"

    code = fields.Selection(
        selection_add=[("mobile_money", "Mobile Money")],
        ondelete={"mobile_money": "set default"},
    )

    mobile_money_api_base_url = fields.Char(
        string="API Base URL",
        required_if_provider="mobile_money",
    )
    mobile_money_client_id = fields.Char(
        string="Client ID",
        groups="base.group_system",
    )
    mobile_money_client_secret = fields.Char(
        string="Client Secret",
        groups="base.group_system",
    )
    mobile_money_callback_secret = fields.Char(
        string="Callback Secret",
        groups="base.group_system",
    )

这些字段只用于展示模块设计方式,并不是 Mobitel 或 Orange Money 的官方参数。实际字段必须以签约后取得的接口文档为准。

创建支付请求

支付请求通常应由服务端发起,不能把 client_secret、证书私钥等凭据暴露给浏览器。下面使用占位字段展示基本结构:

import requests

from odoo import _
from odoo.exceptions import ValidationError


def create_payment(provider, transaction):
    payload = {
        "merchant_reference": transaction.reference,
        "amount": str(transaction.amount),
        "currency": transaction.currency_id.name,
        "customer_msisdn": transaction.partner_phone,
        "callback_url": provider.get_base_url()
            + "/payment/mobile-money/webhook",
    }

    headers = {
        "Authorization": f"Bearer {provider._get_access_token()}",
        "Content-Type": "application/json",
        "Idempotency-Key": transaction.reference,
    }

    try:
        response = requests.post(
            f"{provider.mobile_money_api_base_url}/PAYMENT_ENDPOINT",
            json=payload,
            headers=headers,
            timeout=30,
        )
        response.raise_for_status()
    except requests.RequestException as exc:
        raise ValidationError(
            _("Unable to create the mobile money payment request.")
        ) from exc

    result = response.json()

    if not result.get("transaction_id"):
        raise ValidationError(
            _("The gateway response does not contain a transaction identifier.")
        )

    return result

/PAYMENT_ENDPOINT、请求字段和返回字段都是占位符,不能直接用于生产环境。接入时必须将它们替换为运营商文档规定的实际内容。

金额格式也要遵守网关要求。有些接口接收小数金额,有些则要求传入以最小货币单位表示的整数。例如,10.50 可能需要传为 1050,具体格式只能根据接口文档确定。

处理异步回调

移动钱包支付通常采用异步处理。创建请求成功只说明网关已经受理,并不表示客户完成了付款。Odoo 应通过 Webhook 或主动查询来确认最终状态。

import hmac
import hashlib

from odoo import http
from odoo.http import request


class MobileMoneyController(http.Controller):

    @http.route(
        "/payment/mobile-money/webhook",
        type="http",
        auth="public",
        methods=["POST"],
        csrf=False,
        save_session=False,
    )
    def mobile_money_webhook(self, **kwargs):
        raw_body = request.httprequest.get_data()
        received_signature = request.httprequest.headers.get(
            "X-Signature", ""
        )

        provider = request.env["payment.provider"].sudo().search(
            [("code", "=", "mobile_money")],
            limit=1,
        )

        expected_signature = hmac.new(
            provider.mobile_money_callback_secret.encode(),
            raw_body,
            hashlib.sha256,
        ).hexdigest()

        if not hmac.compare_digest(
            received_signature,
            expected_signature,
        ):
            return request.make_response("Invalid signature", status=401)

        data = request.httprequest.get_json()
        transaction = request.env["payment.transaction"].sudo().search(
            [("reference", "=", data.get("merchant_reference"))],
            limit=1,
        )

        if not transaction:
            return request.make_response(
                "Transaction not found",
                status=404,
            )

        gateway_status = data.get("status")

        if gateway_status == "SUCCESS":
            transaction._set_done()
        elif gateway_status == "FAILED":
            transaction._set_error(
                data.get("message") or "Payment failed"
            )

        return request.make_response("OK", status=200)

这段代码也只是结构示例。X-Signature、HMAC-SHA256 和 SUCCESS 等名称不代表任何特定网关的真实协议。如果官方要求使用 RSA 签名、mTLS、JWT 或指定的响应正文,就必须严格按照官方规范实现。

账单支付还需要处理销账

普通订单支付只需关联 payment.transaction。账单支付还需要把交易与 Odoo 发票对应起来,建议使用唯一且稳定的业务编号,例如:

  • Odoo 发票号
  • 单独生成的 bill reference
  • 由客户号和账期组成的标识

收到支付成功通知后,应按顺序完成以下处理:

  1. 验证回调签名和商户身份。
  2. 根据 gateway transaction ID 检查这笔交易是否已经处理。
  3. 核对金额、币种和账单号。
  4. 将 payment.transaction 标记为成功。
  5. 创建或确认对应的 account.payment。
  6. 将付款与客户发票核销。
  7. 保存网关流水号,供退款和对账使用。

不能只凭回调中的账单号就把发票标记为已付。金额、币种和交易状态必须一并验证。

安全与可靠性注意事项

生产环境至少需要处理以下问题:

  • 签名验证:回调 URL 对外公开,并不意味着收到的内容可信。
  • 幂等处理:同一个回调可能发送多次,系统不能重复创建付款或重复核销。
  • 服务端状态查询:对于重要交易,最好在收到回调后再次调用查询接口确认状态。
  • 超时状态:网络超时不等于付款失败,不能立即要求客户重复支付。
  • 敏感数据保护:不要把 API Secret、证书私钥和 Access Token 写入日志。
  • 手机号格式:统一使用网关要求的国家代码格式。
  • 金额校验:使用精确的小数或最小货币单位,避免浮点数误差。
  • 权限隔离:只有系统管理员可以查看或修改生产凭据。
  • 日志脱敏:日志记录 reference、transaction ID 和状态即可,不应保存完整认证信息。
  • 环境隔离:Sandbox 与 Production 的地址和密钥应分别配置。
  • 对账机制:定期使用状态查询接口或对账文件检查漏单、重复单和金额差异。

关于现成 Odoo module

可以在 Odoo Apps、GitHub 或当地支付服务商处查找现成模块,但使用前需要核对以下事项:

  • 支持的 Odoo 主版本
  • 对应的国家和运营主体
  • 使用的是官方接口还是网页模拟
  • 是否支持异步回调和主动查询
  • 是否支持退款、对账和账单查询
  • 模块是否仍在维护
  • 商户凭据是否得到安全保存
  • 是否能正确创建并核销会计付款

模块名称中包含 “Orange Money” 或 “Mobitel”,不代表它可以直接使用。移动支付接口具有很强的地区差异,即使使用同一品牌,后端协议也可能完全不同。

如果运营商不提供直接接入,可以考虑当地授权的支付聚合商。聚合商可能通过一套 API 同时接入 Orange Money、Mobitel 和其他钱包,但仍需单独确认手续费、结算周期、退款能力和合规责任。

备注:内容仅供参考。