﻿# 技术问题

## 1. 全球收单

### 1.1 商户App中Webview注意事项

部分支付渠道在交易过程中可能会将用户重定向到其他应用（如电子钱包）。PayerMax强烈推荐商户尽量避免在App以内使用 WebView打开 PayerMax收银台，请您优先考虑使用非 WebView的方式进行对接。

- 推荐方法： 使用 用户移动端系统浏览器打开 PayerMax收银台

- 推荐范围：使用【全量支付方式收银台】或【指定支付方式收银台】的商户

- 优势：
  - 系统浏览器不会限制跳转行为
  - 能确保重定向钱包支付方式顺利跳转至对应App

若商户必须或偏好使用 WebView 打开 PayerMax 收银台及 PayerMax 返回的支付中间页 URL，但其 App 的 WebView 容器对重定向行为存在限制，则可能导致跳转流程被阻断；进而会造成用户无法顺利完成支付，或被迫进入支付方式降级后的 H5 流程（需要额外填写信息并在钱包应用内完成授权）。
为支持商户 App 在使用 WebView 时能够正确处理重定向（Redirection），请确保商户App WebView 已允许对以下链接进行重定向操作。

| 地区       | 目标机构    | 重定向URL- 跳转WEB              | 重定向URL- 跳转APP                     |
| ---------- | ----------- | ------------------------------- | -------------------------------------- |
| 巴西       | MERCADOPAGO | https://www.mercadopago.com.br/ |                                        |
| 印度尼西亚 | DANA        | https://m.dana.id/              |                                        |
| 印度尼西亚 | GOPAY       | https://gopay.co.id/            | gopay.co.id/*          *.gopay.co.id/* |
| 印度尼西亚 | SHOPEEPAY   | https://app.shopeepay.co.id/    | shopeepayid://                         |
| 日本       | PAYPAY      | https://www.paypay.ne.jp/       |                                        |
| 韩国       | NAVERPAY    | https://m.pay.naver.com/        |                                        |
| 韩国       | PAYCO       | https://bill.payco.com/         |                                        |
| 韩国       | TOSS        | https://pay.toss.im/            |                                        |
| 马来西亚   | TNG         | https://m.tngdigital.com.my/    |                                        |
| 菲律宾     | GCASH       | https://payments.gcash.com/     |                                        |
| 新加坡     | SHOPEEPAY   | shopeesg://                     | https://shopeepay.sg/                  |
| 泰国       | PAOTANGPAY  | paotang://                      |                                        |
| 泰国       | SHOPEEPAY   | https://app.shopeepay.co.th/    | shopeeth://                            |
| 泰国       | TRUEMONEY   | https://tmn.app.link/           |                                        |

### 1.2 有些支付渠道需要打开三方app进行支付，如果webView不处理，拉起三方app失败。
<h3>问题：</h3>

有些支付渠道需要打开三方app进行支付，如果webView不处理，拉起三方app失败。比如：使用GoPay支付时，提示“"ERR_UNKNOWN_URL_SCHEME"错误；LinePay, AirPay没有办法正常拉起相应app。
<h3>解答：</h3>

如果打开API的容器是Android的WebView，需要复写WebViewClient，下面是示例代码。

``` java
public class TestWebViewClient extends WebViewClient {
    private static final String TAG = "TestWebViewClient";
    private Activity mContext;
    private List<String> HTTP_SCHEMES = Arrays.asList("http", "https");

    public TestWebViewClient(Activity context, WebView webView) {
        this.mContext = context;
    }

    @Override
    public boolean shouldOverrideUrlLoading(WebView view, String url) {
        Log.d(TAG, "shouldOverrideUrlLoading url1=" + url);
        if(shouldOverrideUrlLoadingInner(view, url)) {
            return true;
        }
        return super.shouldOverrideUrlLoading(view, url);
    }

    @Override
    @TargetApi(Build.VERSION_CODES.LOLLIPOP)
    public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) {
        String url = request != null && request.getUrl() != null ? request.getUrl().toString() : "";
        Log.d(TAG, "shouldOverrideUrlLoading url=" + (request != null ? request.getUrl().toString() : ""));
        if(shouldOverrideUrlLoadingInner(view, url)) {
            return true;
        }
        return super.shouldOverrideUrlLoading(view, request);
    }

    /**
     * Parse the url and open it by system function.
     *   case 1: deal "intent://xxxx" url.
     *   case 2: deal custom scheme. url
     * @param view: WebView
     * @param url
     * @return
     */
    private boolean shouldOverrideUrlLoadingInner(WebView view, String url) {
        if(!TextUtils.isEmpty(url)) {
            Uri uri = Uri.parse(url);
            if(uri != null) {
                if ("intent".equals(uri.getScheme())) {
                    try {
                        Intent intent = Intent.parseUri(uri.toString(), Intent.URI_INTENT_SCHEME);
                        if(intent != null) {
                            PackageManager pm = mContext.getPackageManager();
                            ResolveInfo info = pm.resolveActivity(intent, PackageManager.MATCH_DEFAULT_ONLY);
                            if(info != null) {
                                mContext.startActivity(Intent.parseUri(uri.toString(), Intent.URI_INTENT_SCHEME));
                                return true;
                            }
                            else {
                                String fallbackUrl = intent.getStringExtra("browser_fallback_url");
                                if (!TextUtils.isEmpty(fallbackUrl)) {
                                    if(fallbackUrl.startsWith("market://"))
                                        startAppMarketWithUrl(mContext, fallbackUrl, false);
                                    else
                                        view.loadUrl(fallbackUrl);
                                    return true;
                                }
                            }
                        }
                    } catch (Exception e) {
                    }
                }
                if (!HTTP_SCHEMES.contains(uri.getScheme())) {
                    startUrl(mContext, url, true);
                    return true;
                }
            }
        }

        return false;
    }

    public static void startUrl(Context context, String url, boolean isNewTask) {
        if(context != null && !TextUtils.isEmpty(url)) {
            try {
                Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
                if(isNewTask) {
                    intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
                }
                context.startActivity(intent);

            } catch (Exception e) {
            }
        }
    }

    public static boolean hasActivity(Context context, Intent intent, String packageName) {
        PackageManager pm = context.getPackageManager();
        List<ResolveInfo> appList = pm.queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY);

        for (ResolveInfo info : appList) {
            if (info.activityInfo.packageName.equals(packageName))
                return true;
        }
        return false;
    }

    public static void startAppMarketWithUrl(Context context, String url, boolean forceUseGoogle) {
        try {
            Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
            if (forceUseGoogle || hasActivity(context, intent, "com.android.vending"))
                intent.setPackage("com.android.vending");
            intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
            context.startActivity(intent);
        } catch (Exception e) {
            try {
                startUrl(context, url, true);
            } catch (Exception e1) {}
        }
    }
}
```

**点击下载：**[TestWebViewClient.java](https://docs-server-sg.payermax.com/Public/Uploads/2021-03-17/60520ef974a3c.java "[TestWebViewClient.java")

如果打开API的容器是IOS的WebView：

**Objective-C**：在类似`WebViewViewController.m`里增加函数实现

```Objective-c
- (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler {

    NSURL *requestUrl = navigationAction.request.URL;
    NSString *scheme = requestUrl.scheme;
    
    if (scheme && ![scheme isEqualToString:@"http"] && ![scheme isEqualToString:@"https"] ) {

        // 取消 WebView 导航
        decisionHandler(WKNavigationActionPolicyCancel);

        UIApplication *application = [UIApplication sharedApplication];
        // 处理 scheme
        if (@available(iOS 10.0, *)) {
            [application openURL:requestUrl options:@{} completionHandler:^(BOOL success) {
                if (success) {
                    // do something.
                    NSLog(@"成功打开应用");
                } else {
                    NSLog(@"无法打开应用");
                    [self openAppStoreSearch:scheme];
                }
            }];
        } else {
            // iOS 10 以下版本
            if (![application openURL:requestUrl]) {
                NSLog(@"无法打开应用");
                [self openAppStoreSearch:scheme];
            }
        }
        return;
    }
    
    // 允许标准 HTTP/HTTPS 导航
    decisionHandler(WKNavigationActionPolicyAllow);
}

- (void)openAppStoreSearch:(NSString *)keyword {
    // 对关键词进行 URL 编码（处理空格、中文等）
    NSString *encodedKeyword = [keyword stringByAddingPercentEncodingWithAllowedCharacters:[NSCharacterSet URLQueryAllowedCharacterSet]];
    NSString *urlString = [NSString stringWithFormat:@"itms-apps://apple.com/search?term=%@", encodedKeyword];
    NSURL *url = [NSURL URLWithString:urlString];
    
    if ([[UIApplication sharedApplication] canOpenURL:url]) {
        [[UIApplication sharedApplication] openURL:url options:@{} completionHandler:nil];
    }
}
``` 

**Swift**：在类似`WebViewViewController.swift`中增加代码实现

```swift

func webView(_ webView: WKWebView,
             decidePolicyFor navigationAction: WKNavigationAction,
             decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) {
    
    guard let requestUrl = navigationAction.request.url,
          let scheme = requestUrl.scheme else {
        // 如果无法获取 URL 或 scheme，允许导航继续
        decisionHandler(.allow)
        return
    }

    // if the request is a non-http(s) schema, then have the UIApplication handle opening the request
    if scheme != "http" && scheme != "https" {
        // 取消 WebView 导航
        decisionHandler(.cancel)
        
        // 尝试打开外部应用
        let application = UIApplication.shared
        
        if #available(iOS 10.0, *) {
            // iOS 10+ 使用带回调的新方法
            application.open(requestUrl, options: [:]) { success in
                if success {
                    print("成功打开应用")
                } else {
                    print("无法打开应用")
                    self.openAppStoreSearch(keyword: scheme)
                }
            }
        } else {
            // iOS 9 及以下使用旧方法
            if application.openURL(requestUrl) {
                print("成功打开应用")
            } else {
                print("无法打开应用")
                self.openAppStoreSearch(keyword: scheme)
            }
        }
        return
    }
    
    // 允许标准 HTTP/HTTPS 导航
    decisionHandler(.allow)
}

func openAppStoreSearch(keyword: String) {
    guard let encodedKeyword = keyword.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed),
          let url = URL(string: "itms-apps://apple.com/search?term=\(encodedKeyword)") else { return }
    
    if UIApplication.shared.canOpenURL(url) {
        UIApplication.shared.open(url, options: [:], completionHandler: nil)
    }
}
```

### 1.3 打开DANA支付时，页面显示空白或"The network connection is unstable. Please try again later."
<h3>问题：</h3>

API标准入款接入方式， 打开DANA支付方式， 页面提示错误， 看到的现象是空白，实际页面比较大， 移动页面可以看到页面错误：“The network connection is unstable. Please try again later."
<h3>解答：</h3>

如果打开PaySDK的容器是Android 的WebView，需要设置webView的WebSettings属性，代码如下
```
webSettings.setDomStorageEnabled(true);
webSettings.setTextZoom(100);
webSettings.setUseWideViewPort(true);
```
### 1.4 支付成功后想返回自己的APP的方法
<h3>解答：</h3>

在自己APP内部打开收银台url，在支付完成打开跳转地址（前端回调地址frontCallBackUrl）也是在App内部。
如果用外部浏览器打开收银台URL，在支付完成打开跳转地址（前端回调地址frontCallBackUrl）时是在外部浏览器中打开的，这时如果要跳转到App内部，需要通过（schema DeepLink）跳转。

### 1.5 异步回调通知注意事项

<h3>解答：</h3>

1、回调地址必须真实存在；
2、协议格式: application/json；
3、协议返回内容：请查看[【异步通知】](https://docs.payermax.com/202606-version/receipt/result-notifications/callback-notification.md)
地址不通的问题（如果商户服务器对外部 IP 地址访问有限制，需要将payermax服务器IP地址添加到商户服务器的白名单中。

### 1.6 点击"确认支付"按钮后，没有打开后续支付页面
<h3>问题：</h3>

API标准入款接入方式，点击”确认支付”按钮后，没有打开后续支付页面

<h3>解答：</h3>

如果打开API的容器是Android的WebView，需要对WebView做如下处理，下面是示例代码：
```
mWebView.setWebViewClient(new WebViewClient());
```
### 1.7 进入收银台，页面提示“请求参数格式错误”
<h3>问题：</h3>

商户使用String.format(Locale.default(), "%.2f", price)格式化金额，造成金额格式为乱码

<h3>解答：</h3>

由于Locale.getDefault()是获取当前设备的所在地区的locale，不同地区格式后的字符串不一样。建议使用
String.format(Locale.ENGLISH, "%.2f", price)

### 1.8 使用UPI支付时，出现界面没有办法退出
<h3>问题：</h3>

使用UPI支付进入到渠道页，操作然后点击返回键没有办法回到上个页面或者退出后，再次进入打不开收银台

<h3>解答：</h3>

如果商户在离开webView时，使用“webView.loadDataWithBaseURL(null, "", "text/html", "utf-8", null);"关闭当前JS线程。需要修改为“webView.loadUrl("about:blank");”

### 1.9 收银台页面支付方式展示不全或者样式混乱等适配问题
<h3>问题：</h3>

如果遇到收银台页面支付方式展示不全或者样式混乱，例如：[示例.png](https://docs-server-sg.payermax.com/Public/Uploads/2020-09-03/5f5058747e6ab.png "[示例.png")
<h3>解答：</h3>

```
商户需要设置webView的下面参数：
mWebView.getSettings().setTextZoom(100);

```

### 1.10 支付时软键盘覆盖输入框

<h3>问题：</h3>

在支付时，有些输入方式需要输入手机号或邮箱，这时候点击输入框，软键盘会覆盖输入框。

<h3>解答：</h3>

出现上面问题，需要对容器做如下检查：

1. 不要设置全屏模式
2. windowSoftInputMode可以不设置或者设置为adjustResize,  不要设置为adjustPan
3.  如果是沉浸式状态栏，需要在布局中设置fitSystemWindows=true

### 1.11 商户传给PayerMax的URI是大写的APP://开头，为什么我方在跳转的时候变成了app：//开头的？

因为浏览器和webview自动转换的，cheme在浏览器里是不分大小写的，会统一转为小写。
在manifest中配置时，scheme 和 host 都要全为小写

### 1.12 页面报这类错误'X-Frame-Options' 是什么原因
一般是渠道侧页面不支持iframe嵌套，请检查下 web服务中是否设置 X-Frame-Options。

### 1.13 如果打开收银台链接地址的容器是Android WebView,请参考下面代码设置

```java
        WebSettings webSettings = mWebView.getSettings();
        webSettings.setJavaScriptEnabled(true);

        webSettings.setSupportMultipleWindows(true);
        webSettings.setJavaScriptCanOpenWindowsAutomatically(true);

        webSettings.setPluginState(WebSettings.PluginState.ON); //enable plugin. Ex: flash. deprecated on API 18
        //whether the zoom controls display on screen.
        webSettings.setBuiltInZoomControls(true);
        webSettings.setSupportZoom(true);
        webSettings.setDisplayZoomControls(false);

        //disable the webview font size changes according the phone font size.
        webSettings.setTextZoom(100);
        webSettings.setSaveFormData(true);
        webSettings.setUseWideViewPort(true);
        webSettings.setLoadWithOverviewMode(true);
        webSettings.setAllowFileAccess(true);

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
            webSettings.setMixedContentMode(WebSettings.MIXED_CONTENT_ALWAYS_ALLOW);
            CookieManager.getInstance().setAcceptThirdPartyCookies(this, true);
        }
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
            webSettings.setAllowUniversalAccessFromFileURLs(true);
        }

        webSettings.setAppCacheEnabled(true);
        String appCacheDir = getDir("cache", Context.MODE_PRIVATE).getPath();
        webSettings.setAppCachePath(appCacheDir);
        webSettings.setAppCacheMaxSize(1024*1024*20);
        webSettings.setDomStorageEnabled(true);
        webSettings.setDatabaseEnabled(true);
        webSettings.setCacheMode(WebSettings.LOAD_DEFAULT);

        try {
            mWebView.removeJavascriptInterface("searchBoxJavaBridge_");
            mWebView.removeJavascriptInterface("accessibility");
            mWebView.removeJavascriptInterface("accessibilityTraversal");
        } catch (Exception e) {}

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
            mWebView.enableSlowWholeDocumentDraw();
        }
```

*步骤2.4：可依商户对下单参数的约定，设定为直接跳转至商户结果页。
步骤3.1：通知前提为：支付成功、或支付失败*

### 1.14 页面报"net::ERR_CACHE_MISS"
![](http://docs-server-sg.payermax.com/Public/Uploads/2023-07-13/64aff8f1996bc.png)

这是因为网络权限配置异常。可以更改 AndroidManifest.xml 中的权限配置尝试。
old: <uses-permission android:name="android.permission.internet"/>
new: <uses-permission android:name="android.permission.INTERNET"/>
参考：https://stackoverflow.com/questions/30637654/android-webview-gives-neterr-cache-miss-message

### 1.15 系统浏览器正常，用 iOS 的 wkwebview 跳转会失败，无法正常打开页面

这个可能原因是wkwebview 更改 url 编码，导致链接异常，链接中 # 被 urlEncode 为 %23。可以webview 增加代码逻辑，避免 url 被异常编码
```
OC
-(NSString *)WM_FUNC_urlEncode:(NSString *)urlStr{
NSMutableCharacterSet *set  = [[NSCharacterSet URLQueryAllowedCharacterSet] mutableCopy];
[set addCharactersInString:@"#"];
return [urlStr stringByAddingPercentEncodingWithAllowedCharacters:set];
}
```

```swift
swift 4.0
func WM_FUNC_urlEncode(_ urlStr:String) -> String {
if urlStr.isEmpty {
   return ""
}
var charSet = CharacterSet.urlQueryAllowed
charSet.insert(charactersIn: "#")
let encodingURLStr = urlStr.addingPercentEncoding(withAllowedCharacters: charSet)
return encodingURLStr ?? ""
}
```

下面模仿oc写法： CharacterSet 转换 NSMutableCharacterSet 来操作
``` 
func WM_FUNC_urlEncode(_ urlStr:String) -> String {
if urlStr.isEmpty {
   return ""
}
let charSet = CharacterSet.urlQueryAllowed as NSCharacterSet
let mutSet = charSet.mutableCopy() as! NSMutableCharacterSet
mutSet.addCharacters(in: "#")
let encodingURLStr = urlStr.addingPercentEncoding(withAllowedCharacters: mutSet as CharacterSet)
return encodingURLStr ?? ""
}

func WM_FUNC_urlEncode(_ urlStr:String) -> String {
if urlStr.isEmpty {
return ""
}
let charSet = NSMutableCharacterSet()
charSet.formUnion(with: CharacterSet.urlQueryAllowed)
charSet.addCharacters(in: "#")
let encodingURLStr = urlStr.addingPercentEncoding(withAllowedCharacters: charSet as CharacterSet)
return encodingURLStr ?? ""
}
```

参考：
https://www.jianshu.com/p/e4938ada31e6

### 1.16 安卓9.0 以上手机打不开页面，协议改成 https，9.0 以下的都好了，以上的不行。

![](http://docs-server-sg.payermax.com/Public/Uploads/2023-07-13/64aff9b88aa02.png)

这个原因可能是Android P 限制了 http 协议的明文流量的网络请求，非加密的流量请求都会被系统禁止掉。您可以将相关 api / 页面更换成 https 协议
若仍需使用 http，可通过下列方法使 Android webview 支持 http 协议：

方法一：
1.res 目录下新建 xml 目录，xml 目录下新建 network_security_config.xml文件：
```xml
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
  <base-config cleartextTrafficPermitted="true" />
</network-security-config>
```

2.AndroidManifest.xml 

```xml
<application android:networkSecurityConfig="@xml/network_security_config">
  
  <uses-library android:name="org.apache.http.legacy" android:required="false" />
</application>
```

方法二：
AndroidManifest.xml添加：
```xml
<application android:usesCleartextTraffic="true">
```

参考
https://juejin.cn/post/6844903813929762824

### 1.17 webview 加载页面报错，net::ERR_BLOCKED_BY_RESPONSE。
![](http://docs-server-sg.payermax.com/Public/Uploads/2023-07-13/64affa3f6fd74.png)
这个原因是该地址不支持在 iframe 中使用。建议您将页面地址直接打开，不在 iframe 中使用。

## 2. 全球付款

### 2.1 误认为出款接口响应码APPLY_SUCCESS为支付成功
<h3>问题：</h3>

收到出款接口响应码是APPLY_SUCCESS， 以为支付成功

<h3>解答：</h3>

响应码APPLY_SUCCESS只是说明当前协议响应是成功的；如果要查询是否支付成功，以服务端回调结果为准（商户传入或配置服务端回调地址）或以主动查询为准。

## 3. 参数配置

### 3.1 如何生成密钥？
您可登录商户平台，通过「开发参数」频道生成和重置密钥。配置时，需注意区分测试环境和正式环境。

### 3.2 如何添加回调地址？

PayerMax支持两种方式设置回调地址：
1. 拥有开发参数权限的操作员或超级管理员可在商户平台中的「开发参数」频道生成和修改回调地址。
2. 在下单时传入回调地址。
   注意：若您传入的回调地址和商户平台配置的回调地址有冲突，以您传入的回调地址为准。

### 3.3 为何没收到回调通知？
1. 请确认是否设置回调地址。
2. 请确认回调地址是否填写正确，若仍有问题，请联系PayerMax平台查明原因。
