# 自管货币

> 为 Tapjoy Offerwall 设置自管虚拟货币，包括配置奖励回调以及在自有服务器上处理交易。

使用自管货币直接在您自己的服务器上管理用户的虚拟货币。这种方法相比 Tapjoy 托管货币具有更高的控制权，但也需要您自行处理所有后端流程，包括存储和更新货币余额。

与 Tapjoy 托管货币不同，自管货币不支持 `getCurrencyBalance`、`awardCurrency` 或 `spendCurrency` 等函数。此外，对于自管货币，Tapjoy 不会向客户端或应用端发送通知。当 Tapjoy 连接您的回调服务器时，应用和用户的通知由您自行处理。

自管货币有以下独特要求：

* 奖励延迟：Tapjoy 致力于尽快向用户发放奖励，但无法保证即时到账。为确保货币余额准确更新，请在应用启动、恢复事件、关卡切换、视频广告完成以及商店交互前等情况下定期检查余额变化。请告知用户，完成任务后的奖励可能需要一段时间才会到账。
* 特定于平台的货币：每个平台（iOS、Android 等）即使采用自管货币，也需要独立设置货币。

## 回调 URL##callback-url

当用户通过完成任务获得货币时，Tapjoy 会向您指定的回调 URL 发送 HTTP GET 请求。该请求包含以下参数：

* `snuid`：用户 ID。
* `currency`：要向用户帐户添加的货币数量。
* `mac_address`：用户的 Wi-Fi MAC 地址（如果可用）。

请参阅以下回调示例：

```java
  <callback_url>?snuid=<user_id>&currency=<currency>&mac_address=<mac_address>

  // Example
  http://www.sampledoman.com/payments/offers/tapjoy?&amp;snuid=42&amp;currency=50&amp;mac_address=00-16-41-34-2C-A6
```

## 服务器响应要求##server-response-requirements

您的服务器必须在响应中包含以下 HTTP 状态代码之一：

| 状态代码            | 原因                                                        |
| --------------- | --------------------------------------------------------- |
| `200 OK`        | 用户已收到其货币。                                                 |
| `403 Forbidden` | * verifier（校验值）参数与预期值不匹配。
* snuid 参数未知。
* 任何其他阻止进一步重试的错误。 |

在以下情况下，Tapjoy 会重试请求：

* 如果 Tapjoy 未收到 `200` 或 `403` 响应，则会每五分钟重试一次请求，最多持续四天。
* 如果您的服务器响应时间超过 5 秒，Tapjoy 会将该请求视为失败，并进行重试。
* 响应必须采用 UTF-8 编码，否则即使服务器返回 `200`，Tapjoy 也会进行重试。

> **Note:**
>
> 如果您的服务器返回非 200 的响应，不要向用户发放货币奖励。Tapjoy 会持续重试，而这可能导致重复奖励。

## 欺诈检测或预防参数##fraud-detection-or-prevention-parameters

使用虚拟货币密钥可提升安全性。要配置您的密钥，请在 Tapjoy 后台中转到 **Monetize（变现）**> **Virtual Currency（虚拟货币）**> **Create/Edit（创建/编辑）**。此密钥用于对回调进行签名，不同于应用程序 SDK 密钥。

带有虚拟货币密钥的货币具有以下额外安全参数：

* ID：奖励请求的唯一标识符（与 `currency_id` 不同）
* 校验值：ID、snuid、货币和密钥的 MD5 哈希值。

### 需要关注的欺诈场景##fraud-scenarios-to-monitor

管理货币时，请注意以下欺诈情况：

* 如果出现重复使用的 ID 或校验值不正确，那么回调 URL 可能不是 Tapjoy 发出的，您可以将其视为欺诈。
* 如果您的用户 ID 仅包含整数，请验证 snuid 以防止被操控。例如，Tapjoy 会将 `001234` 和 `1234` 视为两个不同的用户 ID。但是，您的后端服务器逻辑可能不会这样处理。

## 校验值计算##verifier-calculation

校验值是以下格式的 MD5 哈希值：

```java
Digest::MD5.hexdigest("#{id}:#{snuid}:#{currency}:#{secret_key}")
```

您的服务器必须重新计算校验值，并在任何请求不匹配时返回 `403 Forbidden` 响应。请为每个应用程序使用唯一的密钥，以免出现跨应用程序的奖励问题。

每个应用程序必须具有独立的密钥。请勿为所有应用程序使用同一密钥，否则可能导致用户在多个应用程序中同时获得奖励。

## 改进回调 URL##improved-callback-url

Tapjoy 支持团队可以提供更安全的回调格式配置。此版本采用 POST 请求，包含 JSON 有效负载，并使用 SHA256 基于哈希的消息身份验证代码 (HMAC) 进行校验。请联系您的客户经理或支持团队来启用这一功能。

当用户获得货币时，Tapjoy 会向您所提供的 URL 发送 POST 请求。Tapjoy 会根据请求正文和您的共享密钥（在 Tapjoy 后台中可以找到）生成校验值，并将其包含在请求标头中。

Tapjoy 按照如下示例结构对请求进行签名：

```javascript
{
   "cp": "some_string",
   "currency": {
      "currency_sale":"1.0",
      "id": "reward_id",
      "reward":100
   },
   "id": "some_id",
   "offer": {
      "advertiser_app_name": "a_cool_app",
      "currency": {
         "max_reward_value":1360
      },
      "expires_at":1768175428,
      "icon_url": "some_URL/icon.jpg",
      "name": "eye_catching_headline",
      "task":{
         "is_iap": false,
         "name": "event_name"
      },
      "type": ""
   },
   "placement":{
      "content_type": "offerwall",
      "name": "placement_name"
   },
   "rev":100,
   "timestamp":1762993750,
   "user": {
      "id": "user_id"
   }
}
```

| 参数&#xA;类型&#xA;描述 |

| `id`&#xA;字符串&#xA;此请求的唯一奖励 ID。 |

| `rev`&#xA;double&#xA;获得的收入（以美元为单位）。 |

| `cp`&#xA;字符串&#xA;通过 SDK 的 setCustomParameter 方法传递的自定义参数。 |

| `currency.id`&#xA;字符串&#xA;此货币的唯一标识符。 |

| `currency.reward`&#xA;整数&#xA;奖励给用户的货币金额。 |

| `currency.currency_sale`&#xA;浮点&#xA;货币促销乘数（如果适用）。 |

| `currency.max_reward_value`&#xA;整数&#xA;用户完成此次所有任务后可以获得的最大货币量。 |

| `offer.name`&#xA;字符串&#xA;广告主任务的名称。 |

| `offer.type`&#xA;字符串&#xA;目前不支持此参数。 |

| `offer.icon_url`&#xA;字符串&#xA;任务的图标 URL。 |

| `offer.advertiser_app_name`&#xA;字符串&#xA;广告主的应用名称。 |

| `offer.task.name`&#xA;字符串&#xA;用户完成并得到奖励的任务的名称或描述。 |

| `offer.task.is_iap`&#xA;布尔值&#xA;如果奖励事件对应的是 IAP 事件，则值为 true。 |

| `offer.expires_at`&#xA;timestamp&#xA;用户在该时间戳（以秒为单位）之后无法再通过此任务获得奖励。 |

| `placement.name`&#xA;字符串&#xA;与此次转化关联的广告位。 |

| `placement.content_type`&#xA;字符串&#xA;使用的广告位类型。 |

| `user.id`&#xA;字符串&#xA;通过 SDK 的 setUserID 方法传递的用户 ID。 |

| `timestamp`&#xA;timestamp&#xA;此次交易的时间戳。 |

Tapjoy 使用以下格式对请求进行签名：

```java
HMAC_SHA-256(<Request-Body>,<Secret-Key>)
```

以下示例展示了包含签名的 Tapjoy 标头：

```java
X-Tapjoy-Signature => 7205ccfdfa1fe28cd05a1b56a9508d898cc938aa555a6c18848097fe4ee0975b
```

## 设置用户 ID##setting-the-user-id

对于自管货币，在任何 Tapjoy 交互之前设置唯一的 `user_id` 对于确保用户能够收到奖励至关重要。此值在回调 URL 中会变为 snuid。应在初始连接阶段通过 connect 标志设置 `user_id`，并确保在发起任何内容请求之前完成该设置。如果设置不正确，用户无法收到奖励，您也无法获得收入。

设置货币的 `user_id` 时，请参阅以下要求：

* 使用唯一性的数字 ID（最多 190 个字符）。
* 为符合 GDPR，避免使用个人身份信息（例如：用户名、真实姓名、电子邮箱）。
* 为确保安全和欺诈检测，保证每个用户的 ID 在其生命周期内保持一致。

如果未设置，Tapjoy 默认使用设备 ID（通常为广告 ID），该 ID 可能因 SDK 版本、设备或操作系统而异。

以下代码示例演示了如何使用 connect 标志，以及必要时如何直接调用 setUserID API（连接后）。直接调用该 API 时，请使用回调来确保已成功设置 ID。最好是尽可能使用 connect 标志。

1. **iOS**

   ```objc title="Objective-C"
   // Recommended approach using connect flag
   NSDictionary *connectFlags = @{TJC_OPTION_USER_ID : @"<USER_ID_HERE>"};
   [Tapjoy connect:@"SDK_KEY_GOES_HERE" options:connectFlags];

   // Setting the user id directly
   [Tapjoy setUserIDWithCompletion:@"<USER_ID_HERE>" completion:^(BOOL success, NSError *error) {

   }];
   ```

2. **Android**

   ```java title="Java" 
   // Recommended approach using connect flag 
   Hashtable<string, object>
   connectFlags = new Hashtable&lt;string, object>(); 
   connectFlags.put(TapjoyConnectFlag.USER_ID, "<USER_ID_HERE>"); // Important for self-managed currency

   Tapjoy.connect(getApplicationContext(), "SDK_KEY_GOES_HERE", connectFlags, new TJConnectListener() {...});

   // Setting the user id directly

   Tapjoy.setUserID("<USER_ID_HERE>", new TJSetUserIDListener() {
     @Override
     public void onSetUserIDSuccess() {
       
     }

     @Override
     public void onSetUserIDFailure(String error) {

     }
   });
   ```

3. **Unity**

   ```c# title="C#"
   // Recommended approach using connect flag
   Dictionary&lt;string,string> connectFlags = new Dictionary&lt;string,string>();
   connectFlags.Add("TJC_OPTION_USER_ID", "<USER_ID_HERE>");

   #if UNITY_ANDROID
     Tapjoy.Connect("your_android_sdk_key", connectFlags);
   #elif UNITY_IOS
     Tapjoy.Connect("your_ios_sdk_key", connectFlags);
   #endif

   // Callbacks for SetUserID
   TJPlacement.OnSetUserIDSuccess += HandleOnSetUserIDSuccess;
   TJPlacement.OnSetUserIDFailure += HandleOnSetUserIDFailure;

   // Setting the user id directly
   Tapjoy.SetUserID("<USER_ID_HERE>")
   ```

4. **React Native**

   ```js title="Javascript"
   // Recommended approach using connect flag
   try {
     let flags: object = { TJC_OPTION_USER_ID: '<userId>' };
     await Tapjoy.connect('<sdk_key>', flags);
   } catch (error) {
     console.log(error);
   }

   // Setting the user id directly
   try {
     await Tapjoy.setUserId('<userId>');
   } catch (error) {
     console.log(error);
   }
   ```

您可以在每个平台对应的快速入门页面上找到更多示例。

## 设置用户余额##setting-the-user-balance

每次请求广告位并加载内容之前，都需要更新 Tapjoy 中的用户货币余额。这样可以确保自管货币的追踪结果准确无误。

1. **iOS**

   ```objc title="Objective-C"
   TJPlacement *placement = [TJPlacement placementWithName:@"placementName" delegate:nil];
   [placement setBalance:100 forCurrencyId:@"1234" withCompletion:^(NSError * _Nullable error) {
       if (error != nil) {
           //Failure
           NSString *message = error.localizedDescription;
       } else {
           //Success
       }
   }];
   ```

2. **Android**

   ```java title="Java"
   TJPlacement placement = Tapjoy.getPlacement("placement", this);
   placement.setCurrencyBalance("1234", 100, new TJSetCurrencyBalanceListener() {
       @Override
       public void onSetCurrencyBalanceSuccess() {
           
       }

       @Override
       public void onSetCurrencyBalanceFailure(int code, String error) {

       }
   }); 
   ```

3. **Unity**

   ```c# title="C#"
   TJPlacement placement = TJPlacement.CreatePlacement("placementName");
   placement.SetCurrencyBalance("[CURRENCY_ID]", 100);

   // Optional callbacks

   void OnEnable() 
   {	
       TJPlacement.OnSetCurrencyBalanceSuccess += HandleSetCurrencyBalanceSuccess;	
       TJPlacement.OnSetCurrencyBalanceFailure += HandleSetCurrencyBalanceFailure;	
   }

   void OnDisable() 
   {	
       TJPlacement.OnSetCurrencyBalanceSuccess -= HandleSetCurrencyBalanceSuccess;	
       TJPlacement.OnSetCurrencyBalanceFailure -= HandleSetCurrencyBalanceFailure;
   }

   public void HandleSetCurrencyBalanceSuccess(TJPlacement placement) 
   {

   }

   public void HandleSetCurrencyBalanceFailure(TJPlacement placement, int code, string error)
   {

   }
   ```

4. **React Native**

   ```js title="Javascript"
   let placement = new TJPlacement('placementName');
   try {
     await placement?.setCurrencyBalance('1234', 100);
   } catch (e: any) {
     let code = e.code;
     let message = e.message;
   }
   ```

## 所需金额##required-amount

如果设置了用户余额，还可以设置广告位所需的金额值。

1. **iOS**

   ```objc title="Objective-C"
   TJPlacement* placement = [TJPlacement placementWithName:@"placementName" delegate:nil];
   placement setRequiredAmount:100 forCurrencyId:@"1234" withCompletion:^(NSError * _Nullable error) {
       if (error != nil) {
           //Failure
           NSString *message = error.localizedDescription;
       } else {
           //Success
       }
   } 
   ```

2. **Android**

   ```java title="Java"
   TJPlacement placement = Tapjoy.getPlacement("placement", this);
   placement.setCurrencyAmountRequired("1234", 100, new TJSetCurrencyAmountRequiredListener() {
       @Override
       public void onSetCurrencyAmountRequiredSuccess() {
           
       }

       @Override
       public void onSetCurrencyAmountRequiredFailure(int code, String error) {

       }
   }); 
   ```

3. **Unity**

   ```c# title="C#"
   TJPlacement placement = TJPlacement.CreatePlacement("placementName");
   placement.SetRequiredAmount("[CURRENCY_ID]", 200); 

   // Optional callbacks

   void OnEnable() 
   {	
       TJPlacement.OnSetCurrencyAmountRequiredSuccess += HandleSetRequiredAmountSuccess;	
       TJPlacement.OnSetCurrencyAmountRequiredFailure += HandleSetRequiredAmountFailure;}

   void OnDisable() 
   {	
       TJPlacement.OnSetCurrencyAmountRequiredSuccess -= HandleSetRequiredAmountSuccess;	
       TJPlacement.OnSetCurrencyAmountRequiredFailure -= HandleSetRequiredAmountFailure;
   }

   public void HandleSetCurrencyBalanceSuccess(TJPlacement placement)
   {

   }

   public void HandleSetCurrencyBalanceFailure(TJPlacement placement, int code, string error)
   {

   }

   public void HandleSetRequiredAmountSuccess(TJPlacement placement)
   {

   }
   public void HandleSetRequiredAmountFailure(TJPlacement placement, int code, string error)
   {

   } 
   ```

4. **React Native**

   ```javascript title="Javascript"
   let placement = new TJPlacement('placementName');
   try {
   await offerwallPlacement?.setRequiredAmount(100, '100');
   } catch (e: any) {
     let code = e.code;
     let message = e.message;
   } 
   ```

## 奖励回调 IP 白名单##reward-callback-ip-allowlisting

如果您的回调服务器会限制访问，请将以下 Tapjoy IP 地址加入白名单：

> **Note:**
>
> 此列表截至 2024 年 5 月 12 日（星期日）准确无误。

* `18.215.207.89`
* `18.235.142.165`
* `23.20.255.113`
* `23.23.134.165`
* `3.210.188.32`
* `3.215.42.140`
* `3.217.209.177`
* `3.218.95.35`
* `3.219.236.53`
* `3.231.137.161`

## 故障排除##troubleshooting

如果在回调 URL 中收到异常的 snuid，请确保每次应用启动时，在 connect 调用之后，用户访问任务之前，调用 `setUserID`。

如果设备没有关联的用户 ID，Tapjoy 将在回调 URL 中发送 snuid 形式的设备 ID。例如，用户可能启动应用后就导航到 Tapjoy 网站，此时您的应用尚未发送 userID。为了防止这种情况，请确保服务器在每次应用启动时进行 connect 调用后都调用 `setUserID`。

如果未设置用户 ID，系统将尝试使用可用的最优设备 ID。通常情况下，这个 ID 值就是设备的广告 ID。但是，根据 SDK 版本、设备型号/版本、设备操作系统 (OS) 版本和 Google Play 服务，具体的 ID 可能会有所不同。其他可能的值包括 Android ID、udid 和 mac\_address。
