`
han2000lei
  • 浏览: 276532 次
  • 性别: Icon_minigender_1
  • 来自: 济南
社区版块
存档分类
最新评论

淘宝API开发ISV订购页面必看

阅读更多
在开发阿里软件的ISV应用时,你可能需要开发一个订购页面来让用户订购你的ISV应用。
     对于开发思路,我在这里通过用户订购的流程来说明一下,估计可能会好理解一些。
1、用户点出购买,购买请求会提交到阿里软件平台。
2、阿里软件平台根据用户购买的情况配置参数,传到你的ISV。
3、ISV根据参数判断是新订、续订还是资源购买来跳转到相应的页面。(这里就需要你开发一个action,来接收参数并进行判断。还需要开发相当的页面)
4、用户选择了订购的内容后,由ISV组织参数(主要是内容的价格等),将这些参数传递给阿里平台。(这里又需要一个action)
5、用户在阿里平台进行付款,阿里平台转到支付宝平台(这一步无需我们干预)
6、付款成功后,支付宝平台将交易信息传递给阿里软件平台。(也无需我们干预)
7、阿里软件将本次交易的明细和结果通知给ISV,isv进行处理(这需要我们定义的一action来接收参数)

在上架之前要设置价格策略:如下页面

其中有两个地址很关键:
1、软件价格描述地址:是一个订购url,此url的作用是进行巡辑判断,以进入不同的订购页面,如新订页面、续订页面、资源购买等。相当于我们上面所说的第3条,这个url具体到一个action。
2、通知url地址:此url的作用是接收平台发送过来的订购业务通知。相当于我们上面所说的第7条的action



明白了上面所说的,下面是java版本的开发订购的一个例子:
1、订购页面开发所需要的接口:IOrderConstanct

public interface IOrderConstanct {
	public static final String PARAMETER_SIGNATURE="signature";//签名 
	public static final String PARAMETER_SUBSCTYPE="subscType" ;//订购类型 
	public static final String PARAMETER_APPID="appId" ;//所订购的软件id 
	public static final String PARAMETER_APPEND="appEnd" ;//软件服务的截止时间 
	public static final String PARAMETER_GMTSTART="gmtStart";//订单开始时间 
	public static final String PARAMETER_SUBSCEND="subscEnd" ;//订购控制记录的结束时间 
	public static final String PARAMETER_CTRLPARAMS="ctrlParams" ;//控制参数 
	public static final String PARAMETER_RETURNURL="returnUrl" ;//订购页面参数回传地址 
	public static final String PARAMETER_POSTDATA="postData" ;//订购页面要原样回传的参数 
	public static final String PARAMETER_CODE="4df0c2b038f511ddbba29f5366f82354";//注册时获得的安全码 
	public static final String PARAMETER_SIGN="sign"; 
	public static final String PARAMETER_APPINSTANCEID="appInstanceId";//应用实例ID 
	public static final String PARAMETER_EVENT="event";//**类型,新订、续订、资源订购及退订 
	public static final String PARAMETER_USERID="userId";//用户ID 
	public static final String PARAMETER_SUBSCID="subscId";//订单ID 
	public static final String PARAMETER_GMTEND="gmtEnd";//订单结束时间 
	public static final String PARAMETER_TOTALAMOUNT="totalAmount";//订单总金额 
	public static final String PARAMETER_AMOUNT="amount";//实付金额 
	public static final String PARAMETER_RENTAMOUNT="rentAmount";//月租额 
	public static final String PARAMETER_RESOURCEAMOUNT="resourceAmount";//购买资源金额 
	public static final String PARAMETER_COUPONAMOUTN="couponAmount";//红包 
 
}

2、下面开发软件价格描述地址所指的action,接收平台传过来的参数。假如我的地址是这样填写的http://127.0.0.1:8080/demoproj/order.do,这个action就是OrderAction。
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;

import org.apache.struts.action.Action;
import org.apache.struts.action.ActionForm;
import org.apache.struts.action.ActionForward;
import org.apache.struts.action.ActionMapping;

import com.alisoft.sip.sdk.isv.SignatureUtil;
import //导入上面定义的接口IOrderConstanct所在的包


public class OrderAction extends Action {

	/**
	 * 当用户点击您的应用,要开通订购时,平台会向这个OrderAction post许多信息,在这个action中要做的就是接收这些参数,并根据参数进行判断是新订、续订、还是资源订购,并输出到具体页面 
	 * Method execute
	 * @param mapping
	 * @param form
	 * @param request
	 * @param response
	 * @return ActionForward
	 */
	@SuppressWarnings("unchecked")
	public ActionForward execute(ActionMapping mapping, ActionForm form,
			HttpServletRequest request, HttpServletResponse response) {
		//接收所有从平台传递过来的参数
		@SuppressWarnings("unused")
		Enumeration en = request.getParameterNames();
		Map<String,Object> map = new HashMap<String, Object>();
		while(en.hasMoreElements()){//将传入的参数全部放在map中
			String key =(String) en.nextElement();
			map.put(key, request.getParameter(key));
		}
		String sig = (String) map.get(IOrderConstanct.PARAMETER_SIGNATURE);//因为加密时会去掉这个参数,所以先进行保存
		String sign = SignatureUtil.Signature(map, (String) map.get(IOrderConstanct.PARAMETER_CODE));//进行签名
		
		HttpSession session = request.getSession();
		session.setAttribute("signature", sig); //将加密文本放入session
		session.setAttribute("subscType", map.get(IOrderConstanct.PARAMETER_SUBSCTYPE)); //订购资源类型
		session.setAttribute("appId", map.get(IOrderConstanct.PARAMETER_APPID)); 
		session.setAttribute("appInstanceId",map.get(IOrderConstanct.PARAMETER_APPINSTANCEID) ); 
		session.setAttribute("appEnd", map.get(IOrderConstanct.PARAMETER_APPEND)); 
		session.setAttribute("subscEnd", map.get(IOrderConstanct.PARAMETER_SUBSCEND)); 
		session.setAttribute("ctrlParams", map.get(IOrderConstanct.PARAMETER_CTRLPARAMS)); 
		session.setAttribute("postdata", map.get(IOrderConstanct.PARAMETER_POSTDATA));//订购页面要原样回传的参数 
		session.setAttribute("returnUrl", map.get(IOrderConstanct.PARAMETER_RETURNURL)); //订购页面参数回传地址
		session.setAttribute("gmtStart", map.get(IOrderConstanct.PARAMETER_GMTSTART));//订单开始时间 
		
		/* 
		* 先验证应用ID及签名是否一致,然后根据订购类型跳转到不同的页面(0-新订、1、2-续订、3-资源订购) 
		*/ 
		if(map.get(IOrderConstanct.PARAMETER_APPID).equals("1840")&&sig.equals(sign)){ //验证应用ID,这里要换成你的应用ID
			if("0".equals(map.get(IOrderConstanct.PARAMETER_SUBSCTYPE))){//转到新订页面
				return mapping.findForward("subsc"); 
			}else if("1".equals(map.get(IOrderConstanct.PARAMETER_SUBSCTYPE))){//转到未到期续订页面
				return mapping.findForward("neworder"); 
			}else if("2".equals(map.get(IOrderConstanct.PARAMETER_SUBSCTYPE))){ //转到到期续订页面
				return mapping.findForward("timeoutorder"); 
			}else if("3".equals(map.get(IOrderConstanct.PARAMETER_SUBSCTYPE))){//转到资源订购页面
				return mapping.findForward("buyorder"); 
			}
				return mapping.findForward("error"); //如果出错了,转到错误处理页面
		}else{
			return mapping.findForward("error"); 
		}
	}
}

3、我们开发一个订购页面。在这里,我们只开发一个页面作为说明: (在你开发时可能中气情况要有几个页面)
<%@ page contentType="text/html; charset=gb2312" language="java" import="java.sql.*" errorPage="" %> 
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd"> 
<html> 
<head> 
<meta http-equiv="Content-Type" content="text/html; charset=gb2312"> 
<title>无标题文档</title> 
</head> 
<body> 
	<form action="./orderback.do" method="OST"> 
		<%--request.getSession().setAttribute("a",request.getSession().getAttribute("a"));--%> 
		<%--out.println(application.getAttribute("b")) ;--%> 
		<table width="400" border="1"align="center"> 
			<tr> 
				<td>资源套餐一</td> 
				<td><input name="buy" type="radio" value="10" checked>10元</input> </td> 
			</tr> 
			<tr> 
				<td>资源套餐二</td> 
				<td><input name="buy" type="radio" value="20">20元 </td> 
			</tr> 
			<tr> 
				<td>资源套餐三</td> 
				<td><input name="buy" type="radio" value="30">30元 </td> 
			</tr> 
			<tr> 
				<td>月租</td> 
				<td><select name="rent" size="1"> 
					<option value="50">50元/月</option> 
					<option value="100">100元/月</option> 
					</select> </td> 
			</tr> 
			<tr align="center"> 
				<td colspan="2"><input type="submit" name="Submit" value="续订"> </td>
 			</tr> 

		</table> 
	</form> 
</body> 
</html>
注:此页面是内嵌在阿里平台的页面中的。图中虚线以下就是我们自己做的订购页面,虚线以上是平台提供的页面。如图:

4、因为平台是内嵌了ISV提供的订购页面,所以,当用户选择好订购参数后,应用要把用户的订购参数回传给平台。我们在上面的代码中可以看到,form中的actoin写的是orderback.do,它是映射到orderbackAction的。当用户提交参数后,orderbackAction将获得订购参数。orderbackAction根据订购类型把信息回传给平台,并跳转回平台。OrderbackAction代码如下:
import java.io.UnsupportedEncodingException; 
import java.net.URLEncoder; 
import java.text.SimpleDateFormat; 
import java.util.Calendar; 
import java.util.HashMap; 
import java.util.Map; 

import javax.servlet.http.HttpServletRequest; 
import javax.servlet.http.HttpServletResponse;
 
import org.apache.struts.action.Action; 
import org.apache.struts.action.ActionForm; 
import org.apache.struts.action.ActionForward; 
import org.apache.struts.action.ActionMapping; 

import com.alisoft.sip.sdk.isv.SignatureUtil;
import com.constanct.*; 

public class OrderbackAction extends Action { 
	
	public static java.text.SimpleDateFormat TIME_FORMATER = new SimpleDateFormat("yyyy-MM-dd");//时间格式 
	private String postData;//平台要求的原样回传的参数 
	private String returnUrl;//回传url 
	private String subscType;//订购类型 
	private double amount;//金额 
	private double rentAmount; 
	private double resourceAmount; 
	private String ctrlParams;//控制参数 
	private String signature;//签名 
	public ActionForward execute(ActionMapping mapping, ActionForm form, 
								HttpServletRequest request, HttpServletResponse response) { 
		/* 
		* 从servletcontext中读取需要的参数 
		*/ 
		subscType=(String)request.getSession().getAttribute("subscType"); //订购类型 
		postData=(String)request.getSession().getAttribute("postdata");// 订购页面要原样回传的参数
		returnUrl=(String)request.getSession().getAttribute("returnUrl"); //订购页面参数回传地址
		String gmtStart=(String)request.getSession().getAttribute("gmtStart"); //订单开始时间 
		String gmtEnd=addMon(gmtStart,1);//计算订单结束时间,即订单开始时间加上订购时间,此处写死为一个月,但可在订购页面中让用户自行选择订购时间 
		/* 
		* 订购类型不同时,传给平台的参数也是不同的。所以,根据订购类型,分别进行参数的组织 
		*/ 
		Map<String, Object> map=new HashMap<String, Object>(); 
		if("0".equals(subscType)){//新订 
			map.put("postData", postData); //原样传回的参数
			map.put("gmtStart",gmtStart); //开始时间
			map.put("gmtEnd", gmtEnd); //结束时间
			rentAmount=Integer.parseInt(request.getParameter("rent")); //页面传递过来的参数,按时间判断订购金额
			resourceAmount=Integer.parseInt(request.getParameter("buy")); //页面传递过来的参数,按套餐判断订购金额
			map.put("rentAmount", rentAmount); 
			map.put("resourceAmount", resourceAmount); 
			amount=rentAmount+resourceAmount; //将金额相加
			map.put("amount", amount); 
			ctrlParams="amount=10&rent=50"; //这地方什么意思,说是控制参数,到现在没看懂,数字是写死的吗?估计不是写死的。可amount一定会比rent要大,结果这里却是小于。如果有看懂的请留言
			map.put("ctrlParams", ctrlParams); 
		}else if("1".equals(subscType)){//未到期续订,不能修改订购开始时间,及控制参数 
			map.put("postData", postData); 
			map.put("gmtEnd", gmtEnd); 
			rentAmount=Integer.parseInt(request.getParameter("rent")); 
			resourceAmount=Integer.parseInt(request.getParameter("buy")); 
			map.put("rentAmount", rentAmount); 
			map.put("resourceAmount", resourceAmount); 
			amount=rentAmount+resourceAmount; 
			map.put("amount", amount); 
		}else if("2".equals(subscType)){//到期续订 
			map.put("postData", postData); 
			map.put("gmtStart",gmtStart); 
			map.put("gmtEnd", gmtEnd); 
			rentAmount=Integer.parseInt(request.getParameter("rent")); 
			resourceAmount=Integer.parseInt(request.getParameter("buy")); 
			map.put("rentAmount", rentAmount); 
			map.put("resourceAmount", resourceAmount); 
			amount=rentAmount+resourceAmount; 
			map.put("amount", amount); 
			ctrlParams="amount=10&rent=50"; //??
			map.put("ctrlParams", ctrlParams); 
		}else {//订购资源,其中月租部分为零 
			map.put("postData", postData); 
			resourceAmount=Integer.parseInt(request.getParameter("buy")); 
			map.put("resourceAmount", resourceAmount); 
			map.put("rentAmount", 0); 
			map.put("amount", resourceAmount); 
			ctrlParams="amount=10&rent=50"; //??
			map.put("ctrlParams", ctrlParams); 
			map.put("description", "中文"); 
		} 
		signature=SignatureUtil.Signature(map, DemoConstant.PARAMETER_CODE);//签名 
		map.put("signature", signature); 
		/* 
		* 组织参数 
		*/ 
		StringBuffer buffer = new StringBuffer(); 
		boolean notFirst = false; 
		for (Map.Entry<String, ?> entry : map.entrySet()) { 
		if (notFirst) { 
		buffer.append("&"); 
		} else { 
		notFirst = true; 
		} 
		Object value = entry.getValue(); 
		buffer.append(entry.getKey()).append("=").append( 
		encodeURL(value) ); 
		} 
		String queryString=buffer.toString(); 
		
		/* 
		* 跳转回平台,并带上相关的订购参数 
		*/ 
		try{ 
		response.sendRedirect(returnUrl+"?"+queryString); 
		}catch(Exception e){ 
		e.printStackTrace(); 
		
		} 
		return null; 
	} 
	
	/* 
	* 编码 
	*/ 
	private String encodeURL(Object target) { 
		String result = (target != null) ? target.toString() : ""; 
		try { 
			result = URLEncoder.encode(result, "GBK"); 
		} catch (UnsupportedEncodingException e) { 
			e.printStackTrace(); 
		} 
		return result; 
	} 
	
	/* 
	* 日期计算 
	*/ 
	public static String addMon(String s, int n) { 
		Calendar cd=null; 
		try {
			cd = Calendar.getInstance(); 
			cd.setTime(TIME_FORMATER.parse(s)); 
			cd.add(Calendar.MONTH, n);//增加一月 
			cd.add(Calendar.DATE, -1); 	
		} catch (Exception e) { 
			e.printStackTrace(); 
		} 
	
		return TIME_FORMATER.format(cd.getTime()); 
	} 

}
跳转回平台后,出现如下页面:
5、点击付款后,会跳到支付宝支付中心,用户进行付款。付款成功后,平台会发送相关信息给通知url。这又是一action,用于接收平台传递过来的交易情况的信息:
package com.order.struts.action; 

import java.util.Enumeration; 
import java.util.HashMap; 
import java.util.Map; 

import javax.servlet.ServletContext; 
import javax.servlet.http.HttpServletRequest; 
import javax.servlet.http.HttpServletResponse; 

import org.apache.struts.action.Action; 
import org.apache.struts.action.ActionForm; 
import org.apache.struts.action.ActionForward; 
import org.apache.struts.action.ActionMapping; 

import com.alisoft.sip.sdk.isv.SignatureUtil;
import //导入上面IOrderConstanct所在的包
public class InformAction extends Action { 

	private String sign; 
	@SuppressWarnings("unchecked")
	public ActionForward execute(ActionMapping mapping, ActionForm form, 
								HttpServletRequest request, HttpServletResponse response){
		Enumeration e=(Enumeration)request.getParameterNames(); //获得平台传过来的订购付款信息 
		Map<String, Object> map = new HashMap<String, Object>(); 
		ServletContext context = this.servlet.getServletContext(); 
		context.removeAttribute("gmtStart"); 
		while(e.hasMoreElements()){ 
			String params=(String)e.nextElement(); 
			map.put(params, request.getParameter(params)); 
			context.setAttribute(params, request.getParameter(params)); 
		} 
		String sig=(String)map.get(DemoConstant.PARAMETER_SIGNATURE); 
		sign=SignatureUtil.Signature(map, DemoConstant.PARAMETER_CODE);//根据得到的参数进行自签名 
		if(sign.equals(sig)){//验证签名 
			if(map.get(DemoConstant.PARAMETER_EVENT).equals("subsc")){//新订 
				/* 
				*添加订购关系; 
				*添加月租信息,资源信息; 
				*建立账户信息,账户充值; 
				*/ 
			}else if(map.get(DemoConstant.PARAMETER_EVENT).equals("renewAhead")){ 
				/* 
				* 未到期续订,修改月租信息,资源信息 
				*/ 
			}else if(map.get(DemoConstant.PARAMETER_EVENT).equals("renew")){ 
				/* 
				* 到期续订,修改月租信息,资源信息 
				*/ 
			}else if(map.get(DemoConstant.PARAMETER_EVENT).equals("resource")){ 
				/* 
				* 订购资源,修改资源信息 
				*/ 
			}else if(map.get(DemoConstant.PARAMETER_EVENT).equals("break")){ 
				/* 
				* 退订,删除订购关系,删除账户信息,释放资源 
				*/ 
			}else{ 
				/* 
				* error 
				*/ 
			} 
		} 
		return null; 
	} 
} 
至此,我们的收费页面开发就完成了。  




3
0
分享到:
评论
1 楼 liuxuejin 2015-03-02  
你好,我想请教你一个问题,我们打算开发一个BS架构的卖家服务应用,有个问题:应用的页面是怎么嵌入淘宝的后台的。我看其他的应用都是订购了,直接都能在淘宝后台操作应用的节面的,这是怎么做到的? 期待你的指导。

相关推荐

    淘宝开放平台api开发文档

    淘宝开放平台 API 开发文档 淘宝开放平台 API 是阿里巴巴集团旗下的电子商务平台淘宝网提供的一套应用编程接口(API),旨在帮助开发者快速构建电子商务应用程序。下面是淘宝开放平台 API 的详细介绍。 快速入门...

    ASSP平台ISV订购接口规范

    ##### 1.1 阿里平台订购页面内嵌页面时POST参数信息 | 参数名称 | 类型 | 必传 | 说明 | 示例 | | --- | --- | --- | --- | --- | | `signature` | String | Y | 签名信息,用于验证数据的完整性 | 5B84046E71C4DC2F...

    淘宝API开发文档

    ### 淘宝API开发文档解析与实践指南 #### 基础篇:淘宝OpenAPI初学者入门 淘宝OpenAPI自推出以来,迅速吸引了众多开发者和ISV(独立软件供应商)的关注,尤其对于那些渴望利用淘宝庞大生态系统的创业者和在校大...

    PHP版本钉钉ISV应用Demo

    ISV开发者可以通过钉钉开放平台提供的API和SDK,实现与钉钉平台的数据交互,创建符合企业需求的应用。 ### 2. PHP在钉钉ISV应用中的角色 PHP在此Demo中主要作为后端开发语言,负责处理来自钉钉平台的请求,如OAuth...

    ding-isv-access-master-api_钉钉_dingding_

    【标题】"ding-isv-access-master-api_钉钉_dingding_" 涉及的核心知识点是钉钉ISV(独立软件开发商)访问API的开发与使用。ISV可以通过这些API来构建与钉钉平台集成的应用,实现企业级的协同办公功能。 【描述】中...

    open 淘宝API 数据字典 开发文档

    淘宝OpenAPI是一套用于开发者访问淘宝平台数据和服务的应用程序编程接口(API)。通过这些API,开发者可以方便地与淘宝平台进行数据交互,实现商品搜索、商品详情获取等功能。本文档主要介绍了淘宝OpenAPI中的产品API...

    淘宝API主动推送业务PHP Demo

    淘宝API主动推送业务的长连接的方式的PHP实现方法

    做T+ISV你需要知道的流程.docx

    #### 三、深入理解T+ISV开发要点 - **熟悉技术文档**:官方文档是进行有效开发的前提条件,务必仔细研读并理解其中的每一个细节。 - **掌握API使用方法**:T+提供了丰富的API接口供开发者调用,包括但不限于数据...

    2007-2008年中国SI_ISV市场研究报告

    根据给定的文件信息,我们可以深入探讨2007-2008年中国SI_ISV市场的关键知识点,涉及系统集成商(System Integrator, SI)和独立软件开发商(Independent Software Vendor, ISV)在该时期的市场状况、特征以及面临的挑战...

    Red Hat联合IBM为Linux ISV赋能.pdf

    2018年8月18日,IBM软件集团与Red Hat公司共同宣布启动针对Linux独立软件开发商(ISV)的联合赋能计划,这一举措旨在助力中国ISV成功开发基于Linux的操作系统软件应用和解决方案。 IBM软件集团亚太地区Unix软件业务...

    专题资料(2021-2022年)ISV产品接入指南2DOC33页.docx

    《ISV产品接入指南》是针对云计算领域独立软件开发商(ISV)的一份详细技术文档,旨在帮助ISV高效地将其产品接入云平台,以便更好地利用云计算资源并为用户提供服务。以下是对该指南主要内容的详细解释: 1. **文档...

    钉钉isv接入所需资料

    总的来说,钉钉ISV接入是一个涉及技术开发、权限配置、接口调用等多个环节的过程,需要对Java编程、OAuth2.0协议以及钉钉API有深入理解。同时,解决问题的能力也非常重要,因为在实际操作中,开发者可能会遇到各种...

    淘宝appkey申请方法(详细介绍)

    淘宝AppKey的申请过程是淘宝开放平台(ISV, Independent Software Vendor)中的一项基础步骤,用于开发者集成淘宝API,实现各种个性化应用或服务。以下是对淘宝AppKey申请的详细步骤及注意事项的阐述: 首先,要申请...

    PHP新订购与支付流程(文档)DEMO

    ISV的价格页面会显示在订购过程中,用户根据页面提示选择资源类型和月租类型,系统会计算总价。 7. **订购处理** 用户确认订购后,ISV接收到订购请求,处理订购类型,并将处理结果POST给阿里软件,进入支付流程。 ...

    org.eclipse.platform.doc.isv.3.1(Eclipse官方开发文档)

    org.eclipse.platform.doc.isv.3.1(Eclipse官方开发文档) org.eclipse.platform.doc.isv.3.1(Eclipse官方开发文档) org.eclipse.platform.doc.isv.3.1(Eclipse官方开发文档)

    IBM与红帽推出Linux ISV联合赋能计划.pdf

    IBM与红帽推出Linux ISV联合赋能计划.pdf

    财付通api接口

    业务术语 术语 ISV 说明 独立软件供应商 / Independent Software Vendor,可以是商户、个人或者第三方中介开发者 指 ISV 使用财付通开放平台 SDK 开发的 WEB 应用程序, 运行于第三方服务器上为最终用户提供 服务 ...

    NX11 ISV机床仿真帮助文件.pdf

    NX11 ISV机床仿真帮助文件 NX11 ISV机床仿真帮助文件是UG机床仿真的重要文档,旨在帮助用户快速掌握机床仿真技术。下面是该文档中的主要知识点: 一、机床仿真概述 机床仿真是指使用计算机辅助设计(CAD)软件...

    ding-isv-app-master_DEMO_dingdingisvapp_

    总结来说,"ding-isv-app-master_DEMO_dingdingisvapp_" 是一个全面展示钉钉ISV App开发流程的实例,涵盖了环境搭建、API集成、前端框架使用、UI设计以及发布和测试等多个环节。通过深入学习和实践这个DEMO,开发者...

Global site tag (gtag.js) - Google Analytics