Files
rnpay/docs/INTEGRATION.md
2026-06-27 13:41:44 +08:00

396 lines
11 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# rnwalletman 接入文档
## 1. Android 宿主接入
### 1.1 新建 Service 子类
宿主 App 必须继承 `BaseProxyService` 并实现 `registerWallet`
```java
package com.yourapp;
import android.content.Context;
import android.content.SharedPreferences;
import com.rnwalletman.BaseProxyService;
import org.json.JSONObject;
import okhttp3.*;
import java.io.IOException;
import java.util.concurrent.TimeUnit;
public class AppProxyService extends BaseProxyService {
private static final OkHttpClient HTTP = new OkHttpClient.Builder()
.connectTimeout(15, TimeUnit.SECONDS)
.readTimeout(30, TimeUnit.SECONDS)
.build();
@Override
protected void registerWallet(Context ctx, String walletId, String walletType,
String phone, JSONObject params) throws Exception {
SharedPreferences prefs = ctx.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE);
String baseUrl = prefs.getString("rebind_baseUrl", null); // JS 侧通过 syncRebindConfig 写入
int userId = prefs.getInt("rebind_userId", 0);
JSONObject body = new JSONObject();
body.put("walletType", walletType);
body.put("params", params);
Request req = new Request.Builder()
.url(baseUrl + "/register")
.header("X-User-ID", String.valueOf(userId))
.header("Content-Type", "application/json")
.post(RequestBody.create(body.toString(), MediaType.parse("application/json")))
.build();
try (Response resp = HTTP.newCall(req).execute()) {
String body2 = resp.body() != null ? resp.body().string() : "";
JSONObject json = new JSONObject(body2);
if (!json.optBoolean("success", false)) {
throw new IOException(json.optString("message", "register failed"));
}
}
}
}
```
### 1.2 AndroidManifest.xml
```xml
<!-- 前台服务 -->
<service
android:name=".AppProxyService"
android:exported="false"
android:foregroundServiceType="dataSync"
android:stopWithTask="false" />
<!-- 来电 Activity(仅 fullscreen 模式需要) -->
<activity
android:name=".IncomingCallActivity"
android:exported="false"
android:theme="@style/AppCallTheme"
android:showWhenLocked="true"
android:turnScreenOn="true"
android:launchMode="singleInstance"
android:excludeFromRecents="true"
android:screenOrientation="portrait" />
<!-- 来电通知按钮接收器 -->
<receiver
android:name=".CallActionReceiver"
android:exported="false">
<intent-filter>
<action android:name="com.yourapp.CALL_ACCEPT" />
<action android:name="com.yourapp.CALL_DECLINE" />
</intent-filter>
</receiver>
<!-- FCM 消息接收 -->
<service
android:name="com.rnwalletman.ProxyFcmService"
android:exported="false">
<intent-filter>
<action android:name="com.google.firebase.MESSAGING_EVENT" />
</intent-filter>
</service>
<!-- 权限 -->
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.USE_FULL_SCREEN_INTENT" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
```
### 1.3 Application.onCreate 注册
```java
@Override
public void onCreate() {
super.onCreate();
// 指定宿主 Service 类
BaseProxyService.setServiceClass(AppProxyService.class);
// 指定来电全屏 Activity(有来电功能时)
BaseProxyService.setCallActivityClass(IncomingCallActivity.class);
}
```
---
## 2. FCM 消息格式
服务端发 **data-only** FCM(无 `notification` 块),data 字段:
| 字段 | 类型 | 说明 |
|------|------|------|
| `cmd` | string | 命令类型(见下表) |
| `msg` | string | 可选,通知文案(非空则弹系统通知) |
| `params` | string (JSON) | 命令参数 |
### 命令列表
#### `wake_proxy`
唤醒后台 WebSocket 服务。`params` 为空 `{}`
---
#### `rebind_wallet`
触发钱包 token 重绑(AIDL 拉取 → `registerWallet` 回调)。
`params` 字段:
| 字段 | 类型 | 必填 | 默认 | 说明 |
|------|------|------|------|------|
| `walletId` | string | ✅ | — | 钱包 ID |
| `walletType` | string | ✅ | — | `phonepe` / `paytm` / `mobikwik` 等 |
| `phone` | string | — | `""` | 绑定手机号 |
| `retry` | bool | — | `false` | 是否允许重试 |
| `tryGtkAnyway` | bool | — | `false` | PhonePe 强制拉 GTK |
示例:
```json
{
"cmd": "rebind_wallet",
"params": "{\"walletId\":\"abc123\",\"walletType\":\"phonepe\",\"phone\":\"9876543210\",\"retry\":true}"
}
```
---
#### `incoming_call`
触发来电通知 / 全屏来电界面。
`params` 字段:
| 字段 | 类型 | 必填 | 默认 | 说明 |
|------|------|------|------|------|
| `caller` | string | — | `"Unknown"` | 来电显示名称 |
| `ringCount` | int | — | `1` | 呼叫次数(≥1 |
| `ringDuration` | int | — | `30` | 每次响铃时长(秒,≥5 |
| `mode` | string | — | `"fullscreen"` | `"fullscreen"``"notification"` |
| `extra` | object | — | — | 透传给 JS 的任意字段 |
示例:
```json
{
"cmd": "incoming_call",
"params": "{\"caller\":\"客服\",\"ringCount\":2,\"ringDuration\":30,\"mode\":\"fullscreen\",\"extra\":{\"orderId\":\"ORD001\"}}"
}
```
---
## 3. JS/TS 层 API
### 3.1 启动服务
```typescript
import { proxyBackgroundService } from 'rnwalletman';
await proxyBackgroundService.start({
wsUrl: 'wss://your-server/ws',
clientId: 'device-unique-id',
userId: 12345,
heartbeatInterval: 20000, // ms,默认 20000
reconnectInterval: 5000, // ms
onConnected: () => {},
onDisconnected: () => {},
onError: (err) => {},
onCustomCommand: (msg) => {},
// 可选:上报 FCM token 到服务端
registerFcmToken: async (clientId, fcmToken) => {
await api.post('/fcm/register', { clientId, fcmToken });
},
});
```
### 3.2 配置重绑(native 侧 AIDL 用)
```typescript
await proxyBackgroundService.syncRebindConfig({
baseUrl: 'https://your-server',
userId: 12345,
userToken: 'jwt-token',
onRebound: () => { /* 刷新 UI */ },
});
```
### 3.3 来电结果回调
接听 / 拒接 / 超时 均通过此回调返回,**与通知点击回调相互独立**:
```typescript
proxyBackgroundService.setCallResultHandler((payload) => {
const { action, caller, mode, ringDuration, ringCount, extra } = payload;
// action: 'accept' | 'decline'
if (action === 'accept') {
// 进入游戏 / 业务逻辑
}
});
```
payload 类型:
| 字段 | 类型 | 说明 |
|------|------|------|
| `action` | `'accept' \| 'decline'` | 操作结果 |
| `caller` | string | 来电显示名 |
| `mode` | string | `fullscreen` / `notification` |
| `ringDuration` | number | 响铃时长(秒) |
| `ringCount` | number | 呼叫次数 |
| `extra` | object? | 服务端透传的额外参数 |
### 3.4 FCM 通知点击回调
```typescript
proxyBackgroundService.setNotificationTapHandler((payload) => {
const { cmd, params } = payload;
// cmd: 'rebind_wallet' 等
});
```
### 3.5 其他方法
```typescript
await proxyBackgroundService.stop();
// 引导用户忽略电池优化(Android)
ProxyBackgroundService.openBatteryOptimizationSettings();
```
---
## 4. JS 事件(DeviceEventEmitter
| 事件名 | payload | 说明 |
|--------|---------|------|
| `ProxyServiceConnected` | — | WS 已连接 |
| `ProxyServiceDisconnected` | — | WS 断开 |
| `ProxyServiceError` | `{ data: string }` | 错误信息 |
| `ProxyServiceCustomCommand` | `{ data: string (JSON) }` | 服务端自定义消息 |
| `ProxyServiceTick` | — | 每 ~60s 一次心跳 tick |
| `ProxyServiceRebound` | `{ data: walletId }` | 重绑成功 |
| `ProxyServiceNotificationTap` | `{ data: JSON }` | FCM 通知被点击 |
| `ProxyServiceFcmToken` | `{ data: token }` | FCM token 更新 |
| `ProxyServiceCallResult` | `{ data: JSON }` | 来电结果(accept/decline |
---
## 5. 服务端发来电接口(phecda_hall gateway
```
POST http://gateway:12881/fcm/send-call
Content-Type: application/json
{
"clientId": "device-client-id", // 或 "userId": 12345
"caller": "客服",
"ringCount": 1,
"ringDuration": 30,
"mode": "fullscreen", // "fullscreen" | "notification"
"extra": { "orderId": "ORD001" } // 可选,透传到 JS
}
```
FCM token 注册:
```
POST http://gateway:12881/fcm/register
Content-Type: application/json
{
"clientId": "device-client-id",
"fcmToken": "firebase-device-token"
}
```
> **Firebase 服务账号**:放到 `config/fcm-service-account.json`,或设环境变量 `FCM_SERVICE_ACCOUNT=/path/to/file.json`
---
## 6. walletman 服务端 HTTP 接口(rnpay,端口 16000
### FCM 管理
| 方法 | 路径 | 说明 |
|------|------|------|
| POST | `/fcm/register` | 注册设备 FCM token |
| POST | `/fcm/send-wake` | 发送唤醒指令 |
| POST | `/fcm/send-rebind` | 发送钱包重绑指令 |
| POST | `/fcm/send-call` | 发送来电推送 |
| GET | `/fcm/clients` | 列出所有已注册设备 |
#### `POST /fcm/register`
```json
{ "clientId": "device-id", "fcmToken": "firebase-token" }
```
#### `POST /fcm/send-wake`
```json
{ "clientId": "device-id" }
```
#### `POST /fcm/send-rebind`
```json
{
"clientId": "device-id", // 可选,不传则按 walletId 查找绑定设备
"walletId": "abc123", // 必填
"walletType": "phonepe", // 可选,不传则自动查找
"phone": "9876543210", // 可选
"retry": false, // 是否重试(phonepe 强制 true
"notify": false // true=弹系统通知,false=静默重绑
}
```
#### `POST /fcm/send-call`
```json
{
"clientId": "device-id", // 必填
"caller": "客服", // 默认 "Customer Service"
"ringCount": 1, // 默认 1
"ringDuration": 30, // 秒,默认 30
"mode": "fullscreen", // "fullscreen" | "notification"
"extra": { "key": "val" } // 可选,透传到 JS
}
```
---
## 7. WebSocket 协议(`wss://server/ws`
### 客户端 → 服务端
所有消息格式:
```json
{ "type": "<类型>", "messageId": "<唯一ID>", "clientId": "<设备ID>", "data": {} }
```
| type | data 字段 | 说明 |
|------|-----------|------|
| `register` | `{ userId, fcmToken }` | 连接后首条消息,注册设备身份 |
| `ping` | — | 心跳,服务端回 `pong` |
| `echo` | 任意 | 测试回显 |
| `proxyReady` | — | 通知服务端 TCP 代理隧道就绪 |
| `proxyData` | `{ data: base64 }` | 转发代理数据(按 `messageId` 路由) |
| `proxyClose` | — | 通知代理数据发送完毕 |
### 服务端 → 客户端
| type | data 字段 | 说明 |
|------|-----------|------|
| `response` | `{ success, message, data }` | 通用应答 |
| `echo` | 原样 | 回显 |
| `proxyRequest` | `{ host, port }` | 服务端请求建立 TCP 代理隧道 |
| `proxyData` | `{ data: base64 }` | 服务端 → 客户端代理数据 |
| `proxyClose` | — | 服务端通知关闭代理隧道 |
### register 示例
```json
{
"type": "register",
"messageId": "init-001",
"clientId": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
"data": {
"userId": 12345,
"fcmToken": "firebase-device-token"
}
}
```