支付宝小程序扩展能力 AntBuilder 会员接入指南
操作手册
本产品暂为定向输出,若有对外输出诉求或需求洽谈事项,请联系合作伙伴技术组:partner-booster@service.alipay.com
业务系统配置
不打通 CRM(默认)
只需要配置卡管系统的公网 URL(就是小程序后端 web-mini 的公网 URL)。
打通 CRM
配置项:
请前往 AntBuilder 安装目录 antbuilder-installer/application/web-mini/config 以及 antbuilder-installer/application/web-management/config,编辑 application-prod.yml 文件,参考下列实例底部配置如下内容。
前置条件:
CRM 提供以下接口,接口格式参考下面技术接入手册:
- 开卡信息接口:用户领卡时调用,传入用户 alipay UID + 领卡表单信息,换取用户会员卡号、积分、等级信息。
- 卡信息变动接口:信息变更(新增、删除)时调用,以同步信息,如果不需要删除,接口中可以不做操作。
为了保证接口调用安全性,CRM 和web-mini调用之间有安全验证
- web-mini 调用 CRM(开卡信息接口和卡信息变动接口),在 HEADER 中加入 token,CRM 可以验证 token 是否一致。
- CRM 调用 mini-core(更新积分和模板):使用 RSA2 加签验签,CRM 使用私钥加签,mini-core 使用公钥验签。
配置会员卡
- 配置会员卡应用。会员卡应用建议使用 WEB 应用,方便后续会员卡单独对外漏出。
- 创建会员卡模板。
- 配置基本信息。为创建的模板取名字,选择会员卡有效期,适用门店列表。
- 配置卡样式。主要配置卡的图片,显示的名称,以及字体颜色,卡码类型(动态码需配置刷新时间)。 说明:这一步可以验证支付宝应用配置是否正确,如果上传失败,请前往文档最后的常见问题。
- 配置栏位。分为标准栏位和自定义栏位。标准栏位,目前仅支持积分。
- 行动点配置。行动点可以支持跳小程序,而且支持在卡包列表中显示。
- 开卡表单配置。配置需要获取到的用户信息。
配置会员卡组件
- 在模板中添加一个会员卡组件,然后编辑中选择模板。
- 选择需要配置的模板,然后将组件上架即可。
附录:准备支付宝应用
- 登录 支付宝开放平台,根据需求创建网页&移动应用。
- 添加会员卡功能
注意:配置应用网关(http(s)://web-mini的域名地址)和回调地址(http(s)://web-mini的域名地址/aliCallback)。
附录:常见问题
Q:生成的领卡链接的回调地址错误,导致支付宝不能访问,如何解决?
A:领卡链接中的回调地址的 IP 是支付宝不能访问的地址,比如:localhost,域内 IP,导致领卡异常。系统,访问卡管系统,不能用 localhost 和内网地址,要用公网地址访问。
Q:卡管系统不能访问到业务系统,如何解决?
A:在业务系统配置页面,配置业务系统。
Q:会员卡领卡报错 ERR010,如何解决?
A:校验授权回调地址失败,请检查 callback 参数和应用授权回调地址是否一致。进入开放平台,将应用的回调地址修改为 http://卡管系统 HOST:卡关系统 PORT/aliCallback。
领卡失败
系统服务商(ISV)权限不足,建议在开发者中心检查对应功能是否已经添加。 问题原因:账号没有卡包权限。 解决方案:添加会员分类下的权限。
适用门店配置后效果
门店列表配置后再会员卡首页最下方添加 适用门店 选项,门店列表按照 LBS 聚力显示。
查询门店 ID
登录 商家中心,进入门店管理,查看门店信息,复制门店 ID。
技术接入手册
工作 | 描述 | 是否必须 |
---|---|---|
提供用户信息接口(含会员数据同步+查询) | 传入用户提交的会员信息,CRM 系统保存或更新会员信息,同时业务系统返回会员卡号和积分等会员信息。 | 是 |
卡状态变更接口 | 支付宝会员卡新增或删除时,将变更的会员卡信息发送给业务系统, | 是 |
获取领卡链接 | 获取支付宝领卡表单的链接地址 | 否 |
更新会员卡积分 | 调用卡管更新支付宝会员卡积分 | 否 |
业务系统提供接口给卡管系统(必须)
开卡信息接口
示例代码,注意看注释。
/** * 开卡信息接口 * token:在管理系统配置的访问业务系统TOKEN,可以用来防止被攻击 * * <p> * HTTP,POST请求,入参放在BODY中,入参和出参都是MAP * <p> * 入参中包含以下参数: * name(姓名) * mobile(手机号) * certNo(身认证) * gender(性别) * templateId(支付宝模板ID) * outString(outString) * alipayUserId(支付宝USERID, 2088开头) * 说明:上面的参数alipayUserId肯定存在,其他参数根据配置的领卡表单获取,可能没有值 * <p> * 出参,要求包含以下参数 * point(当前用户积分,必填,整数,如果没有就传0) * bizCardNo(业务系统卡号,二维码显示这个卡号,必填,字符串,更新积分等接口都使用业务系统卡号,不用保存支付宝用户ID) * templateId(用户等级对应的卡模板不是开卡链接中的模板时,将真正的模板ID传回来) * @param params * @return */@PostMapping("/card/openCardInfo")
public Map<String, Object> openCardInfo(@RequestHeader("token") String token, @RequestBody Map<String, Object> params ) throws Exception {
LogUtil.info(log,"token==" + token);
String bizCardNo = String.valueOf( params.get("bizCardNo"));
if(StringUtils.isEmpty(bizCardNo)){
//如果没有传会员卡号,走注册逻辑 LogUtil.info(log, "走注册逻辑");
//TODO: 根据map中传的身份证、手机号、姓名、支付宝UID等匹配业务系统的用户,返回业务系统用户ID和积分信息 String name = params.get("name")!= null ? String.valueOf(params.get("name")) : null;
String mobile = params.get("mobile")!= null ? String.valueOf(params.get("mobile")) : null;
String certNo = params.get("certNo")!= null ? String.valueOf(params.get("certNo")) : null;
String gender = params.get("gender")!= null ? String.valueOf(params.get("gender")) : null;
String templateId = String.valueOf(params.get("templateId"));
String outString = String.valueOf(params.get("outString"));
String alipayUserId = String.valueOf(params.get("alipayUserId"));
name = DESUtils.encrypt(name);
mobile = DESUtils.encrypt(mobile);
certNo = DESUtils.encrypt(certNo);
gender = DESUtils.encrypt(gender);
LogUtil.info(log, "开卡信息:name:{}, mobile:{},certNo:{},gender:{},templateId:{},outString:{}",
name, mobile, certNo, gender, templateId, outString);
Map<String, Object> result = new HashMap<>();
// 必填,业务系统用户卡号,有两个场景使用,参数有点不一致 result.put("bizCardNo", "xxx");
result.put("cardNo", "xxx");
// 非必填,用户已有积分,如果没有就传0 result.put("point", "yyyy");
// 非必填,用户登记 result.put("level", "zzzz");
// 非必填,用户余额 result.put("balance", "nnnn");
// 非必填,如果用户等级对应的模板和开卡对应的模板不一致,则重新传一个模板ID // result.put("templateId", "20200227000000002181302000300947"); return result;
}else{
//如果传了会员卡,走会员查询逻辑 LogUtil.info(log, "走查询逻辑");
// 必填,业务系统用户卡号,有两个场景使用,参数有点不一致 Map<String, Object> result = new HashMap<>();
result.put("bizCardNo", bizCardNo);
result.put("cardNo", bizCardNo);
// 非必填,用户已有积分,如果没有就传0 result.put("point", "yyyy");
// 非必填,用户登记 result.put("level", "zzzz");
// 非必填,用户余额 result.put("balance", "nnnn");
return result;
}
}
使用以下 shell 命令,测试是否能够正常访问。
curl -H "Content-Type:application/json"
-XPOST http://localhost:8082/card/openCardInfo -d '{"alipayUserId":"2088"}'
卡信息变更接口
示例代码,注意看注释。
/** * 卡信息变更接口 * token:在管理系统配置的访问业务系统TOKEN,可以用来防止被攻击 * * 入参中包含以下参数: * type(变动类型): ADD\DEL * templateId(支付宝模板ID) * alipayUserId(支付宝USERID, 2088开头):ADD、DEL类型会传入 * bizCardNo(开卡时返回的业务系统卡号):ADD、DEL类型会传入 * alipayCardNo(支付宝会员卡号):ADD、DEL类型会传入 * * @param params * @return */@PostMapping("/card/cardChange")
public boolean cardChange(@RequestHeader("token") String token, @RequestBody
Map<String, Object> params) {
LogUtil.info(logger, "卡信息变更:token:{} ",token);
//TODO: 业务系统根据自己需求 String type = String.valueOf(params.get("type"));
String templateId = String.valueOf(params.get("templateId"));
String alipayUserId = String.valueOf(params.get("alipayUserId"));
String bizCardNo = String.valueOf(params.get("bizCardNo"));
String alipayCardNo = String.valueOf(params.get("alipayCardNo"));
LogUtil.info(logger, "卡信息变更:type:{},templateId:{},alipayUserId:{}, bizCardNo:{},alipayCardNo:{}", type,templateId, alipayUserId, bizCardNo, alipayCardNo);
return true;
}
商户动态码获取接口
示例代码,注意看注释。
/** * 商户动态卡码值查询接口 * token:在管理系统配置的访问业务系统TOKEN,可以用来防止被攻击 * <p> * 入参中包含以下参数: * appId * templateId * alipayUserId * * @param params * @return */ @PostMapping("/card/mdCodeChange")
public Map<String, Object> mdCodeChange(@RequestHeader("token") String token, @RequestBody Map<String, Object> params) {
LogUtil.info(log, "卡信息变更:token:{} , {}", token, JSON.toJSONString(params));
String appId = String.valueOf(params.get("appId"));
String templateId = String.valueOf(params.get("templateId"));
String alipayUserId = String.valueOf(params.get("alipayUserId"));
Integer expireTime = 120;
if (params.containsKey("expireTime")) {
expireTime = Integer.valueOf(String.valueOf(params.get("expireTime")));
}
Map<String, Object> result = new HashMap<String, Object>();
result.put("alipayUserId", alipayUserId);
//TODO: 业务系统根据自己需求,生成用户动态码值 result.put("codeValue", System.currentTimeMillis() / 1000L + "_" + expireTime);
//TODO: 业务系统根据自己需求,设置码值过期时间, params.get("expireTime") 是卡管系统中配置的过期时间(秒) result.put("codeExpire", DateUtil.format(new Date(System.currentTimeMillis() + expireTime * 1000), "yyyy-MM-dd HH:mm:ss"));
return result;
}
业务系统调用卡管系统(可选)
在业务代码中更新用户积分
/** * 更新积分 * <p> * 模拟调用卡管系统更新用户积分 * <p> * 真实场景应该放在业务层中调用 * * @param templateId 模板ID * @param bizCardNo 业务系统卡号 * @param point 用户最先积分 * @param changeReason 积分更新原因 * @return */@PostMapping("/card/update")
public HttpResult updateCard(String templateId, String bizCardNo, String point, String changeReason)
throws Exception {
Map<String, String> map = new HashMap<>();
map.put("templateId", templateId);
map.put("bizCardNo", bizCardNo);
map.put("point", point);
map.put("changeReason", changeReason);
MapUtils.removeNullValue(map);
try {
String signature = SignHelper.signStringParam(map, sdkConfig.getPrivateKey());
map.put(SignHelper.SIGNATURE_PARAM_KEY, signature);
} catch (Exception e) {
throw new Exception("加签失败", e);
}
String result = restTemplate.postForEntity(sdkConfig.getListOfServers() +
"/card/update", map, String.class).getBody();
Map parseResult = (Map) JSON.parse(result);
if (parseResult.get("status").equals("OK")) {
return HttpResult.newCorrectResult("积分更新成功");
}
return HttpResult.newErrorResult(parseResult.get("errmsg").toString());
}
在业务代码中更新用户模板
用户登记发生变化,需要替换成不同的模板(可能包含不同的权益说明)。
/** * 更新模板 * <p> * 模拟调用卡管系统更新用户模板 * <p> * 真实场景应该放在业务层中调用 * * @param templateId 模板ID * @param bizCardNo 业务系统卡号 * @param targetTemplateId 目标模板ID * @param changeReason 积分更新原因 * @return */ @PostMapping("/card/changeTemplate")
public HttpResult changeTemplate(String templateId, String bizCardNo, String targetTemplateId, String changeReason)
throws Exception {
Map<String, String> map = new HashMap<>();
map.put("templateId", templateId);
map.put("bizCardNo", bizCardNo);
map.put("targetTemplateId", targetTemplateId);
map.put("changeReason", changeReason);
MapUtils.removeNullValue(map);
try {
String signature = SignHelper.signStringParam(map, sdkConfig.getPrivateKey());
map.put(SignHelper.SIGNATURE_PARAM_KEY, signature);
} catch (Exception e) {
throw new Exception("加签失败", e);
}
String result = restTemplate.postForEntity(sdkConfig.getListOfServers() +
"/card/changeTemplate", map, String.class).getBody();
Map parseResult = (Map) JSON.parse(result);
if (parseResult.get("status").equals("OK")) {
return HttpResult.newCorrectResult("模板更新成功");
}
return HttpResult.newErrorResult(parseResult.get("errmsg").toString());
}