自有APP集成关爱通H5收银台服务说明¶
一、说明¶
-
- 本说明旨在指导企业在自有APP(安卓、IOS)平台上集成关爱通开放平台相关服务(关爱商场,关爱通支付等)。
-
- 企业在自有App上集成关爱通相关服务,在部分场景下需要企业对自有App项目进行一定开发工作,以适配支持相应的功能服务。请详见第三章节。
二、集成工作¶
-
- 关爱通开发平台的相关服务基本上都以H5页面的形式对外提供,企业自有客户端可以使用标准的Web容器来加载并呈现。(安卓: WebView组件、iOS: WKWebView组件)
-
- 在通过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端:¶
-
在Xcode中,在info.plist文件添加“LSApplicationQueriesSchemes”数组,然后再添加weixin,以支持当前app唤起微信客户端。
-
在Xcode中,在info.plist文件中“URL types”数据项中,添加 "excashier.guanaitong.com",以支持微信H5支付回调唤起app。
-
在 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应用¶
- 对于flutter应用在native端上的相关配置请参考上面2个章节,请在flutter工程下的pubspec.yaml里添加以下第三方库。
- flutter_inappwebview: ^5.7.2+3
- url_launcher: ^6.1.10
- uni_links: ^0.5.1
- event_bus: ^2.0.0
flutter_inappWebview: flutter环境下H5容器。 url_launcher: 支持flutter环境下外跳其他第三方app。 uni_links: 支持flutter环境下接收当前app被唤起的通知(universal url)。 event_bus: 用于广播通知。
-
利用flutter_inappWebview实现webview容器页面,请参考官方文档,不在这里赘述。
-
在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 -
为了能在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”等通用功能,商户可以根据自己的工程需求,选择其他方式实现或者替换。