提供應用內購買服務?

In-App Purchase (IAP) 允許通過 Odoo 應用提供持續服務的提供者獲得補償,而不是僅僅進行一次初始購買。

在這種情況下,Odoo 主要充當客戶和 Odoo 應用程序開發人員之間的 代理人

  • 用戶從Odoo購買服務令牌。

  • 當服務被請求時,服務提供商從用戶的Odoo賬戶中提取令牌。

注意

本文檔旨在為 服務提供商 提供幫助,介紹了如何通過直接使用JSON-RPC2_或使用Odoo提供的便利工具來實現。

概覽?

../../_images/players.png

玩家們?

  • 服務提供者(可能是您,讀者),您將以按使用付費的形式向客戶提供服務價值。

  • 客戶端安裝了您的Odoo應用程序,從那里請求服務。

  • Odoo 代理記賬,客戶向其賬戶添加信用額度,您可以從中提取信用額度以提供服務。

  • 外部服務是一個可選的角色: 可以直接提供服務,也可以委托實際服務作為Odoo系統和實際服務之間的橋梁/翻譯器。

../../_images/credits.jpg

鳴謝?

注解

2018年10月 開始,積分從整數變為浮點數。仍然支持整數值。

通過IAP平臺提供的每項服務都可以由客戶使用代幣或 積分 。積分是一個浮點單位,其貨幣價值取決于服務,并由提供者決定。這可能是:

  • 對于短信服務:1個積分=1條短信;

  • 對于廣告服務:1個積分=1個廣告;或者

  • 對于郵寄服務:1積分=1郵票。

信用額度也可以與固定金額關聯,以彌補價格的變化(例如,短信和郵票的價格可能會因國家而異)。

客戶可以在 https://iap.odoo.com 上購買預付費信用包來確定信用的價值(請參見 Packs )。

注解

在接下來的解釋中,我們將忽略外部服務,它們只是您提供的服務的一個細節。

../../_images/normal.png

‘正?!?服務流程?

如果一切順利,正常的流程如下:

  1. 客戶端請求某種服務。

  2. 服務提供商向Odoo詢問客戶賬戶中是否有足夠的積分用于該服務,并創建一個等于該金額的交易。

  3. 服務提供商提供服務(可以自己提供或調用外部服務)。

  4. 服務提供商返回Odoo,以捕獲(如果可以提供服務)或取消(如果無法提供服務)在步驟2中創建的交易。

  5. 最后,服務提供商通知客戶端服務已經提供,可能會在客戶端系統中顯示或存儲其結果(取決于服務)。

../../_images/no-credit.png

余額不足?

然而,如果客戶的賬戶沒有足夠的服務信用,流程將如下所示:

  1. 客戶以前請求服務。

  2. 服務提供商向Odoo詢問客戶賬戶上是否有足夠的信用額度,但得到了否定的回復。

  3. 這將被傳回給客戶端。

  4. 被重定向到其Odoo賬戶以充值并重試的用戶。

構建您的服務?

在這個例子中,我們提供的服務是為了獲得一次積分而燒掉10秒的CPU。對于你自己的服務,你可以,例如:

  • 提供自己的在線服務(例如,將報價轉換為傳真,為日本的企業服務);

  • 提供自己的 離線 服務(例如提供會計服務);或者

  • 充當其他服務提供商的中介(例如,橋接到MMS網關)。

在Odoo上注冊服務?

第一步是在IAP端點(生產和/或測試)上注冊您的服務,然后才能實際查詢用戶賬戶。要創建服務,請轉到IAP端點上的 Portal Accounthttps://iap.odoo.com用于生產,https://iap-sandbox.odoo.com用于測試,這些端點是 獨立的不同步的 )?;蛘?,您可以轉到Odoo上的門戶網站(https://iap.odoo.com/my/home)并選擇 In-App Services 。

注解

在生產環境中,使用真實交易前需要進行手動驗證。在沙盒環境中,此步驟會自動通過以便于測試。

登錄后,進入 我的賬戶 ? 您的應用內服務 ,點擊創建并提供您的服務信息。

該服務有 七個 重要字段:

  • name - ServiceName : 這是你在從Odoo請求交易時需要在客戶端的 app 中提供的字符串。 (例如: self.env['iap.account].get(name) )。作為良好的實踐,這應該與你的應用程序的技術名稱匹配。

  • label - Label : 在購物門戶上為客戶顯示的名稱。

警告

ServiceNameLabel 都是唯一的。作為良好的實踐, ServiceName 通常應與您的Odoo客戶端應用程序的名稱匹配。

  • icon - Icon : 一個通用的圖標,將作為您的 禮包 的默認圖標。

  • key - ServiceKey : 開發者密鑰,用于在IAP中識別您(請參閱: 您的服務 ),并允許從客戶賬戶中提取積分。它只會在創建服務時顯示一次,并可以隨意重新生成。

危險

你的 ServiceKey 是一個秘密 ,泄露你的服務密鑰會讓其他應用程序開發者使用你購買的服務點數。

  • trial credits - Float : 這對應于您在首次使用時準備為您的應用用戶提供的信用額度。請注意,此類服務僅適用于具有有效企業合同的客戶。

  • 隱私政策 - PrivacyPolicy : 這是您服務的隱私政策網址。它應明確說明您收集的 信息 ,如何 使用它 ,它對于使您的服務工作的 相關性 ,并告知客戶如何 訪問、更新或刪除他們的個人信息 。

../../_images/menu.png ../../_images/service_list.png ../../_images/creating_service.png ../../_images/service_created.png

您可以創建 積分包 ,客戶可以購買這些積分包以使用您的服務。

禮包?

信用包實質上是具有五個特征的產品:

  • 名稱:套餐名稱,

  • 圖標:包的特定圖標(如果未提供,則會回退到服務圖標)

  • 描述:在商店頁面和發票上顯示的包的詳細信息。

  • 數量:客戶購買包時有權獲得的積分數量,

  • 價格:以歐元計價(目前計劃支持美元)。

注解

Odoo對所有套餐銷售收取25%的傭金。請相應調整您的銷售價格。

注解

根據策略,每個積分包的價格可能會有所不同。

../../_images/package.png

Odoo 應用程序?

第二步是開發一個 Odoo 應用,客戶可以在他們的 Odoo 實例中安裝該應用,并通過該應用 請求 您提供的服務。我們的應用只會在合作伙伴表單中添加一個按鈕,讓用戶請求在服務器上消耗一些 CPU 時間。

首先,我們將創建一個依賴于 iapodoo模塊 。IAP是一個標準的V11模塊,依賴關系確保本地帳戶被正確設置,并且我們將可以訪問一些必要的視圖和有用的幫助程序。

coalroller/__manifest__.py?
{
    'name': "Coal Roller",
    'category': 'Tools',
    'depends': ['iap'],
}

第二,整合的“本地”方面。在這里,我們只會向合作伙伴視圖添加一個操作按鈕,但是您當然可以通過您的應用程序提供顯著的本地價值,并通過遠程服務提供其他部分。

coalroller/__manifest__.py?
{
    'name': "Coal Roller",
    'category': 'Tools',
    'depends': ['iap'],
    'data': [
        'views/res_partner_views.xml',
    ],
}
coalroller/views/res_partner_views.xml?
<odoo>
    <record model="ir.ui.view" id="partner_form_coalroll">
        <field name="name">partner.form.coalroll</field>
        <field name="model">res.partner</field>
        <field name="inherit_id" ref="base.view_partner_form" />
        <field name="arch" type="xml">
            <xpath expr="//div[@name='button_box']">
                <button type="object" name="action_partner_coalroll"
                        class="oe_stat_button" icon="fa-gears">
                    <div class="o_form_field o_stat_info">
                        <span class="o_stat_text">Roll Coal</span>
                    </div>
                </button>
            </xpath>
        </field>
    </record>
</odoo>
../../_images/button.png

我們現在可以實現操作方法/回調。這將 調用我們自己的服務器 。

在服務器或應用與我們服務器之間的通信協議方面沒有任何要求,但是 iap 提供了一個 iap_jsonrpc() 助手來調用另一個Odoo實例上的JSON-RPC2_端點,并透明地重新引發相關的Odoo異常 ( InsufficientCreditError , odoo.exceptions.AccessErrorodoo.exceptions.UserError ).

在該調用中,我們需要提供:

  • 任何相關的客戶端參數(這里沒有),

  • 當前客戶端的 tokeniap.account 模型的 account_token 字段提供。您可以通過調用 env['iap.account'].get(service_name) 來獲取您的服務的賬戶,其中 service_name 是在IAP端點上注冊的服務名稱。

coalroller/models/res_partner.py?
from odoo import api, models
from odoo.addons.iap import jsonrpc, InsufficientCreditError

# whichever URL you deploy the service at, here we will run the remote
# service in a local Odoo bound to the port 8070
DEFAULT_ENDPOINT = 'http://localhost:8070'
class Partner(models.Model):
    _inherit = 'res.partner'

    def action_partner_coalroll(self):
        # fetch the user's token for our service
        user_token = self.env['iap.account'].get('coalroller')
        params = {
            # we don't have any parameter to provide
            'account_token': user_token.account_token
        }
        # ir.config_parameter allows locally overriding the endpoint
        # for testing & al
        endpoint = self.env['ir.config_parameter'].sudo().get_param('coalroller.endpoint', DEFAULT_ENDPOINT)
        jsonrpc(endpoint + '/roll', params=params)
        return True

注解

iap 自動處理來自操作的 InsufficientCreditError ,并提示用戶向其賬戶添加信用額度。

iap_jsonrpc() 會自動處理 InsufficientCreditError 的重新拋出。

危險

如果您沒有使用 iap_jsonrpc() ,您必須小心地重新引發 InsufficientCreditError 在您的處理程序中,否則用戶將不會被提示充值他們的賬戶,下一次調用將以同樣的方式失敗。

服務?

雖然這不是 必需的 ,因為 iap 提供了JSON-RPC2_調用的客戶端助手( iap_jsonrpc() )和交易的服務助手( iap_charge ),但我們也將實現Odoo模塊作為服務端:

coalroller_service/__manifest__.py?
{
    'name': "Coal Roller Service",
    'category': 'Tools',
    'depends': ['iap'],
}

由于客戶端的查詢以JSON-RPC2_的形式出現,因此我們需要相應的控制器,可以調用 iap_charge 并在其中執行服務:

coalroller_service/controllers/main.py?
from passlib import pwd, hash

from odoo import http
from odoo.addons.iap.tools.iap_tools import iap_charge

class CoalBurnerController(http.Controller):
    @http.route('/roll', type='json', auth='none', csrf='false')
    def roll(self, account_token):
        # the service key *is a secret*, it should not be committed in
        # the source
        service_key = http.request.env['ir.config_parameter'].sudo().get_param('coalroller.service_key')

        # we charge 1 credit for 10 seconds of CPU
        cost = 1
        # we set the transaction to expire after 1 hour
        ttl = 1
        # TODO: allow the user to specify how many (tens of seconds) of CPU they want to use
        with iap_charge(http.request.env, service_key, account_token, cost, ttl=ttl):

            # 10 seconds of CPU per credit
            end = time.time()  (10 * cost)
            while time.time() < end:
                # we will use CPU doing useful things: generating and
                # hashing passphrases
                p = pwd.genphrase()
                h = hash.pbkdf2_sha512.hash(p)
        # here we don't have anything useful to the client, an error
        # will be raised & transmitted in case of issue, if no error
        # is raised we did the job

輔助程序 iap_charge 將會:

  1. 使用指定數量的積分授權(創建)交易,如果賬戶沒有足夠的積分,則會引發相關錯誤

  2. 執行 with 語句的主體部分

  3. 如果 with 語句體成功執行,則根據需要更新交易價格

  4. 確認(捕獲)交易

  5. 否則,如果在 with 語句體中出現錯誤,則取消事務(并釋放對積分的保留)

危險

默認情況下, iap_charge 會連接到 生產 IAP 端點 https://iap.odoo.com。在開發和測試服務時,您可能希望將其指向 開發 IAP 端點 https://iap-sandbox.odoo.com。

為此,請在您的服務Odoo中設置 iap.endpoint 配置參數:在調試/開發者模式下, 設置 ? 技術 ? 參數 ? 系統參數 ,如果不存在,請為鍵 iap.endpoint 定義一個條目。

iap_charge 輔助工具有兩個可選參數,我們可以使用它們來使最終用戶更清晰明了。

description

是一條將與交易相關聯并顯示在用戶儀表板中的消息,它有助于提醒用戶為什么存在此項費用。

credit_template

<template_name> 是一個 QWeb模板 模板的名稱,如果用戶的賬戶余額不足以支付服務提供商請求的費用,將會渲染并顯示給用戶,其目的是告訴用戶為什么他們應該對您的 IAP 提供感興趣。

coalroller_service/__manifest__.py?
{
    'name': "Coal Roller Service",
    'category': 'Tools',
    'depends': ['iap'],
    'data': [
        'views/no-credit.xml',
    ],
}
coalroller_service/controllers/main.py?
@http.route('/roll', type='json', auth='none', csrf='false')
def roll(self, account_token):
    # the service key *is a secret*, it should not be committed in
    # the source
    service_key = http.request.env['ir.config_parameter'].sudo().get_param('coalroller.service_key')

    # we charge 1 credit for 10 seconds of CPU
    cost = 1
    # we set the transaction to expire after 1 hour
    ttl = 1
    # TODO: allow the user to specify how many (tens of seconds) of CPU they want to use
    with charge(http.request.env, service_key, account_token, cost, ttl=ttl
                description="We're just obeying orders",
                credit_template='coalroller_service.no_credit'):

        # 10 seconds of CPU per credit
        end = time.time()  (10 * cost)
        while time.time() < end:
            # we will use CPU doing useful things: generating and
            # hashing passphrases
            p = pwd.genphrase()
            h = hash.pbkdf2_sha512.hash(p)
coalroller_service/views/no-credit.xml?
<odoo>
    <template id="no_credit" name="No credit warning">
        <div>
            <div class="container-fluid">
                <div class="row">
                    <div class="col-md-7 offset-lg-1 mt32 mb32">
                    <h2>Consume electricity doing nothing useful!</h2>
                    <ul>
                        <li>Heat our state of the art data center for no reason</li>
                        <li>Use multiple watts for only 0.1€</li>
                        <li>Roll coal without going outside</li>
                    </ul>
                    </div>
                </div>
            </div>
        </div>
    </template>
</odoo>

JSON-RPC2 交易 API?

../../_images/flow.png
  • IAP 交易 API 在實現服務器網關時不需要使用 Odoo,調用是標準的 JSON-RPC2。

  • 調用使用不同的 端點 ,但在所有端點上使用相同的 方法call )。

  • 當出現異常時,將返回 JSON-RPC2 錯誤,正式的異常名稱可在 data.name 中進行編程操作。

另請參閱

有關更多信息,請參閱 iap.odoo.com文檔。

授權?

/iap/1/authorize

驗證用戶賬戶至少有 credit 可用,并 創建該金額的保留(待處理交易) 。

任何當前由待處理交易保留的金額都被視為無法用于進一步的授權調用。

返回一個 TransactionToken ,用于標識待處理的交易,可以用于確認或取消該交易 ( iap.odoo.com documentation).

參數
  • key (ServiceKey) –

  • account_token (UserToken) –

  • credit (float) –

  • description (str) – 可選項,幫助用戶識別其賬戶上的費用原因

  • dbuuid (str) – 可選項,如果用戶的數據庫符合條件(參見: 服務注冊 ),則允許用戶受益于試用積分。

  • ttl (int) – 可選項,交易有效期限,以小時為單位。如果在交易過期時未捕獲信用,則交易將被取消。默認值設置為4320小時(= 180天)。

返回

如果授權成功,則返回 TransactionToken 。

引發

如果服務令牌無效,則引發 AccessError 異常。

引發

如果賬戶沒有足夠的信用,會出現 InsufficientCreditError 錯誤

引發

credit 值不是整數或浮點數時會出現 TypeError

r = requests.post(ODOO + '/iap/1/authorize', json={
    'jsonrpc': '2.0',
    'id': None,
    'method': 'call',
    'params': {
        'account_token': user_account,
        'key': SERVICE_KEY,
        'credit': 25,
        'description': "Why this is being charged",
        'ttl': 1
    }
}).json()
if 'error' in r:
    # handle authorize error
tx = r['result']

# provide your service here

捕獲?

/iap/1/capture

確認指定的交易,將用戶賬戶中保留的積分轉移到服務提供商的賬戶中。

捕獲調用是冪等的:對已經捕獲的交易執行捕獲調用不會產生進一步的影響。

參數
引發

AccessError

  r2 = requests.post(ODOO + '/iap/1/capture', json={
      'jsonrpc': '2.0',
      'id': None,
      'method': 'call',
      'params': {
          'token': tx,
          'key': SERVICE_KEY,
          'credit_to_capture': credit or False,
      }
  }).json()
  if 'error' in r:
      # handle capture error
  # otherwise transaction is captured

取消?

/iap/1/cancel

取消指定的交易,釋放用戶信用額度的保留。

Cancel調用是冪等的:對已經取消的交易執行捕獲調用不會產生任何進一步的影響。

參數
引發

AccessError

r2 = requests.post(ODOO + '/iap/1/cancel', json={
    'jsonrpc': '2.0',
    'id': None,
    'method': 'call',
    'params': {
        'token': tx,
        'key': SERVICE_KEY,
    }
}).json()
if 'error' in r:
    # handle cancel error
# otherwise transaction is cancelled

類型?

除非有例外,否則這些都是用于清晰度的 抽象類型 ,您不需要關心它們是如何實現的。

class ServiceName?

https://iap.odoo.com(生產環境)上標識您的服務的字符串,以及與客戶數據庫中您的服務相關聯的帳戶。

class ServiceKey?

為提供者服務生成的標識符。每個鍵(和服務)都與由服務提供者生成的固定值的令牌匹配。

多種類型的令牌對應多種服務。例如,短信和彩信可以是同一種服務(其中一個彩信相當于多個短信),也可以是不同價格點上的獨立服務。

危險

您的服務密鑰 是機密 ,泄露您的服務密鑰將允許其他應用程序開發人員使用您購買的服務積分。

class UserToken?

用戶賬戶的標識符。

class TransactionToken?

交易標識符,由授權過程返回并由捕獲或取消交易消耗。

exception odoo.addons.iap.tools.iap_tools.InsufficientCreditError?

如果賬戶上當前沒有足夠的積分或者有太多未完成的交易/持有,交易授權期間會引發此異常。

exception odoo.exceptions.AccessError

提出者:

  • 如果服務令牌無效,則需要服務令牌的任何操作;或者

  • 在跨服務器調用中出現任何故障。(通常出現在 iap_jsonrpc() 函數中)

exception odoo.exceptions.UserError

由應用程序開發者( )自行決定,針對任何意外行為引發。

測試 API?

為了測試開發的應用程序,我們提供了一個沙盒平臺,可以讓您:

  1. 從客戶端的角度測試整個流程 - 實際的服務和可查詢的交易。(這需要更改端點,參見 Service 中的危險提示)。

  2. 測試 API。

后者包括僅在 IAP-Sandbox 上有效的特定令牌。

  • Token 000000 : 表示一個不存在的賬戶。在授權嘗試時返回 InsufficientCreditError 。

  • Token 000111 : 表示一個沒有足夠信用來執行任何服務的賬戶。在授權嘗試時返回 InsufficientCreditError 。

  • 令牌 111111 :代表一個帳戶具有足夠的信用來執行任何服務。授權嘗試將返回一個虛擬交易令牌,該令牌將由捕獲和取消路由處理。

注解

  • 這些令牌僅在IAP-Sanbox服務器上有效。

  • 在此流程中,服務密鑰將被完全忽略。如果您想對您的服務進行全面測試,您應該忽略這些令牌。

Odoo 助手?

如果您正在使用Odoo實現您的服務,為了方便起見, iap 模塊提供了一些幫助程序,使IAP流程變得更加簡單。

充電中?

class odoo.addons.iap.tools.iap_tools.iap_charge(env, key, account_token, credit[, dbuuid, description, credit_template, ttl])?

一個 上下文管理器 ,用于授權并自動捕獲或取消后端/代理中使用的交易。

工作方式類似于光標上下文管理器:

  • 立即使用指定的參數授權交易;

  • 執行 with 語句體;

  • 如果請求體完整執行且沒有錯誤,則捕獲交易;

  • 否則取消它。

參數
  @route('/deathstar/superlaser', type='json')
  def superlaser(self, user_account,
                 coordinates, target,
                 factor=1.0):
      """
      :param factor: superlaser power factor,
                     0.0 is none, 1.0 is full power
      """
      credits = int(MAXIMUM_POWER * factor)
      description = "We will demonstrate the power of this station on your home planet of Alderaan."
      with iap_charge(request.env, SERVICE_KEY, user_account, credits, description) as transaction:
          # TODO: allow other targets
          transaction.credit = max(credits, 2)
          # Sales ongoing one the energy price,
          # a maximum of 2 credits will be charged/captured.
          self.env['systems.planets'].search([
              ('grid', '=', 'M-10'),
              ('name', '=', 'Alderaan'),
          ]).unlink()

授權?

class odoo.addons.iap.tools.iap_tools.iap_authorize(env, key, account_token, credit[, dbuuid, description, credit_template, ttl])?

將授權所有內容。

參數
  @route('/deathstar/superlaser', type='json')
  def superlaser(self, user_account,
                 coordinates, target,
                 factor=1.0):
      """
      :param factor: superlaser power factor,
                     0.0 is none, 1.0 is full power
      """
      credits = int(MAXIMUM_POWER * factor)
      description = "We will demonstrate the power of this station on your home planet of Alderaan."
      #actual IAP stuff
      transaction_token = authorize(request.env, SERVICE_KEY, user_account, credits, description=description)
      try:
          # Beware the power of this laser
          self.put_galactical_princess_in_sorrow()
      except Exception as e:
          # Nevermind ...
          r = cancel(env,transaction_token, key)
          raise e
      else:
          # We shall rule over the galaxy!
          capture(env,transaction_token, key, min(credits, 2))

取消?

class odoo.addons.iap.tools.iap_tools.iap_cancel(env, transaction_token, key)?

將取消已授權的交易。

參數
  @route('/deathstar/superlaser', type='json')
  def superlaser(self, user_account,
                 coordinates, target,
                 factor=1.0):
      """
      :param factor: superlaser power factor,
                     0.0 is none, 1.0 is full power
      """
      credits = int(MAXIMUM_POWER * factor)
      description = "We will demonstrate the power of this station on your home planet of Alderaan."
      #actual IAP stuff
      transaction_token = authorize(request.env, SERVICE_KEY, user_account, credits, description=description)
      try:
          # Beware the power of this laser
          self.put_galactical_princess_in_sorrow()
      except Exception as e:
          # Nevermind ...
          r = cancel(env,transaction_token, key)
          raise e
      else:
          # We shall rule over the galaxy!
          capture(env,transaction_token, key, min(credits, 2))

捕獲?

class odoo.addons.iap.tools.iap_tools.iap_capture(env, transaction_token, key, credit)?

將在給定交易中捕獲金額為 credit 的款項。

參數
  @route('/deathstar/superlaser', type='json')
  def superlaser(self, user_account,
                 coordinates, target,
                 factor=1.0):
      """
      :param factor: superlaser power factor,
                     0.0 is none, 1.0 is full power
      """
      credits = int(MAXIMUM_POWER * factor)
      description = "We will demonstrate the power of this station on your home planet of Alderaan."
      #actual IAP stuff
      transaction_token = authorize(request.env, SERVICE_KEY, user_account, credits, description=description)
      try:
          # Beware the power of this laser
          self.put_galactical_princess_in_sorrow()
      except Exception as e:
          # Nevermind ...
          r = cancel(env,transaction_token, key)
          raise e
      else:
          # We shall rule over the galaxy!
          capture(env,transaction_token, key, min(credits, 2))