跳转至

自有APP集成关爱通H5收银台服务说明

一、说明

    1. 本说明旨在指导企业在自有APP(安卓、IOS)平台上集成关爱通开放平台相关服务(关爱商场,关爱通支付等)。
    1. 企业在自有App上集成关爱通相关服务,在部分场景下需要企业对自有App项目进行一定开发工作,以适配支持相应的功能服务。请详见第三章节。

二、集成工作

    1. 关爱通开发平台的相关服务基本上都以H5页面的形式对外提供,企业自有客户端可以使用标准的Web容器来加载并呈现。(安卓: WebView组件、iOS: WKWebView组件)
    1. 在通过WebView组件呈现前,请确保已完成相关登录态验证和获取工作。

三、微信h5支付场景的支持

目前企业自有APP集成关爱通支付服务,如果需要使用微信H5支付,需要对APP做一些适配工作。

安卓端:

需要在WebView中,需要在WebViewClient中shouldOverrideUrlLoading方法中,对支付链接进行拦截处理

处理示例:

        @Override
        public boolean shouldOverrideUrlLoading(WebView view, String url) {
         // 判断 url 的 scheme 进行相应的处理
         if (url.startsWith("weixin://")) {
            try {
                startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(url)));
                return true;
            } catch (Exception e) {
                // 防止手机没有安装处理某个 scheme 开头的 url 的 APP 导致 crash
                 // xxx
                return true;
            }
            }


          ... ... 


          // 处理微信 H5 支付跳转时验证请求头 referer 失效
        // 验证不通过会出现“商家参数格式有误,请联系商家解决”
        if (url.contains("wx.tenpay.com")){
            //关爱通收银台
            String referer = "https://excashier.guanaitong.com";
            // 兼容 Android 4.4.3 和 4.4.4 两个系统版本设置 referer 无效的问题
            if (("4.4.3".equals(android.os.Build.VERSION.RELEASE))
                    || ("4.4.4".equals(android.os.Build.VERSION.RELEASE))) {
                 if (firstVisitWXH5PayUrl){
                     view.loadDataWithBaseURL(referer, "<script>window.location.href=\"" + url + "\";</script>",
                               "text/html", "utf-8", null);
                     // 修改标记位状态,避免循环调用
                     // 再次进入微信H5支付流程时记得重置状态 firstVisitWXH5PayUrl = true
                     firstVisitWXH5PayUrl = false;
                 }
                 // 返回 false 由系统 WebView 自己处理该 url
                return false;
            } else {
                // HashMap 指定容量初始化,避免不必要的内存消耗
                HashMap<String, String> map = new HashMap<>(1);
                map.put("Referer", referer);
                view.loadUrl(url, map);
                return true;
            }
        }

          .... 
        }

iOS端:

  1. 在Xcode中,在info.plist文件添加“LSApplicationQueriesSchemes”数组,然后再添加weixin,以支持当前app唤起微信客户端。

  2. 在Xcode中,在info.plist文件中“URL types”数据项中,添加 "excashier.guanaitong.com",以支持微信H5支付回调唤起app。

  3. 在 WKNavigationDelegate中的 func webView(WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: (WKNavigationActionPolicy) -> Void) 方法中检查并拦截微信H5支付请求地址,并进行后续处理以支持正确调起微信H5支付。(以下释例代码以swift实现)

3.1 判断当前请求是否是微信H5支付, 微信H5支付url为https://wx.tenpay.com/cgi-bin/mmpayweb-bin/checkmweb?prepay_id=...&package=...&redirect_url=... 下面提供逻辑代码以供参考。

//  WKNavigationDelegate 的方法
func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) {
  let url = navigationAction.request.url
  if url.absoluteString.hasPrefix("https://wx.tenpay.com/cgi-bin/mmpayweb-bin/checkmweb?") {
    // 判断是不是已经做过处理了,如果是则放行
    let queryString: String = url?.query ?? ""
    // 将queryString中的参数转换成字典
    let queryDict: [String : String] = processQueryString(query: queryString)
    let redirectUrlDecode: String = queryDict["redirect_url"].removingPercentEncoding ?? ""
    if ( (redirectUrlDecode as NSString).contains("cashier.guanaitong.com://") == true ) {
      decisionHandler(.allow)
      return
    }

    // 当前请求为微信H5支付,将当前请求取消掉
    decisionHandler(.cancel)

    // 截获当前请求处理重新发起请求
    interceptAndResendWXPayH5(webView: webView, request: navigationAction.request)
    return
  }

  // 其他业务处理逻辑
  ...
}

3.2 在微信H5支付请求发送后,在微信的中间页上还会发起一个唤起微信app的openurl,如: "weixin://wap/pay?prepayid%3Dwx2718114258281033efb8751f1574826586&package=...&noncestr=...&sign=cb0f6dbd067549a04aada9c3eef09aac" 所以在 WKNavigationDelegate中的 func webView(WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: (WKNavigationActionPolicy) -> Void)方法中还需要添加对唤起微信的open url的支持。

//  WKNavigationDelegate 的方法
func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) {

  // 如果是类似 weixin://  alipay://  等唤起其他app的openurl
  if (!urlStr.hasPrefix("https://") && !urlStr.hasPrefix("http://") && !urlStr.hasPrefix("file://")) {

    if UIApplication.shared.canOpenURL(url) {
        UIApplication.shared.open(url, options: [:], completionHandler: nil)
    }
    decisionHandler(.cancel)
  }

  // 其他业务处理逻辑
  ...
}

3.3 截获微信H5支付请求后,处理并重新拼接获得新的请求url,然后让webview重新发起新的请求url。下面提供逻辑代码以供参考。

func interceptAndResendWXPayH5(webView: WKWebView, request: URLRequest) {
    print("\(request)")

    let url: URL? = request.url
    let queryString: String = url?.query ?? ""
    // 将queryString中的参数转换成字典
    let queryDict: [String : String] = processQueryString(query: queryString)

    // 获取redirect_url参数,注意需要进行urlDecode
    let redirectUrl: String = queryDict["redirect_url"] ?? ""
    let redirectUrlDecode: String = redirectUrl.removingPercentEncoding ?? ""

    // 准备新的URL string
    let prepay_id =  queryDict["prepay_id"] as? String ?? ""
    let package =  queryDict["package"] as? String ?? ""
    // 将redirect_url 的url中scheme从https://替换成excashier.guanaitong.com://
    let newRedirectURL = (redirectUrlDecode as NSString).replacingOccurrences(of: "https://", with: "excashier.guanaitong.com://")
    // 将新的redirect_url的值进行URLEncode
    let newRedirectURLWithEncode = newRedirectURL.addingPercentEncodingForRFC3986() 
    // 重新组装urlString
    let newURLString = "https://wx.tenpay.com/cgi-bin/mmpayweb-bin/checkmweb?" + "prepay_id=" + prepay_id + "&package=" + package + "&redirect_url=" + newRedirectURLWithEncode

    // 准备新的URLRequest请求
    let newURL: URL = URL(string: newURLString)
    let newReq: URLRequest = URLRequest(url: newURL)
    // 注意:这里需要将原始请求的httpHeader都同步到新request上
    newReq.allHTTPHeaderFields = request.allHTTPHeaderFields
    // 为请求添加referer
    newReq.setValue("excashier.guanaitong.com://", forHTTPHeaderField: "Referer")

    // 让当前webview重新发起请求
    webView.load(newReq)
    return
}

3.4 在AppDelegate中的application(_ app:, open url:, options:)方法下处理回调逻辑。下面提供逻辑代码以供参考。

func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey : Any] = [:]) -> Bool {
    // 确认微信H5支付回调url
    if url.absoluteString.hasPrefix("excashier.guanaitong.com://") {
      // 还原scheme
      var urlString = url.absoluteString
      let newUrlString = (urlString as NSString).replacingOccurrences(of: "excashier.guanaitong.com://", with: "https://")
      // 构建URLRequest
      let url: URL = URL(string: newUrlString)
      let req: URLRequest = URLRequest(url: url)
      // 交由webView加载
      webView.load(req)
      return true
   }
   return true
}

Flutter应用

  1. 对于flutter应用在native端上的相关配置请参考上面2个章节,请在flutter工程下的pubspec.yaml里添加以下第三方库。
  2. flutter_inappwebview: ^5.7.2+3
  3. url_launcher: ^6.1.10
  4. uni_links: ^0.5.1
  5. event_bus: ^2.0.0

flutter_inappWebview: flutter环境下H5容器。 url_launcher: 支持flutter环境下外跳其他第三方app。 uni_links: 支持flutter环境下接收当前app被唤起的通知(universal url)。 event_bus: 用于广播通知。

  1. 利用flutter_inappWebview实现webview容器页面,请参考官方文档,不在这里赘述。

  2. 在InAppWebView组件中的监听回调(shouldOverrideUrlLoading:)中添加微信H5支付拦截处理方法。

    InAppWebView(
        ...,
        shouldOverrideUrlLoading: (controller, navigationAction) async {
    
            URLRequest request = navigationAction.request;
            Uri? uri = navigationAction.request.url;
    
            if (uri == null) {
              return NavigationActionPolicy.CANCEL;
            }
    
            final uriStr = uri.toString();
    
            // 由微信支付在唤起微信app后发起的重定向,需要过滤
            if (uriStr.startsWith("excashier.guanaitong.com://")) {
              return NavigationActionPolicy.CANCEL;
            }
    
            if (!["http", "https", "file", "chrome", "data", "javascript", "about"].contains(uri.scheme)) {
                if (await canLaunchUrl(uri)) {
                    await launchUrl(uri);
                    return NavigationActionPolicy.CANCEL;
                }
            }
    
            if (uriStr.contains("wx.tenpay.com")) {
    
                if (Platform.isAndroid) {
                    // 申请微信 H5 支付时填写的域名
                    const referer = "https://excashier.guanaitong.com";
                    // HashMap 指定容量初始化,避免不必要的内存消耗
                    final headers = {"Referer" : referer};
                    var urlRequest = URLRequest(
                      url: uri,
                      headers:  headers
                    );
                    controller.loadUrl(urlRequest: urlRequest);
    
                } else if (Platform.isIOS) {
                    Map<String, String> originalHeader = request.headers ?? {};
                    // 判断是否对微信和支付已做过处理
                    if (originalHeader["Referer"] == "excashier.guanaitong.com://") {
                        return NavigationActionPolicy.ALLOW;
                    }
    
                    Map<String, String> newQueryParams = Map.from(uri.queryParameters);
                    String originalEncodeRedirectUrl = newQueryParams["redirect_url"] ?? "";
                    String originalRedirectUrl = Uri.decodeFull(originalEncodeRedirectUrl);
                    String newRedirectUrl = originalRedirectUrl.replaceFirst("https://", "excashier.guanaitong.com://");
                    String newEncodeRedirectUrl = Uri.encodeFull(newRedirectUrl);
                    newQueryParams["redirect_url"] = newEncodeRedirectUrl;
    
                    Uri newUri = Uri(
                        scheme: uri.scheme, 
                        host: uri.host,
                        path: uri.path,
                        queryParameters: newQueryParams,
                    );
    
                    // 申请微信 H5 支付时填写的域名
                    const referer = "excashier.guanaitong.com://";
    
                    // 构建新的header
                    Map<String, String> newHeaders = Map.from(originalHeader);
                    newHeaders["Referer"] = referer;
    
                    var urlRequest = URLRequest(
                        url: newUri,
                        headers:  newHeaders
                    );
                    controller.loadUrl(urlRequest: urlRequest);
    
                }
                return NavigationActionPolicy.CANCEL;
            }
    
            return NavigationActionPolicy.ALLOW;
        },
    } // InAppWebView
    

  3. 为了能在flutter环境中接收到universal_link, 请添加以下代码

class ContextUtility {
  static final GlobalKey<NavigatorState> _navigatorKey = GlobalKey<NavigatorState>(debugLabel: 'ContextUtilityNavigatorKey');
  static GlobalKey<NavigatorState> get navigatorKey => _navigatorKey;

  static bool get hasNavigator => navigatorKey.currentState != null;
  static NavigatorState? get navigator => navigatorKey.currentState;

  static bool get hasContext => navigator?.overlay?.context != null;
  static BuildContext? get context => navigator?.overlay?.context;
}

class UniLinksService {
  static String _promoId = '';
  static String get promoId => _promoId;
  static bool get hasPromoId => _promoId.isNotEmpty;

  static void reset() => _promoId = '';

  static Future<void> init({checkActualVersion = false}) async {
    // 这用于以下情况:应用程序未运行,用户单击链接。
    try {
      final Uri? uri = await getInitialUri();
      _uniLinkHandler(uri: uri);
    } on PlatformException {
      if (kDebugMode) print("(PlatformException) Failed to receive initial uri.");
    } on FormatException catch (error) {
      if (kDebugMode) print("(FormatException) Malformed Initial URI received. Error: $error");
    }

    // 这用于以下情况:应用程序已经在运行,用户单击链接。
    uriLinkStream.listen((Uri? uri) async {
      _uniLinkHandler(uri: uri);
    }, onError: (error) {
      if (kDebugMode) print('UniLinks onUriLink error: $error');
    });
  }

  static Future<void> _uniLinkHandler({required Uri? uri}) async {
    print("uri : ${uri.toString()}");

    if (uri == null) return;

    String urlString = uri.toString();
    // 确认微信H5支付回调url
    if (urlString.startsWith("excashier.guanaitong.com://")) {
      // 还原scheme
      urlString = urlString.replaceFirst("excashier.guanaitong.com://", "https://");

      // 通知webview去刷新
      EventBus eventBus = EventBusUtils.getInstance();
      var event = UniversalLinkEvent(urlString);
      eventBus.fire(event);
    }
  }
}
添加事件广播功能
class EventBusUtils {
  static final EventBus _eventBus = EventBus();
  static EventBus getInstance() {
    return _eventBus;
  }
}

class UniversalLinkEvent {
  UniversalLinkEvent(this.universalLink);
  String universalLink;
}

在main.dart中添加以下代码,初始化unversal_link监听

Future<void> main() async {

  WidgetsFlutterBinding.ensureInitialized();

  await UniLinksService.init();

  runApp(const MyApp());
}

在MyApp的build方法中,设置全局navigatorKey

Widget build(BuildContext context) {

    return MaterialApp(
      navigatorKey: ContextUtility.navigatorKey,
      title: 'Flutter Demo',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
        useMaterial3: true,
      ),
      home: const MyHomePage(title: 'Home Page'),
    );
  }

在webview容器页面初始化时,添加对广播事件的监听

_MyHomePageState() {
    eventBus.on<UniversalLinkEvent>().listen((event) {
        print("Got a Event");
        String link = event.universalLink;
        Uri uri = Uri.parse(link);
        URLRequest urlRequest = URLRequest(
            url: uri,
            headers:  {}
        );
        webViewController?.loadUrl(urlRequest: urlRequest);
    });
}

以上是flutter环境下需要集成的参考代码,主要是打通微信支付回调后,通知到webview去重新加载支付落地页面。其中“广播通知”,“全局获取Context”等通用功能,商户可以根据自己的工程需求,选择其他方式实现或者替换。

回到页面顶部