三方电子卡券扩展点使用说明,超详细!

什么是电子卡券

电子卡券,一种虚拟商品,按照类型,可适用在多个行业,包括服务、餐饮,票务等多种行业,分为套餐券,单品券,服务券等。通过在线上购买电子卡券,在线下兑换服务或兑换商品

场景说明

商家有自己的发券系统,有赞电子卡券商品与外部发券系统打通,且商家自有发券系统作为数据中心,有赞读取商家自有发券系统来显示券码及更新券码状态等,商家自有券码可用于线下门店核销;

流程图

流程说明:

1. 卡券创建流程

  • 首先,用户在有赞商城购买电子卡券;
  • 触发有赞云的创建电子卡券业务扩展点;
  • 扩展点中调用三方系统接口生成卡券信息;
  • 扩展点中调用三方卡券创建 API 接口,传入三方系统生成的卡券编码和券码;
  • 扩展点中调用三方接口存储订单号、卡券编码和券码信息;
  • 根据电子卡券创建扩展点所需要的返回信息返回创建成功的响应信息给有赞;
  • 用户端展示生成的卡券券码和编码、二维码信息;

2. 卡券整单核销流程

  • 线下出示二维码获取卡券编码;
  • 调用卡券核销接口对获取到的卡券进行核销;
  • 判断是否核销成功,如果成功记录卡券状态并同步至有赞;

依赖扩展点和接口

接口 说明 备注
电子卡券创建扩展点 对接线下卡券系统,创建商家自有卡券
三方卡券创建接口 创建商家自有的卡券信息 卡券编码长度不要超过 20 位,卡券券码建议不超过 12 位,需要先申请开通 API 接口
电子卡券整单核销 电子卡券整单核销,用于一次核销整个电子卡券下的所有码券 需要先申请开通 API 接口

准备工作

  • 注册有赞云账号
  • 申请有容器应用
  • 本地安装 Git
  • idea 或 eclipse 等 IDE

说明: 准备工作,请参考 https://doc.youzanyun.com/doc#/content/35700/35702

操作步骤

代码下载

1. 首先,打开有赞云官网 登录后,选择对应的有容器应用,然后在 **「应用概况」-「Git 仓库信息」** 选项中,点击拉取代码到本地,点击复制按钮,如图所示:

2. 粘贴复制的内容到终端将项目克隆到本地.

git clone https://手机号:token@diy.youzanyun.com/git/pass-support.git

说明: 这部分可以参考代码下载及配置教程

工程导入及配置

  1. 打开 IDE,导入项目,找到 xxxxx-biz 模块,展开 com.youzan.cloud.xxxxx.biz,这里的 xxxx 代表你的应用名称,你可以在该包中新建 package,然后将对应的扩展点代码写在子包中.

关于工程的介绍,请参考Java 应用代码介绍 文档说明

这里我们创建一个 CreateTicketExtImplDemo 的 class,并实现 CreateTicketExtPoint 接口,这部分可以阅读创建电子卡券扩展点文档

public class CreateTicketExtImplDemo implements CreateTicketExtPoint {

}
  1. 然后,我们需要在 class 上面加上扩展点的注解并重写 CreateTicketExtPoint 接口中的 create 方法,@ExtensionService(“createticketfang”) 表示该 class 启用了业务扩展点,注解 @ExtensionService 表示业务扩展点,大括号里面的 createticketfang 表示扩展点的名称,当代码提交到有赞云,在控制台可以看到对应名称的扩展点,这个后面再具体说明
@ExtensionService("createticketfang")
public class CreateTicketExtImplDemo implements CreateTicketExtPoint {

public OutParam<CreateTicketResponseDTO> create(CreateTicketRequestDTO createTicketRequestDTO) {

}


}

CreateTicketExtPoint 接口中的 create 方法

public interface CreateTicketExtPoint {

    OutParam<CreateTicketResponseDTO> create(CreateTicketRequestDTO request);

}

CreateTicketRequestDTO 这个对象中包含了用户购买的电子卡券的一些基本信息,包括订单号、买家信息、商品信息等。具体信息请阅读电子卡券扩展点文档的请求参数部分内容

请求外部域名

  1. 接下来我们需要在 create 方法里面实现三方卡券的创建,在创建三方卡券之前,我们需要在该类中新建一个 GetCode 类,因为我们的卡券信息是来自商家自有系统,这里我们调用一个接口/api/v1/getcode 来获取卡券信息.
/**
     * 从三方系统获取电子卡券信息
     */
    class GetCode {
        private JSONObject GetCodeInfo(){
            // 请求接口获取卡券信息
            ResponseEntity<String> response = restTemplate.getForEntity(GetCodeUrl,String.class);
            log.info("状态码 {}",response.getStatusCode());
            JSONObject result = JSON.parseObject(response.getBody());
            log.info("返回值 {}",result.toJSONString());
            return result;

        }
    }

其返回值如下:

{"code":"65669421942545054986","ticketnos":["432729038126152910"]}

代码中的 GetCodeUrl, 我们可以在有赞云控制台的应用变量中配置,然后通过 SpringBoot 的 @Value 获取到值

public class CreateTicketExtImplDemo implements CreateTicketExtPoint {

    /**
     * 获取三方卡券接口
     */
    @Value("${getcode.url}")
    private String GetCodeUrl;
    /**
     * 同步卡券信息到三方系统
     */
    @Value("${syncticket.url}")
    private String SyncTicketUrl;

如图所示

提示: 在扩展点中调用外部域名,需要在有赞云控制台配置统一接出,且必须使用有赞提供的 restTemplate

统一接出配置,如图所示,在有赞云后台的 **「应用管理」-「配置管理」** 新增后需要联系有赞云审核,审核通过状态为生效即表示该域名可用

restTemplate 注解参考下面的代码.

public class CreateTicketExtImplDemo implements CreateTicketExtPoint {

  

    /**
     * 有容器应用请求外部域名,需要使用RestTemplate 注解,不能 new 该对象,否则会报请求超时
     */
    @Autowired
    private RestTemplate restTemplate;
    
    ……

API 接口的调用以及应用变量的配置

  1. 接下来,我们需要在扩展点里面调用 API 接口,在有容器应用中我们需要使用如下方式获取 Token
public class CreateTicketExtImplDemo implements CreateTicketExtPoint {

     /**
     * 店铺ID
     */
    @Value("${kdt.id}")
    private String kdt_id;

    /**
     * 获取三方卡券接口
     */
    @Value("${getcode.url}")
    private String GetCodeUrl;
    /**
     * 同步卡券信息到三方系统
     */
    @Value("${syncticket.url}")
    private String SyncTicketUrl;
    @SneakyThrows
    @Override
    public OutParam<CreateTicketResponseDTO> create(CreateTicketRequestDTO createTicketRequestDTO) {
        log.info("创建商家自有卡券, 请求体,{}", JSON.toJSONString(createTicketRequestDTO));
        CreateTicketResponseDTO createTicketResponseDTO = new CreateTicketResponseDTO();
        OAuthToken token = bifrostSdkService.getToken(kdt_id, OAuthEnum.TokenType.silent);
        String accessToken = token.getAccessToken();

其中@Value("${kdt.id}") 是从配置文件中获取店铺 ID ,我们可以在有赞云后台的 **「应用管理」** - 「配置管理」-**「应用变量」** 中新增配置,然后通过 springBoot 的注解方式将值绑定到对应的变量中在程序中使用,这里的应用变量与 springBoot 的 application.properties 概念一样.

当拿到 token 后,我们就可以调用创建三方卡券的 API 接口了,调用接口我们需要使用 bifrostSdkService 方法。这里与无容器应用是有区别的.

public OutParam<CreateTicketResponseDTO> create(CreateTicketRequestDTO createTicketRequestDTO) {
       
        /**
         *  从第三方系统获取卡券信息,包括卡券的编码和券码
         */
        JSONObject getCode = new GetCode().GetCodeInfo();
        // 获取卡券编码
        String Code = getCode.getString("code");
        // 获取卡券券码
        JSONArray jsonArray = JSONObject.parseArray(getCode.getString("ticketnos"));
        List TicketNos = new ArrayList<>();
        for(Object jstr:jsonArray){
            TicketNos.add(jstr);
        }

        /**
         * 说明:
         * 调用三方卡券创建接口,入参需要:
         *  1.订单号,订单号在createTicketRequestDTO 中获取,createTicketRequestDTO 中的返回参数参考电子卡券扩展点文档
         *  2.设置卡券编码和券码,其中券码是List 数组,值为 String 类型,卡券编码最大长度不能超过20位,如果超过有赞只会取前20位
         *  券码建议不超过12位
         */
        YouzanTradeVirtualticketThirdCreate youzanTradeVirtualticketThirdCreate = new YouzanTradeVirtualticketThirdCreate();
        YouzanTradeVirtualticketThirdCreateParams youzanTradeVirtualticketThirdCreateParams = new YouzanTradeVirtualticketThirdCreateParams();
        // 获取订单号
        youzanTradeVirtualticketThirdCreateParams.setTid(createTicketRequestDTO.getOrderNo()); //订单号
        // 设置电子卡券编码
        youzanTradeVirtualticketThirdCreateParams.setCode(Code);
        // 设置卡券券码
        youzanTradeVirtualticketThirdCreateParams.setTicketNos(TicketNos);
        log.info("入参 {} {}",Code,TicketNos);
        youzanTradeVirtualticketThirdCreate.setAPIParams(youzanTradeVirtualticketThirdCreateParams);
        YouzanTradeVirtualticketThirdCreateResult result = bifrostSdkService.invoke(youzanTradeVirtualticketThirdCreate, new Token(accessToken), YouzanTradeVirtualticketThirdCreateResult.class);
        log.info("自定义电子卡券返回信息 {}",result.getMessage() + " " + result.getCode() +" "+ result.getSuccess());
        ……
        }

小技巧: 在代码中我们可以使用 log 输入一些日志进行 debug

其中 setTicketNos 需要传入一个 List ,因为卡券的券码可以有多个,因此,我们从第三方系统获取 ticketnos .

/**
         *  从第三方系统获取卡券信息,包括卡券的编码和券码
         */
        JSONObject getCode = new GetCode().GetCodeInfo();
        // 获取卡券编码
        String Code = getCode.getString("code");
        // 获取卡券券码
        JSONArray jsonArray = JSONObject.parseArray(getCode.getString("ticketnos"));
        List TicketNos = new ArrayList<>();
        for(Object jstr:jsonArray){
            TicketNos.add(jstr);
        }

最后,我们根据扩展点所需要的返回值,设置返回信息

			
        youzanTradeVirtualticketThirdCreate.setAPIParams(youzanTradeVirtualticketThirdCreateParams);
        YouzanTradeVirtualticketThirdCreateResult result = bifrostSdkService.invoke(youzanTradeVirtualticketThirdCreate, new Token(accessToken), YouzanTradeVirtualticketThirdCreateResult.class);
				/**
         * 根据电子卡券创建扩展点的文档,配置返回值
         */
        createTicketResponseDTO.setSuccess(true);
        return OutParamUtil.successResult(createTicketResponseDTO);

最终代码

package com.youzan.cloud.pass.support.biz.fang.scrm;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.youzan.api.rpc.annotation.ExtensionService;
import com.youzan.cloud.base.api.BifrostService;
import com.youzan.cloud.extension.api.trade.CreateTicketExtPoint;
import com.youzan.cloud.extension.param.trade.CreateTicketRequestDTO;
import com.youzan.cloud.extension.param.trade.CreateTicketResponseDTO;
import com.youzan.cloud.metadata.common.OutParam;
import com.youzan.cloud.open.sdk.common.constant.OAuthEnum;
import com.youzan.cloud.open.sdk.core.client.auth.Token;
import com.youzan.cloud.open.sdk.core.oauth.model.OAuthToken;
import com.youzan.cloud.open.sdk.gen.v3_0_1.api.YouzanTradeVirtualticketThirdCreate;
import com.youzan.cloud.open.sdk.gen.v3_0_1.model.YouzanTradeVirtualticketThirdCreateParams;
import com.youzan.cloud.open.sdk.gen.v3_0_1.model.YouzanTradeVirtualticketThirdCreateResult;
import com.youzan.cloud.pass.support.biz.util.OutParamUtil;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.client.RestTemplate;
import javax.annotation.Resource;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;

/**
 * @Auth: fangwenjun
 * @E-mail: fangwenjun@youzan.com
 * @title: CreateTicketExtImplDemo
 * @projectName: youzanyun-demo
 * @description: 电子卡券创建扩展点
 * @Date 2020/4/17 7:23 PM
 **/


//三方卡券创建
@Slf4j
@ExtensionService("createticketfang")
public class CreateTicketExtImplDemo implements CreateTicketExtPoint {

    /**
     * 有容器应用请求SDK 需要使用bifrostSdkService
     */
    @Resource
    private BifrostService bifrostSdkService;

    /**
     * 请求三方域名需要使用restTemplate ,否则会报错
     */
    @Autowired
    private RestTemplate restTemplate;


    /**
     * 店铺ID
     */
    @Value("${kdt.id}")
    private String kdt_id;

    /**
     * 获取三方卡券接口
     */
    @Value("${getcode.url}")
    private String GetCodeUrl;
    /**
     * 同步卡券信息到三方系统
     */
    @Value("${syncticket.url}")
    private String SyncTicketUrl;


    /**
     * createTicketRequestDTO 中包含了用户端购买卡券的相关信息,包括订单号、用户信息、商品信息等
     * @param createTicketRequestDTO
     * @return
     */

    @SneakyThrows
    @Override
    public OutParam<CreateTicketResponseDTO> create(CreateTicketRequestDTO createTicketRequestDTO) {
        log.info("创建商家自有卡券, 请求体,{}", JSON.toJSONString(createTicketRequestDTO));
        CreateTicketResponseDTO createTicketResponseDTO = new CreateTicketResponseDTO();
        /**
         * 生成Token
         */
        OAuthToken token = bifrostSdkService.getToken(kdt_id, OAuthEnum.TokenType.silent);
        String accessToken = token.getAccessToken();
        log.info("获取到的token {}",accessToken);

        /**
         * 配置卡券参数,包括卡券编码,卡券券码
         */
        JSONObject getCode = new GetCode().GetCodeInfo();
        log.info("getCode 值 {}",getCode);
        String Code = getCode.getString("code");
        JSONArray jsonArray = JSONObject.parseArray(getCode.getString("ticketnos"));
        List TicketNos = new ArrayList<>();
        for(Object jstr:jsonArray){
            TicketNos.add(jstr);
        }
        /**
         * 调用三方卡券创建接口,传入订单好和卡券编码和卡券券码
         */
        YouzanTradeVirtualticketThirdCreate youzanTradeVirtualticketThirdCreate = new YouzanTradeVirtualticketThirdCreate();
        YouzanTradeVirtualticketThirdCreateParams youzanTradeVirtualticketThirdCreateParams = new YouzanTradeVirtualticketThirdCreateParams();
        // 获取订单号
        youzanTradeVirtualticketThirdCreateParams.setTid(createTicketRequestDTO.getOrderNo()); //订单号
        // 设置券码
        youzanTradeVirtualticketThirdCreateParams.setCode(Code); // 编码
        // 设置卡券ID
        youzanTradeVirtualticketThirdCreateParams.setTicketNos(TicketNos); //卡券ID
        log.info("入参 {} {}",Code,TicketNos);
        youzanTradeVirtualticketThirdCreate.setAPIParams(youzanTradeVirtualticketThirdCreateParams);
        YouzanTradeVirtualticketThirdCreateResult result = bifrostSdkService.invoke(youzanTradeVirtualticketThirdCreate, new Token(accessToken), YouzanTradeVirtualticketThirdCreateResult.class);
        log.info("自定义电子卡券返回信息 {}",result.getMessage() + " " + result.getCode() +" "+ result.getSuccess());

        /**
         * 同步订单号以及卡券到第三方系统,实际生产环境,还需要对代码健壮性进行补充,比如异常的处理
         */
        try {
            SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd HH🇲🇲ss");//设置日期格式
            JSONObject jsonObject = new JSONObject();
            jsonObject.put("orderno",createTicketRequestDTO.getOrderNo());
            jsonObject.put("code",Code);
            jsonObject.put("ticketnos",TicketNos);
            jsonObject.put("createdate",df.format(new Date()));
            jsonObject.put("verifystate",1);
            JSONObject syncTicket = new SyncTicket().SyncTicketInfo(jsonObject);
            log.info("同步到第三方的数据 {}",syncTicket.toJSONString());

        }catch (Exception e){
            log.warn("卡券同步异常{}", e);
        }

        createTicketResponseDTO.setSuccess(true);
        return OutParamUtil.successResult(createTicketResponseDTO);
    }

    /**
     * 从三方系统获取电子卡券信息
     */
    class GetCode {
        private JSONObject GetCodeInfo(){
            // 请求接口获取卡券信息
            ResponseEntity<String> response = restTemplate.getForEntity(GetCodeUrl,String.class);
            log.info("状态码 {}",response.getStatusCode());
            JSONObject result = JSON.parseObject(response.getBody());
            log.info("返回值 {}",result.toJSONString());
            return result;

        }
    }

    /**
     * 同步卡券信息到三方系统
     */
    class SyncTicket{
        private JSONObject SyncTicketInfo(JSONObject paramMap){
            //创建请求头
            HttpHeaders headers = new HttpHeaders();
            headers.setContentType(MediaType.APPLICATION_JSON);
            // 请求实体内容
            HttpEntity<JSONObject> httpEntity = new HttpEntity<>(paramMap,headers);
            ResponseEntity<String> response = restTemplate.postForEntity(SyncTicketUrl,httpEntity, String.class);
            JSONObject result = JSON.parseObject(response.getBody());
            return result;

        }
    }

}

提交代码

8. 当本地代码编写完成后,我们就可以提交代码了

➜  pass-support git:(master) ✗ git pull
➜  pass-support git:(master) ✗ git add .
➜  pass-support git:(master) ✗ git commit -m '说明'
[master 66488e0] 说明
 1 file changed, 43 insertions(+), 8 deletions(-)
➜  pass-support git:(master) git push origin

开启扩展点

9. 打开有赞云控制台,找到对应的应用,默认都是在开发环境下,切换到业务配置 - 路由配置,在交付中切换到后端流程,找到扩展点 - 电子卡券创建扩展点,把实现的 createticketfang 这个扩展点启用

当代码提交到有赞云,会自动扫码注解,因此提交代码后即可在路由配置中看到对应的扩展点

发布

10. 然后我们切换到 应用管理 - 发布管理,点击发布

选择发布服务端

说明:

  • 服务端 当有代码变更时候需要选择服务端
  • 配置,当代码没有变更,只是配置变更了,例如增加了应用变量、更改了路由配置
  • 小程序 发布小程序时候需要选择该选项

填写发布说明

确认我们已经开启了对应的电子卡券扩展点

然后点击确认发布

然后在发布列表中,我们可以看到应用的发布状态,当状态是发布成功时,我们就可以去测试对应的功能是否正常了

如果开发环境预览效果没有问题,可以切换到生产环境,再次发布一次,选择最新的版本号,其他流程一样,发布生产环境注意生产环境的应用变量要正确,否则会报错

核销券码

  1. 在应用管理 - 店铺管理,点击新增 / 查看测试店铺。我们找到对应的测试店铺

  1. 在有赞商城后台选择商品,创建一个电子卡券类商品,然后在商品列表用手机扫码二维码

购买对应的电子卡券类商品,我们会得到一个二维码,当用户拿着二维码去商家的门店扫码就可以核销了

我们可以在订单管理中找到对应的订单,在订单详情中,可以看到当前的卡券状态是未核销

在移动端,我们可以找到对应的订单,在订单详情中,我们也可以看到卡券的信息包括卡券的券码以及二维码

点击可以跳转到卡券凭证页面

接下来,我们要从第三方模拟核销该卡券编码为 01679896134338996139 的电子卡券,这里我使用 springBoot 实现了一个 controller,实现了电子卡券整单核销,代码如下,仅供参考,在实际的生成环境中,可能还需要同步卡券的状态到第三方系统中,这里只是做一个简单的演示:

@RestController
@RequestMapping("/v1")
@Slf4j
public class TicketController {

@PostMapping("/verifycode")
    public String verifycode(@RequestBody VirtualTicket virtualticket) throws SDKException {
    
    	/** 无容器应用生成 Token,从配置文件读取ClientId、ClientSecret、grantId
    	* ClientId、ClientSecret 可以在有赞云的控制台应用管理中获取
    	* grantId 是对于的店铺ID
    	**/
        DefaultYZClient yzClient = new DefaultYZClient();
        TokenParameter tokenParameter = TokenParameter.self()
                .clientId(ClientId)
                .clientSecret(ClientSecret)
                .grantId(grantId) 
                .refresh(true)
                .build();
        OAuthToken oAuthToken = yzClient.getOAuthToken(tokenParameter);
        Token token = new Token( oAuthToken.getAccessToken());
        // 调用整单核销接口
        YouzanTradeVirtualticketVerifycode youzanTradeVirtualticketVerifycode = new YouzanTradeVirtualticketVerifycode();
        //创建参数对象,并设置参数
        YouzanTradeVirtualticketVerifycodeParams youzanTradeVirtualticketVerifycodeParams = new YouzanTradeVirtualticketVerifycodeParams();
        log.info("{}",virtualticket.getVirtualCode());
        youzanTradeVirtualticketVerifycodeParams.setCode(virtualticket.getVirtualCode()); // 传入卡券编码
        youzanTradeVirtualticketVerifycode.setAPIParams(youzanTradeVirtualticketVerifycodeParams);
        YouzanTradeVirtualticketVerifycodeResult result = yzClient.invoke(youzanTradeVirtualticketVerifycode, token, YouzanTradeVirtualticketVerifycodeResult.class);
        JSONObject jsonObject = new JSONObject();
        try {
            log.info(result.getResponse().getCode().toString());
            // 如果卡券状态为 1 表示核销成功
            if (result.getResponse().getCode() == 1){
                jsonObject.put("Code","200");
                jsonObject.put("Message","卡券核销成功");
            }else {
                jsonObject.put("Code","40201");
                jsonObject.put("Message","参数错误");
            }
        }catch (Exception e){
            // 如果已经核销了则报 400 错误
            jsonObject.put("Code","400");
            jsonObject.put("Message",result.getErrorResponse().getMsg());
        }
        return jsonObject.toJSONString();

    }
    
  }

其中的 client_id 和 client_secret 可在有赞云控制台对应的容器应用的应用概况下的应用信息中查看,直接点击复制粘贴过来使用即可.

接口地址 https://api.fangwenjun.com/api/v1/verifycode,我们使用 postman 调用该接口传入参数

可以看到提示

{"Message":"卡券核销成功","Code":"200"}

如果再次请求则会提示

{"Message":"该卡券已经核销","Code":"400"}

同步的卡券信息,可以在三方数据库中看到, 这里只做演示,存储了一些基本的信息,例如订单号、卡券编码、卡券券码和创建时间以及卡券是否核销和核销时间等信息。

MariaDB [virtualticket]> select * from virtualtable;
+----+--------------------------+----------------------+------------------------+------------+------------+-------------+
| id | orderno                  | code                 | ticketnos              | createdate | verifydate | verifystate |
+----+--------------------------+----------------------+------------------------+------------+------------+-------------+
|  2 | E20200604074517054400001 | 30494094665677034527 | ["555267203621507214"] | 2020-06-03 | NULL       |           1 |
+----+--------------------------+----------------------+------------------------+------------+------------+-------------+
1 row in set (0.000 sec)

异常定位

当出现异常,我们可以在有赞云的控制台 **「应运维管理」-「日志管理」** 中查看代码中输出的 log 信息用来定位问题. 可以根据日志级别和时间来筛选日志

如图所示,可以看到我们在电子卡券扩展点中打印的日志信息,这些信息可以有利于我们定位故障和排查问题.

如果你希望实现针对单个卡券的核销,可以参考电子卡券单个码券核销 接口来实现.